Compare commits

...
Author SHA1 Message Date
b57867ab70 Global Elements: Reusable Content of Blocks (#22448)
* Slice 1: Reference Model

A block layout item's contentKey can point to either local inline
content or a library element. For shared content, the `isSharedContent`
flag is set to `true`.

* Slice 2: Insert Block from Library

* Slice 3: Transfer to Library

* Slice 4: Disconnect from Library

* Slice 5: Inline Element Editing from Block Context

* [WIP] Slice 6: Publish Awareness

* fix(block): address code review findings

Critical:
- disconnectFromLibrary now sets initial expose for new local content
  and cleans up resolved variant state entry
- Extract #updateExposedState() in entry elements, called from all three
  observers (hasExpose, isLibraryElement, sharedContentVariantState) to
  prevent stale unpublished state on library blocks

Important:
- Guard #fetchLibraryElement against already-resolved elements to prevent
  redundant server requests
- Hoist UmbElementDetailRepository to class field in entry elements to
  avoid accumulating dead controllers

Suggestion:
- Fix umb-localize key attributes to use literal keys instead of
  resolved strings from localize.term()

* Enable reusable elements in block editors, including indexing for search and output rendering

* Update cache levels for property value converters.

* Add keys to block layout items

* fix(block): address review findings for reusable block content

- Use DocumentVariantStateModel.DRAFT enum instead of magic string
  in both block-list and block-grid entry elements
- Strip isSharedContent from layout during clipboard write to prevent
  pasted blocks from incorrectly appearing as library references
- Guard #setInitialBlockExpose in disconnectFromLibrary against missing
  content type structure
- Store all element variants and resolve against active variantId for
  correct multi-culture state display
- Add already-resolved guard to #fetchLibraryElement
- Add JSDoc on isLibraryElement and sharedContentVariantState observables
- Add .trim() to transfer modal name validation

* feat(block): add layout key and migrate identity from contentKey to key

BREAKING: UmbBlockLayoutBaseModel now requires a `key: string` property.
Plugin code that creates layout objects without `key` will get a compile
error.

- Change UmbArrayState identity functions to use `(x) => x.key`
- Add `layout` setter on entry elements (list, grid, single, rte) that
  extracts both layoutKey and contentKey from the layout object
- Deprecate `contentKey` setter on entry elements (use `layout` instead)
- Add `layoutKey` read-only getter for sorter identity
- Add `setLayoutKey()` / `layoutByKey()` / `getLayoutByKey()` methods
- Update `transferToLibrary` and `disconnectFromLibrary` to take layoutKey
- Update delete operations to find by layout key, only remove shared
  content/settings/exposes if no other layout references the same contentKey
- Migrate grid recursive area operations to use key for identity
- Update `unique` observable on entry context to derive from layout key
- Generate new key on property value clone
- Backwards compat: `setLayouts` assigns `key ??= contentKey` for
  persisted data without key
- Strip `isSharedContent` from clipboard layout clone
- Update sorter configs and repeat key functions

* Add tests proving that reusable content can work with RTEs too

* i18n: capitalize Element/Library in disconnect-from-library strings

Per Niels' feedback on PR #22448 — Element and Library are product nouns
and should be capitalized to distinguish from generic uses.

* refactor(block): rename layout/library APIs for consistency

- `setLayoutKey`/`getLayoutKey` → `setKey`/`getKey` on entry context
- `layoutByKey`/`getLayoutByKey` → `byKey`/`getByKey` on entries context
- `layoutKey` property → `key` on block entry elements (list/grid/single/rte)
- `data-layout-key` attribute → `data-key` on `umb-rte-block`
- `insertLibraryElementReference` → `insertLibraryElement` on manager
- `transferToLibrary`/`disconnectFromLibrary` `layoutKey` param → `key`
- `delete(layoutKey)` param → `delete(key)` on entries context
- `allowedLibraryElementTypeKeys` → `libraryAllowedElementTypeKeys` on catalogue modal data

* refactor(block): convert catalogue modal value to discriminated union

`UmbBlockCatalogueModalValue` was a single object with optional `create`,
`clipboard`, and `library` fields, which let invalid combinations type-check.
Convert it to a true discriminated union so consumers must narrow with `'in'`
before accessing the variant payload.

Update all four entries contexts (block-list, block-grid, block-rte, block-single)
to use `value && 'create' in value` style narrowing in their `onSubmit` handlers.

* refactor(block): move library transfer/disconnect handlers to Block Manager

The block entry elements (`umb-block-list-entry`, `umb-block-grid-entry`) each
duplicated the orchestration for transferring a local block's content to the
Element Library and disconnecting a referenced library element back to local
content. The handlers opened modals, scaffolded element data, called the
element repository, and finally mutated manager state — all from the UI element.

Move that logic to the manager as `requestTransferToLibrary(key)` and
`requestDisconnectFromLibrary(key)`. The "request" prefix marks the
user-confirmed flows; the bare `transferToLibrary` / `disconnectFromLibrary`
methods remain as the pure state mutations.

Entry elements now delegate to the manager, which also lets us drop the
per-element `UmbElementDetailRepository` field and the modal-related imports
from the elements.

Confirm modal headlines/labels are now passed as localization keys, letting
the modal handle its own string resolution (per Niels' review feedback).

* refactor(block): deprecation hygiene around contentKey setters

- Stop calling the (deprecated) `setContentKey()` from `set layout` on the
  entry elements. The layout already carries the contentKey, so internal flows
  no longer need the fallback path.
- Add `UmbDeprecation` runtime warnings to all four `set contentKey` element
  setters (list/grid/single/rte) and to `UmbBlockEntryContext.setContentKey`.
  JSDoc `@deprecated` alone is not enough — runtime warnings are required per
  the Web.UI.Client deprecation policy.

* fix(block): preserve isSharedContent through clipboard copy/paste

When copying a block that references a library Element, we were stripping
`isSharedContent` from the cloned layout so that pasting always produced a
local copy. Per Niels' review feedback, the expected behaviour is the inverse:
a copied library-referencing block should paste as a reference. If the user
wants a local copy after paste, they explicitly disconnect from the library.

- Remove the `delete clonedLayout.isSharedContent` in `#copyToClipboard` for
  both block-list and block-grid.
- Branch in `_insertBlockFromPropertyValue` so layouts with `isSharedContent`
  route through the manager's `insertLibraryElement(contentKey, originData)`
  flow rather than expecting matching `contentData` (which the clipboard
  payload deliberately doesn't carry for references).

* refactor(block): centralise library element resolution in the Block Manager

Per Niels' review: the entry context shouldn't be the place where the safety
fetch for library element content lives — there could be other call sites,
and the manager already owns the resolved-elements state.

Add a layouts observer in `UmbBlockManagerContext` that watches `_layouts`
and, for any layout where `isSharedContent` is set, kicks off
`#fetchLibraryElement(contentKey)`. The fetch already dedupes, so this is
safe to call repeatedly.

In return, drop both `_manager.ensureContentResolved(contentKey)` calls from
`UmbBlockEntryContext` (`setContentKey` and `#observeContentData`). Also
derive `#contentKey` from the observed layout so internal flows have access
to it without callers having to push it through the deprecated setter.

* docs(block): add follow-up TODOs from review

- Mark `#fetchLibraryElement` for `@madsrasmussen` to replace with a batching
  manager that bundles multiple element requests into a single round-trip.
  Today's per-key fetch becomes N+1 on pages with many shared blocks.
- Update the catalogue modal TODO to reflect that the catalogue is conceptually
  a Modal/Flow extension point — not a Workspace as the previous comment
  implied. Captures the open question about an extensible "Library tab"
  surface for other content sources.

* fix(block): break circular dep between manager context and modals barrel

`UmbBlockManagerContext` imported `UMB_BLOCK_TRANSFER_TO_LIBRARY_MODAL` from
`../modals/index.js` (the barrel). That barrel transitively pulled in the
catalogue modal element, which sits downstream of the manager — creating:

  context/index → block-manager.context → modals/index
    → modals/block-catalogue/index → block-catalogue-modal.element

Import the token directly from `transfer-to-library-modal.token.ts` instead.

* Elements: Contextualize variant blocks rendering for invariant content (#22790)

* Contextualize variant blocks rendering for invariant content

* Initialize local language variables in a more readable way

* Also filter out whitespace cultures

* Add XML docs.

* feat(block): migrate library transfer/disconnect to blockAction extensions

The `#renderTransferToLibraryAction()` / `#renderDisconnectFromLibraryAction()`
render methods (and their handlers) were commented out when `main` was merged
in, leaving these flows unrendered. Migrate them to the new `blockAction`
extension type (PR #22459) so they:

- Render through `<umb-block-action-list>` like the other common actions.
- Reuse automatically across Block List, Block Grid, Single Block, and RTE
  Block editors — no per-editor code.
- Honour visibility via manifest conditions, not inline state branches.

Additions:
- `UMB_BLOCK_ENTRY_IS_LIBRARY_ELEMENT_CONDITION` — boolean-match condition
  observing the existing `context.isLibraryElement` observable. Used inverted
  by the two new actions.
- `Umb.BlockAction.TransferToLibrary` (weight 250, `icon-link`) — visible when
  `isLibraryElement` is false and the entry is not read-only.
- `Umb.BlockAction.DisconnectFromLibrary` (weight 250, `icon-unlink`) — visible
  when `isLibraryElement` is true and the entry is not read-only. The two
  actions are mutually exclusive so sharing a weight is safe.
- Two thin proxy methods on `UmbBlockEntryContext`
  (`requestTransferToLibrary()` / `requestDisconnectFromLibrary()`) mirroring
  the established `requestDelete()` pattern — actions consume only the entry
  context and call into the manager via these proxies.

Removals:
- The commented-out `#renderTransferToLibraryAction` /
  `#renderDisconnectFromLibraryAction` blocks and their handlers in
  `block-list-entry.element.ts` and `block-grid-entry.element.ts`.

* Renamed `sharedContentVariantStateOf` to `elementStateOf`

* TODO comments and prettify

* Removed `ensureContentResolved`

turns out it was redundant.

* Inlined `transferToLibrary` and `disconnectFromLibrary`

Both were single-use imperative helpers called only by their respective
`request*` counterparts in the same file, with no external callers. The
"request" / "do" split was speculative; folding them in reduces surface
area and matches the recent `ensureContentResolved` cleanup.

* Lifted library-allowed element-type fetch to base entries context

All four block variants (list, grid, rte, single) had the same six-line
block fetching the element-type uniques that overlap with the block
types. Moved into a protected helper `_getLibraryAllowedElementTypeKeys`
on UmbBlockEntriesContext so each variant just calls it.

* Removed `@property` decorator from `layout` setter

The setter had no matching getter, which Lit warns about (and will error
on in a future version) for reactive properties. Since no render template
reads `this.layout` and the setter's effects flow through the entry
context's own observables, the reactive tracking is unused — dropping the
decorator silences the warning without behaviour change.

Consumers using `.layout=${x}` in Lit templates are unaffected; that's
property assignment, not attribute reflection, and doesn't require the
property to be reactive.

* feat(components): adds `umb-entity-frame` component + Storybook stories

Cherry picked from PR https://github.com/umbraco/Umbraco-CMS/pull/22844

* Fixed block delete passing contentKey where layout key is required

`UmbBlockEntriesContext.delete()` was changed earlier on this branch to
take the layout `key` (so that multiple layouts referencing one shared
contentKey can be deleted independently). Two callers still passed
`contentKey`, which made `delete` throw "Cannot delete block, missing
layout for X" the moment a user tried to remove a block:

- `UmbBlockEntryContext.delete()` — fires on user delete from the UI.
- `block-workspace.context.ts` modal-rejected handler — fires when
  cancelling a brand-new block in live-editing mode.

Both now pass the layout key.

* Added `umb-entity-frame` to Block editor entry UI

Adds `--umb-color-reference` and `--umb-color-reference-contrast` CSS variables

* 🧹 Linting

* Block Single: derive `_exposed` from library element variant state

Aligns block-single-entry with block-list-entry and block-grid-entry:
library-element references now compute their unpublished/draft state
from the shared element's variant state instead of the (always-missing)
expose entry. Without this, inserted Library Elements always appeared
as Draft in single-block editors.

Also sets the `is-reference` attribute when the block is a library
reference, which activates the existing `:host([is-reference])` styles.

* Block entries: collapse `_isReferenceAttr` into `_isLibraryElement`

The two fields were always set together to the same value across all
three entry elements. `_isReferenceAttr` existed only because `@state`
doesn't reflect to an HTML attribute. Decorating the existing
`_isLibraryElement` field with `@property({ attribute: 'is-reference',
reflect: true })` covers both jobs — it reflects to the attribute (for
the existing `:host([is-reference])` CSS) and is still read from JS by
`#updateExposedState()`.

* Fix build errors after merges

* Refine block-catalogue-modal Library tab

- Convert _hasLibraryElements from @state() to native private field
  (set once in connectedCallback before first render; no reactivity needed)
- Promote inline .props object to #libraryTreeProps class field
  (stable reference avoids re-setting umb-tree props on every render)
- Remove self-documenting comment from #librarySelectableFilter
- Remove stale TODO comment

* Wire Library tab search in block-catalogue-modal

- Route tree selection through pickerContext.selection (unified path with
  search-result selections; removes direct writes to this.value from tree handlers)
- Observe pickerContext.selection.selection to drive this.value
- Observe pickerContext.search.query to hide tree while a search is active
- Configure selection as single-select (setMultiple(false))
- Pass selectionManager to tree props for visual selection state
- Add Umb.PickerSearchResultItem.Element manifest and element under
  src/packages/elements/picker/ so search results render correctly

* Backoffice: Simplify insertLibraryElement in block-manager.context

Library elements do not need an expose entry — exposure is derived from
the element's own variant state. Remove the redundant fetchLibraryElement
call and expose-setting logic; the layout observer already handles the
fetch automatically when the layout is appended.

* Backoffice: Rename transfer-to-library to transfer-to-element-library

* Sets the Entity Frame color for non-references

* "Transfer to Library" modal updates

Pre-populates the name field.

* Backoffice: Rename disconnect-from-library to disconnect-from-element-library

* Checks published visibility for Block entry items

+ markup tweaks

* Backoffice: Fix block showing as unsupported after Transfer to Element Library

After a transfer the manager assigns a new UUID (created.unique) to the
layout's contentKey. The entry context was not re-observing content for
the new key, leaving it permanently watching the old (now-gone) content.

Two interacting issues:

1. #observeContentData() was never re-called when layout.contentKey
   changed — only when the layout key itself changed or the manager
   first connected.  A new observer on this.contentKey now re-calls it
   on every contentKey change, with this.#contentKey synced first
   (because #observeLayout() assigns it AFTER _layout.setValue() emits,
   so downstream callbacks would otherwise read the stale value).

2. The guard 'if (unsupported !== true)' permanently locked the flag once
   it was set by the transient {content:undefined, isLibrary:false}
   emission during the transfer.  Replaced with #structurallyUnsupported
   — only set by #getContentStructure / #observeBlockType when the block
   type or element type is genuinely absent — so the content observer can
   freely reset the flag for all other transitions including transfer.

* Block Single: CSS selector fix

* Backoffice: Show link icon in block entry tabs for library elements

When a block is transferred to the Element Library (a shared element), add a
<uui-icon name="link"> to the entity-frame tab to make the library/shared
status visually clearer alongside the existing purple colour theme.

Applies to block-list, block-grid, and block-single entry components.
The icon is shown conditionally when _isLibraryElement is true.

* `requestTransferToElementLibrary` removed the `name` parameter

as can be retrieved from the context itself.

* fix(block): make block action href and validation data path reactive

`umb-block-action.element.ts` previously resolved `getHref()` and
`getValidationDataPath()` once in the `api` setter via `.then()`,
freezing the values for the lifetime of the action component. When a
block's `contentKey` changes at runtime (e.g. after disconnecting from
the Element Library), the edit button kept navigating to the stale path.

Add optional `hrefObservable` and `validationDataPathObservable` to
`UmbBlockAction`. When an action provides these observables the element
subscribes to them reactively; otherwise it falls back to the existing
one-shot promise path (non-breaking for third-party actions).

`UmbEditContentBlockAction` now observes `workspaceEditContentPath` and
`contentKey` from the block entry context and pushes updates into states,
resolving the stale-href bug on disconnect.

Resolves the [LK] TODO in block-action.element.ts.

* fix(block): refresh expose observer after disconnect from element library

After "Disconnect from Element Library" the block's layout.contentKey
changes from the shared element's UUID to a fresh local content key. The
expose observer ('observeExpose' in #gotVariantId) was bound to the old
key and never re-bound because #gotVariantId only runs when variantId
changes, not when contentKey changes — leaving _hasExpose false and the
block showing a stale "Draft"/unpublished badge.

Re-running #gotVariantId alongside #observeContentData in the contentKey
observer ensures the expose subscription always targets the current key,
mirroring the existing pattern already applied for the content observer.

* fix(block): correct workspace tabs and submit label for library elements

When a block references a Library Element (isSharedContent: true), opening
the block workspace via the "Edit Settings" action now shows only the
"Settings" tab. The "Content" tab is hidden because the content is owned
by the shared element and is not editable in the local block workspace.

Surfaces `hasContent` on the block workspace context, and gates
the Content workspace view on a new `Umb.Condition.BlockWorkspaceHasContent`
condition — mirroring the existing `Umb.Condition.BlockWorkspaceHasSettings`
pattern. Also removes the dead `TODO_conditions` block from the Content
view manifest.

* refactor(block): rename LibraryElement to SharedContent for naming consistency

Aligns block symbols that describe a block's content being shared/referenced
with the existing 'isSharedContent' layout flag and 'sharedContentVariantState',
retiring the inconsistent 'LibraryElement' naming for that concept.

- Entry state: isLibraryElement -> isSharedContent; #libraryElementWorkspacePath
  -> #sharedContentWorkspacePath; the three entry elements' _isLibraryElement
  -> _isSharedContent.
- Manager: insertLibraryElement -> insertSharedContent; #fetchLibraryElement
  -> #fetchSharedContent; #resolvedLibraryElements(Variants)
  -> #resolvedSharedContent(Variants).
- Condition: UmbBlockEntryIsLibraryElementCondition
  -> UmbBlockEntryHasSharedContentCondition (alias 'Umb.Condition.BlockEntryHasSharedContent').
- Route segment 'library-element' -> 'library'.

Genuine Element Library feature references are intentionally kept: the
transfer/disconnect actions and modals, and the catalogue picker UI
(#hasLibraryElements, #renderLibrary, blockEditor_tabLibrary, the
{ library: { elementKey } } modal value, libraryAllowedElementTypeKeys).

Pure rename, no behaviour change.

* fix(block): address PR review feedback on client-side files

- block-catalogue-modal: add UmbDeselectedEvent import; correctly type
  #onLibraryElementDeselected parameter (was UmbSelectedEvent)
- block-catalogue-modal: fix #librarySelectableFilter to handle
  undefined documentType.unique via nullish coalesce
- block-manager: setLayouts no longer mutates incoming layout objects
  in-place; uses map+spread to ensure backwards-compat key backfill
  without side effects on the caller's array
- block-grid-to-block-copy-translator: clipboard layout key now uses
  gridLayout.key (layout identity) rather than gridLayout.contentKey,
  which would break when the same shared-content element appears in
  multiple layout entries

* Fix low-hanging PR review comments

* Clarify why top-level aggregation works in effect

* Rename IsSharedContent (server-side)

* Rename IsSharedContent (client-side)

to `IsExternalContent`

* Added comments to clarify retries in tests

* Replace "isSharedContent" with "isExternalContent"

* Block: address review feedback — naming, comments, and small refactors

- requestTransferToElementLibrary / requestDisconnectFromElementLibrary → requestTransferToExternalContent / requestDisconnectFromExternalContent (manager + entry context + action callers)
- .addAdditionalPath('library') → 'element'
- #resolvedExternalContent / #resolvedExternalContentVariants → #externalContentValues / #externalContentVariants
- elementStateOf → externalContentStateOf
- hrefObservable / validationDataPathObservable → href / validationDataPath (interface + action impls + default kind element)
- _hasExpose → _localExpose (grid, list, single entry elements)
- BlockWorkspaceHasContentConditionConfig / BlockEntryHasSettingsConditionConfig: type alias → interface
- Remove implementation-specific / AI-ish comments from block-entry, block-manager, action files, block-workspace
- Reuse #elementRepository field in requestTransfer/Disconnect; remove local instantiations
- #fetchExternalContent now accepts an array — one call per layout-state update instead of N
- getHref / getValidationDataPath in edit-content/edit-settings actions now resolve via the observable

* Block: fix CI lint errors — remove unused #context fields and suppress empty-interface rule

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-06-23 15:35:35 +01:00
leekelleher f365493f0b Bump version to 19.0.0-beta1. 2026-06-01 18:08:32 +01:00
Andy Butland c10e23fd92 Merge branch 'v17/dev' 2026-06-01 14:41:53 +02:00
38d73b3a41 Developer Experience: Improve cohost editor polyfill to work with dotnet watch (closes #22773) (#22999)
* Improve cohost polyfill

* Apply suggestions from code review

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Move <target/> part of the polyfill to targets file.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-06-01 12:34:24 +00:00
6143b10643 E2E: QA Added acceptance tests for backoffice element search (#22884)
* Added tests

* Cleaned up

* Updated command

* Fixes based on comments

* Split tests

* Updated helpers

* Fixed constant helper after merge

* Use correct helper

* Added constant for element search

* Added ui helper for element backoffice search

* Added tests for element backoffice search

* Updated tests for finding element by name

* Apply suggestion from @andr317c

Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>

* Cleaned up

* Reverted npm command

* Fixed npm command

---------

Co-authored-by: Andreas Zerbst <andr317c@live.dk>
Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
2026-06-01 11:07:22 +00:00
577652f707 Backoffice: Strip inherited class comments from TypeDoc API docs (#23004)
* Backoffice: Strip inherited class comments from TypeDoc API docs

TypeDoc copies the nearest documented ancestor's class comment onto every
undocumented subclass, which meant every UmbLitElement descendant on
apidocs.umbraco.com showed "The base class for all Umbraco LitElement
elements." as its own description. This plugin clears class-level
comments whose sourcePath doesn't match the reflection's own file, so
classes with no JSDoc render blank instead of borrowing the base's text.
Inherited member comments (methods, properties) are left alone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Backoffice: Address review comments on TypeDoc strip-inherited plugin

Drop the misleading "strip trailing line/column" sentence — nothing actually
strips, and a future TypeDoc release that appends positions to sourcePath
would now self-document its breakage instead of being hidden by a comment.

Document the sources[0]-only limitation around declaration merging in the
docblock so the constraint is visible to future maintainers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:59:42 +02:00
Niels Lyngsø 5c0cb154f5 cherry picked #23027 2026-06-01 10:04:01 +02:00
Niels LyngsøandGitHub 0d73243b7c Property Editors: Add value summary extensions for collection views (#23027)
Squashed commit of the following:

commit 146b41889b
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed May 27 13:47:48 2026 +0200

    Delete package-lock.json

commit dd68594995
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed May 27 13:43:24 2026 +0200

    Simplify content-picker resolved item shape

commit 2bbbd40d9a
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed May 27 13:21:41 2026 +0200

    content picker value summary add tests & observable support

commit 6a8ee67f54
Author: engjlr <enl@umbraco.dk>
Date:   Wed May 27 12:45:58 2026 +0200

    Inline markdown editor value-type constant

commit 40bd631be5
Author: engjlr <enl@umbraco.dk>
Date:   Wed May 27 11:01:47 2026 +0200

    Remove unused import

commit d066ac4ce0
Author: engjlr <enl@umbraco.dk>
Date:   Wed May 27 10:40:10 2026 +0200

    Enable table text clipping; remove value-summary styles

commit 38a8c77c7b
Author: Mads Rasmussen <madsr@hey.com>
Date:   Tue May 26 17:00:00 2026 +0200

    Add user-picker value-summary tests and mock

commit 45bcf34186
Author: Mads Rasmussen <madsr@hey.com>
Date:   Tue May 26 16:44:49 2026 +0200

    Add tests for member-group value resolver

commit a9aad9cff0
Author: Mads Rasmussen <madsr@hey.com>
Date:   Tue May 26 16:20:37 2026 +0200

    Add member picker value-summary resolver tests

commit e39f3c15ba
Author: Mads Rasmussen <madsr@hey.com>
Date:   Tue May 26 15:00:36 2026 +0200

    Add media picker value summary resolver tests

commit 9694ac2870
Author: Mads Rasmussen <madsr@hey.com>
Date:   Tue May 26 14:58:54 2026 +0200

    Update value-summary.resolver.test.ts

commit 641d5f2817
Author: Mads Rasmussen <madsr@hey.com>
Date:   Tue May 26 14:37:23 2026 +0200

    add tests for the document picker value summary resolver

commit ac8fb96339
Author: Mads Rasmussen <madsr@hey.com>
Date:   Tue May 26 14:15:39 2026 +0200

    Use check icon for non-empty value summaries

    Replace the document icon with a check icon in value-summary components to indicate non-empty content. Updated the value-summary element in code-editor, markdown-editor, and tiptap-rte to return <uui-icon name='icon-check'> when a value is present, standardizing the visual cue across these editors.

commit 89499c6862
Author: Mads Rasmussen <madsr@hey.com>
Date:   Tue May 26 13:41:02 2026 +0200

    align member picker

commit 358c8a8629
Author: Mads Rasmussen <madsr@hey.com>
Date:   Tue May 26 13:38:56 2026 +0200

    combine resolver and element into one file to only lazy load one file

commit b759a348c0
Author: engjlr <enl@umbraco.dk>
Date:   Tue May 26 10:30:13 2026 +0200

    Add value-summary manifests to packages

commit 1c692a471b
Merge: d34361b78c fc261d1ce4
Author: engjlr <enl@umbraco.dk>
Date:   Tue May 26 10:29:31 2026 +0200

    Merge remote-tracking branch 'origin/main' into v17/feature/value-summary-property-editors

commit d34361b78c
Author: engjlr <enl@umbraco.dk>
Date:   Tue May 26 10:09:08 2026 +0200

    Update imports

commit 34847e952e
Merge: c194023069 09af8c044b
Author: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
Date:   Wed May 20 10:40:23 2026 +0200

    Merge branch 'main' into v17/feature/value-summary-property-editors

commit c194023069
Author: engjlr <enl@umbraco.dk>
Date:   Wed May 20 10:38:04 2026 +0200

    Centralize value summary truncation styles in umb-value-summary-extension

commit 3788be8266
Author: engjlr <enl@umbraco.dk>
Date:   Tue May 19 17:16:51 2026 +0200

    Use item models for resolvers instead of raw string IDs

commit d48fe020f3
Author: engjlr <enl@umbraco.dk>
Date:   Tue May 19 15:32:29 2026 +0200

    Update the visual of tags and block list

commit faf840b67f
Author: engjlr <enl@umbraco.dk>
Date:   Tue May 19 15:22:36 2026 +0200

    Render all the values in the checkbox list

commit 4818c6aef4
Author: engjlr <enl@umbraco.dk>
Date:   Tue May 19 14:36:30 2026 +0200

    Rename value summary element tags and classes to include property-editor

commit 97e4d3474b
Author: engjlr <enl@umbraco.dk>
Date:   Tue May 19 12:36:28 2026 +0200

    Remove undefined from UmbValueTypeMap declarations

commit de4683f6da
Author: engjlr <enl@umbraco.dk>
Date:   Tue May 19 11:44:16 2026 +0200

    Add value summary to element picker

commit d38af1da0c
Author: engjlr <enl@umbraco.dk>
Date:   Mon May 18 16:54:54 2026 +0200

    Guard against raw string value in member group picker value summary

commit f74b1a742a
Author: engjlr <enl@umbraco.dk>
Date:   Mon May 18 14:57:32 2026 +0200

    Export value type constants from package indexes

commit e0067845bf
Author: engjlr <enl@umbraco.dk>
Date:   Mon May 18 12:48:29 2026 +0200

    Fix imports

commit 320dd8fe6f
Author: engjlr <enl@umbraco.dk>
Date:   Mon May 18 11:59:20 2026 +0200

    Use relative repository imports in pickers

commit 9a16b774db
Author: engjlr <enl@umbraco.dk>
Date:   Mon May 18 11:41:10 2026 +0200

    Use DOM parsing for RTE value summary

commit 320bae917c
Merge: 728aedbe0b 18e27151b0
Author: engjlr <enl@umbraco.dk>
Date:   Mon May 18 11:35:12 2026 +0200

    Merge branch 'v17/feature/value-summary-property-editors' of https://github.com/umbraco/Umbraco-CMS into v17/feature/value-summary-property-editors

commit 728aedbe0b
Author: engjlr <enl@umbraco.dk>
Date:   Mon May 18 11:35:09 2026 +0200

    Truncate the fallback element

commit 18e27151b0
Merge: d3c62629d3 4b82828a23
Author: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
Date:   Mon May 18 11:25:04 2026 +0200

    Merge branch 'main' into v17/feature/value-summary-property-editors

commit d3c62629d3
Author: engjlr <enl@umbraco.dk>
Date:   Mon May 18 11:09:21 2026 +0200

    Add checkbox-list value summary; truncate labels

commit c593ed2cff
Author: engjlr <enl@umbraco.dk>
Date:   Fri May 15 13:11:16 2026 +0200

    Add valueSummary components for editors

commit 87043d6d2a
Author: engjlr <enl@umbraco.dk>
Date:   Fri May 15 09:17:16 2026 +0200

    Add value summaries for user and member-group pickers

commit 9f1838c581
Author: engjlr <enl@umbraco.dk>
Date:   Wed May 13 12:16:36 2026 +0200

    Add value summaries for content/member/document picker

commit dc9ad61225
Author: engjlr <enl@umbraco.dk>
Date:   Wed May 13 10:37:20 2026 +0200

    Add value summaries for media picker and cropper

commit c97928ebf0
Author: engjlr <enl@umbraco.dk>
Date:   Tue May 12 11:44:41 2026 +0200

    Add value-summary support for several editors

commit ff2ae07a02
Author: engjlr <enl@umbraco.dk>
Date:   Mon May 11 15:59:10 2026 +0200

    Add value summary for multiple text string and tags

commit f7b3da2b7e
Author: engjlr <enl@umbraco.dk>
Date:   Mon May 11 15:14:48 2026 +0200

    Add value summary for Toggle and date/time editors
2026-06-01 10:00:37 +02:00
Andy Butland 84ad9a1443 Merge branch 'v17/dev' 2026-06-01 08:33:13 +02:00
4a621a13bc Performance: Parallelize independent boot API requests (server status/config + public extensions) (#23020)
* perf(core): parallelize independent boot API requests

UmbServerConnection.connect() awaited server status and configuration
sequentially even though they are independent reads; run them with
Promise.allSettled so both errors surface (the app cannot function
without either) while saving a round-trip.

During app startup, public (login) extension registration was awaited
before the auth flow; kick it off in parallel and await it only before
routing, where the login screen actually needs it.

Each serialized call costs a full management-API round-trip, which is
negligible locally but ~150 ms each on high-latency (e.g. Cloud) hosts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(core): only mark connection connected once both calls succeed

Move isConnected.setValue(true) out of #setStatus() into connect() after
the allSettled check, so the observable never reflects a partially
established connection when configuration fails but status succeeded.

Addresses review feedback on the parallelized connect().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 08:20:34 +02:00
Nhu DinhandGitHub 346a22aeca E2E: QA Added acceptance tests for audit log in element (#22972)
* Added constant variables for element audit trail message

* Added ui helper for history item of element

* Updated tests for audit lofg for element
2026-05-29 15:52:05 +07:00
5cd0f2278c Login: Removes @hey-api/openapi-ts from the login project (#22757)
* feat: removes @hey-api/openapi-ts from the login project

this is an ongoing project to be able to finally  merge 'login' into 'client'

* docs(login): update CLAUDE.md to reflect removal of @hey-api/openapi-ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 16:57:06 +01:00
bc2048573c Link Picker: Render picked content/media as non-interactive when their workspace URL can't be resolved (closes #22955) (#22964)
* Entity refs render readonly when their workspace URL can't be resolved.
Also fixes name on remove dialog.

* Apply read-only on the picked content ref only in the non-routable link picker

* Address PR feedback: simplify document item resolver guard in the link picker, document the implicit uui-card-media disabled dependency in input-media, and cover the disabled card state with a test.

* Drop out of date comments.

* Simplify updates.

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-05-28 13:23:48 +00:00
Andy ButlandandGitHub 424209ac06 References: Fix missing node name when content referenced by media or member (closes #22990) (#22965)
Fix missing node name when content referenced by media or member.
2026-05-28 13:34:25 +01:00
Andy Butland 5cf577bf57 Fix broken tiptap reference for storybook build. 2026-05-28 13:32:09 +02:00
ed4b207fe7 Backoffice: Render $index in block detail overlay label (closes #21154) (#22959)
* Render $index in block detail overlay label.

* Cache $index, resolve append sentinel, and cover with unit tests.

* refactor(block): use pipeline for index deduplication and clean up stale observer

* Rename function to remove the unnecessary umb prefix.

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-05-28 11:15:21 +00:00
Andy ButlandandGitHub 40e027d0ea Content Type Editor: Fix empty Design tab when opened in a modal workspace (closes #22855) (#22954)
* Fix empty content type Design tab when opened in a modal workspace.

* Addressed code review comments.

* Fixed failing E2E tests.
2026-05-28 09:39:11 +00:00
Andy Butland 70258066d4 Merge branch 'v17/dev' 2026-05-28 10:46:31 +02:00
Andy Butland 5cfbfe7cc3 Merge branch 'v17/dev' 2026-05-28 10:44:20 +02:00
Andy ButlandandGitHub a6f6bdf8bc Backoffice: Hide "Edit permissions" button in create modals from users without Settings section access (closes #22981) (#22984)
* Show the edit permissions for document type button only for users with settings access.

* Fix translation for message (the "Permissions" tab is not called "Structure").

* Addressed code review feedback.
2026-05-28 09:23:26 +01:00
Andy Butland 4b9c0eb667 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-05-28 09:56:39 +02:00
2581e3fdbc Code Quality: Fix CS0618 obsolete API warnings in Umbraco.Examine.Lucene project (#22978)
* Suppress CS0618 obsolete API warnings in Umbraco.Examine.Lucene

Each obsolete API in this project cannot be migrated to its
non-obsolete replacement without either a breaking public API
change or a change in runtime behaviour:

- LuceneIndex.CommitCount: obsolete with no replacement; retained
  in diagnostics metadata to preserve existing output
- IHostingEnvironment.MapPathContentRoot: the IHostEnvironment
  extension replacement resolves a different environment
  abstraction
- FileSystemDirectoryFactory base constructor: the non-obsolete
  overload alters Lucene directory configuration behaviour

Each warning is suppressed locally with an explanatory comment
rather than changed, preserving existing behaviour.

Fixes #15015

* Tightened up comments. Added obsoletion version on unversioned attributes.
Removed warning supressions and fixed constructors.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-28 09:54:55 +02:00
Jacob Overgaard 4f5985c4d0 build: fixed timeoutInMinutes which should be on the task-level and not job-level 2026-05-28 09:28:40 +02:00
Jacob Overgaard 7d63afe8d6 Merge branch 'release/18.0' 2026-05-28 08:56:53 +02:00
Jacob OvergaardandClaude Opus 4.7 45686c982e Backoffice: Drop redundant search manifest import from Storybook preview
`core/manifests.ts` already imports and spreads `core/search/manifests.ts`
into its aggregate (line 23 + 58), so importing `searchManifests`
separately in `.storybook/preview.js` and spreading it next to
`coreManifests` registered the same manifests twice. Remove the redundant
import and spread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:52:38 +02:00
Jacob OvergaardandClaude Opus 4.7 d97d508a48 Backoffice: Repoint Storybook preview imports at umbraco-package.ts
PR #22957 deleted every package's `manifests.ts` and consolidated the
exports into `umbraco-package.ts`, but `.storybook/preview.js` still
imported from the old paths. The result was a Vite resolve error during
`npm run build-storybook` (first failure: "Could not resolve
../src/packages/block/manifests from .storybook/preview.js").

37 import paths swapped from `…/<pkg>/manifests` to
`…/<pkg>/umbraco-package`. The two packages that still expose their
manifests via a standalone `manifests.ts` — `core` and `core/search` —
are left untouched.

Verified by `npm run build-storybook` — succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:49:58 +02:00
Jacob OvergaardandClaude Opus 4.7 db590b0724 Tiptap: Fix dead manifests.js import in the input-tiptap story
PR #22995 added `input-tiptap.stories.ts` with an import from
`'../../manifests.js'`, but PR #22957 (already on release/17.5.0) had
deleted that file and moved the `manifests` array into
`umbraco-package.ts`. The merge into release/17.5.0 didn't catch the dead
import, so Storybook 404s on the story load.

Point the import at the new home — `manifests` is still exported by name,
so this is a one-line path fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:49:07 +02:00
Andy ButlandandJacob Overgaard 6a4b792ff1 Fix StoryBook build failure following updates in #22957. 2026-05-28 08:34:03 +02:00
Jacob Overgaard 85e0169100 Merge branch 'release/18.0' 2026-05-28 08:28:32 +02:00
Jacob OvergaardandClaude Opus 4.7 0b5438935d Tiptap: Load enabled extensions in parallel and inline manifest APIs (#22995)
* Tiptap: Load enabled extensions in parallel and inline manifest APIs

Replace the for…of/await loop in umb-input-tiptap's #loadExtensions with
Promise.all over .map, so all enabled Tiptap extension APIs are fetched
in parallel. Configured-extension order in _extensions is preserved.

Inline the first-party Tiptap manifest API references: every
`api: () => import('./X.tiptap-api.js')` and the equivalent toolbar /
statusbar / kind references now use a static top-of-file import and
`api: ClassName`. The dynamic `await import('rich-text-essentials.tiptap-api.js')`
fallback in input-tiptap.element.ts is inlined for the same reason.

External (plugin-supplied) Tiptap extensions and the lazy modal/toolbar
UI element imports are unchanged.

Why: on Umbraco Cloud, opening a document workspace with a rich text
editor takes ~16 s uncached, of which ~14.6 s is a single serial
waterfall — 31 extension APIs fetched one after the other from a
for…of await loop, ~170 ms RTT stacked. Replacing the loop with
Promise.all collapses that to roughly one round-trip; eagerly bundling
the first-party manifests removes the dynamic chunk explosion that made
the waterfall so long in the first place. The toolbar APIs (~20 of them)
already load in a sub-100 ms parallel burst against the same server,
confirming HTTP/2 multiplexing handles bulk parallel requests fine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tiptap: Inline element references for toolbar/statusbar/modal/clipboard manifests

Extends the manifest-inlining pass to the remaining `element: () => import(...)`
and runtime API loader sites in the Tiptap package — toolbar/menu/action-button
kinds, the table & character-map & anchor modals, the colour-picker button, the
property-editor configuration UIs, both clipboard translators, the style-menu
kind, and the default toolbar API fallback in tiptap-toolbar.element.ts.

Result on the same Cloud test site (uncached, 17.5-rc):
  Tiptap chunk count: 71 → 4
  Total tiptap bytes: ~3.2 MB → ~3.1 MB (essentially unchanged)
  Phase 5 of the load — the serial extension chain — collapses to a single
  consolidated chunk fetch.

`input-tiptap.element.ts` and `property-editor-ui-tiptap.element.ts` are
intentionally not inlined into anything else: `<umb-input-tiptap>` is a public
element usable standalone (custom dashboards, workspace views), and the
property-editor shell loads via the property-editor UI loader. They remain
exported as their own modules.

CLAUDE.md updated to document the new convention for first-party Tiptap
extensions (direct class refs) and the carve-out for external plugin
extensions that may keep `() => import(...)` to ship their API code in a
separate chunk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tiptap: Move extension APIs and elements into a shared lazy boundary chunk

The previous PR collapsed ~70 Tiptap chunks into 3 by inlining first-party API
and element references directly into manifest files. That win came with a real
downside flagged in code review (lke / mra): the API/element implementation
bytes ended up in the manifest registration bundle, so every workspace —
including ones without an RTE — paid ~700 KB of Tiptap code on boot.

This commit keeps the chunk-coalescing win but restores the lazy boundary by
routing every first-party manifest's `api` / `element` reference through a
single shared bundle file `extensions/extension-apis.bundle.ts`. Each manifest
holds a dynamic-import thunk pointing at that one bundle, so:

- Rollup still emits a single chunk for all Tiptap extension code (no chunk
  explosion).
- The manifest registration bundle stays slim — it carries only metadata
  (alias / label / icon / group / kind / forExtensions) plus the thunks.
- The bundle is only fetched the first time `<umb-input-tiptap>` actually
  mounts.

Data-type configuration UIs (`extensions-configuration`,
`toolbar-configuration`, `statusbar-configuration`) read manifest metadata
via `umbExtensionsRegistry.byType(...)` only — they never call
`loadManifestApi` / `loadManifestElement`, so the data-type editor continues
to work without loading any Tiptap implementation code.

Property-editor UI elements (`tiptap-rte`, the three configuration UIs) also
revert to `() => import('./X.element.js')` so each loads on demand from its
own chunk rather than being inlined into the manifest bundle.

`umb-input-tiptap` no longer statically imports the Rich Text Essentials API;
it prepends the alias to the observed list instead, so essentials resolves
through the same lazy bundle as every other extension.

Added a test and stories file that mount `<umb-input-tiptap>` standalone (no
property-editor wrapper) to make the public usage pattern explicit.

Built and verified via `npm run build:for:cms`:
- `dist-cms/packages/tiptap/manifests.js`           48 KB  (eager at boot)
- `dist-cms/packages/tiptap/extension-apis.bundle-*.js` 84 KB  (lazy)
- `dist-cms/packages/tiptap/tiptap-toolbar-element-api-base-*.js` 654 KB
  (lazy dependency of the bundle)
- per-element property-editor UI chunks load on demand when settings open

`npm run check:circular`, `npm run compile`, `npx wtr src/packages/tiptap`
all pass.

Related to #21152, builds on #22995.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tiptap: Don't mount <umb-input-tiptap> in the standalone test

Mounting the element via fixture() spins up an UmbTiptapRteContext that
consumes UMB_SERVER_CONTEXT. In the unit-test runtime no server context
provider exists, so the context request stays pending. When @open-wc's
fixture tears down at end-of-file the request rejects with
"host disconnected" — surfaced as an unhandled promise rejection that
web-test-runner counts as a fatal runner error, exiting 1 even though every
individual test passed. The rejection happened to be in flight while a
block-grid clipboard test was active in CI, which is why the failure surfaced
there rather than in the tiptap test file itself.

Drop the manifest-registration assertion too — pulling the package-level
`manifests.ts` aggregator triggers a transitive 404 on the
`@umbraco-cms/backoffice/tiptap` importmap entry in the wtr environment.

The class-export + custom-element-registration checks are enough to prove
standalone exportability. The Storybook stories still cover the visual
end-to-end load path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:28:18 +02:00
Jacob Overgaard f1bc1db6ce Merge branch 'v17/dev' of https://github.com/umbraco/Umbraco-CMS into v17/dev 2026-05-28 08:27:15 +02:00
Jacob Overgaard c4d5b89fc5 Merge remote-tracking branch 'origin/release/17.5.0' into v17/dev 2026-05-28 08:27:04 +02:00
7597a8ad40 Sort Dialog: Show current language node names (closes #22872) (#22948)
* Display variant node name on sort children dialog.

* Preserve user sort order when patching variant names on culture change

Patch names in-place on the existing _tableItems rather than rebuilding
from _children, so a user's drag-sorted or column-ordered arrangement is
not silently reverted if the app culture changes while the modal is open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Refactor to reduce cyclomatic complexity of #resolveName method.

* Resolve sort dialog variant names and icons via item data resolvers

Replace the inlined variant-name logic in the content sort dialog with the
shared UmbItemDataResolver abstraction, and add UmbMediaItemDataResolver so
media items resolve their active-culture name and icon the same way documents
do. Each content sort entity action now supplies its resolver through manifest
meta, flowing into the modal via a new content-specific modal data type and a
base-action _getModalData() hook. This also removes the previously hard-coded
document icon in the dialog.

* Disable load more when page of items is being retrieved.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-05-28 05:50:16 +00:00
d28507e2e5 Migrations: Convert all sibling RTE blocks (closes #22979) (#22980)
* Fix migration of embedded block data when blocks are direct siblings in the 13 RTE source code.

* Apply suggestions from code review to update comments.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Address review: keep RteBlockHelper in original namespace; tidy docs and comment

- Move RteBlockHelper back to Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0.LocalLinks
  to avoid a binary breaking change within the obsolete window (scheduled removal in v18).
  Kept as its own file rather than reverting it into LocalLinkRteProcessor.cs.
- Add a <remarks> note on ConvertBlockUdisToKeys explaining that blocks with malformed UDIs
  are dropped rather than preserved.
- Replace the opaque "fix recursive hiccup" comment in LocalLinkRteProcessor with one that
  describes what the line actually does.
- Move RteBlockHelperTests back to mirror the production namespace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 07:45:01 +02:00
7b75324172 Migrations: Convert all sibling RTE blocks (closes #22979) (#22980)
* Fix migration of embedded block data when blocks are direct siblings in the 13 RTE source code.

* Apply suggestions from code review to update comments.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Address review: keep RteBlockHelper in original namespace; tidy docs and comment

- Move RteBlockHelper back to Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0.LocalLinks
  to avoid a binary breaking change within the obsolete window (scheduled removal in v18).
  Kept as its own file rather than reverting it into LocalLinkRteProcessor.cs.
- Add a <remarks> note on ConvertBlockUdisToKeys explaining that blocks with malformed UDIs
  are dropped rather than preserved.
- Replace the opaque "fix recursive hiccup" comment in LocalLinkRteProcessor with one that
  describes what the line actually does.
- Move RteBlockHelperTests back to mirror the production namespace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 07:38:56 +02:00
172a3be5ac Repositories: Batch WHERE IN queries to avoid SQL Server 2100-parameter limit (#22987)
* Batch WHERE IN queries to avoid SQL Server 2100-parameter limit and add memory files.

* Drop past-incident references from SQL parameter-limit docs

The memory files should describe the current rule and safe patterns;
specific historical bugs belong in commit history, not CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update comments from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Addressed memory file feedback.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 10:44:15 +09:00
ca28195b7c Tiptap: Load enabled extensions in parallel and inline manifest APIs (#22995)
* Tiptap: Load enabled extensions in parallel and inline manifest APIs

Replace the for…of/await loop in umb-input-tiptap's #loadExtensions with
Promise.all over .map, so all enabled Tiptap extension APIs are fetched
in parallel. Configured-extension order in _extensions is preserved.

Inline the first-party Tiptap manifest API references: every
`api: () => import('./X.tiptap-api.js')` and the equivalent toolbar /
statusbar / kind references now use a static top-of-file import and
`api: ClassName`. The dynamic `await import('rich-text-essentials.tiptap-api.js')`
fallback in input-tiptap.element.ts is inlined for the same reason.

External (plugin-supplied) Tiptap extensions and the lazy modal/toolbar
UI element imports are unchanged.

Why: on Umbraco Cloud, opening a document workspace with a rich text
editor takes ~16 s uncached, of which ~14.6 s is a single serial
waterfall — 31 extension APIs fetched one after the other from a
for…of await loop, ~170 ms RTT stacked. Replacing the loop with
Promise.all collapses that to roughly one round-trip; eagerly bundling
the first-party manifests removes the dynamic chunk explosion that made
the waterfall so long in the first place. The toolbar APIs (~20 of them)
already load in a sub-100 ms parallel burst against the same server,
confirming HTTP/2 multiplexing handles bulk parallel requests fine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tiptap: Inline element references for toolbar/statusbar/modal/clipboard manifests

Extends the manifest-inlining pass to the remaining `element: () => import(...)`
and runtime API loader sites in the Tiptap package — toolbar/menu/action-button
kinds, the table & character-map & anchor modals, the colour-picker button, the
property-editor configuration UIs, both clipboard translators, the style-menu
kind, and the default toolbar API fallback in tiptap-toolbar.element.ts.

Result on the same Cloud test site (uncached, 17.5-rc):
  Tiptap chunk count: 71 → 4
  Total tiptap bytes: ~3.2 MB → ~3.1 MB (essentially unchanged)
  Phase 5 of the load — the serial extension chain — collapses to a single
  consolidated chunk fetch.

`input-tiptap.element.ts` and `property-editor-ui-tiptap.element.ts` are
intentionally not inlined into anything else: `<umb-input-tiptap>` is a public
element usable standalone (custom dashboards, workspace views), and the
property-editor shell loads via the property-editor UI loader. They remain
exported as their own modules.

CLAUDE.md updated to document the new convention for first-party Tiptap
extensions (direct class refs) and the carve-out for external plugin
extensions that may keep `() => import(...)` to ship their API code in a
separate chunk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tiptap: Move extension APIs and elements into a shared lazy boundary chunk

The previous PR collapsed ~70 Tiptap chunks into 3 by inlining first-party API
and element references directly into manifest files. That win came with a real
downside flagged in code review (lke / mra): the API/element implementation
bytes ended up in the manifest registration bundle, so every workspace —
including ones without an RTE — paid ~700 KB of Tiptap code on boot.

This commit keeps the chunk-coalescing win but restores the lazy boundary by
routing every first-party manifest's `api` / `element` reference through a
single shared bundle file `extensions/extension-apis.bundle.ts`. Each manifest
holds a dynamic-import thunk pointing at that one bundle, so:

- Rollup still emits a single chunk for all Tiptap extension code (no chunk
  explosion).
- The manifest registration bundle stays slim — it carries only metadata
  (alias / label / icon / group / kind / forExtensions) plus the thunks.
- The bundle is only fetched the first time `<umb-input-tiptap>` actually
  mounts.

Data-type configuration UIs (`extensions-configuration`,
`toolbar-configuration`, `statusbar-configuration`) read manifest metadata
via `umbExtensionsRegistry.byType(...)` only — they never call
`loadManifestApi` / `loadManifestElement`, so the data-type editor continues
to work without loading any Tiptap implementation code.

Property-editor UI elements (`tiptap-rte`, the three configuration UIs) also
revert to `() => import('./X.element.js')` so each loads on demand from its
own chunk rather than being inlined into the manifest bundle.

`umb-input-tiptap` no longer statically imports the Rich Text Essentials API;
it prepends the alias to the observed list instead, so essentials resolves
through the same lazy bundle as every other extension.

Added a test and stories file that mount `<umb-input-tiptap>` standalone (no
property-editor wrapper) to make the public usage pattern explicit.

Built and verified via `npm run build:for:cms`:
- `dist-cms/packages/tiptap/manifests.js`           48 KB  (eager at boot)
- `dist-cms/packages/tiptap/extension-apis.bundle-*.js` 84 KB  (lazy)
- `dist-cms/packages/tiptap/tiptap-toolbar-element-api-base-*.js` 654 KB
  (lazy dependency of the bundle)
- per-element property-editor UI chunks load on demand when settings open

`npm run check:circular`, `npm run compile`, `npx wtr src/packages/tiptap`
all pass.

Related to #21152, builds on #22995.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tiptap: Don't mount <umb-input-tiptap> in the standalone test

Mounting the element via fixture() spins up an UmbTiptapRteContext that
consumes UMB_SERVER_CONTEXT. In the unit-test runtime no server context
provider exists, so the context request stays pending. When @open-wc's
fixture tears down at end-of-file the request rejects with
"host disconnected" — surfaced as an unhandled promise rejection that
web-test-runner counts as a fatal runner error, exiting 1 even though every
individual test passed. The rejection happened to be in flight while a
block-grid clipboard test was active in CI, which is why the failure surfaced
there rather than in the tiptap test file itself.

Drop the manifest-registration assertion too — pulling the package-level
`manifests.ts` aggregator triggers a transitive 404 on the
`@umbraco-cms/backoffice/tiptap` importmap entry in the wtr environment.

The class-export + custom-element-registration checks are enough to prove
standalone exportability. The Storybook stories still cover the visual
end-to-end load path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:32:20 +00:00
Jacob Overgaard 4520bb6e91 Merge branch 'release/18.0' 2026-05-27 14:33:46 +02:00
Mads RasmussenandJacob Overgaard 18c559a3bb Backoffice: Embed implementations directly in core manifests to reduce startup network requests (#22944)
* Use direct imports in core manifests

* Extract theme aliases into constants file

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-05-27 14:32:57 +02:00
Mads RasmussenandJacob Overgaard 893bb61d0d Backoffice: Embed package root manifests into umbraco-package.ts to reduce startup requests (#22957)
* Consolidate block package into index export

* keep umbraco-package.ts and embed manifests instead

* Inline package manifests into umbraco-package

* update docs
2026-05-27 14:31:36 +02:00
Mads RasmussenandJacob Overgaard 5b800deb3c Backoffice: Swap relative imports to @umbraco-cms/backoffice module imports in core packages (#22942)
Use @umbraco-cms/backoffice imports

Replace numerous relative/internal import paths with centralized '@umbraco-cms/backoffice' package entry points across core modules.This consolidates exports, simplifies import paths.
2026-05-27 14:31:29 +02:00
Jacob Overgaard 5a5902e1d4 Merge remote-tracking branch 'origin/v17/dev' 2026-05-27 14:28:44 +02:00
Jacob Overgaard 4c1fde9e0c Merge branch 'release/17.5.0' into v17/dev 2026-05-27 14:28:03 +02:00
Mads RasmussenandJacob Overgaard f0013330e6 Backoffice: Embed package root manifests into umbraco-package.ts to reduce startup requests (#22957)
* Consolidate block package into index export

* keep umbraco-package.ts and embed manifests instead

* Inline package manifests into umbraco-package

* update docs
2026-05-27 14:26:56 +02:00
Mads RasmussenandJacob Overgaard bd2c985187 Backoffice: Embed implementations directly in core manifests to reduce startup network requests (#22944)
* Use direct imports in core manifests

* Extract theme aliases into constants file

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-05-27 14:26:34 +02:00
Mads RasmussenandJacob Overgaard 9711a5d012 Backoffice: Swap relative imports to @umbraco-cms/backoffice module imports in core packages (#22942)
Use @umbraco-cms/backoffice imports

Replace numerous relative/internal import paths with centralized '@umbraco-cms/backoffice' package entry points across core modules.This consolidates exports, simplifies import paths.
2026-05-27 14:24:57 +02:00
Engiber Lozadaandleekelleher 9cf3a7e296 Content Editor: Fix workspace footer breadcrumb overflow hiding (closes #20132) (#22323)
* Allow the breadcrumbs to collapse in the workspace view

* Remove redundant styles

(cherry picked from commit 52cccafa86)
2026-05-27 11:56:06 +01:00
Engiber LozadaandGitHub 52cccafa86 Content Editor: Fix workspace footer breadcrumb overflow hiding (closes #20132) (#22323)
* Allow the breadcrumbs to collapse in the workspace view

* Remove redundant styles
2026-05-27 10:52:49 +00:00
Jacob Overgaard c2416429b7 Merge remote-tracking branch 'origin/v17/dev' 2026-05-27 09:45:15 +02:00
Andreas Zerbst 2fa7067803 Fixed required value 2026-05-27 09:27:13 +02:00
Andy ButlandandGitHub 808cba2747 Members: Default Approved to true when creating a member (closes #22991) (#22993)
Default new members created via the backoffice to approved.
2026-05-27 06:59:24 +00:00
mole b0a825e6c0 Fix test filters 2026-05-26 12:31:44 +02:00
Mads Rasmussen da0117f240 Merge branch 'v17/dev' of https://github.com/umbraco/Umbraco-CMS into v17/dev 2026-05-26 09:50:48 +02:00
mole fc261d1ce4 Fix intergration tests 2026-05-26 09:40:12 +02:00
Jacob Overgaard 31944675c0 Merge remote-tracking branch 'origin/v17/dev' 2026-05-26 08:33:28 +02:00
Jacob OvergaardandGitHub 61d3e4c53d Backoffice: Add Cache-Control headers to cache-busted backoffice assets (AB#68478) (#22951)
* Backoffice: Add Cache-Control headers to cache-busted backoffice assets (AB#68478)

Adds a UseUmbracoBackOfficeCacheHeaders middleware that sets
Cache-Control: public, max-age=31536000, immutable on responses served
from the cache-busted backoffice path (/umbraco/backoffice/<hash>/*).
The hash in the URL is derived from the Umbraco version, so the URL
itself invalidates on every release - making 'immutable' safe regardless
of whether individual filenames contain a content hash.

In debug mode the cache-bust hash changes per request, so the header is
set to 'no-cache' to avoid filling the browser disk cache with single-use
entries.

Design is non-destructive to consumer customisation, addressing the
review feedback on the v14 attempt (#14475):

- Does not touch StaticFileOptions; consumer
  services.Configure<StaticFileOptions>(...) and OnPrepareResponse
  callbacks continue to work unchanged.
- Sets the header via Response.OnStarting with a ContainsKey guard, so
  any synchronous Cache-Control set upstream wins; consumer OnStarting
  callbacks registered later fire first (LIFO) and also win.
- Skips non-2xx responses to avoid long-lived caching of error responses.

Related: GH #21152, PR #22896.

* Backoffice: Correct rationale for no-cache in debug mode

Reword the XML doc on UseUmbracoBackOfficeCacheHeaders to reflect that
IBackOfficePathGenerator is a singleton, so the cache-bust hash is
computed once at startup even in debug mode (per Copilot review on
#22951). The reason for no-cache is not "hash changes per request" but
that built assets may change in place during dev iteration; no-cache
allows fast 304 revalidation while no-store would force full
re-downloads.

No functional change.

* Backoffice: Add unit tests for UseUmbracoBackOfficeCacheHeaders

Covers six scenarios via a minimal in-process pipeline composed with
Microsoft.AspNetCore.TestHost:

- Production: 200 under hash prefix gets immutable header
- Debug: 200 under hash prefix gets no-cache
- Non-2xx under prefix: header not set (status gate)
- Path outside prefix: header not set (path gate)
- Consumer synchronous override: ContainsKey guard skips, consumer wins
- Consumer OnStarting override: LIFO ordering lets consumer win

Adds Microsoft.AspNetCore.TestHost to Umbraco.Tests.UnitTests (standard
Microsoft package, version pinned in tests/Directory.Packages.props).

* Backoffice: Extract cache-headers logic into IMiddleware class

Matches the existing Umbraco middleware convention (BootFailedMiddleware,
PreviewAuthenticationMiddleware, UmbracoRequestMiddleware, etc.) per
Kenn's note: prefer UseMiddleware<T>() with a DI-resolved class over
inline builder.Use lambdas.

The new UmbracoBackOfficeCacheHeadersMiddleware:
- Implements IMiddleware; registered as a singleton in AddWebComponents
- Computes prefix and header value once in the constructor (both
  dependencies are singletons themselves, so this is stable)
- Behaviour is unchanged from the inline version

The UseUmbracoBackOfficeCacheHeaders extension method becomes a thin
UseMiddleware<T>() wrapper. Tests updated to register the middleware in
the TestServer DI container so it can be resolved through UseMiddleware.

* Backoffice: Document IMiddleware convention in Web.Common CLAUDE.md

Adds an explicit "Convention" note before the middleware list so future
contributors (and AI assistants) default to the IMiddleware class +
AddSingleton + UseMiddleware<T>() pattern rather than inline
builder.Use(async ...) lambdas. Also lists the new
UmbracoBackOfficeCacheHeadersMiddleware in the folder structure and
middleware reference.

* Backoffice: Tighten middleware convention note with full corroboration

Lists every IMiddleware implementer in the codebase (10/10) and calls
out the two known inline-lambda exceptions (CspNonceExtensions,
WebApplicationExtensions) so the rule reads as the established
convention rather than an absolute, while still steering new work
toward IMiddleware + AddSingleton + UseMiddleware<T>().

* Backoffice: Register cache-headers middleware in AddBackOfficeCore

DI scope validation runs in Development/CI and pre-checks every
singleton's dependency graph can be constructed. The middleware was
registered in AddWebComponents (which runs for every Umbraco bootstrap),
but its IBackOfficePathGenerator dependency is only registered by
AddBackOffice(). The previous CI run on this branch surfaced the
problem in four Delivery-only/Website-only bootstrap tests
(CoreWithDeliveryApi_BootsSuccessfully, DeliveryOnlyScenario_BootsSuccessfully,
etc.) with "Unable to resolve service for type 'IBackOfficePathGenerator'
while attempting to activate 'UmbracoBackOfficeCacheHeadersMiddleware'".

Move the registration alongside IBackOfficePathGenerator in
AddBackOfficeCore (Api.Management), which is the same scope as the
backoffice itself. This also matches the wire-up gate in
UmbracoApplicationBuilder.cs that only calls UseUmbracoBackOfficeCacheHeaders
when IBackOfficeEnabledMarker is registered.

CLAUDE.md updated with the rule ("register the middleware next to its
dependencies' registration") and a pitfall note about DI scope validation.

* Backoffice: Address review feedback from AndyButland (PR #22951)

- Move UseUmbracoBackOfficeCacheHeadersTests from Umbraco.Tests.UnitTests
  to Umbraco.Tests.Integration. It uses HostBuilder + TestServer to
  exercise the real HTTP pipeline, which is integration-shaped rather
  than unit-shaped. Drop Microsoft.AspNetCore.TestHost from UnitTests
  (Mvc.Testing in Integration provides it transitively) and from
  tests/Directory.Packages.props.
- Soften the misleading "no trailing slash" comment in
  UmbracoBackOfficeCacheHeadersMiddleware — we trim anyway, so the
  comment is now framed as defensive normalisation.
- Trim the dense middleware convention note in Web.Common/CLAUDE.md to
  one paragraph (rule + the two known inline-lambda exceptions). Move
  the DI-scope-validation pitfall narrative out of CLAUDE.md and into a
  three-line code comment next to the AddSingleton call in
  AddBackOfficeCore where it actually applies.

* Backoffice: HTTP verb gate, 304 inclusion, namespace + unused using (PR #22951 review)

Three more from AndyButland's review:

1. Verb gate + 304 inclusion in UmbracoBackOfficeCacheHeadersMiddleware.
   Restrict the path-prefix match to GET and HEAD so POST/PUT/DELETE
   responses and OPTIONS (CORS preflight) responses don't get tagged as
   immutable. Include 304 alongside 2xx in the status gate so
   intermediate caches (CDN/proxy) receive the Cache-Control directive on
   revalidation responses too. Extended the test suite with four new
   cases: NotModifiedResponseUnderPrefix_SetsImmutable,
   HeadRequestUnderPrefix_SetsImmutable,
   OptionsRequestUnderPrefix_DoesNotSetHeader,
   PostRequestUnderPrefix_DoesNotSetHeader. All 10 tests pass.
2. Test namespace updated to Umbraco.Cms.Tests.Integration.* to match
   the convention used by ~629 other files in Umbraco.Tests.Integration
   (vs the 2 outliers I copied from).
3. Drop unused 'using Umbraco.Extensions;' from the test file.

* Backoffice: Extract conditional checks to satisfy CodeScene complexity gate

CodeScene flagged InvokeAsync with "Complex Conditional" (advisory rule,
code health impact 9.69) after the verb + 304 additions in the prior
commit. Extract the two checks into IsCacheableAssetRequest and
ShouldSetCacheControl helper methods. No behaviour change; tests still
green (10/10, 149 ms).
2026-05-26 08:31:10 +02:00
Andy Butland 5a28f6e0f1 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-05-26 06:33:52 +02:00
Zeegaan 6e2ba699ee Merge remote-tracking branch 'origin/v17/dev' 2026-05-26 12:22:28 +09:00
51d70877d1 QA: Stabilise rollback content versioning E2E test (#22975)
* Stabilise rollback E2E test by waiting for document reload before asserting.

* Condense rollback wait comment per code-review feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Addressed code review feedback.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:21:03 +09:00
Andy ButlandandGitHub 2dcfe68208 Backoffice search: Preserve Lucene score order through content-picker lookups (closes #22862) (#22977)
* Preserve Lucene score order through content-picker lookups.

* Removed unnecessary tests.  Addressed code review comments.
2026-05-26 12:15:21 +09:00
ce06c4ba4d Management API: ensure the order from the search endpoints taking a collection of keys is preserved (#22973)
* ensure the order from the search endpoints taking a collection of keys is preserved

* Align cosmetic changes to ensure later merge up doesn't run into conflicts.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-25 17:32:17 +00:00
Andy Butland c2b6210137 Merge branch 'v17/dev' 2026-05-25 10:21:31 +02:00
Andy Butland a91de6e677 Fix StoryBook build failure following updates in #22957. 2026-05-25 10:20:45 +02:00
Andy Butland f7a45dc9d6 Merge branch 'v17/dev' 2026-05-25 10:13:58 +02:00
faf3824a0a Backoffice: Embed implementations directly in core manifests to reduce startup network requests (#22944)
* Use direct imports in core manifests

* Extract theme aliases into constants file

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-05-25 10:07:24 +02:00
9cd2e6ecd2 Management API: ensure the order from the search endpoints taking a collection of keys is preserved (#22920)
* update order search result for element, member type, dictionary...

* undo dictionary search API

* reorder search value

* Apply OrderByRequestedIds

* add unit tests for search order

* Reverted unnecessarily changed files, minor test clean-up, aligned controllers for XML docs.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-25 07:38:49 +00:00
Andy Butland c64c6cfd92 Merge branch 'v17/dev' 2026-05-25 08:35:28 +02:00
Andy Butland ca30a7604e Merge branch 'release/18.0' 2026-05-25 08:35:09 +02:00
Ronald BarendseandAndy Butland 3d430f7f83 Background Jobs: Refine RecurringBackgroundJobBase API (#22966)
* Add IgnoredDelayChanged event to allow updates during back-off

* Make Period and IgnoredDelay settable on RecurringBackgroundJobBase with auto-raising events

* Address PR review: handle CTS race, restore negative-IgnoredDelay guard, clarify setter remarks

- Swallow ObjectDisposedException in OnIgnoredDelayChanged for the shutdown race where an in-flight handler reads the to-be-disposed CTS via Interlocked.Exchange before Dispose disposes it.
- Restore "skip back-off when IgnoredDelay <= TimeSpan.Zero (and not Timeout.InfiniteTimeSpan)" guard in IgnoreAndWaitAsync to defend against direct IRecurringBackgroundJob implementations / property overrides returning a negative value that would otherwise tight-loop via ComputeNextDelay clamping to zero.
- Add regression test for the negative-IgnoredDelay skip path.
- Mirror the constructor "stored without raising" remark on the Period and IgnoredDelay setter doc comments.

* Dispose newly-installed CTS when shutdown race wins the rotate-and-cancel

* Clarify XML docs.

* Introduce helper for cancellation source rotate and cancel.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-25 08:02:14 +02:00
Andy Butland 58ed9899be Merge branch 'release/17.5.0' into v17/dev 2026-05-25 08:01:04 +02:00
Engiber LozadaandGitHub bc7bd9a32a Body Layout: Replace overflow: auto with uui-scroll-container (#22950)
* replace overflow: auto with uui-scroll-container in layout components

* Remove stale comment
2026-05-25 07:57:22 +02:00
8160ede4b6 Background Jobs: Refine RecurringBackgroundJobBase API (#22966)
* Add IgnoredDelayChanged event to allow updates during back-off

* Make Period and IgnoredDelay settable on RecurringBackgroundJobBase with auto-raising events

* Address PR review: handle CTS race, restore negative-IgnoredDelay guard, clarify setter remarks

- Swallow ObjectDisposedException in OnIgnoredDelayChanged for the shutdown race where an in-flight handler reads the to-be-disposed CTS via Interlocked.Exchange before Dispose disposes it.
- Restore "skip back-off when IgnoredDelay <= TimeSpan.Zero (and not Timeout.InfiniteTimeSpan)" guard in IgnoreAndWaitAsync to defend against direct IRecurringBackgroundJob implementations / property overrides returning a negative value that would otherwise tight-loop via ComputeNextDelay clamping to zero.
- Add regression test for the negative-IgnoredDelay skip path.
- Mirror the constructor "stored without raising" remark on the Period and IgnoredDelay setter doc comments.

* Dispose newly-installed CTS when shutdown race wins the rotate-and-cancel

* Clarify XML docs.

* Introduce helper for cancellation source rotate and cancel.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-25 05:42:41 +00:00
Jacob Overgaard d2e32d6fcb Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-05-23 10:11:21 +02:00
Jacob Overgaard bec333e37b Merge remote-tracking branch 'origin/v17/dev' 2026-05-23 10:11:10 +02:00
Mads RasmussenandGitHub e06a583f1a Backoffice: Embed package root manifests into umbraco-package.ts to reduce startup requests (#22957)
* Consolidate block package into index export

* keep umbraco-package.ts and embed manifests instead

* Inline package manifests into umbraco-package

* update docs
2026-05-23 10:09:03 +02:00
Andy Butland 61cc37ad1d Revert "Entity refs render readonly when their workspace URL can't be resolved."
This reverts commit dc7b34eb58.
2026-05-23 09:42:42 +02:00
Andy Butland dc7b34eb58 Entity refs render readonly when their workspace URL can't be resolved.
Also fixes name on remove dialog.
2026-05-23 09:35:42 +02:00
Andy Butland dfe98ffa3b Disable failing link to selected content from link picker launched from RTE.
Also fixes name on remove dialog.
2026-05-23 09:16:47 +02:00
Andreas Lykke BorgandAndy Butland 6b8e8935fd Accessibility: Added missing labels to webhook details and headers (#22918)
* Added missing labels to webhoot details and headers

* Changed toggle label to aria-label to remove visible text
2026-05-22 19:04:41 +02:00
Andreas Lykke BorgandGitHub 4344fe9060 Accessibility: Added missing labels to webhook details and headers (#22918)
* Added missing labels to webhoot details and headers

* Changed toggle label to aria-label to remove visible text
2026-05-22 19:02:10 +02:00
Andy ButlandandSven Geusens 53c74efd35 Migrations: Append data-anchor value to href when missing in local link migration (closes #22860) (#22936)
* Support anchor fragments that are included in the data attribute but missing in the href when migrating local links.

* Addressed code review feedback.
2026-05-22 12:04:12 +02:00
Jacob Overgaard 485257a949 Merge branch 'v17/dev' 2026-05-22 11:23:07 +02:00
Jacob Overgaard 1c058a32d9 Merge branch 'release/17.5.0' into v17/dev 2026-05-22 11:22:13 +02:00
Andy ButlandandGitHub 12f838277c Migrations: Append data-anchor value to href when missing in local link migration (closes #22860) (#22936)
* Support anchor fragments that are included in the data attribute but missing in the href when migrating local links.

* Addressed code review feedback.
2026-05-22 11:20:05 +02:00
Jacob OvergaardandClaude Opus 4.7 1ae2a780dd Workspace Actions: Restore waiting state for buttons with additional options (closes #18670, #20593) (#22554)
* Workspace Actions: Restore waiting state for buttons with additional options

The waiting state was suppressed whenever a workspace action reported
hasAdditionalOptions() (e.g. Save and publish on multi-variant sites),
so users saw the button jump straight from idle to the success tick
with no in-flight feedback.

Always set 'waiting' on click (unless the action is a link). The
variant-picker modal still opens on top of the button, so the spinner
is effectively invisible during selection — but it becomes visible
as soon as the modal closes and the publish request is in flight.

Fixes #22551

* Workspace Actions: Spin button only while real work is in flight

Replace the eager always-set-waiting behaviour from the previous commit
with an opt-in `isPending` signal so the spinner appears only while
actual work (validation + HTTP) is happening - not while the variant
picker modal is open, and never as a spurious success tick when the
user cancels the modal.

Changes:

- Add optional `isPending: Observable<boolean>` to UmbWorkspaceAction
  and a default UmbBooleanState + protected setPending() on the base
  class. Optional + backwards compatible for external implementers.

- Add optional `onActionStarting` callback (via a shared
  UmbWorkspaceActionExecutionOptions type) to
  UmbPublishableWorkspaceContext.saveAndPublish and
  UmbSaveableWorkspaceContext.requestSave. The document publishing
  context and content detail workspace base invoke the callback at the
  join point right after the variant picker resolves (or is skipped
  for the single-variant case), so it never fires when the modal is
  cancelled.

- Wire the document save and save-and-publish actions to clear pending
  at the start of execute() and pass an onActionStarting callback that
  flips it true when work begins.

- Update the workspace action element to observe api.isPending: when
  the observable is present the waiting state is driven by the
  observable (and the success tick is suppressed if the action
  resolves without ever signalling pending - i.e. a cancellation).
  When the observable is absent the element falls back to the legacy
  eager-waiting behaviour. Failures always surface the failed tick.

Fixes #22551

* Reduce cyclomatic complexity of #onClick and _handleSave

CodeScene Code Health Review flagged two complexity issues:

- UmbWorkspaceActionElement.#onClick reached cyclomatic complexity 9
  (threshold is < 9). Extracted the api-execution branch into a new
  private #runApiAction helper so #onClick collapses to a simple
  link-vs-action dispatch.
- _handleSave was already over the threshold (14); my optional-chain
  callback invocation pushed it to 16. Moved the
  `executionOptions?.onActionStarting?.()` call into a #notifyActionStarting
  helper so the call site is a plain method call and contributes zero
  cyclomatic complexity to _handleSave.

No behavioural change.

* Reduce cyclomatic complexity of #handleSaveAndPublish

Same fix as the previous commit's #notifyActionStarting extraction in
content-detail-workspace-base: move the optional-chain callback
invocation into a private helper so #handleSaveAndPublish stays at its
pre-PR cyclomatic complexity (15) instead of degrading to 17.

No behavioural change.

* DRY: extract notifyWorkspaceActionStarting into a shared utility

Both UmbDocumentPublishingWorkspaceContext.#handleSaveAndPublish and
UmbContentDetailWorkspaceContextBase._handleSave had identical private
optional-chain callback off the host method's cyclomatic complexity.

Replace both with a single exported notifyWorkspaceActionStarting()
utility co-located with UmbWorkspaceActionExecutionOptions. This:

- Removes a duplication point between the two contexts.
- Gives future workspace context implementations a ready-made way to
  honour the optional callback without re-inventing the helper or
  paying the cyclomatic-complexity cost at the call site.

No behavioural change.

* Rename isPending -> isExecuting to mirror the execute() method

Niels suggested correlating the observable's name with the action's
`execute()` method, so the symbol set is now:

- isExecuting (observable on UmbWorkspaceAction interface)
- _isExecuting / setExecuting (UmbWorkspaceActionBase)
- #observeIsExecuting / #executionStarted (workspace-action element)
- isExecutingObserver (observer alias)

Pure rename; no behavioural change.

* Address Copilot review: lazy isExecuting, observer scope, finally reset

Five Copilot findings on PR #22554. Three real regressions + two
contract violations, all addressed:

1. UmbWorkspaceActionBase always exposing `isExecuting` made every
   existing subclass appear to opt in to the new modal-aware flow,
   suppressing waiting/success states for actions that never call
   setExecuting(true). Made `_isExecuting`/`isExecuting` lazy: only
   created on the first setExecuting() call. Opt-in subclasses call
   `setExecuting(false)` in their constructor so the observable is
   exposed before the workspace-action element reads it. Subclasses
   that don't opt in keep `isExecuting` undefined and the element
   falls back to legacy eager waiting feedback.

2. Element observation of `isExecuting` now lives inside #runApiAction
   so it tracks whichever api is actually invoked (`_actionApi ?? #api`),
   correctly handling subclasses like UmbSaveAndPreviewWorkspaceActionElement
   that swap in a different api at runtime. The shared observer alias
   replaces any previous observation on re-clicks.

3. UmbSaveWorkspaceAction and UmbDocumentSaveAndPublishWorkspaceAction
   now wrap their execute() body in try/finally and reset
   setExecuting(false) on completion so the observable honours the
   "true while execute() is performing real work, false otherwise"
   contract instead of getting stuck at true between executions.

No behavioural change for actions that already worked correctly before
this PR; the regression-prone "always exposed" behaviour is gone.

* Address Claude review: Elements gap, type placement, tests + cleanup

Three follow-ups on top of c15eb2d0bc:

1. Elements gap — UmbElementSaveAndPublishWorkspaceAction +
   UmbElementPublishingWorkspaceContext now wire through the same
   onActionStarting/notifyWorkspaceActionStarting handshake as the
   Document equivalents, so multi-variant Elements (Forms, Commerce, etc.)
   get the spinner-after-modal behaviour rather than no spinner at all.

2. Type placement — moved UmbWorkspaceActionExecutionOptions out of
   publishable-workspace-context.interface.ts into its own file so the
   saveable interface no longer has a directional dependency on the
   publishable one. Both peer contexts now import from the same neutral
   location.

3. Unit tests — added blackbox coverage for notifyWorkspaceActionStarting
   (no-op on undefined options/callback, invokes when present) and the
   UmbWorkspaceActionBase.setExecuting lazy-opt-in contract (undefined
   until first call, observable then exposed, value flips, sequential
   emissions, stable reference across calls).

Code-review cleanup applied on the same pass:

- Dropped the redundant `setExecuting(false)` at the start of execute()
  in the save and save-and-publish actions; the finally block plus
  UmbBooleanState's value-dedup already cover idempotency on retries.
- Removed an overlong block comment on `_isExecuting`; the JSDoc on
  setExecuting already documents the lazy/opt-in contract for subclasses.
- Trimmed an internal motivation comment from notify-workspace-action-
  starting.function.ts that referenced cyclomatic complexity.
- Extracted a tiny makeAction() helper in the controller test to remove
  the `{ meta: {} as never }` repetition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Workspace Actions: Align Save button state with Save & Publish (Andy review feedback)

Two related fixes addressing the variant-Save inconsistency Andy reported:

- Element catch block now only sets `failed` once `#executionStarted` is
  true. Pre-flight rejections (user cancelling a variant-picker modal,
  context-missing throws, etc.) leave the button idle, matching the
  silent-cancel path used by `#handleSaveAndPublish`. Legacy actions
  that don't opt in to `isExecuting` are unaffected because they set
  `#executionStarted = true` eagerly on click.

- `UmbDocumentWorkspaceContext._handleSave` and
  `UmbElementWorkspaceContext._handleSave` now accept and forward the
  `UmbWorkspaceActionExecutionOptions` argument to `super._handleSave`.
  The previous overrides dropped the parameter, so the
  `onActionStarting` callback supplied by `UmbSaveWorkspaceAction` never
  fired - which is why Save showed no waiting/success indicator even
  on a successful submit.

Result: Save and Save-and-publish now behave identically -
cancel = no indicator, submit = waiting then success - for both
invariant and multi-variant documents and elements.

* Docs: Document the modal-aware execution feedback contract for workspace actions

New 'Button state when the action opens a modal' subsection in
docs/workspaces.md explaining the three-piece contract:
UmbWorkspaceActionExecutionOptions + notifyWorkspaceActionStarting +
UmbWorkspaceActionBase.setExecuting. Covers third-party authoring of
modal-aware buttons, the cancel/pre-flight idle behaviour, and the
silent-parameter-drop pitfall on _handleSave overrides.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:00:49 +02:00
Jacob OvergaardandClaude Opus 4.7 cef6a467eb Workspace Actions: Restore waiting state for buttons with additional options (closes #18670, #20593) (#22554)
* Workspace Actions: Restore waiting state for buttons with additional options

The waiting state was suppressed whenever a workspace action reported
hasAdditionalOptions() (e.g. Save and publish on multi-variant sites),
so users saw the button jump straight from idle to the success tick
with no in-flight feedback.

Always set 'waiting' on click (unless the action is a link). The
variant-picker modal still opens on top of the button, so the spinner
is effectively invisible during selection — but it becomes visible
as soon as the modal closes and the publish request is in flight.

Fixes #22551

* Workspace Actions: Spin button only while real work is in flight

Replace the eager always-set-waiting behaviour from the previous commit
with an opt-in `isPending` signal so the spinner appears only while
actual work (validation + HTTP) is happening - not while the variant
picker modal is open, and never as a spurious success tick when the
user cancels the modal.

Changes:

- Add optional `isPending: Observable<boolean>` to UmbWorkspaceAction
  and a default UmbBooleanState + protected setPending() on the base
  class. Optional + backwards compatible for external implementers.

- Add optional `onActionStarting` callback (via a shared
  UmbWorkspaceActionExecutionOptions type) to
  UmbPublishableWorkspaceContext.saveAndPublish and
  UmbSaveableWorkspaceContext.requestSave. The document publishing
  context and content detail workspace base invoke the callback at the
  join point right after the variant picker resolves (or is skipped
  for the single-variant case), so it never fires when the modal is
  cancelled.

- Wire the document save and save-and-publish actions to clear pending
  at the start of execute() and pass an onActionStarting callback that
  flips it true when work begins.

- Update the workspace action element to observe api.isPending: when
  the observable is present the waiting state is driven by the
  observable (and the success tick is suppressed if the action
  resolves without ever signalling pending - i.e. a cancellation).
  When the observable is absent the element falls back to the legacy
  eager-waiting behaviour. Failures always surface the failed tick.

Fixes #22551

* Reduce cyclomatic complexity of #onClick and _handleSave

CodeScene Code Health Review flagged two complexity issues:

- UmbWorkspaceActionElement.#onClick reached cyclomatic complexity 9
  (threshold is < 9). Extracted the api-execution branch into a new
  private #runApiAction helper so #onClick collapses to a simple
  link-vs-action dispatch.
- _handleSave was already over the threshold (14); my optional-chain
  callback invocation pushed it to 16. Moved the
  `executionOptions?.onActionStarting?.()` call into a #notifyActionStarting
  helper so the call site is a plain method call and contributes zero
  cyclomatic complexity to _handleSave.

No behavioural change.

* Reduce cyclomatic complexity of #handleSaveAndPublish

Same fix as the previous commit's #notifyActionStarting extraction in
content-detail-workspace-base: move the optional-chain callback
invocation into a private helper so #handleSaveAndPublish stays at its
pre-PR cyclomatic complexity (15) instead of degrading to 17.

No behavioural change.

* DRY: extract notifyWorkspaceActionStarting into a shared utility

Both UmbDocumentPublishingWorkspaceContext.#handleSaveAndPublish and
UmbContentDetailWorkspaceContextBase._handleSave had identical private
#notifyActionStarting helpers introduced in this PR purely to keep the
optional-chain callback off the host method's cyclomatic complexity.

Replace both with a single exported notifyWorkspaceActionStarting()
utility co-located with UmbWorkspaceActionExecutionOptions. This:

- Removes a duplication point between the two contexts.
- Gives future workspace context implementations a ready-made way to
  honour the optional callback without re-inventing the helper or
  paying the cyclomatic-complexity cost at the call site.

No behavioural change.

* Rename isPending -> isExecuting to mirror the execute() method

Niels suggested correlating the observable's name with the action's
`execute()` method, so the symbol set is now:

- isExecuting (observable on UmbWorkspaceAction interface)
- _isExecuting / setExecuting (UmbWorkspaceActionBase)
- #observeIsExecuting / #executionStarted (workspace-action element)
- isExecutingObserver (observer alias)

Pure rename; no behavioural change.

* Address Copilot review: lazy isExecuting, observer scope, finally reset

Five Copilot findings on PR #22554. Three real regressions + two
contract violations, all addressed:

1. UmbWorkspaceActionBase always exposing `isExecuting` made every
   existing subclass appear to opt in to the new modal-aware flow,
   suppressing waiting/success states for actions that never call
   setExecuting(true). Made `_isExecuting`/`isExecuting` lazy: only
   created on the first setExecuting() call. Opt-in subclasses call
   `setExecuting(false)` in their constructor so the observable is
   exposed before the workspace-action element reads it. Subclasses
   that don't opt in keep `isExecuting` undefined and the element
   falls back to legacy eager waiting feedback.

2. Element observation of `isExecuting` now lives inside #runApiAction
   so it tracks whichever api is actually invoked (`_actionApi ?? #api`),
   correctly handling subclasses like UmbSaveAndPreviewWorkspaceActionElement
   that swap in a different api at runtime. The shared observer alias
   replaces any previous observation on re-clicks.

3. UmbSaveWorkspaceAction and UmbDocumentSaveAndPublishWorkspaceAction
   now wrap their execute() body in try/finally and reset
   setExecuting(false) on completion so the observable honours the
   "true while execute() is performing real work, false otherwise"
   contract instead of getting stuck at true between executions.

No behavioural change for actions that already worked correctly before
this PR; the regression-prone "always exposed" behaviour is gone.

* Address Claude review: Elements gap, type placement, tests + cleanup

Three follow-ups on top of c15eb2d0bc:

1. Elements gap — UmbElementSaveAndPublishWorkspaceAction +
   UmbElementPublishingWorkspaceContext now wire through the same
   onActionStarting/notifyWorkspaceActionStarting handshake as the
   Document equivalents, so multi-variant Elements (Forms, Commerce, etc.)
   get the spinner-after-modal behaviour rather than no spinner at all.

2. Type placement — moved UmbWorkspaceActionExecutionOptions out of
   publishable-workspace-context.interface.ts into its own file so the
   saveable interface no longer has a directional dependency on the
   publishable one. Both peer contexts now import from the same neutral
   location.

3. Unit tests — added blackbox coverage for notifyWorkspaceActionStarting
   (no-op on undefined options/callback, invokes when present) and the
   UmbWorkspaceActionBase.setExecuting lazy-opt-in contract (undefined
   until first call, observable then exposed, value flips, sequential
   emissions, stable reference across calls).

Code-review cleanup applied on the same pass:

- Dropped the redundant `setExecuting(false)` at the start of execute()
  in the save and save-and-publish actions; the finally block plus
  UmbBooleanState's value-dedup already cover idempotency on retries.
- Removed an overlong block comment on `_isExecuting`; the JSDoc on
  setExecuting already documents the lazy/opt-in contract for subclasses.
- Trimmed an internal motivation comment from notify-workspace-action-
  starting.function.ts that referenced cyclomatic complexity.
- Extracted a tiny makeAction() helper in the controller test to remove
  the `{ meta: {} as never }` repetition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Workspace Actions: Align Save button state with Save & Publish (Andy review feedback)

Two related fixes addressing the variant-Save inconsistency Andy reported:

- Element catch block now only sets `failed` once `#executionStarted` is
  true. Pre-flight rejections (user cancelling a variant-picker modal,
  context-missing throws, etc.) leave the button idle, matching the
  silent-cancel path used by `#handleSaveAndPublish`. Legacy actions
  that don't opt in to `isExecuting` are unaffected because they set
  `#executionStarted = true` eagerly on click.

- `UmbDocumentWorkspaceContext._handleSave` and
  `UmbElementWorkspaceContext._handleSave` now accept and forward the
  `UmbWorkspaceActionExecutionOptions` argument to `super._handleSave`.
  The previous overrides dropped the parameter, so the
  `onActionStarting` callback supplied by `UmbSaveWorkspaceAction` never
  fired - which is why Save showed no waiting/success indicator even
  on a successful submit.

Result: Save and Save-and-publish now behave identically -
cancel = no indicator, submit = waiting then success - for both
invariant and multi-variant documents and elements.

* Docs: Document the modal-aware execution feedback contract for workspace actions

New 'Button state when the action opens a modal' subsection in
docs/workspaces.md explaining the three-piece contract:
UmbWorkspaceActionExecutionOptions + notifyWorkspaceActionStarting +
UmbWorkspaceActionBase.setExecuting. Covers third-party authoring of
modal-aware buttons, the cancel/pre-flight idle behaviour, and the
silent-parameter-drop pitfall on _handleSave overrides.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:56:22 +02:00
c542b1b4fd Workspace Actions: Restore waiting state for buttons with additional options (closes #18670, #20593) (#22554)
* Workspace Actions: Restore waiting state for buttons with additional options

The waiting state was suppressed whenever a workspace action reported
hasAdditionalOptions() (e.g. Save and publish on multi-variant sites),
so users saw the button jump straight from idle to the success tick
with no in-flight feedback.

Always set 'waiting' on click (unless the action is a link). The
variant-picker modal still opens on top of the button, so the spinner
is effectively invisible during selection — but it becomes visible
as soon as the modal closes and the publish request is in flight.

Fixes #22551

* Workspace Actions: Spin button only while real work is in flight

Replace the eager always-set-waiting behaviour from the previous commit
with an opt-in `isPending` signal so the spinner appears only while
actual work (validation + HTTP) is happening - not while the variant
picker modal is open, and never as a spurious success tick when the
user cancels the modal.

Changes:

- Add optional `isPending: Observable<boolean>` to UmbWorkspaceAction
  and a default UmbBooleanState + protected setPending() on the base
  class. Optional + backwards compatible for external implementers.

- Add optional `onActionStarting` callback (via a shared
  UmbWorkspaceActionExecutionOptions type) to
  UmbPublishableWorkspaceContext.saveAndPublish and
  UmbSaveableWorkspaceContext.requestSave. The document publishing
  context and content detail workspace base invoke the callback at the
  join point right after the variant picker resolves (or is skipped
  for the single-variant case), so it never fires when the modal is
  cancelled.

- Wire the document save and save-and-publish actions to clear pending
  at the start of execute() and pass an onActionStarting callback that
  flips it true when work begins.

- Update the workspace action element to observe api.isPending: when
  the observable is present the waiting state is driven by the
  observable (and the success tick is suppressed if the action
  resolves without ever signalling pending - i.e. a cancellation).
  When the observable is absent the element falls back to the legacy
  eager-waiting behaviour. Failures always surface the failed tick.

Fixes #22551

* Reduce cyclomatic complexity of #onClick and _handleSave

CodeScene Code Health Review flagged two complexity issues:

- UmbWorkspaceActionElement.#onClick reached cyclomatic complexity 9
  (threshold is < 9). Extracted the api-execution branch into a new
  private #runApiAction helper so #onClick collapses to a simple
  link-vs-action dispatch.
- _handleSave was already over the threshold (14); my optional-chain
  callback invocation pushed it to 16. Moved the
  `executionOptions?.onActionStarting?.()` call into a #notifyActionStarting
  helper so the call site is a plain method call and contributes zero
  cyclomatic complexity to _handleSave.

No behavioural change.

* Reduce cyclomatic complexity of #handleSaveAndPublish

Same fix as the previous commit's #notifyActionStarting extraction in
content-detail-workspace-base: move the optional-chain callback
invocation into a private helper so #handleSaveAndPublish stays at its
pre-PR cyclomatic complexity (15) instead of degrading to 17.

No behavioural change.

* DRY: extract notifyWorkspaceActionStarting into a shared utility

Both UmbDocumentPublishingWorkspaceContext.#handleSaveAndPublish and
UmbContentDetailWorkspaceContextBase._handleSave had identical private
#notifyActionStarting helpers introduced in this PR purely to keep the
optional-chain callback off the host method's cyclomatic complexity.

Replace both with a single exported notifyWorkspaceActionStarting()
utility co-located with UmbWorkspaceActionExecutionOptions. This:

- Removes a duplication point between the two contexts.
- Gives future workspace context implementations a ready-made way to
  honour the optional callback without re-inventing the helper or
  paying the cyclomatic-complexity cost at the call site.

No behavioural change.

* Rename isPending -> isExecuting to mirror the execute() method

Niels suggested correlating the observable's name with the action's
`execute()` method, so the symbol set is now:

- isExecuting (observable on UmbWorkspaceAction interface)
- _isExecuting / setExecuting (UmbWorkspaceActionBase)
- #observeIsExecuting / #executionStarted (workspace-action element)
- isExecutingObserver (observer alias)

Pure rename; no behavioural change.

* Address Copilot review: lazy isExecuting, observer scope, finally reset

Five Copilot findings on PR #22554. Three real regressions + two
contract violations, all addressed:

1. UmbWorkspaceActionBase always exposing `isExecuting` made every
   existing subclass appear to opt in to the new modal-aware flow,
   suppressing waiting/success states for actions that never call
   setExecuting(true). Made `_isExecuting`/`isExecuting` lazy: only
   created on the first setExecuting() call. Opt-in subclasses call
   `setExecuting(false)` in their constructor so the observable is
   exposed before the workspace-action element reads it. Subclasses
   that don't opt in keep `isExecuting` undefined and the element
   falls back to legacy eager waiting feedback.

2. Element observation of `isExecuting` now lives inside #runApiAction
   so it tracks whichever api is actually invoked (`_actionApi ?? #api`),
   correctly handling subclasses like UmbSaveAndPreviewWorkspaceActionElement
   that swap in a different api at runtime. The shared observer alias
   replaces any previous observation on re-clicks.

3. UmbSaveWorkspaceAction and UmbDocumentSaveAndPublishWorkspaceAction
   now wrap their execute() body in try/finally and reset
   setExecuting(false) on completion so the observable honours the
   "true while execute() is performing real work, false otherwise"
   contract instead of getting stuck at true between executions.

No behavioural change for actions that already worked correctly before
this PR; the regression-prone "always exposed" behaviour is gone.

* Address Claude review: Elements gap, type placement, tests + cleanup

Three follow-ups on top of c15eb2d0bc:

1. Elements gap — UmbElementSaveAndPublishWorkspaceAction +
   UmbElementPublishingWorkspaceContext now wire through the same
   onActionStarting/notifyWorkspaceActionStarting handshake as the
   Document equivalents, so multi-variant Elements (Forms, Commerce, etc.)
   get the spinner-after-modal behaviour rather than no spinner at all.

2. Type placement — moved UmbWorkspaceActionExecutionOptions out of
   publishable-workspace-context.interface.ts into its own file so the
   saveable interface no longer has a directional dependency on the
   publishable one. Both peer contexts now import from the same neutral
   location.

3. Unit tests — added blackbox coverage for notifyWorkspaceActionStarting
   (no-op on undefined options/callback, invokes when present) and the
   UmbWorkspaceActionBase.setExecuting lazy-opt-in contract (undefined
   until first call, observable then exposed, value flips, sequential
   emissions, stable reference across calls).

Code-review cleanup applied on the same pass:

- Dropped the redundant `setExecuting(false)` at the start of execute()
  in the save and save-and-publish actions; the finally block plus
  UmbBooleanState's value-dedup already cover idempotency on retries.
- Removed an overlong block comment on `_isExecuting`; the JSDoc on
  setExecuting already documents the lazy/opt-in contract for subclasses.
- Trimmed an internal motivation comment from notify-workspace-action-
  starting.function.ts that referenced cyclomatic complexity.
- Extracted a tiny makeAction() helper in the controller test to remove
  the `{ meta: {} as never }` repetition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Workspace Actions: Align Save button state with Save & Publish (Andy review feedback)

Two related fixes addressing the variant-Save inconsistency Andy reported:

- Element catch block now only sets `failed` once `#executionStarted` is
  true. Pre-flight rejections (user cancelling a variant-picker modal,
  context-missing throws, etc.) leave the button idle, matching the
  silent-cancel path used by `#handleSaveAndPublish`. Legacy actions
  that don't opt in to `isExecuting` are unaffected because they set
  `#executionStarted = true` eagerly on click.

- `UmbDocumentWorkspaceContext._handleSave` and
  `UmbElementWorkspaceContext._handleSave` now accept and forward the
  `UmbWorkspaceActionExecutionOptions` argument to `super._handleSave`.
  The previous overrides dropped the parameter, so the
  `onActionStarting` callback supplied by `UmbSaveWorkspaceAction` never
  fired - which is why Save showed no waiting/success indicator even
  on a successful submit.

Result: Save and Save-and-publish now behave identically -
cancel = no indicator, submit = waiting then success - for both
invariant and multi-variant documents and elements.

* Docs: Document the modal-aware execution feedback contract for workspace actions

New 'Button state when the action opens a modal' subsection in
docs/workspaces.md explaining the three-piece contract:
UmbWorkspaceActionExecutionOptions + notifyWorkspaceActionStarting +
UmbWorkspaceActionBase.setExecuting. Covers third-party authoring of
modal-aware buttons, the cancel/pre-flight idle behaviour, and the
silent-parameter-drop pitfall on _handleSave overrides.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 08:32:48 +00:00
Jacob OvergaardandClaude Opus 4.7 26232e0276 Backoffice: Coalesce small Rollup chunks across all workspaces (AB#67983) (#22896)
* Backoffice: Coalesce small Rollup chunks across all workspaces (AB#67983)

Set experimentalMinChunkSize=10_000 as the default in the shared Vite
helper. Every workspace inherits the coalescing automatically; the
threshold can still be overridden per workspace (pass 0 to disable).

Impact on dist-cms output:
- packages/core .js files: 981 -> 272 (-72%)
- All workspaces combined .js files: 2194 -> 1401 (-36%)
- Welcome dashboard .js requests: 510 -> 497 (-2.5%)
- packages/ufm requests in particular: 23 -> 12 (-48%)
- Gzipped bundle total: -1.2%
- Raw bytes: +1.4% (small overhead from merged chunks; gzip wins it back)

All entry chunks are preserved, so every public
@umbraco-cms/backoffice/<sub> import keeps resolving without changes
to package.json exports or tsconfig paths.

Further consolidation (collapsing core's per-subpath entries into a
single bundle with stubs) was prototyped but hits a TDZ cycle between
the eager entry and its dynamic-import descendants. Tracked for v18,
not part of this change.

* Backoffice: Normalise umbraco-package + manifests shapes (AB#67983)

Aligns the two outliers with the conventions used by the other 38
first-party packages:

- documents/umbraco-package.ts now uses the lazy bundle pattern
  (type: 'bundle', js: () => import('./manifests.js')) instead of
  eagerly importing manifests at module evaluation. The bundle
  initializer auto-loads the manifests at boot, so behaviour is
  unchanged.
- umbraco-news/manifests.ts now exports `manifests: Array<...>`
  instead of a bare `dashboard` object. The bundle initializer
  enumerates exports regardless of name, so behaviour is unchanged.

Preparatory cleanup so future build-time manifest aggregation can
treat every workspace uniformly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:39:06 +02:00
Jacob Overgaard 767fafe06d Merge remote-tracking branch 'origin/v17/dev' 2026-05-22 09:38:31 +02:00
Jacob OvergaardandClaude Opus 4.7 04f0e229c7 Backoffice: Coalesce small Rollup chunks across all workspaces (AB#67983) (#22896)
* Backoffice: Coalesce small Rollup chunks across all workspaces (AB#67983)

Set experimentalMinChunkSize=10_000 as the default in the shared Vite
helper. Every workspace inherits the coalescing automatically; the
threshold can still be overridden per workspace (pass 0 to disable).

Impact on dist-cms output:
- packages/core .js files: 981 -> 272 (-72%)
- All workspaces combined .js files: 2194 -> 1401 (-36%)
- Welcome dashboard .js requests: 510 -> 497 (-2.5%)
- packages/ufm requests in particular: 23 -> 12 (-48%)
- Gzipped bundle total: -1.2%
- Raw bytes: +1.4% (small overhead from merged chunks; gzip wins it back)

All entry chunks are preserved, so every public
@umbraco-cms/backoffice/<sub> import keeps resolving without changes
to package.json exports or tsconfig paths.

Further consolidation (collapsing core's per-subpath entries into a
single bundle with stubs) was prototyped but hits a TDZ cycle between
the eager entry and its dynamic-import descendants. Tracked for v18,
not part of this change.

* Backoffice: Normalise umbraco-package + manifests shapes (AB#67983)

Aligns the two outliers with the conventions used by the other 38
first-party packages:

- documents/umbraco-package.ts now uses the lazy bundle pattern
  (type: 'bundle', js: () => import('./manifests.js')) instead of
  eagerly importing manifests at module evaluation. The bundle
  initializer auto-loads the manifests at boot, so behaviour is
  unchanged.
- umbraco-news/manifests.ts now exports `manifests: Array<...>`
  instead of a bare `dashboard` object. The bundle initializer
  enumerates exports regardless of name, so behaviour is unchanged.

Preparatory cleanup so future build-time manifest aggregation can
treat every workspace uniformly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:35:44 +02:00
5d76706553 Backoffice: Coalesce small Rollup chunks across all workspaces (AB#67983) (#22896)
* Backoffice: Coalesce small Rollup chunks across all workspaces (AB#67983)

Set experimentalMinChunkSize=10_000 as the default in the shared Vite
helper. Every workspace inherits the coalescing automatically; the
threshold can still be overridden per workspace (pass 0 to disable).

Impact on dist-cms output:
- packages/core .js files: 981 -> 272 (-72%)
- All workspaces combined .js files: 2194 -> 1401 (-36%)
- Welcome dashboard .js requests: 510 -> 497 (-2.5%)
- packages/ufm requests in particular: 23 -> 12 (-48%)
- Gzipped bundle total: -1.2%
- Raw bytes: +1.4% (small overhead from merged chunks; gzip wins it back)

All entry chunks are preserved, so every public
@umbraco-cms/backoffice/<sub> import keeps resolving without changes
to package.json exports or tsconfig paths.

Further consolidation (collapsing core's per-subpath entries into a
single bundle with stubs) was prototyped but hits a TDZ cycle between
the eager entry and its dynamic-import descendants. Tracked for v18,
not part of this change.

* Backoffice: Normalise umbraco-package + manifests shapes (AB#67983)

Aligns the two outliers with the conventions used by the other 38
first-party packages:

- documents/umbraco-package.ts now uses the lazy bundle pattern
  (type: 'bundle', js: () => import('./manifests.js')) instead of
  eagerly importing manifests at module evaluation. The bundle
  initializer auto-loads the manifests at boot, so behaviour is
  unchanged.
- umbraco-news/manifests.ts now exports `manifests: Array<...>`
  instead of a bare `dashboard` object. The bundle initializer
  enumerates exports regardless of name, so behaviour is unchanged.

Preparatory cleanup so future build-time manifest aggregation can
treat every workspace uniformly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:28:31 +02:00
Mads RasmussenandGitHub 1fdcb835bc Devops: Report bidirectional import detection for both Core and Packages modules (#22938)
report bidirectional imports for core modules
2026-05-22 07:29:37 +02:00
Mads RasmussenandGitHub 6b3bdb59b7 Backoffice: Swap relative imports to @umbraco-cms/backoffice module imports in core packages (#22942)
Use @umbraco-cms/backoffice imports

Replace numerous relative/internal import paths with centralized '@umbraco-cms/backoffice' package entry points across core modules.This consolidates exports, simplifies import paths.
2026-05-22 07:24:39 +02:00
Andy Butland 216fee987b Added a TODO for a future major. 2026-05-22 06:44:36 +02:00
Andy Butland 36ea0a494d Bump version to 18.0.0-rc1. 2026-05-21 19:45:51 +02:00
Andy Butland ca6a32088a Merge branch 'v17/dev' 2026-05-21 19:41:41 +02:00
Andy ButlandandClaude Opus 4.6 d6f6e31c68 Background Jobs: Rewrite RecurringHostedServiceBase with SemaphoreSlim and add signalling support (#22331)
* Compute next delay to compensate for time drift

* Use SemaphoreSlim to properly handle exceptions, cancellation tokens and triggering immediate executions

* Add RecurringBackgroundJobBase to contain default values and hide obsoleted method

* Add NextExecutionStrategy parameter to adjust the schedule after triggered executions

* Add TriggerExecution methods to RecurringBackgroundJobHostedServiceRunner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Handle cancellation (application shutdown) and publish RecurringBackgroundJobCanceledNotification

* Match hosted services by Type instead of type name string

* Extract shared helper for TriggerExecution tests

* Clear trigger state when initial delay is interrupted

* Clear _nextExecutionSkipOnOvershoot unconditionally

* Combine ComputeNextDelay tests

* Consolidate trigger state into an immutable record for thread safety

* Use ConcurrentDictionary for thread-safe hosted service lookup

* Remove hosted services from dictionary on stop

* Fix API compatibility errors

* Removed unneeded using.

* Register RecurringBackgroundJobHostedServiceRunner as resolvable singleton

* Remove failed hosted service from dictionary when StartAsync throws

* Use semaphore signaling instead of Task.Delay in trigger tests

Use semaphore signaling instead of Task.Delay in trigger tests 2

* Inject TimeProvider into RecurringHostedServiceBase for deterministic testing

Fix timeprovider

* Use DelayCalculator.GetDelay instead of RecurringHostedServiceBase.GetDelay

* Fix Exception_In_PerformExecuteAsync_Does_Not_Kill_Loop test

* Avoid disposing period-change CTS while wait loop may still reference it

* Configure IEventMessagesFactory mock to return real EventMessages

* Clarify TriggerExecution(TimeSpan) docs and add ChangePeriod test

* Validate period is positive and use GetOrAdd to avoid creating unused hosted services

* Set up Period and Delay on mock job to satisfy constructor validation

* Ensure PeriodChanged event is unsubscribed again

* Fix trigger state race, simplify ReleaseSignal, and add canceled notification test

Fix trigger state

* Use Interlocked for _period reads/writes and implement thread-safe dispose pattern

* Remove hosted service from dictionary before stopping to prevent triggering during shutdown

* Replace Task.Yield with semaphore timeouts in negative assertions

* Tidy RecurringBackgroundJobBase docs and runner error handling

* Wait IgnoredDelay after ignored execution to prevent tight looping when Period is short or zero

* Add IRecurringBackgroundJobTrigger<TJob> for opt-in job triggering

* Register IRecurringBackgroundJobTrigger as open generic and drop AddTriggerableRecurringBackgroundJob

* Fix and add parameter validation

* Allow Timeout.InfiniteTimeSpan as Period for manual-trigger-only recurring jobs

* Migrate built-in jobs to RecurringBackgroundJobBase and require ITriggerableRecurringBackgroundJob in runner trigger overloads

* Support infinite Delay and honor TriggerExecution(TimeSpan) issued during the initial delay

* Handle edge case of backoff via InfiniteTimeSpan.

* Refactored large method.

* Added clarifying documentation.

* Suppress ExecutionContext flow when starting the recurring background loop, restoring previous timer behaviour.

* Relocate Suppress ExecutionContext flow to avoid package validation error.

* Align IRecurringBackgroundJobTrigger generic type constraint with AddRecurringBackgroundJob

* Rename ApplyTriggerState to ComputeNextDelayFromTriggerState

* Allow Timeout.InfiniteTimeSpan as IgnoredDelay to fully disable a job for the remaining application lifecycle

* Fix generic type constraint

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-21 19:40:54 +02:00
65ab1c0b2b Background Jobs: Rewrite RecurringHostedServiceBase with SemaphoreSlim and add signalling support (#22331)
* Compute next delay to compensate for time drift

* Use SemaphoreSlim to properly handle exceptions, cancellation tokens and triggering immediate executions

* Add RecurringBackgroundJobBase to contain default values and hide obsoleted method

* Add NextExecutionStrategy parameter to adjust the schedule after triggered executions

* Add TriggerExecution methods to RecurringBackgroundJobHostedServiceRunner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Handle cancellation (application shutdown) and publish RecurringBackgroundJobCanceledNotification

* Match hosted services by Type instead of type name string

* Extract shared helper for TriggerExecution tests

* Clear trigger state when initial delay is interrupted

* Clear _nextExecutionSkipOnOvershoot unconditionally

* Combine ComputeNextDelay tests

* Consolidate trigger state into an immutable record for thread safety

* Use ConcurrentDictionary for thread-safe hosted service lookup

* Remove hosted services from dictionary on stop

* Fix API compatibility errors

* Removed unneeded using.

* Register RecurringBackgroundJobHostedServiceRunner as resolvable singleton

* Remove failed hosted service from dictionary when StartAsync throws

* Use semaphore signaling instead of Task.Delay in trigger tests

Use semaphore signaling instead of Task.Delay in trigger tests 2

* Inject TimeProvider into RecurringHostedServiceBase for deterministic testing

Fix timeprovider

* Use DelayCalculator.GetDelay instead of RecurringHostedServiceBase.GetDelay

* Fix Exception_In_PerformExecuteAsync_Does_Not_Kill_Loop test

* Avoid disposing period-change CTS while wait loop may still reference it

* Configure IEventMessagesFactory mock to return real EventMessages

* Clarify TriggerExecution(TimeSpan) docs and add ChangePeriod test

* Validate period is positive and use GetOrAdd to avoid creating unused hosted services

* Set up Period and Delay on mock job to satisfy constructor validation

* Ensure PeriodChanged event is unsubscribed again

* Fix trigger state race, simplify ReleaseSignal, and add canceled notification test

Fix trigger state

* Use Interlocked for _period reads/writes and implement thread-safe dispose pattern

* Remove hosted service from dictionary before stopping to prevent triggering during shutdown

* Replace Task.Yield with semaphore timeouts in negative assertions

* Tidy RecurringBackgroundJobBase docs and runner error handling

* Wait IgnoredDelay after ignored execution to prevent tight looping when Period is short or zero

* Add IRecurringBackgroundJobTrigger<TJob> for opt-in job triggering

* Register IRecurringBackgroundJobTrigger as open generic and drop AddTriggerableRecurringBackgroundJob

* Fix and add parameter validation

* Allow Timeout.InfiniteTimeSpan as Period for manual-trigger-only recurring jobs

* Migrate built-in jobs to RecurringBackgroundJobBase and require ITriggerableRecurringBackgroundJob in runner trigger overloads

* Support infinite Delay and honor TriggerExecution(TimeSpan) issued during the initial delay

* Handle edge case of backoff via InfiniteTimeSpan.

* Refactored large method.

* Added clarifying documentation.

* Suppress ExecutionContext flow when starting the recurring background loop, restoring previous timer behaviour.

* Relocate Suppress ExecutionContext flow to avoid package validation error.

* Align IRecurringBackgroundJobTrigger generic type constraint with AddRecurringBackgroundJob

* Rename ApplyTriggerState to ComputeNextDelayFromTriggerState

* Allow Timeout.InfiniteTimeSpan as IgnoredDelay to fully disable a job for the remaining application lifecycle

* Fix generic type constraint

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-21 19:39:21 +02:00
a54769758b Background Jobs: Rewrite RecurringHostedServiceBase with SemaphoreSlim and add signalling support (#22331)
* Compute next delay to compensate for time drift

* Use SemaphoreSlim to properly handle exceptions, cancellation tokens and triggering immediate executions

* Add RecurringBackgroundJobBase to contain default values and hide obsoleted method

* Add NextExecutionStrategy parameter to adjust the schedule after triggered executions

* Add TriggerExecution methods to RecurringBackgroundJobHostedServiceRunner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Handle cancellation (application shutdown) and publish RecurringBackgroundJobCanceledNotification

* Match hosted services by Type instead of type name string

* Extract shared helper for TriggerExecution tests

* Clear trigger state when initial delay is interrupted

* Clear _nextExecutionSkipOnOvershoot unconditionally

* Combine ComputeNextDelay tests

* Consolidate trigger state into an immutable record for thread safety

* Use ConcurrentDictionary for thread-safe hosted service lookup

* Remove hosted services from dictionary on stop

* Fix API compatibility errors

* Removed unneeded using.

* Register RecurringBackgroundJobHostedServiceRunner as resolvable singleton

* Remove failed hosted service from dictionary when StartAsync throws

* Use semaphore signaling instead of Task.Delay in trigger tests

Use semaphore signaling instead of Task.Delay in trigger tests 2

* Inject TimeProvider into RecurringHostedServiceBase for deterministic testing

Fix timeprovider

* Use DelayCalculator.GetDelay instead of RecurringHostedServiceBase.GetDelay

* Fix Exception_In_PerformExecuteAsync_Does_Not_Kill_Loop test

* Avoid disposing period-change CTS while wait loop may still reference it

* Configure IEventMessagesFactory mock to return real EventMessages

* Clarify TriggerExecution(TimeSpan) docs and add ChangePeriod test

* Validate period is positive and use GetOrAdd to avoid creating unused hosted services

* Set up Period and Delay on mock job to satisfy constructor validation

* Ensure PeriodChanged event is unsubscribed again

* Fix trigger state race, simplify ReleaseSignal, and add canceled notification test

Fix trigger state

* Use Interlocked for _period reads/writes and implement thread-safe dispose pattern

* Remove hosted service from dictionary before stopping to prevent triggering during shutdown

* Replace Task.Yield with semaphore timeouts in negative assertions

* Tidy RecurringBackgroundJobBase docs and runner error handling

* Wait IgnoredDelay after ignored execution to prevent tight looping when Period is short or zero

* Add IRecurringBackgroundJobTrigger<TJob> for opt-in job triggering

* Register IRecurringBackgroundJobTrigger as open generic and drop AddTriggerableRecurringBackgroundJob

* Fix and add parameter validation

* Allow Timeout.InfiniteTimeSpan as Period for manual-trigger-only recurring jobs

* Migrate built-in jobs to RecurringBackgroundJobBase and require ITriggerableRecurringBackgroundJob in runner trigger overloads

* Support infinite Delay and honor TriggerExecution(TimeSpan) issued during the initial delay

* Handle edge case of backoff via InfiniteTimeSpan.

* Refactored large method.

* Added clarifying documentation.

* Suppress ExecutionContext flow when starting the recurring background loop, restoring previous timer behaviour.

* Relocate Suppress ExecutionContext flow to avoid package validation error.

* Align IRecurringBackgroundJobTrigger generic type constraint with AddRecurringBackgroundJob

* Rename ApplyTriggerState to ComputeNextDelayFromTriggerState

* Allow Timeout.InfiniteTimeSpan as IgnoredDelay to fully disable a job for the remaining application lifecycle

* Fix generic type constraint

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-21 19:36:04 +02:00
Andy Butland 62db2c06bb Merge branch 'v17/dev' 2026-05-21 19:07:14 +02:00
Andreas Lykke BorgandAndy Butland ad681cfde3 Image Cropper: Improve contrast of append label in crop options editor (closes #22878) (#22917)
Improve contrast of input append label in image crops editor
2026-05-21 19:06:36 +02:00
Andreas Lykke BorgandAndy Butland 1616997409 Image Cropper: Improve contrast of append label in crop options editor (closes #22878) (#22917)
Improve contrast of input append label in image crops editor
2026-05-21 19:06:01 +02:00
Andreas Lykke BorgandGitHub 609b74b475 Image Cropper: Improve contrast of append label in crop options editor (closes #22878) (#22917)
Improve contrast of input append label in image crops editor
2026-05-21 19:05:33 +02:00
Andy Butland e756e40003 Fixed front-end build issue after merge. 2026-05-21 18:45:09 +02:00
Andy Butland b8d6c83d53 Merge branch 'v17/dev' 2026-05-21 18:13:55 +02:00
Engiber LozadaandAndy Butland 0e7eb11c60 Block Grid: Fix inline create button width not updating on workspace resize (closes #22527) (#22928)
* Add ResizeObserver for inline create buttons

* Disconnect layout resize observer on destroy

* Initialize ResizeObserver and simplify cleanup

* Remove optional chaining on layout observer disconnect
2026-05-21 18:13:10 +02:00
Engiber LozadaandAndy Butland 63289e22cb Block Grid: Fix inline create button width not updating on workspace resize (closes #22527) (#22928)
* Add ResizeObserver for inline create buttons

* Disconnect layout resize observer on destroy

* Initialize ResizeObserver and simplify cleanup

* Remove optional chaining on layout observer disconnect
2026-05-21 18:12:23 +02:00
Engiber LozadaandGitHub 82b2991a18 Block Grid: Fix inline create button width not updating on workspace resize (closes #22527) (#22928)
* Add ResizeObserver for inline create buttons

* Disconnect layout resize observer on destroy

* Initialize ResizeObserver and simplify cleanup

* Remove optional chaining on layout observer disconnect
2026-05-21 18:11:24 +02:00
Andy Butland 0f357f595e Merge branch 'v17/dev' 2026-05-21 18:01:17 +02:00
Andy Butland 721cf53d40 Fix styling of redirect tracker enabled/disabled icon. 2026-05-21 17:59:27 +02:00
Andy Butland f8ba3d8cfc Merge branch 'release/17.5.0' into v17/dev 2026-05-21 17:41:20 +02:00
Andy ButlandandLan Nguyen Thuy 7f832d261d Reset password: Add inline validation messaging for password pattern requirements (#22880)
* add custom validation for password input in reset password

* update remove invalid listeners in disconnectedCallback

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2026-05-21 17:34:05 +02:00
Andy ButlandandLan Nguyen Thuy 3523fbbed2 Reset password: Add inline validation messaging for password pattern requirements (#22880)
* add custom validation for password input in reset password

* update remove invalid listeners in disconnectedCallback

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2026-05-21 17:19:05 +02:00
Andy Butland 0116ddb81d Fixed code styling. 2026-05-21 17:16:54 +02:00
Andy Butland a4a2d4ee35 Fixed incorrect documentation. 2026-05-21 17:16:46 +02:00
4ecbface60 Reset password: Add inline validation messaging for password pattern requirements (#22880)
* add custom validation for password input in reset password

* update remove invalid listeners in disconnectedCallback

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2026-05-21 17:13:35 +02:00
Lee KelleherandGitHub 22340a40f8 Global Elements: Trashed item restore, checks "Move" permission (#22925) 2026-05-21 15:08:48 +00:00
Lee KelleherandGitHub 831b1ac7ad Element Picker: fixes "Not Found" name on removal prompt (#22922)
* Element Tree Picker Data Source: adds item data resolver

This follows PR #22915, which fixes the Entity Data Picker's
removal confirmation message with the entity's name.

* Adds support for `UmbElementFolderItemDataResolver`
2026-05-21 15:07:45 +00:00
Andy ButlandandGitHub 9276dd757a Cache: Invalidate element GUID-keyed cache on delete (closes #22911) (#22940)
Fix GUID key lookup for clearing the element by key cache after deletion.
2026-05-21 16:52:33 +02:00
Lee KelleherandGitHub 73195ca7a3 Global Elements: Workspace hotfix for 'Unique is missing' warning (#22935)
fix(elements): resolve 'Unique is missing' race when navigating element workspaces
2026-05-21 15:32:25 +02:00
Andy Butland 9c6550207d Fix failing unit tests. 2026-05-21 15:03:47 +02:00
Jacob Overgaard 80e6b481c0 Merge branch 'release/18.0' 2026-05-21 12:38:06 +02:00
Jacob OvergaardandClaude Opus 4.7 79065052c6 Mocks: Add missing allowedInLibrary and noAccess to document type mock data
Backfills the two required properties on the seven mock document type
entries that were missing them, so the file type-checks against
DocumentTypeResponseModel and DocumentTypeTreeItemResponseModel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 12:33:44 +02:00
Jacob Overgaard e3bee4cdb3 fix: exports condition configs and fixes test imports 2026-05-21 12:28:40 +02:00
Jacob Overgaard 68a633047e Test: adds 'mocha' and 'chai' as types for tsconfig (#22889)
fix(test): adds 'mocha' and 'chai' as types for tsconfig
2026-05-21 12:23:22 +02:00
Mads Rasmussenandleekelleher 06c2055e22 Collections: Replace direct filter pass-through in collection server data sources (#22921)
Pass explicit skip/take to collection services

(cherry picked from commit f79e9586b4)
2026-05-21 09:30:08 +01:00
Mads Rasmussenandleekelleher 55e5ae789d Entity Data Picker: Fix "Not Found" in remove dialog for entities without a top-level name (#22915)
* Add item data resolver support to picker data sources

* add js docs

* remove duplicated fallback logic

* wip unit tests of requestItemName method

* Use DocumentVariantStateModel in mock documents to fix compiler

* Update input-entity-data.context.ts

* Update input-entity-data.context.test.ts

(cherry picked from commit c74a58246f)
2026-05-21 09:27:42 +01:00
Mads RasmussenandGitHub f79e9586b4 Collections: Replace direct filter pass-through in collection server data sources (#22921)
Pass explicit skip/take to collection services
2026-05-21 09:18:36 +01:00
nikolajlauridsen 60948d197a Merge branch 'v17/dev'
# Conflicts:
#	src/Umbraco.Core/Services/DocumentUrlService.cs
2026-05-21 10:15:13 +02:00
Mads RasmussenandGitHub c74a58246f Entity Data Picker: Fix "Not Found" in remove dialog for entities without a top-level name (#22915)
* Add item data resolver support to picker data sources

* add js docs

* remove duplicated fallback logic

* wip unit tests of requestItemName method

* Use DocumentVariantStateModel in mock documents to fix compiler

* Update input-entity-data.context.ts

* Update input-entity-data.context.test.ts
2026-05-21 09:13:45 +01:00
nikolajlauridsen 62048dc5a8 Merge branch 'release/17.4.2' into release/18.0
# Conflicts:
#	src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs
#	src/Umbraco.Core/Services/DocumentUrlService.cs
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	src/Umbraco.Web.UI.Client/src/external/uui/package.json
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
#	tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/PublishedContent/PublishedValueFallbackTests.cs
#	version.json
2026-05-21 09:25:44 +02:00
nikolajlauridsen 06b15157cf Merge branch 'release/17.4.2' into release/17.5.0
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
#	version.json
2026-05-21 09:18:11 +02:00
nikolajlauridsen 82f7830d26 Merge branch 'release/17.4.2' into v17/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
#	version.json
2026-05-21 09:16:39 +02:00
Andy Butland 337cd32258 Merge branch 'v17/dev' 2026-05-21 07:45:20 +02:00
MoleandGitHub b87d519bf2 Cache: Only write to url table on a single server in load balanced environments to remove lock contention (#22890)
* Move database writes out of cache refreshers

* add tests

* Fix up tests
2026-05-20 17:08:33 +02:00
Jacob OvergaardandClaude Opus 4.7 247935cc38 Tests: Fix unit tests broken by sync element fast-path and lazy property materialization
- ElementPickerValueConverterTests: also stub the new synchronous IPublishedElementCache.GetById,
  since Moq does not execute default interface implementations.
- PropertyCacheLevelTests.CacheUnknownTest: access a property inside Assert.Throws to trigger the
  now-lazy property wrapper materialization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:51:01 +02:00
Jacob OvergaardandGitHub b9c4c5be74 Test: adds 'mocha' and 'chai' as types for tsconfig (#22889)
fix(test): adds 'mocha' and 'chai' as types for tsconfig
2026-05-20 10:59:31 +01:00
Andy Butland d9ea76857b Merge branch 'release/18.0' of https://github.com/umbraco/Umbraco-CMS into release/18.0 2026-05-20 11:01:24 +02:00
Laura NetoandGitHub 2f520981aa Extension template: Use backoffice JSON options and other fixes (#22885)
* Extension template: Configure BackOffice JSON options and replace IUser response with WhoAmIResponseModel

Sets the extension template's backoffice API to use the BackOffice named JsonOptions so the extension's serializer is insulated from consumer-level overrides.

The sample whoAmI endpoint previously returned IUser directly. IUser is a Umbraco.Core domain interface, not an API contract - it has no JSON polymorphism configuration and its nested interface properties (e.g. IReadOnlyUserGroup) are not designed to be serialized as part of an HTTP response. Once the BackOffice JsonOptions activated UmbracoJsonTypeInfoResolver for the extension's OpenAPI document, schema generation produced incomplete output (no type information on the Groups property).

Replaces the return type with a flat WhoAmIResponseModel exposing only the fields the dashboard UI consumes (name, email, groups). Domain interfaces should not be exposed directly on a controller - always project into a dedicated response model.

* Extension template: Fully-qualify Cms.Core references and drop Umbraco.Extensions import

The composer and controller base referenced `Cms.Core.Constants...` in short form, which relied on namespace fallback from `Umbraco.Extension.Controllers` finding `Umbraco.Cms.Core`. When consumers instantiate the template with a non-Umbraco root namespace, that fallback breaks. References are now fully qualified as `Umbraco.Cms.Core.Constants...`.

Additionally, the `whoAmI` controller's `using Umbraco.Extensions;` was getting mangled by the template engine's token substitution of `Umbraco.Extension` into the consumer's name. Replaces `WhereNotNull()` with the BCL-only `OfType<string>()` so the controller no longer depends on the `Umbraco.Extensions` namespace.

* Extension template: Tighten whoAmI 204 guard in dashboard

The generated client returns a truthy empty data object (or null body) for a 204 response, so the previous `if (data)` check could pass and render `undefined` values in the notification. Checks `data?.email` instead - it's a required field on a real 200 response and absent in the 204 fallback.

* Extension template: Return 401 Unauthorized from whoAmI and simplify dashboard handling

When `BackOfficeSecurity.CurrentUser` is null, the sample `whoAmI` endpoint now returns `Unauthorized()` instead of `NoContent()`, matching the `GetCurrentUserController` pattern in the Management API. Drops the 204 ProducesResponseType so the OpenAPI spec only advertises 200 plus the framework-emitted 401.

The dashboard collapses its empty-data check into a single `error || !data` guard, moves the notification into the success branch, and regenerates the client to drop the now-unused 204 response.
2026-05-20 10:55:45 +02:00
af04872023 Content Editing: Fix save composition values on invariant content and save of default segment (closes #22800, #22865) (#22846)
* Fix edit of a variant property composed to an invariant document.

* Collapse multi-line guard comment to a single line per project policy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add unit test coverage for invariant content with a culture-variant composition property.

Adds a third mock document/document-type pair representing an invariant
content type whose flattened property list contains a culture-variant
property (the runtime shape produced when a variant composition is applied
to an invariant content type) and a setPropertyValue test asserting the
value is stored as a culture/segment-invariant entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove null guard for segment variant documents, as null segment is the default segment.

* update mock data and tests to include real compositions

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-05-20 10:53:46 +02:00
8aaac65f83 Content Editing: Fix save composition values on invariant content and save of default segment (closes #22800, #22865) (#22846)
* Fix edit of a variant property composed to an invariant document.

* Collapse multi-line guard comment to a single line per project policy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add unit test coverage for invariant content with a culture-variant composition property.

Adds a third mock document/document-type pair representing an invariant
content type whose flattened property list contains a culture-variant
property (the runtime shape produced when a variant composition is applied
to an invariant content type) and a setPropertyValue test asserting the
value is stored as a culture/segment-invariant entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove null guard for segment variant documents, as null segment is the default segment.

* update mock data and tests to include real compositions

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-05-20 10:52:09 +02:00
8d5826c61f Content Editing: Fix save composition values on invariant content and save of default segment (closes #22800, #22865) (#22846)
* Fix edit of a variant property composed to an invariant document.

* Collapse multi-line guard comment to a single line per project policy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add unit test coverage for invariant content with a culture-variant composition property.

Adds a third mock document/document-type pair representing an invariant
content type whose flattened property list contains a culture-variant
property (the runtime shape produced when a variant composition is applied
to an invariant content type) and a setPropertyValue test asserting the
value is stored as a culture/segment-invariant entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove null guard for segment variant documents, as null segment is the default segment.

* update mock data and tests to include real compositions

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-05-20 10:43:02 +02:00
Lee KelleherandGitHub 5a73f63cfd feat(components): adds umb-entity-frame component + Storybook stories (#22844)
* feat(components): adds `umb-entity-frame` component + Storybook stories

* fix(components): address review feedback for `umb-entity-frame`

- Remove `pointer-events: auto` from `.tab` so the overlay is truly passive
  (was intercepting events above the parent and causing hover flicker when
  toggled via opacity).
- Replace `--uui-color-surface` tab text with `--uui-color-selected-contrast`
  (the proper paired contrast token) and expose
  `--umb-entity-frame-contrast-color` so consumers can override when supplying
  a non-default `--umb-entity-frame-color`. Fixes contrast in dark and
  high-contrast themes.
- Add `aria-hidden="true"` to `.tab`; the frame is purely decorative and the
  parent owns the real semantics.
- Add a unit test verifying slot content takes precedence over the `label`
  property.

* Removed `aria-hidden` from the label tab

As will need to be used with assistive technologies.
2026-05-20 08:39:49 +00:00
Andy Butland 09af8c044b Merge branch 'v17/dev' 2026-05-20 10:32:08 +02:00
e463cd3a0c Log Viewer: Defensively handle corrupt log files (closes #22820) (#22826)
* Defensively handle log file corruptions by amalgamating errors per file and reporting as warning.

* Addressed code review comments.

* Use local reference to Newtonsoft.Json so it's clear we are only using it for exception handling.

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2026-05-20 07:22:54 +00:00
2901be793a Redirect URL Tracker: Remove ability to toggle from the UI (#22830)
* Remove the ability to enable or disable the redirect tracker from the UI.

* Addressed code review feedback.

* Update OpenApi.json.

* Regenerate backend SDK from updated OpenApi.json

* Update UI to use lozenge status indicator rather than an imperative action.

* Further UX tweak.

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-05-20 09:20:37 +02:00
Andy Butlandandmole f255fd7bff Bump version to 17.4.2. 2026-05-20 09:18:56 +02:00
7527de7c56 Migrations: Add auto upgrade coordination for load-balanced setups (#22815)
* Add auto upgrade coordination for load balanced setups

* Add tests

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(infrastructure): move TryBecomeLeaderAsync inside try/catch in UnattendedUpgradeBackgroundService

Ensures DB exceptions thrown during migration coordination set BootFailed
rather than faulting the background service silently.

* Fix feedback

* Update src/Umbraco.Infrastructure/Install/MigrationCoordinator.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Recheck state

* fix(tests): update concurrent race test for post-claim DetermineRuntimeLevel check

The winner now calls DetermineRuntimeLevel() once from the post-claim check
and must see Upgrading; the loser polls twice before seeing Run. Transition
the mock on the second call instead of the first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 09:18:56 +02:00
df12a3e467 Cache: Add scope-level cache version tier to reduce DB hits in bulk operations (#22563)
* Cache cacheversion on scope

* Add tests

* Cache: Use ConcurrentDictionary for the inner per-scope version map

The inner Dictionary<string, Guid> was not thread-safe. Replacing it
with ConcurrentDictionary<string, Guid> removes the hidden assumption
that the root scope is only accessed from a single thread at a time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update src/Umbraco.Core/Cache/IRepositoryCacheVersionAccessor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessorTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Skips failing test so we can run nightly. THIS NEEDS TO BE REVERTED

* Revert "Skips failing test so we can run nightly. THIS NEEDS TO BE REVERTED"

This reverts commit c34d1736c336b3fcf7803b44e88f6018fa45c275.

* Only write version once pr. scope

* Add tests

* Remove unnececary locks

* Fix thread-safety: replace HashSet with ConcurrentHashSet and use GetOrAdd to eliminate TOCTOU races

* Add unit tests for RepositoryCacheVersionService

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andreas Zerbst <andr317c@live.dk>
2026-05-20 09:18:56 +02:00
Andy Butland 1dc47bf4a1 Merge branch 'release/18.0' 2026-05-19 18:44:42 +02:00
Andy Butland 5f26c16c8e Add synchronous fast path for retrieval of cached elements (aligning documents and media from the previous cherry-pick from 17). 2026-05-19 18:43:27 +02:00
Andy Butland 896f449343 Children/Descendants: improve traversal performance (closes #22646) (#22742)
* Add benchmark test for measuring improvements to children and descendant retrieval.

* Remove unnecessary sort from retrieval of children.

* Return the result of the filtered collection of children/decendants without materialising.

* Lazily build property wrappers when materializing IPublishedContent.

* Cache the ordered children list on NavigationNode.

* Cache descendants per parent on the navigation snapshot.

* Add synchronous fast path for retrieved of cached content.

* Additional unit tests.

* Add TODO to make UpdateSortOrder internal.

* Addressed code review feedback.

* Further unit tests.

* Future-proofed code comments.
2026-05-19 18:32:09 +02:00
Andy Butland 0b86312f52 Children/Descendants: improve traversal performance (closes #22646) (#22742)
* Add benchmark test for measuring improvements to children and descendant retrieval.

* Remove unnecessary sort from retrieval of children.

* Return the result of the filtered collection of children/decendants without materialising.

* Lazily build property wrappers when materializing IPublishedContent.

* Cache the ordered children list on NavigationNode.

* Cache descendants per parent on the navigation snapshot.

* Add synchronous fast path for retrieved of cached content.

* Additional unit tests.

* Add TODO to make UpdateSortOrder internal.

* Addressed code review feedback.

* Further unit tests.

* Future-proofed code comments.
2026-05-19 18:01:12 +02:00
Andy ButlandandGitHub f4592111fa Children/Descendants: improve traversal performance (closes #22646) (#22742)
* Add benchmark test for measuring improvements to children and descendant retrieval.

* Remove unnecessary sort from retrieval of children.

* Return the result of the filtered collection of children/decendants without materialising.

* Lazily build property wrappers when materializing IPublishedContent.

* Cache the ordered children list on NavigationNode.

* Cache descendants per parent on the navigation snapshot.

* Add synchronous fast path for retrieved of cached content.

* Additional unit tests.

* Add TODO to make UpdateSortOrder internal.

* Addressed code review feedback.

* Further unit tests.

* Future-proofed code comments.
2026-05-19 18:00:34 +02:00
Andy Butland d6893dbf0b Merge branch 'v17/dev' 2026-05-19 17:59:45 +02:00
Niels LyngsøandAndy Butland c00e470d70 Slider: fix duplicated property editor settings properties (#22898)
* fix duplicate slider pe-settings properties

* remove comment

* avoid throws
2026-05-19 17:57:56 +02:00
Andy Butland 93ec661240 Output Caching: Correctly gate auto-registration of UseOutputCache() middleware (#22897)
Correct the gating of the call to UseOutputCache() to only proceed Umbraco managed caching via configuration is enabled, and not consider existing implementation specific registrations.
2026-05-19 17:57:48 +02:00
1c3e17740d Content Workspace: Load Data-Types based on Loaded Content Types (#22886)
* Load Data-Types based on Loaded Content Types

* Update Comment

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* mergeObservables approach

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-19 17:57:03 +02:00
Andy Butland 4c909d8ce8 Merge branch 'release/17.4.1' into release/17.5.0 2026-05-19 17:54:19 +02:00
Andy Butland 12c699d5bd Merge branch 'release/17.4.1' into v17/dev 2026-05-19 17:47:47 +02:00
Niels LyngsøandGitHub ba29b91301 Slider: fix duplicated property editor settings properties (#22898)
* fix duplicate slider pe-settings properties

* remove comment

* avoid throws
2026-05-19 14:19:34 +00:00
Andy ButlandandGitHub 336bffe4c4 Output Caching: Correctly gate auto-registration of UseOutputCache() middleware (#22897)
Correct the gating of the call to UseOutputCache() to only proceed Umbraco managed caching via configuration is enabled, and not consider existing implementation specific registrations.
2026-05-19 16:18:21 +02:00
426e516c61 Content Workspace: Load Data-Types based on Loaded Content Types (#22886)
* Load Data-Types based on Loaded Content Types

* Update Comment

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* mergeObservables approach

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-19 15:02:13 +02:00
Andy Butland c718a3ce12 Bump version to 17.4.1. 2026-05-19 15:01:44 +02:00
Jacob Overgaard 81c8afbd44 Merge remote-tracking branch 'origin/v17/dev' 2026-05-19 11:54:36 +02:00
Jacob Overgaard f352a2e90e Docs: clarify DefaultUILanguage vs fallback culture in package-development
Adds a 'Default UI language vs fallback culture' subsection so package
authors don't conflate the active UI locale (en-US by default) with the
fallback dictionary culture (en). A third-party language pack overriding
canonical keys must declare 'culture: en-US' on a default install,
otherwise the registry filters it out — the keys come from en.ts but
the override extension's culture has to match the active locale.

Surfaced by a tester report after PR #22743 merged the login screen's
localization into the backoffice client: the registry was forcing 'en'
active at boot (fixed in PR #22822) which masked the distinction, and
the docs didn't spell it out either.
2026-05-19 11:03:45 +02:00
Jacob Overgaard 259b6787a5 Localization: Honor DefaultUILanguage on initial load (closes #22808) (#22822)
* Localization: Honor DefaultUILanguage on initial load (closes #22808)

Closes #22808.

Previously, the configured DefaultUILanguage was silently overridden to
'en' at startup because the UmbLocalizationRegistry constructor called
loadLanguage(UMB_DEFAULT_LOCALIZATION_CULTURE) unconditionally. The
configured locale rendered into <html lang="..."> by Razor never had a
chance to flow through to the active language.

Changes:

- localization.registry.ts: stop forcing the active language to 'en'
  in the constructor. Initial state is canonicalised from
  document.documentElement.lang, falling back to 'en' for empty or
  malformed input. The extension filter now always includes the
  default culture alongside the active locale so 'en' translations
  remain available as a key-level fallback regardless of which
  language is active. A synchronous tap mirrors the active locale to
  document.lang and the manager when the state changes, so a fresh
  element rendered between loadLanguage() and the async translation
  load picks up the right language immediately.

- localization.manager.ts: drop the MutationObserver on
  document.documentElement and rely on the registry as the single
  channel for language changes. setActiveLanguage accepts a `silent`
  option so the synchronous tap can update fields without firing a
  consumer notification (translations may still be loading). A new
  notifyLanguageChanged() method is fired by the registry once
  translations are in place.

- app.element.ts: subscribe to umbLocalizationRegistry.currentLanguage
  in connectedCallback and mirror it onto the host element's lang
  attribute, so myApp.lang reflects the source of truth rather than a
  stale snapshot of <html lang>.

- auth.element.ts (login app): same lang subscription, plus after the
  slim backoffice controller registers extensions, prefer the
  visitor's navigator.language if a matching localization extension
  exists (falls through baseName -> language -> en automatically).

Tests: new initialization tests for the registry, manager
setActiveLanguage tests, and the controller tests refactored to use
the new explicit setActiveLanguage API instead of writing directly to
document.documentElement.lang.

* Login: Only override DefaultUILanguage with navigator.language when default has no translation

If the admin sets DefaultUILanguage to a language we have a translation for,
respect that choice over the visitor's browser language. Falling back to
navigator.language only when the configured default isn't available avoids
silently ignoring the admin's explicit setting (e.g., DefaultUILanguage='da-DK'
on a site whose visitor's browser is 'en-GB' should still show Danish).

* Simplify: split setActiveLanguage from notifyLanguageChanged

Drop the silent option in favor of two intent-revealing methods:
setActiveLanguage updates the active language and direction without
side-effects; notifyLanguageChanged tells all connected controllers
to re-render against the current state. Callers compose them based
on what they need (the registry's pipeline updates language sync
then flushes notifications after async translation load).

Also extracts baseLocaleOf() helper, simplifies the navigator.language
match logic in the login app's #applyPreferredLanguage, and removes
narration-style comments in the new code.

* Restore deprecated UmbLocalizationManager.updateAll for backward compat

The old MutationObserver-driven updateAll() field was technically part
of the manager's public surface. Restore it as a deprecated alias that
reads document.lang/dir and forwards to setActiveLanguage + notifyLanguageChanged,
with a runtime UmbDeprecation warning pointing consumers at the new API.

* Fix deprecation removal version to v20 + correct baseLocaleOf JSDoc

Per the deprecation policy in CLAUDE.md (current major + 2): a method
deprecated in v18 must remain through v19 before removal, so the
earliest removal is v20, not v19.

Also corrects the baseLocaleOf JSDoc — Intl.Locale.baseName can include
script subtags (e.g. 'zh-Hant-TW'), not just language and region.

* Scope the active language to the host element, drop navigator.language

- Razor now sets `lang` on `<umb-app>` and `<umb-auth>` from
  DefaultUILanguage. The element passes its lang through on connect, so
  the host owns its own scope — future multi-backoffice scenarios (e.g.
  signing into two Umbraco Cloud sites in the same document) get their
  own language without fighting over a global `<html lang>`.

- The registry no longer reads or writes `document.documentElement.lang`.
  Host elements drive it via `loadLanguage()`; `<html lang>` stays as
  whatever Razor rendered.

- Removed the navigator.language preference detection in the login app.
  Not in scope for the bug fix and adds behavior the admin can't opt out
  of. The existing current-user-locale flow already handles per-user
  preference after login.

- Tests updated to assert on `umbLocalizationManager.documentLanguage`
  instead of `document.documentElement.lang`.

* Set <html lang="en"> to match the static (noscript) text in the templates

The page's `<html lang>` should describe the language of the document's
own innate content. Both Index.cshtml files only contain English static
text (the noscript fallback), so the page-level lang is now "en".

The dynamic UI inside <umb-app> / <umb-auth> carries its own `lang`
attribute (from DefaultUILanguage), which overrides for that subtree —
correct per the HTML spec for language inheritance.

* Drop deprecated UmbLocalizationManager.updateAll

It was public as an artifact of being an arrow function so it could be
passed to a MutationObserver without binding — not because it was
intended as part of the public API. External usage is effectively
zero, and the new explicit setActiveLanguage + notifyLanguageChanged
covers anyone who did reach for it.

* Docs: document active-language-on-host pattern in package-development.md

After PR #22822, the active UI language is driven by the shell elements
(<umb-app>, <umb-auth>) via their own lang attribute, not by <html lang>.
Document that so future contributors don't reach for the global.

* Collapse setActiveLanguage + notifyLanguageChanged into one method

The silent-write path is just `manager.documentLanguage = ...` — no new
method needed; the field is already public and was always writable. The
notify path keeps setActiveLanguage, which now both sets and notifies.

Net: one new public method on the manager instead of two.

* Inline the active-language write in the registry, drop setActiveLanguage

The previous version added setActiveLanguage on the manager as
a 'cleaner API' than direct field writes. But the manager's fields
(documentLanguage, documentDirection, connectedControllers) have
always been public, the registry is the only caller, and the wrapper
was just more public surface to maintain through the eventual
manager/registry collapse.

Net: -70 lines across the test file, no new public methods on the
manager, the registry pipeline writes the fields and iterates the
controllers directly where it would have called setActiveLanguage.

* Document that documentLanguage/Direction are read-only for consumers

Note in JSDoc that the only supported way to change the active language
is umbLocalizationRegistry.loadLanguage(). The fields stay writable for
the registry pipeline (cross-module internal); the comment is here so
the next contributor doesn't reach for them as a shortcut and end up
with the manager state out of sync with what's actually loaded.
2026-05-19 10:57:45 +02:00
Jacob Overgaard 1637d9b158 Merge branch 'release/17.5.0' into v17/dev 2026-05-19 10:55:58 +02:00
Jacob Overgaard 7737cd3d40 Localization: Honor DefaultUILanguage on initial load (closes #22808) (#22822)
* Localization: Honor DefaultUILanguage on initial load (closes #22808)

Closes #22808.

Previously, the configured DefaultUILanguage was silently overridden to
'en' at startup because the UmbLocalizationRegistry constructor called
loadLanguage(UMB_DEFAULT_LOCALIZATION_CULTURE) unconditionally. The
configured locale rendered into <html lang="..."> by Razor never had a
chance to flow through to the active language.

Changes:

- localization.registry.ts: stop forcing the active language to 'en'
  in the constructor. Initial state is canonicalised from
  document.documentElement.lang, falling back to 'en' for empty or
  malformed input. The extension filter now always includes the
  default culture alongside the active locale so 'en' translations
  remain available as a key-level fallback regardless of which
  language is active. A synchronous tap mirrors the active locale to
  document.lang and the manager when the state changes, so a fresh
  element rendered between loadLanguage() and the async translation
  load picks up the right language immediately.

- localization.manager.ts: drop the MutationObserver on
  document.documentElement and rely on the registry as the single
  channel for language changes. setActiveLanguage accepts a `silent`
  option so the synchronous tap can update fields without firing a
  consumer notification (translations may still be loading). A new
  notifyLanguageChanged() method is fired by the registry once
  translations are in place.

- app.element.ts: subscribe to umbLocalizationRegistry.currentLanguage
  in connectedCallback and mirror it onto the host element's lang
  attribute, so myApp.lang reflects the source of truth rather than a
  stale snapshot of <html lang>.

- auth.element.ts (login app): same lang subscription, plus after the
  slim backoffice controller registers extensions, prefer the
  visitor's navigator.language if a matching localization extension
  exists (falls through baseName -> language -> en automatically).

Tests: new initialization tests for the registry, manager
setActiveLanguage tests, and the controller tests refactored to use
the new explicit setActiveLanguage API instead of writing directly to
document.documentElement.lang.

* Login: Only override DefaultUILanguage with navigator.language when default has no translation

If the admin sets DefaultUILanguage to a language we have a translation for,
respect that choice over the visitor's browser language. Falling back to
navigator.language only when the configured default isn't available avoids
silently ignoring the admin's explicit setting (e.g., DefaultUILanguage='da-DK'
on a site whose visitor's browser is 'en-GB' should still show Danish).

* Simplify: split setActiveLanguage from notifyLanguageChanged

Drop the silent option in favor of two intent-revealing methods:
setActiveLanguage updates the active language and direction without
side-effects; notifyLanguageChanged tells all connected controllers
to re-render against the current state. Callers compose them based
on what they need (the registry's pipeline updates language sync
then flushes notifications after async translation load).

Also extracts baseLocaleOf() helper, simplifies the navigator.language
match logic in the login app's #applyPreferredLanguage, and removes
narration-style comments in the new code.

* Restore deprecated UmbLocalizationManager.updateAll for backward compat

The old MutationObserver-driven updateAll() field was technically part
of the manager's public surface. Restore it as a deprecated alias that
reads document.lang/dir and forwards to setActiveLanguage + notifyLanguageChanged,
with a runtime UmbDeprecation warning pointing consumers at the new API.

* Fix deprecation removal version to v20 + correct baseLocaleOf JSDoc

Per the deprecation policy in CLAUDE.md (current major + 2): a method
deprecated in v18 must remain through v19 before removal, so the
earliest removal is v20, not v19.

Also corrects the baseLocaleOf JSDoc — Intl.Locale.baseName can include
script subtags (e.g. 'zh-Hant-TW'), not just language and region.

* Scope the active language to the host element, drop navigator.language

- Razor now sets `lang` on `<umb-app>` and `<umb-auth>` from
  DefaultUILanguage. The element passes its lang through on connect, so
  the host owns its own scope — future multi-backoffice scenarios (e.g.
  signing into two Umbraco Cloud sites in the same document) get their
  own language without fighting over a global `<html lang>`.

- The registry no longer reads or writes `document.documentElement.lang`.
  Host elements drive it via `loadLanguage()`; `<html lang>` stays as
  whatever Razor rendered.

- Removed the navigator.language preference detection in the login app.
  Not in scope for the bug fix and adds behavior the admin can't opt out
  of. The existing current-user-locale flow already handles per-user
  preference after login.

- Tests updated to assert on `umbLocalizationManager.documentLanguage`
  instead of `document.documentElement.lang`.

* Set <html lang="en"> to match the static (noscript) text in the templates

The page's `<html lang>` should describe the language of the document's
own innate content. Both Index.cshtml files only contain English static
text (the noscript fallback), so the page-level lang is now "en".

The dynamic UI inside <umb-app> / <umb-auth> carries its own `lang`
attribute (from DefaultUILanguage), which overrides for that subtree —
correct per the HTML spec for language inheritance.

* Drop deprecated UmbLocalizationManager.updateAll

It was public as an artifact of being an arrow function so it could be
passed to a MutationObserver without binding — not because it was
intended as part of the public API. External usage is effectively
zero, and the new explicit setActiveLanguage + notifyLanguageChanged
covers anyone who did reach for it.

* Docs: document active-language-on-host pattern in package-development.md

After PR #22822, the active UI language is driven by the shell elements
(<umb-app>, <umb-auth>) via their own lang attribute, not by <html lang>.
Document that so future contributors don't reach for the global.

* Collapse setActiveLanguage + notifyLanguageChanged into one method

The silent-write path is just `manager.documentLanguage = ...` — no new
method needed; the field is already public and was always writable. The
notify path keeps setActiveLanguage, which now both sets and notifies.

Net: one new public method on the manager instead of two.

* Inline the active-language write in the registry, drop setActiveLanguage

The previous version added setActiveLanguage on the manager as
a 'cleaner API' than direct field writes. But the manager's fields
(documentLanguage, documentDirection, connectedControllers) have
always been public, the registry is the only caller, and the wrapper
was just more public surface to maintain through the eventual
manager/registry collapse.

Net: -70 lines across the test file, no new public methods on the
manager, the registry pipeline writes the fields and iterates the
controllers directly where it would have called setActiveLanguage.

* Document that documentLanguage/Direction are read-only for consumers

Note in JSDoc that the only supported way to change the active language
is umbLocalizationRegistry.loadLanguage(). The fields stay writable for
the registry pipeline (cross-module internal); the comment is here so
the next contributor doesn't reach for them as a shortcut and end up
with the manager state out of sync with what's actually loaded.
2026-05-19 10:52:55 +02:00
Andy Butland beb9fcf4e2 Merge branch 'release/18.0' 2026-05-19 10:39:40 +02:00
Andy Butland 8dd3ab51f4 Updated failing unit test. 2026-05-19 10:39:09 +02:00
Jacob OvergaardandGitHub 23851c872f Localization: Honor DefaultUILanguage on initial load (closes #22808) (#22822)
* Localization: Honor DefaultUILanguage on initial load (closes #22808)

Closes #22808.

Previously, the configured DefaultUILanguage was silently overridden to
'en' at startup because the UmbLocalizationRegistry constructor called
loadLanguage(UMB_DEFAULT_LOCALIZATION_CULTURE) unconditionally. The
configured locale rendered into <html lang="..."> by Razor never had a
chance to flow through to the active language.

Changes:

- localization.registry.ts: stop forcing the active language to 'en'
  in the constructor. Initial state is canonicalised from
  document.documentElement.lang, falling back to 'en' for empty or
  malformed input. The extension filter now always includes the
  default culture alongside the active locale so 'en' translations
  remain available as a key-level fallback regardless of which
  language is active. A synchronous tap mirrors the active locale to
  document.lang and the manager when the state changes, so a fresh
  element rendered between loadLanguage() and the async translation
  load picks up the right language immediately.

- localization.manager.ts: drop the MutationObserver on
  document.documentElement and rely on the registry as the single
  channel for language changes. setActiveLanguage accepts a `silent`
  option so the synchronous tap can update fields without firing a
  consumer notification (translations may still be loading). A new
  notifyLanguageChanged() method is fired by the registry once
  translations are in place.

- app.element.ts: subscribe to umbLocalizationRegistry.currentLanguage
  in connectedCallback and mirror it onto the host element's lang
  attribute, so myApp.lang reflects the source of truth rather than a
  stale snapshot of <html lang>.

- auth.element.ts (login app): same lang subscription, plus after the
  slim backoffice controller registers extensions, prefer the
  visitor's navigator.language if a matching localization extension
  exists (falls through baseName -> language -> en automatically).

Tests: new initialization tests for the registry, manager
setActiveLanguage tests, and the controller tests refactored to use
the new explicit setActiveLanguage API instead of writing directly to
document.documentElement.lang.

* Login: Only override DefaultUILanguage with navigator.language when default has no translation

If the admin sets DefaultUILanguage to a language we have a translation for,
respect that choice over the visitor's browser language. Falling back to
navigator.language only when the configured default isn't available avoids
silently ignoring the admin's explicit setting (e.g., DefaultUILanguage='da-DK'
on a site whose visitor's browser is 'en-GB' should still show Danish).

* Simplify: split setActiveLanguage from notifyLanguageChanged

Drop the silent option in favor of two intent-revealing methods:
setActiveLanguage updates the active language and direction without
side-effects; notifyLanguageChanged tells all connected controllers
to re-render against the current state. Callers compose them based
on what they need (the registry's pipeline updates language sync
then flushes notifications after async translation load).

Also extracts baseLocaleOf() helper, simplifies the navigator.language
match logic in the login app's #applyPreferredLanguage, and removes
narration-style comments in the new code.

* Restore deprecated UmbLocalizationManager.updateAll for backward compat

The old MutationObserver-driven updateAll() field was technically part
of the manager's public surface. Restore it as a deprecated alias that
reads document.lang/dir and forwards to setActiveLanguage + notifyLanguageChanged,
with a runtime UmbDeprecation warning pointing consumers at the new API.

* Fix deprecation removal version to v20 + correct baseLocaleOf JSDoc

Per the deprecation policy in CLAUDE.md (current major + 2): a method
deprecated in v18 must remain through v19 before removal, so the
earliest removal is v20, not v19.

Also corrects the baseLocaleOf JSDoc — Intl.Locale.baseName can include
script subtags (e.g. 'zh-Hant-TW'), not just language and region.

* Scope the active language to the host element, drop navigator.language

- Razor now sets `lang` on `<umb-app>` and `<umb-auth>` from
  DefaultUILanguage. The element passes its lang through on connect, so
  the host owns its own scope — future multi-backoffice scenarios (e.g.
  signing into two Umbraco Cloud sites in the same document) get their
  own language without fighting over a global `<html lang>`.

- The registry no longer reads or writes `document.documentElement.lang`.
  Host elements drive it via `loadLanguage()`; `<html lang>` stays as
  whatever Razor rendered.

- Removed the navigator.language preference detection in the login app.
  Not in scope for the bug fix and adds behavior the admin can't opt out
  of. The existing current-user-locale flow already handles per-user
  preference after login.

- Tests updated to assert on `umbLocalizationManager.documentLanguage`
  instead of `document.documentElement.lang`.

* Set <html lang="en"> to match the static (noscript) text in the templates

The page's `<html lang>` should describe the language of the document's
own innate content. Both Index.cshtml files only contain English static
text (the noscript fallback), so the page-level lang is now "en".

The dynamic UI inside <umb-app> / <umb-auth> carries its own `lang`
attribute (from DefaultUILanguage), which overrides for that subtree —
correct per the HTML spec for language inheritance.

* Drop deprecated UmbLocalizationManager.updateAll

It was public as an artifact of being an arrow function so it could be
passed to a MutationObserver without binding — not because it was
intended as part of the public API. External usage is effectively
zero, and the new explicit setActiveLanguage + notifyLanguageChanged
covers anyone who did reach for it.

* Docs: document active-language-on-host pattern in package-development.md

After PR #22822, the active UI language is driven by the shell elements
(<umb-app>, <umb-auth>) via their own lang attribute, not by <html lang>.
Document that so future contributors don't reach for the global.

* Collapse setActiveLanguage + notifyLanguageChanged into one method

The silent-write path is just `manager.documentLanguage = ...` — no new
method needed; the field is already public and was always writable. The
notify path keeps setActiveLanguage, which now both sets and notifies.

Net: one new public method on the manager instead of two.

* Inline the active-language write in the registry, drop setActiveLanguage

The previous version added setActiveLanguage on the manager as
a 'cleaner API' than direct field writes. But the manager's fields
(documentLanguage, documentDirection, connectedControllers) have
always been public, the registry is the only caller, and the wrapper
was just more public surface to maintain through the eventual
manager/registry collapse.

Net: -70 lines across the test file, no new public methods on the
manager, the registry pipeline writes the fields and iterates the
controllers directly where it would have called setActiveLanguage.

* Document that documentLanguage/Direction are read-only for consumers

Note in JSDoc that the only supported way to change the active language
is umbLocalizationRegistry.loadLanguage(). The fields stay writable for
the registry pipeline (cross-module internal); the comment is here so
the next contributor doesn't reach for them as a shortcut and end up
with the manager state out of sync with what's actually loaded.
2026-05-19 09:21:03 +01:00
Andreas Lykke BorgandGitHub 7e4ef037b9 Issue template: Update version lookup instructions in bug report template (closes #22867) (#22881)
Update version lookup instructions in bug report template
2026-05-19 09:39:46 +02:00
Andy Butland 003656e21e Merge branch 'release/18.0' 2026-05-19 09:36:09 +02:00
432031e287 Elements: Fix UdiEntityTypeHelper.ToUmbracoObjectType() for Element entities (#22874)
* Elements: Fix UdiEntityTypeHelper.ToUmbracoObjectType() for Element entities

Adds the missing Element and ElementContainer cases so the conversion is
symmetric with FromUmbracoObjectType().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove UdiEntityTypeHelperTests

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:33:49 +02:00
Andy Butland 2b85dd5fd6 Merge branch 'v17/dev' 2026-05-19 09:17:23 +02:00
62eb772936 Blueprints: Fix UdiEntityTypeHelper.ToUmbracoObjectType() for document blueprint containers (#22875)
* Blueprints: Fix UdiEntityTypeHelper.ToUmbracoObjectType() for document blueprint containers

Adds the missing DocumentBlueprintContainer case so the conversion is
symmetric with FromUmbracoObjectType().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add missing case for MemberTypeContainer.

* Use reflection to ensure other future missed cases are surfaced without having to explicitly extend the tests.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-19 09:14:52 +02:00
139ac6ad72 Blueprints: Fix UdiEntityTypeHelper.ToUmbracoObjectType() for document blueprint containers (#22875)
* Blueprints: Fix UdiEntityTypeHelper.ToUmbracoObjectType() for document blueprint containers

Adds the missing DocumentBlueprintContainer case so the conversion is
symmetric with FromUmbracoObjectType().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add missing case for MemberTypeContainer.

* Use reflection to ensure other future missed cases are surfaced without having to explicitly extend the tests.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-19 09:13:52 +02:00
cd4521bd77 Blueprints: Fix UdiEntityTypeHelper.ToUmbracoObjectType() for document blueprint containers (#22875)
* Blueprints: Fix UdiEntityTypeHelper.ToUmbracoObjectType() for document blueprint containers

Adds the missing DocumentBlueprintContainer case so the conversion is
symmetric with FromUmbracoObjectType().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add missing case for MemberTypeContainer.

* Use reflection to ensure other future missed cases are surfaced without having to explicitly extend the tests.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-19 09:11:09 +02:00
cd1524f810 Elements: Fix GetUdi() extension methods for Element entities (#22873)
Adds the missing GetUdi() overloads for IElement so v18 Global Elements
produce their umb://element/{key} identifier through the same extension
surface used for documents, media and members.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:36:27 +02:00
80e2764eda Migrations: Add auto upgrade coordination for load-balanced setups (#22815)
* Add auto upgrade coordination for load balanced setups

* Add tests

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(infrastructure): move TryBecomeLeaderAsync inside try/catch in UnattendedUpgradeBackgroundService

Ensures DB exceptions thrown during migration coordination set BootFailed
rather than faulting the background service silently.

* Fix feedback

* Update src/Umbraco.Infrastructure/Install/MigrationCoordinator.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Recheck state

* fix(tests): update concurrent race test for post-claim DetermineRuntimeLevel check

The winner now calls DetermineRuntimeLevel() once from the post-claim check
and must see Upgrading; the loser polls twice before seeing Run. Transition
the mock on the second call instead of the first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 12:06:34 +02:00
d9bb17de2a Relation Type: Migrate custom table collection view to generic table (#22837)
* migrate relation type table collection view to table kind

* update page locator

* Request relations when workspace unique is set

* fix types

* split models

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Use constant for relation type collection alias + remove redundant fields

* Add observer keys in relation-type workspace view

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-15 08:26:42 +00:00
72fdf281fd Backoffice: Provide entity context via UMB_ENTITY_CONTEXT in menu components (#22835)
* Use UmbEntityContext for entity actions

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-15 09:26:07 +02:00
Andy ButlandandGitHub b24c9ba8ac Log Viewer: Updated the saved log viewer searches for new installs to reference Umbraco.Cms instead of Umbraco.Core. (#22843)
* Updated the saved log viewer searches for new installs to reference Umbraco.Cms instead of Umbraco.Core.

* Update mock and default data too.
2026-05-15 08:29:09 +09:00
2377e9a555 Cache: Add scope-level cache version tier to reduce DB hits in bulk operations (#22563)
* Cache cacheversion on scope

* Add tests

* Cache: Use ConcurrentDictionary for the inner per-scope version map

The inner Dictionary<string, Guid> was not thread-safe. Replacing it
with ConcurrentDictionary<string, Guid> removes the hidden assumption
that the root scope is only accessed from a single thread at a time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update src/Umbraco.Core/Cache/IRepositoryCacheVersionAccessor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessorTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Skips failing test so we can run nightly. THIS NEEDS TO BE REVERTED

* Revert "Skips failing test so we can run nightly. THIS NEEDS TO BE REVERTED"

This reverts commit c34d1736c336b3fcf7803b44e88f6018fa45c275.

* Only write version once pr. scope

* Add tests

* Remove unnececary locks

* Fix thread-safety: replace HashSet with ConcurrentHashSet and use GetOrAdd to eliminate TOCTOU races

* Add unit tests for RepositoryCacheVersionService

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andreas Zerbst <andr317c@live.dk>
2026-05-15 08:28:06 +09:00
Andy Butland f964a18b5b Merge branch 'release/17.5.0' of https://github.com/umbraco/Umbraco-CMS into release/17.5.0 2026-05-14 19:10:30 +02:00
Lee KelleherandAndy Butland 2369f00544 Mocks: Add missing signalR property to mock server configuration response (#22849)
Mocks: Add missing signalR property to mock server configuration response

The GetServerConfigurationResponse type was updated in #22700 to require
a signalR.skipNegotiation property, but the MSW mock handler was not
updated to match, causing a tsc compilation error.
2026-05-14 19:08:11 +02:00
Lee KelleherandGitHub 0f438c551c Mocks: Add missing signalR property to mock server configuration response (#22849)
Mocks: Add missing signalR property to mock server configuration response

The GetServerConfigurationResponse type was updated in #22700 to require
a signalR.skipNegotiation property, but the MSW mock handler was not
updated to match, causing a tsc compilation error.
2026-05-14 17:05:55 +00:00
Sebastiaan Janssen d822518dd3 Core: Preserve path case in ShadowFileSystem (#22838)
* Core: Preserve path case in ShadowFileSystem

ShadowFileSystem stored staged files at their original case via _sfs.AddFile
but tracked them under a lowercased key (NormPath calling ToLowerInvariant).
On Complete(), Inner.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)) reconstructed
the staged file's path from the lowercased key, so on case-sensitive file
systems (Linux) File.Move failed with FileNotFoundException whenever a path
contained any uppercase character.

Drop the ToLowerInvariant from NormPath and switch the tracking dictionary
to StringComparer.OrdinalIgnoreCase. Lookups remain case-insensitive
(matching Windows semantics) while the stored key now matches what was
written to disk. IsChild/IsDescendant updated to OrdinalIgnoreCase
StartsWith for consistency.

Added regression test reproducing the original FileNotFoundException with
Views/PageNotFound.cshtml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename regression test to follow Can_ naming convention

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Track canonical staged path per shadow node

The case-insensitive node dictionary preserved only the first inserted
key, so re-staging a logical path with a different case (e.g. AddFile
"Views/Foo.cshtml" then "views/foo.cshtml") wrote a phantom second file
to _sfs on Linux while Complete still resolved the original key — leaving
orphaned shadow files and committing stale content.

Track the original-case staged path on each ShadowNode and route all
_sfs operations (AddFile, OpenFile, GetFullPath, GetLastModified,
GetCreated, GetSize, MoveFile, Complete) through that canonical path.
Inner.AddFile on commit still uses the stored dictionary key, so the
destination case in the inner file system is unchanged.

Expanded the regression test to also exercise OpenFile, GetSize and
AddFile against a different-cased path, and to assert that the staged
file is written exactly once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Make ShadowNode.CanonicalPath non-nullable

Every node now carries the original-case path it tracks, set at construction.
This removes the defensive 'sf.CanonicalPath ?? path' fallbacks at the read
sites (OpenFile, GetFullPath, GetLastModified, GetCreated, GetSize, Complete)
which were unreachable but noise.

The GetCanonicalPath helper is gone; AddFile and MoveFile now use the existing
node variable inline ('sf?.CanonicalPath ?? path' — node can legitimately be
null when staging a path for the first time).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address Copilot review: normalize delete key, cross-platform test

DeleteDirectory(recursive=false) stored the deletion marker under the
caller-supplied path (which can contain backslashes) instead of the
normalized key, so a follow-up NormPath-based lookup could miss the
deletion and IsChild scans could become inconsistent. Use normPath.

The shadow-second-file assertion in the regression test used
File.Exists on a different-cased path; that returns true on
case-insensitive file systems (Windows / default macOS) regardless of
the actual stored case, so the assertion was platform-dependent.
Replaced it with a directory-count check that's cross-platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add cross-platform regression test so any reversion would be caught on a non-case sensitive file system.

* Cleaned up warnings, obsoletions and comments in the existing tests.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
(cherry picked from commit abfa8cb144)
2026-05-14 10:42:20 +02:00
Sebastiaan Janssen 136c494d82 Core: Preserve path case in ShadowFileSystem (#22838)
* Core: Preserve path case in ShadowFileSystem

ShadowFileSystem stored staged files at their original case via _sfs.AddFile
but tracked them under a lowercased key (NormPath calling ToLowerInvariant).
On Complete(), Inner.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)) reconstructed
the staged file's path from the lowercased key, so on case-sensitive file
systems (Linux) File.Move failed with FileNotFoundException whenever a path
contained any uppercase character.

Drop the ToLowerInvariant from NormPath and switch the tracking dictionary
to StringComparer.OrdinalIgnoreCase. Lookups remain case-insensitive
(matching Windows semantics) while the stored key now matches what was
written to disk. IsChild/IsDescendant updated to OrdinalIgnoreCase
StartsWith for consistency.

Added regression test reproducing the original FileNotFoundException with
Views/PageNotFound.cshtml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename regression test to follow Can_ naming convention

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Track canonical staged path per shadow node

The case-insensitive node dictionary preserved only the first inserted
key, so re-staging a logical path with a different case (e.g. AddFile
"Views/Foo.cshtml" then "views/foo.cshtml") wrote a phantom second file
to _sfs on Linux while Complete still resolved the original key — leaving
orphaned shadow files and committing stale content.

Track the original-case staged path on each ShadowNode and route all
_sfs operations (AddFile, OpenFile, GetFullPath, GetLastModified,
GetCreated, GetSize, MoveFile, Complete) through that canonical path.
Inner.AddFile on commit still uses the stored dictionary key, so the
destination case in the inner file system is unchanged.

Expanded the regression test to also exercise OpenFile, GetSize and
AddFile against a different-cased path, and to assert that the staged
file is written exactly once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Make ShadowNode.CanonicalPath non-nullable

Every node now carries the original-case path it tracks, set at construction.
This removes the defensive 'sf.CanonicalPath ?? path' fallbacks at the read
sites (OpenFile, GetFullPath, GetLastModified, GetCreated, GetSize, Complete)
which were unreachable but noise.

The GetCanonicalPath helper is gone; AddFile and MoveFile now use the existing
node variable inline ('sf?.CanonicalPath ?? path' — node can legitimately be
null when staging a path for the first time).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address Copilot review: normalize delete key, cross-platform test

DeleteDirectory(recursive=false) stored the deletion marker under the
caller-supplied path (which can contain backslashes) instead of the
normalized key, so a follow-up NormPath-based lookup could miss the
deletion and IsChild scans could become inconsistent. Use normPath.

The shadow-second-file assertion in the regression test used
File.Exists on a different-cased path; that returns true on
case-insensitive file systems (Windows / default macOS) regardless of
the actual stored case, so the assertion was platform-dependent.
Replaced it with a directory-count check that's cross-platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add cross-platform regression test so any reversion would be caught on a non-case sensitive file system.

* Cleaned up warnings, obsoletions and comments in the existing tests.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
(cherry picked from commit abfa8cb144)
2026-05-14 10:42:07 +02:00
abfa8cb144 Core: Preserve path case in ShadowFileSystem (#22838)
* Core: Preserve path case in ShadowFileSystem

ShadowFileSystem stored staged files at their original case via _sfs.AddFile
but tracked them under a lowercased key (NormPath calling ToLowerInvariant).
On Complete(), Inner.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)) reconstructed
the staged file's path from the lowercased key, so on case-sensitive file
systems (Linux) File.Move failed with FileNotFoundException whenever a path
contained any uppercase character.

Drop the ToLowerInvariant from NormPath and switch the tracking dictionary
to StringComparer.OrdinalIgnoreCase. Lookups remain case-insensitive
(matching Windows semantics) while the stored key now matches what was
written to disk. IsChild/IsDescendant updated to OrdinalIgnoreCase
StartsWith for consistency.

Added regression test reproducing the original FileNotFoundException with
Views/PageNotFound.cshtml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename regression test to follow Can_ naming convention

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Track canonical staged path per shadow node

The case-insensitive node dictionary preserved only the first inserted
key, so re-staging a logical path with a different case (e.g. AddFile
"Views/Foo.cshtml" then "views/foo.cshtml") wrote a phantom second file
to _sfs on Linux while Complete still resolved the original key — leaving
orphaned shadow files and committing stale content.

Track the original-case staged path on each ShadowNode and route all
_sfs operations (AddFile, OpenFile, GetFullPath, GetLastModified,
GetCreated, GetSize, MoveFile, Complete) through that canonical path.
Inner.AddFile on commit still uses the stored dictionary key, so the
destination case in the inner file system is unchanged.

Expanded the regression test to also exercise OpenFile, GetSize and
AddFile against a different-cased path, and to assert that the staged
file is written exactly once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Make ShadowNode.CanonicalPath non-nullable

Every node now carries the original-case path it tracks, set at construction.
This removes the defensive 'sf.CanonicalPath ?? path' fallbacks at the read
sites (OpenFile, GetFullPath, GetLastModified, GetCreated, GetSize, Complete)
which were unreachable but noise.

The GetCanonicalPath helper is gone; AddFile and MoveFile now use the existing
node variable inline ('sf?.CanonicalPath ?? path' — node can legitimately be
null when staging a path for the first time).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address Copilot review: normalize delete key, cross-platform test

DeleteDirectory(recursive=false) stored the deletion marker under the
caller-supplied path (which can contain backslashes) instead of the
normalized key, so a follow-up NormPath-based lookup could miss the
deletion and IsChild scans could become inconsistent. Use normPath.

The shadow-second-file assertion in the regression test used
File.Exists on a different-cased path; that returns true on
case-insensitive file systems (Windows / default macOS) regardless of
the actual stored case, so the assertion was platform-dependent.
Replaced it with a directory-count check that's cross-platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add cross-platform regression test so any reversion would be caught on a non-case sensitive file system.

* Cleaned up warnings, obsoletions and comments in the existing tests.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-14 10:36:47 +02:00
Andy Butland 21ab470d88 Merge branch 'release/17.4.0' into release/17.5.0 2026-05-14 08:31:39 +02:00
Andy Butland 5dd8378c57 Merge branch 'release/17.4.0' into v17/dev 2026-05-14 08:30:01 +02:00
Andy Butland 60655da1c2 Bump version to 18.0.0-beta3. 2026-05-14 08:14:25 +02:00
Andy Butland 4b82828a23 Merge branch 'release/18.0' 2026-05-14 08:11:39 +02:00
Jacob OvergaardandGitHub 756510d9e7 Backoffice: Fix typedoc UI API docs generation (#22836)
The Generate API Docs CI step (npm run generate:ui-api-docs) has been
failing with 3284 TypeScript errors since the TS 5.9.3 -> 6.0.3 bump in
PR #22591. TS 6 stopped auto-loading @types/* under moduleResolution:
"bundler", so every .test.ts file in the program fails to find describe,
it, beforeEach, etc., and typedoc aborts before emitting anything.

Point typedoc at a dedicated tsconfig.typedoc.json that narrows include
to src/**/*.ts + index.ts and excludes *.test.ts and *.stories.ts.
Entry points come from package.json exports and all live under src/, so
the docs build no longer drags test files, stories, mocks, e2e specs,
or storybook stories through the TS program.

Verified locally: npm run generate:ui-api-docs exits 0 and writes
6533 files under src/Umbraco.Web.UI.Client/ui-api/.
2026-05-14 06:43:22 +02:00
Jacob OvergaardandAndy Butland 2954578386 Backoffice: Preserve prerelease tag when hoisting peer dependencies (#22841)
The publish cleanse step strips the prerelease suffix from hoisted dependency
ranges via `semver.minVersion(...).major/minor/patch`. For `^2.0.0-rc.1`
this produced `^2.0.0`, which no published `@umbraco-ui/uui` version
currently satisfies, breaking extension installs against
`@umbraco-cms/backoffice@18.0.0-beta1`+.

Use the full SemVer (including any prerelease) as the floor so
`^2.0.0-rc.1` stays satisfiable by the actual published rc.
2026-05-13 22:52:51 +02:00
Jacob OvergaardandGitHub 3e7c1fa8e4 Backoffice: Preserve prerelease tag when hoisting peer dependencies (#22841)
The publish cleanse step strips the prerelease suffix from hoisted dependency
ranges via `semver.minVersion(...).major/minor/patch`. For `^2.0.0-rc.1`
this produced `^2.0.0`, which no published `@umbraco-ui/uui` version
currently satisfies, breaking extension installs against
`@umbraco-cms/backoffice@18.0.0-beta1`+.

Use the full SemVer (including any prerelease) as the floor so
`^2.0.0-rc.1` stays satisfiable by the actual published rc.
2026-05-13 22:51:55 +02:00
Andy Butland 3aa87fec96 Bump version to 17.4.0. 2026-05-13 17:39:57 +02:00
Mads RasmussenandGitHub 8e7440580a User: Delete unused custom table collection view (#22839)
delete unused user table code
2026-05-13 17:36:03 +02:00
Mads RasmussenandGitHub 358d435948 Member Group: Migrate custom table collection view to generic table kind (#22833)
* Migrate member group custom table to use table kind

* Use data-mark collection view selector for member group view
2026-05-13 17:04:22 +02:00
Andreas Lykke BorgandGitHub dcf1595e74 Accessibility: Added missing labels to code block copy button and embedded media URL input (#22825)
* Added label to copy button

* Added label to url input in editor

* Changed term to url

* Removed unnecessary readonly #localize

* Added copied translation key
2026-05-13 14:17:29 +00:00
Andy Butland 7b579cd021 Merge branch 'v17/dev' 2026-05-13 15:45:47 +02:00
Ronald BarendseandAndy Butland 6fef118a8f SignalR: Mark ServerEventSender as a distributed cache notification handler (#22818)
* Mark ServerEventSender as distributed cache notification handler

* Batch and deduplicate notifications in ServerEventSender

* Introduce IDistributedCacheAsyncNotificationHandler<T> and use it in ServerEventSender

* Add ServerEventSender unit tests and address PR review feedback
2026-05-13 14:44:30 +02:00
Jacob Overgaard 1edd6ec0bc Merge branch 'release/18.0' 2026-05-13 13:44:17 +02:00
Laura Neto c4a3b95195 Bump version to 18.0.0-beta2 2026-05-13 13:16:59 +02:00
d2d0d6d2d8 System information: Adds __uuiVersions to sysinfo output (#22831)
* feat: adds `__uuiVersions` to system information output

* avoid printet the array of version by handle single or multiple versions

* feat: ensures type safety of global variable

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-13 12:07:17 +02:00
f245bc00d0 UI: UI Library adjustments for v18 (#22824)
* transfer style to uui v2

* accordingly interactive state for document-links

* overflow clip for border radius appearance

* link style

* fix block grid area configuration

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-13 10:04:18 +00:00
Jacob OvergaardandGitHub c385991ffb build(deps): bumps @umbraco-ui/uui from 2.0.0-alpha.1 to 2.0.0-rc.0 (#22827) 2026-05-13 09:59:46 +00:00
0add0f5b18 Backoffice: Preserve user-supplied property editor UI group names (closes #22189) (#22196)
* Preserve user-supplied property editor UI group names.

* Add support for localised property editor groups, and use localised values for all core property editors.

* Fixed check to look for '#' as the first character of the provided group name.

* danish translation

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-05-13 11:29:51 +02:00
Jacob Overgaard 14886a9425 build: updates acceptance test lockfile 2026-05-13 09:56:34 +02:00
Nhu DinhandGitHub 79aadd827a Build: Updated nightly E2E test pipeline schedule in v18 (#22802)
Update nightly e2e test pipeline
2026-05-13 06:33:02 +00:00
Ronald BarendseandAndy Butland 4921ab9257 SignalR: Mark ServerEventSender as a distributed cache notification handler (#22818)
* Mark ServerEventSender as distributed cache notification handler

* Batch and deduplicate notifications in ServerEventSender

* Introduce IDistributedCacheAsyncNotificationHandler<T> and use it in ServerEventSender

* Add ServerEventSender unit tests and address PR review feedback
2026-05-13 08:29:22 +02:00
Ronald BarendseandGitHub e07f188bd4 SignalR: Mark ServerEventSender as a distributed cache notification handler (#22818)
* Mark ServerEventSender as distributed cache notification handler

* Batch and deduplicate notifications in ServerEventSender

* Introduce IDistributedCacheAsyncNotificationHandler<T> and use it in ServerEventSender

* Add ServerEventSender unit tests and address PR review feedback
2026-05-13 08:26:50 +02:00
Nhu DinhandGitHub 4bb5864e20 E2E: QA Updated acceptance tests to match the recent changes (#22801)
* Updated locator for user group table

* Updated json builder for user groups permission due to element folder permission

* Updated api helper to match with element folder permission

* Updated tests and add comments for the failing tests
2026-05-13 12:15:43 +07:00
Ronald Barendseandleekelleher af03e1d30a User Permission: Re-export fallback condition config type and global augmentation (#22794)
(cherry picked from commit 55fec1dc2a)
2026-05-12 17:15:56 +01:00
Ronald Barendseandleekelleher 63bae5958a User Permission: Re-export fallback condition config type and global augmentation (#22794)
(cherry picked from commit 55fec1dc2a)
2026-05-12 17:15:28 +01:00
Ronald BarendseandGitHub 55fec1dc2a User Permission: Re-export fallback condition config type and global augmentation (#22794) 2026-05-12 17:14:48 +01:00
Andy Butlandandleekelleher 50727c4bb8 Sort Children: Show loading state on Sort button (closes #22651) (#22813)
* Add submit button state to sort dialog.

* Guard against re-entrant submit in sort-children-of modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Set failed button state when sort-children-of submit throws.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit dfe93c5639)
2026-05-12 17:07:59 +01:00
Andy Butlandandleekelleher 35d726ad31 Sort Children: Show loading state on Sort button (closes #22651) (#22813)
* Add submit button state to sort dialog.

* Guard against re-entrant submit in sort-children-of modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Set failed button state when sort-children-of submit throws.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit dfe93c5639)
2026-05-12 17:06:44 +01:00
dfe93c5639 Sort Children: Show loading state on Sort button (closes #22651) (#22813)
* Add submit button state to sort dialog.

* Guard against re-entrant submit in sort-children-of modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Set failed button state when sort-children-of submit throws.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:04:03 +01:00
Andy ButlandandGitHub def5be0855 18.0-beta1: Recycle Bin: Fix empty action button in collection view (closes #22798) (#22811)
Fix empty action button in collection view.
2026-05-12 16:31:07 +01:00
Niels LyngsøandGitHub 0a3647e4f0 v18 login photo (#22814) 2026-05-12 16:04:48 +02:00
Kenn Jacobsen 3daf7275ff Content: Ensure correct variant change tracking when unpublishing variant content (#22799)
(cherry picked from commit 6766eb9411)
2026-05-12 13:30:04 +02:00
kjac 2a2252474f Merge remote-tracking branch 'origin/main' 2026-05-12 12:32:07 +02:00
Jacob Overgaard 9c15572a49 fix: reinstates ./element export 2026-05-12 12:13:39 +02:00
Jacob Overgaard 045db8d699 fix: reinstate ./library export 2026-05-12 12:13:05 +02:00
kjac ecd29d79ff Merge remote-tracking branch 'origin/main' 2026-05-12 11:56:45 +02:00
Jacob Overgaard 60104e2a0e chore: regenerates tsconfig.json 2026-05-12 11:02:23 +02:00
Jacob Overgaard 2d747c0b43 chore: set version back to 18.1.0 2026-05-12 10:43:05 +02:00
Nhu DinhandGitHub 22a7a9577b Build: Updated nightly E2E test pipeline schedule in v17 (#22803)
Updated nightly E2E test pipeline schedule
2026-05-12 15:36:27 +07:00
Kenn Jacobsen 6766eb9411 Content: Ensure correct variant change tracking when unpublishing variant content (#22799) 2026-05-12 10:21:41 +02:00
Jacob Overgaard c9c4704e1a fix: exports condition configs and fixes test imports 2026-05-12 10:12:46 +02:00
Jacob Overgaard 0b0fea04d9 chore: sets version in backoffice client and regen packagel ock 2026-05-12 10:08:01 +02:00
Kenn JacobsenandAndy Butland fc9ca861b0 Content: Ensure correct variant change tracking when unpublishing variant content (#22799)
* Ensure correct change tracking when unpublishing

* Update src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Add comment

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-12 10:01:20 +02:00
c784858b32 Content: Ensure correct variant change tracking when unpublishing variant content (#22799)
* Ensure correct change tracking when unpublishing

* Update src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Add comment

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-12 09:58:20 +02:00
Laura NetoandGitHub f007855696 Open API: Add fluent builder for registering custom backoffice OpenAPI documents (#22774)
* Add helper for registering custom backoffice OpenAPI documents

Bundles AddOpenApi, the [MapToApi]-aware ShouldInclude predicate, the
Umbraco schema reference ID convention, and AddOpenApiDocumentToUi
behind a single IUmbracoBuilder.AddBackOfficeOpenApiDocument call.
Authors pass documentName, an optional title (used both as Info.Title
and the UI dropdown label), and an optional configure callback that
runs last so it can override anything the helper sets. An optional
jsonOptionsName is forwarded to ReplaceOpenApiSchemaService for
documents that need schema-time JSON serialization aligned to a named
JsonOptions.

Schema reference ID logic moves out of ConfigureUmbracoOpenApiOptionsBase
into UmbracoSchemaIdGenerator.CreateSchemaReferenceId so both the new
helper and the base class share one source of truth. The extension
template's composer collapses to a single AddBackOfficeOpenApiDocument
call, with document Info.Version, backoffice security, and the operation
ID transformer staying in the configure callback.

* Refactor backoffice OpenAPI helper into a fluent builder

Replace the parameter-list AddBackOfficeOpenApiDocument helper with a
callback-based form that yields a BackOfficeOpenApiDocumentBuilder. The
builder owns its state and applies it to the IUmbracoBuilder once the
user callback returns, so authors don't need to remember a terminal
Build call. Extension methods can layer on (e.g.
WithBackOfficeAuthentication in Umbraco.Cms.Api.Management) without the
core helper carrying every opinion.

Defaults stay sensible: filtering by [MapToApi(documentName)], the
Umbraco schema reference IDs, and the tag/sort transformers that v17's
global Swashbuckle pipeline applied. UI dropdown registration is
opt-out via ExcludeFromUi rather than opt-in. JSON options for schema
generation are an opt-in via WithHttpJsonOptions (instance or factory),
described purely in terms of the schema effect.

Move UmbracoSchemaIdGenerator's CreateSchemaReferenceId wrapper out of
ConfigureUmbracoOpenApiOptionsBase so both the base config class and
the new builder share one source of truth, and update the
ContentTypeSchemaTransformer / unit test callsites accordingly. Refresh
the extension template to use the new shape.

* Rename WithHttpJsonOptions to WithJsonOptions

The Http qualifier was naming the .NET type rather than the intent.
The parameter type carries the disambiguation; the method name is now
intent-focused and the XML doc explains the use case (matching the
serialization conventions of the API endpoints the document describes).

* Add WithJsonOptions(string) overload for named HTTP JsonOptions

Convenience overload that accepts the registered name and resolves the
matching Microsoft.AspNetCore.Http.Json.JsonOptions via IOptionsMonitor.
Documents on all three WithJsonOptions overloads now explicitly name
the HTTP JsonOptions type so consumers know which framework type they
are configuring.

* Migrate Management API OpenAPI registration to AddBackOfficeOpenApiDocument

Replaces the AddUmbracoOpenApiDocument<ConfigureUmbracoManagementApiOpenApiOptions>
call with the new fluent builder. The custom config class becomes dead
code and is deleted; all per-document opinions (Info metadata, security
requirements, transformers, JSON options) move into the configuration
callback alongside the document registration.

Behavior preserved: same ShouldInclude (now via [MapToApi]-only since
all Management controllers carry the attribute through their base class),
same schema reference IDs, same operation IDs via UmbracoOperationIdTransformer,
same backoffice security requirements, same schema/operation transformers,
same named JSON options for schema generation.

* Cleanup unused usings

* Address PR review feedback on AddBackOfficeOpenApiDocument

Make UmbracoOperationIdTransformer part of the builder's defaults instead of
the Management API adding it explicitly, and expand the XML docs on
AddBackOfficeOpenApiDocument to spell out the defaults a caller opts into.

Add tests covering the new builder and its defaults:
- Unit tests for BackOfficeOpenApiDocumentBuilder defaults (CreateSchemaReferenceId,
  ShouldInclude, ConfigureOpenApiOptions composition, WithTitle/WithUiTitle UI
  dropdown handling, ExcludeFromUi).
- Integration tests that register sample controllers, fetch the generated OpenAPI
  document and verify the defaults end-to-end: Info.Title from WithTitle,
  MapToApi filtering, Umbraco operation-id and schema-id conventions (including
  the version-suffix branch), tag-by-group-name and tag-first path sorting.
- Integration tests for the three WithJsonOptions overloads (instance, factory,
  named) confirming the configured JsonOptions reach schema generation.

* Remove redundant operation-id override from extension template

UmbracoOperationIdTransformer is now part of the AddBackOfficeOpenApiDocument
defaults, so the template's custom action-name transformer would only overwrite
the work the default just did. Drop it, and consolidate the documentation
pointer to a single link.

* Narrow MimeTypesTransformer to JSON-equivalent variants and register it in AddBackOfficeOpenApiDocument

Filter only removes redundant JSON-equivalent MIME types (text/json,
application/*+json, text/plain) when application/json is present.
Non-JSON types like application/xml are preserved. Register the
transformer as a default in AddBackOfficeOpenApiDocument so custom
backoffice documents get the same treatment as Umbraco's own APIs.

* Register RequireNonNullablePropertiesSchemaTransformer in AddBackOfficeOpenApiDocument

* Apply review notes

- Drop RequireNonNullablePropertiesSchemaTransformer and MimeTypesTransformer
  from the Management API's ConfigureOpenApiOptions block — both are now
  defaults on the builder.
- Expand MimeTypesTransformer XML docs to reflect its broader role (it now
  applies to every backoffice document, not just the Management API) and
  correct the response-side inline comment.
- Move MimeTypesTransformerTests from the Delivery test folder/namespace to
  the Api.Common test folder/namespace, since the transformer is no longer
  Delivery-specific.
- Rename BackOfficeOpenApiDocumentExtensionTests to
  UmbracoBuilderOpenApiExtensionsTests so the test fixture name matches the
  concrete class under test.
2026-05-12 09:55:08 +02:00
Sven Geusensandmole 4286a361d8 Distributed background jobs: Improve gracefull shutdown behaviour (#22796)
* Dont fail silently on missing ambientscope

This makes it in line with other methods in the repo

* Pass on Cancellationtoken to the job to support gracefull job shutdown

(cherry picked from commit 5ae17ace6a)
2026-05-12 09:52:41 +02:00
Sven Geusensandmole 68194e1a27 Distributed background jobs: Improve gracefull shutdown behaviour (#22796)
* Dont fail silently on missing ambientscope

This makes it in line with other methods in the repo

* Pass on Cancellationtoken to the job to support gracefull job shutdown

(cherry picked from commit 5ae17ace6a)
2026-05-12 09:51:25 +02:00
mole 91313fff0e Merge remote-tracking branch 'refs/remotes/origin/v17/dev'
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
#	tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/PublishedContent/PublishedValueFallbackTests.cs
#	version.json
2026-05-12 09:50:50 +02:00
Sven GeusensandGitHub 5ae17ace6a Distributed background jobs: Improve gracefull shutdown behaviour (#22796)
* Dont fail silently on missing ambientscope

This makes it in line with other methods in the repo

* Pass on Cancellationtoken to the job to support gracefull job shutdown
2026-05-12 09:28:00 +02:00
Nhu DinhandGitHub 3dca735a38 E2E: QA Added acceptance tests for current user workspace (#22457)
* Updated acceptance tests for current user profile

* Updated locator for save button

* Reverted npm command
2026-05-11 14:59:03 +00:00
Andy Butlandandleekelleher 57b9a7ef80 Tiptap RTE: Ignore no-op transactions in onUpdate to prevent phantom dirty state (closes #22767) (#22781)
Ignore Tiptap no-op transactions in onUpdate to prevent phantom dirty state.

(cherry picked from commit cd476ab6ed)
2026-05-11 14:14:21 +01:00
Andy ButlandandGitHub cd476ab6ed Tiptap RTE: Ignore no-op transactions in onUpdate to prevent phantom dirty state (closes #22767) (#22781)
Ignore Tiptap no-op transactions in onUpdate to prevent phantom dirty state.
2026-05-11 14:13:21 +01:00
Andy ButlandandGitHub 65a45a4ac6 18.0-beta1: User Management: Invalidate cached element start nodes on user save and fix access summary display (closes #22770) (#22779)
* Users: Invalidate cached element start nodes on user save (closes #22770)

* Fix display of selected element folders under the user's access summary.
2026-05-11 12:40:05 +01:00
Andy ButlandandGitHub 38b6752d24 18.0-beta1: Elements: Show trashed folder ancestor names in breadcrumb (closes #22768) (#22780)
Fix breadcrumb for trashed element within a folder.
2026-05-11 12:23:43 +01:00
Jacob Overgaardandleekelleher 097b9c11f6 Login: Reuse backoffice localization (closes #20082) (#22743)
* Login: Reuse backoffice localization for canonical login_* keys (closes #56402)

The login screen no longer ships its own localization tree. The slim backoffice controller registers the backoffice's built-in localization manifests, so all login screen text resolves from the same dictionary the in-backoffice auth view uses. Translators override one place; both screens reflect it.

All consumers in the Login project moved from auth_* to login_*. The Login project's localization/ directory is removed entirely. The auth.* keys it used to ship (form labels, mfa, invite, password reset) now live under login.* in the backoffice's en/da/de/nb/nl/sv lang files. Other backoffice languages fall back to en for these keys, automatically extending the login screen's language coverage.

* Backoffice localization: drop server-only email keys, add login.setPasswordInstruction in en/da/nb/sv

bottomText, resetPasswordEmailCopySubject, resetPasswordEmailCopyFormat, mfaSecurityCodeSubject and mfaSecurityCodeBody are read only by the server's own localization layer — they were dead weight in every backoffice lang dictionary that carried them. Removed across 23 lang files.

login.setPasswordInstruction is rendered on the new-password screen via the now-canonical login_* namespace; it was missing from en (the fallback), da, nb and sv. Added there using the same translation tone as the existing de/nl entries.

* Login: Honour legacy auth_greeting* overrides with UmbDeprecation warning

Translation packages still shipping 'auth_greeting0..6' overrides keep working on both welcome screens (the standalone login page and the in-backoffice umb-auth-view): when an auth_* greeting is registered the consumer prefers it, otherwise the canonical login_* key is used. Each legacy key triggers a one-time UmbDeprecation warning pointing at the canonical name. Scheduled for removal in v20.

* Fix Prettier formatting and correct issue references in deprecation message

Addresses Copilot review feedback on PR #22743:

- Run Prettier on the 6 backoffice lang files I added keys to (en/da/de/nb/nl/sv); the new entries used double quotes which violated the repo's singleQuote: true config and would have failed the format check.
- Update the UmbDeprecation 'solution' link and the inline source comments from #56402 (an ADO work item id) to #20082 (the actual GitHub issue tracking this work).

* Drop stale login_2fa* and login_mfaSecurityCodeMessage from bs.ts and cy.ts

Surfaced by 'devops/localization/compare-languages.js': bs and cy were the only lang files shipping these keys, and they have no en counterpart. The login_2fa* set is leftover from before the codebase renamed 2fa → mfa in the login flow (the live keys are login_mfa*). login_mfaSecurityCodeMessage is server-side only, like the other email-template keys cleaned up in 53ad52702e0. None of these are referenced anywhere in src/. The user-facing user_2fa* keys (consumed by current-user-mfa modals) are unrelated and untouched.

* Drop dead login_2fa* and login_mfaSecurityCodeMessage from nl, hr, tr

Same pattern as 26bc6211d27 (bs/cy cleanup), surfaced by re-running devops/localization/compare-languages.js after the previous pass:

- nl had both legacy 'login_2fa*' AND the canonical 'login_mfa*' (added in commit 1) sitting side by side after the auth.* → login.* port. Six true duplicates dropped, login_mfa* kept.
- hr and tr shipped legacy 'login_2fa*' that have no en counterpart, no consumer in src/, and no mfa pair locally. Dropped to align with en (the source of truth — every other locale should match it).

- All three files also still carried 'login_mfaSecurityCodeMessage' from the same family of server-side email-template keys cleaned up in 53ad52702e0; removed too.

user_2fa* / member_2fa keys are unrelated and untouched (consumed by current-user-mfa modals).

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
(cherry picked from commit def18e440f)
2026-05-11 12:01:23 +01:00
Jacob Overgaardandleekelleher 30d7161399 Login: Consume sibling Umbraco.Web.UI.Client by source for v18 parity (#22591)
* Login: Consume sibling Umbraco.Web.UI.Client by source for v18 parity

The Login project previously depended on the published `@umbraco-cms/backoffice@^17.3.4` npm package for types, while at runtime the importmap served the in-repo v18 backoffice. The version mismatch forced `as any` workarounds and masked real API drift. Since v18 (with UUI 2.0) isn't on npm yet, switch Login to consume the sibling Client via a local `file:` dep so types and runtime align on v18.

Changes:
- Login `package.json`: `@umbraco-cms/backoffice` → `file:../Umbraco.Web.UI.Client`; added `pre{build,dev,watch}` hooks that run a guard script to fail fast when Client's `dist-cms/` is missing.
- Login `scripts/ensure-client-built.mjs`: new guard with a clear "build the Client first" message.
- Login `CLAUDE.md`: documents the contract and build ordering.
- StaticAssets `.csproj`: `BuildLogin` now depends on `BuildBackoffice` so MSBuild (and therefore the Azure pipeline) builds Client before Login automatically.
- Client `src/tsconfig.build.json`: `declaration: true` so `dist-cms/` ships `.d.ts`.
- Client `package.json`: new `build:types` step (`tsc --emitDeclarationOnly --incremental false && tsc-alias`) wired into `build:for:cms` after `build:workspaces`. Vite workspaces wipe their output dirs before rebuilding JS, stripping the tsc-emitted declarations; re-emitting after workspaces restores them. `tsc-alias` rewrites Client-internal path aliases (e.g. `@umbraco-cms/backoffice/external/lit`) to relative paths so sibling consumers can resolve them.
- `copy-to-cms.js`: filter `.d.ts` and `.tsbuildinfo` from the copy to `wwwroot/umbraco/backoffice` — they're only needed by sibling projects consuming `dist-cms` for types, not at runtime.
- `src/external/uui/vite.config.ts`: set `treeshake: false` so per-component `defineElement()` side-effect calls (used by UUI 2.0 for custom-element registration) are preserved in the bundle. Without this, `<uui-button>` etc. never register and the login screen renders empty controls.
- `src/external/uui/index.ts`: bare `import '@umbraco-ui/uui'` to make the side-effect intent explicit.
- Small v18-compat fixes for `Object.groupBy` (TS 8 types): removed stale `@ts-expect-error`, switched to `Object.entries` + `?? []` to satisfy the `Partial<Record>` return type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address review feedback and fix CI

- Add `ignoreDeprecations: "6.0"` to `tsconfig.json` and the tsconfig generator to silence the TS 6.0 warning about the implicit baseUrl that TypeScript assigns when `paths` is declared. This was the CI `build` failure. The generator is also synced with the user's es2022 → es2024 bump.
- Drop the now-redundant `--declaration` flag from `build:for:npm` (tsconfig.build.json now has `declaration: true`, so the flag was duplicating intent).
- Align Login's `engines` with the Client's (`node >=24.13`, `npm >=11`) so `file:` install doesn't trip EBADENGINE.
- Guard script: hardcode the relative "../Umbraco.Web.UI.Client" path in the error message instead of interpolating the absolute path, which overflowed the ASCII box in CI logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Login: update CLAUDE.md Node/npm versions to match engines

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* check-path-length: skip .d.ts/.tsbuildinfo and directory paths

The 120-char Windows MAX_PATH guard protects files that actually ship to
CMS installs. `.d.ts` and `.tsbuildinfo` live in `dist-cms/` for sibling
projects to consume as types and are filtered out by `copy-to-cms.js`
before reaching `wwwroot/umbraco/backoffice` — they never land on a
Windows CMS install. Directories on their own also don't trigger
MAX_PATH; only files within them do, and those are still checked.

Unblocks CI after enabling `declaration: true` in the Client build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* check-path-length: extract exceedsPathLimit helper (CodeScene)

Decomposes the complex conditional flagged by CodeScene into a named
predicate with a docstring, clarifying when a path is reported.

* Login: switch to generated tsconfig paths; revert dist-cms type machinery

PR #22591 originally aligned Login's TypeScript types with the in-repo v18
backoffice by emitting `.d.ts` into Client's `dist-cms/` and consuming it
via a `file:` dep. That layered six side-effects across the Client build
(declaration: true, build:types step, tsc-alias in postbuild, copy-to-cms
filter, check:paths skip, MSBuild ordering). Reviewers pushed back.

This rework moves the type contract from "ship .d.ts in dist-cms" to
"point Login's tsconfig paths at Client's TypeScript source" — Login's
runtime behaviour is unchanged (vite still externalises /^@umbraco-cms/,
host importmap still serves the JS), only the type-resolution mechanism
swaps.

What's reverted (back to the pre-PR shape):
- src/Umbraco.Web.UI.Client/src/tsconfig.build.json: declaration: false
- src/Umbraco.Web.UI.Client/package.json: drops `build:types` script,
  reverts `postbuild` to global-types only, drops `--declaration` from
  the tsc CLI in `build:for:cms` and restores it in `build:for:npm`
- src/Umbraco.Web.UI.Client/devops/build/copy-to-cms.js: simple cpSync
- src/Umbraco.Web.UI.Client/devops/build/check-path-length.js: original
- src/Umbraco.Web.UI.Client/tsconfig.json + devops/tsconfig/index.js:
  drops `ignoreDeprecations` (not needed once baseUrl is gone)
- src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj:
  `BuildLogin` no longer depends on `BuildBackoffice`

What's new on the Login side:
- src/Umbraco.Web.UI.Login/devops/tsconfig/index.js: generator that
  reads Client's `package.json` exports and emits a full `tsconfig.json`
  with `paths` mapping every `@umbraco-cms/backoffice/<sub>` to
  `../Umbraco.Web.UI.Client/src/.../index.ts`. Mirrors Client's existing
  generator pattern (DON'T EDIT header, JSON.stringify with tabs).
- src/Umbraco.Web.UI.Login/tsconfig.json: regenerated; standalone `tsc`
  works (no `--project` needed) and 140 path aliases resolve types
  directly from Client's source.
- src/Umbraco.Web.UI.Login/package.json: drops `@umbraco-cms/backoffice`
  npm dep entirely (file: was only nominal — types come via paths,
  runtime via importmap, transitives via Client's own `node_modules`
  which is `npm install`-ed by CI's backoffice-install.yml). Replaces
  the `ensure-client-built` guard with the generator on `pre*` hooks
  and adds `generate:tsconfig` for ad-hoc invocation.
- src/Umbraco.Web.UI.Login/CLAUDE.md: documents the new layered
  contract (paths/externalisation/importmap) and the install-Client-
  before-Login prerequisite.
- src/Umbraco.Web.UI.Login/scripts/ensure-client-built.mjs: deleted.

What stays from the original PR (independent fixes):
- src/Umbraco.Web.UI.Client/src/external/uui/{vite.config.ts,index.ts}:
  `treeshake: false` + bare side-effect import — keeps UUI 2.0
  per-component `defineElement` calls in the bundle so `<uui-button>`
  etc. actually register.
- Object.groupBy cleanups in 6 element files (TS 8 type narrowing).
- Client tsconfig generator: target/lib bumped to ES2024, `baseUrl`
  removed.

Verified locally:
- `cd Client && rm -rf dist-cms && cd ../Login && npx tsc` → clean
  (proves Login compiles without Client's dist-cms)
- `cd Client && npm run build:for:cms` → 0 emitted .d.ts (back to
  pre-PR shape), `check:paths` passes
- Login `npm run build` → 64 KB bundle (unchanged)
- Browser at https://localhost:44339/umbraco: UUI 2.0 components
  render, login with `test@umbraco.com`/`test123456` succeeds and
  redirects to /umbraco/section/content

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Login: address review — idempotent generator + correct MSBuild ordering

- StaticAssets.csproj: BuildLogin now depends on RestoreBackoffice (not
  BuildBackoffice — Login doesn't need dist-cms types). Login's tsc walks
  Client source via tsconfig path aliases and resolves transitive deps
  (lit, rxjs, …) from Client's node_modules. Without this dependency a
  fresh local `dotnet build` could run BuildLogin before Client is
  installed; CI was already safe via backoffice-install.yml's npm ci.

- devops/tsconfig/index.js: skip rewrite when content is unchanged. Pre-
  hooks ran the generator on every npm command and bumped tsconfig.json
  mtime even when nothing changed, which can invalidate caches and rattle
  watchers downstream. Read-then-compare-then-write makes the generator
  truly idempotent.

- devops/tsconfig/index.js: derive the alias prefix from
  `clientPkg.name` instead of hardcoding `@umbraco-cms/backoffice` so a
  package rename can't silently break paths.

azure-pipelines.yml needs no changes — backoffice-install.yml already
runs `npm ci` in Client before dotnet build kicks in MSBuild.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Login: postinstall + dev-mode Vite alias + theme CSS path

Audit cleanup pass on the rework:

- Login package.json: collapse predev/prebuild/prewatch into a single
  postinstall hook. The generator runs whenever npm install/ci runs
  (locally + in CI via RestoreLogin's npm i + the dotnet build chain).
  Removes the per-command "tsconfig.json already up to date" noise.

- Login vite.config.ts: in dev mode (`vite serve`), read `paths` from
  the generated tsconfig.json and apply them as `resolve.alias` so Vite
  can resolve `@umbraco-cms/backoffice/*` to Client source. Vite doesn't
  honor tsconfig `paths` natively — without this `npm run dev` failed
  with "Failed to resolve import @umbraco-cms/backoffice/utils ...".
  Build mode (`vite build`) still externalises the namespace via the
  unchanged rollupOptions.external regex; alias is dev-only.

- Login index.html: UUI 2.0 reorganised CSS — the old
  `@umbraco-ui/uui-css/dist/uui-css.css` path no longer exists. Point
  at `@umbraco-ui/uui/dist/themes/light.css` which is what Client now
  ships. Path is relative through Client's node_modules since Login no
  longer declares a UUI dep itself.

- Client input-entity-user-permission.element.ts: prettier flagged a
  multi-line .map() arrow that should be inline; collapse to one line.

- Login CLAUDE.md: document the postinstall-driven generator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Use Vite 8 native tsconfigPaths; drop helper plugin and trim comments

- Both vite.config.ts files use `resolve.tsconfigPaths: true` instead of the
  `vite-tsconfig-paths` plugin. Plugin and dep removed.
- Trim explanatory comments on csproj target, generator, UUI vite config and
  external/uui/index.ts to conclusions only.

* Login: tsconfig generator fails fast on unsupported exports shapes

Distinguish between the legitimate `.` self-reference (target === null) and
unexpected non-string targets (e.g., conditional exports objects). The latter
now throw with a clear message instead of being silently dropped from `paths`,
which would otherwise produce confusing 'Cannot find module' errors at tsc
time later.

* Login: allow Vite dev server to serve Client's UUI assets

The light.css imported from Client's node_modules pulls Lato fonts via
relative URL, which Vite refuses by default since they sit outside
Login's project root. Extend server.fs.allow to the parent directory
(both sibling projects).

* Client: regenerate tsconfig on postinstall

* Login: keep UUI registrations in dev mode

Vite 8's esbuild dep pre-bundle drops the per-component
`customElements.define()` side-effects in @umbraco-ui/uui (a known UUI
issue with Vite 8). Exclude UUI from optimizeDeps so it's served
unbundled in dev. Re-add the bare side-effect import in external/uui
so the entry module evaluates the chain. Production build is unaffected
(workspace's `treeshake: false` already preserves registrations).

Also document the new MSBuild Login targets in StaticAssets CLAUDE.md.

* Login: clarify why optimizeDeps.exclude is needed for UUI

Tested treeshake.moduleSideEffects: true in optimizeDeps.rollupOptions
on Vite 8 / Rolldown 1.0.0-rc.17 — registrations still get stripped.
Excluding the package from the pre-bundle is the only reliable workaround
until UUI's own Vite 8 upgrade lands. Comment captures the conclusion.

* Roll back Vite 8 → 7 in Client and Login

Vite 8.0.10 ships Rolldown 1.0.0-rc.17 which strips UUI 2.0
`customElements.define()` side-effects during dep pre-bundle, leaving
elements unregistered in dev mode. Rather than ship a v18 release tied
to a non-final Rolldown RC, revert the Vite bump and pick it up again
once Rolldown 1.0 final lands.

Changes:
- Client: vite ^8.0.10 → ^7.3.2; vite-plugin-static-copy ^4.1.0 → ^3.2.0;
  re-add vite-tsconfig-paths plugin; drop native `resolve.tsconfigPaths`.
- Login: vite ^8.0.10 → ^7.3.2; add vite-tsconfig-paths; configure plugin
  with `projects: ['./tsconfig.json', '../Umbraco.Web.UI.Client/tsconfig.json']`
  so it can resolve `@umbraco-cms/backoffice/*` imports inside Client
  source files (which would otherwise lack a discoverable tsconfig in
  Login's project tree). Drop `optimizeDeps.exclude` (no longer needed
  without Rolldown). Keep `server.fs.allow` for the cross-project font.

TypeScript 6 + ES2024 + tsconfig path generator + Login architectural
pivot all stay — those are independent of the Vite version.

Verified:
- Production https://localhost:44339/umbraco — login works
- Login dev http://localhost:5191/ — UUI registers, all custom elements defined
- Client dev http://localhost:5192/ — page loads, navigates to /section/content

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address Copilot review

- vite.config.ts (Login): narrow server.fs.allow from the parent dir to
  Login + Client only, reducing the dev server's read scope.
- external/uui/vite.config.ts (Client): replace blanket `treeshake: false`
  with `moduleSideEffects: (id) => id.includes('@umbraco-ui/uui')` so
  Rollup keeps UUI's per-component registration calls but tree-shakes the
  rest. Bundle stays at 516 KB / 96 registered tags.

* fix merge overwrites

* update package lock

* fix: do not autogenerate tsconfig on postinstall

* removes postinstall script

* chore: generates tsconfig

* chore: update lockfile

* docs: updates claude.md

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
(cherry picked from commit 8a73d713cd)
2026-05-11 12:01:13 +01:00
def18e440f Login: Reuse backoffice localization (closes #20082) (#22743)
* Login: Reuse backoffice localization for canonical login_* keys (closes #56402)

The login screen no longer ships its own localization tree. The slim backoffice controller registers the backoffice's built-in localization manifests, so all login screen text resolves from the same dictionary the in-backoffice auth view uses. Translators override one place; both screens reflect it.

All consumers in the Login project moved from auth_* to login_*. The Login project's localization/ directory is removed entirely. The auth.* keys it used to ship (form labels, mfa, invite, password reset) now live under login.* in the backoffice's en/da/de/nb/nl/sv lang files. Other backoffice languages fall back to en for these keys, automatically extending the login screen's language coverage.

* Backoffice localization: drop server-only email keys, add login.setPasswordInstruction in en/da/nb/sv

bottomText, resetPasswordEmailCopySubject, resetPasswordEmailCopyFormat, mfaSecurityCodeSubject and mfaSecurityCodeBody are read only by the server's own localization layer — they were dead weight in every backoffice lang dictionary that carried them. Removed across 23 lang files.

login.setPasswordInstruction is rendered on the new-password screen via the now-canonical login_* namespace; it was missing from en (the fallback), da, nb and sv. Added there using the same translation tone as the existing de/nl entries.

* Login: Honour legacy auth_greeting* overrides with UmbDeprecation warning

Translation packages still shipping 'auth_greeting0..6' overrides keep working on both welcome screens (the standalone login page and the in-backoffice umb-auth-view): when an auth_* greeting is registered the consumer prefers it, otherwise the canonical login_* key is used. Each legacy key triggers a one-time UmbDeprecation warning pointing at the canonical name. Scheduled for removal in v20.

* Fix Prettier formatting and correct issue references in deprecation message

Addresses Copilot review feedback on PR #22743:

- Run Prettier on the 6 backoffice lang files I added keys to (en/da/de/nb/nl/sv); the new entries used double quotes which violated the repo's singleQuote: true config and would have failed the format check.
- Update the UmbDeprecation 'solution' link and the inline source comments from #56402 (an ADO work item id) to #20082 (the actual GitHub issue tracking this work).

* Drop stale login_2fa* and login_mfaSecurityCodeMessage from bs.ts and cy.ts

Surfaced by 'devops/localization/compare-languages.js': bs and cy were the only lang files shipping these keys, and they have no en counterpart. The login_2fa* set is leftover from before the codebase renamed 2fa → mfa in the login flow (the live keys are login_mfa*). login_mfaSecurityCodeMessage is server-side only, like the other email-template keys cleaned up in 53ad52702e0. None of these are referenced anywhere in src/. The user-facing user_2fa* keys (consumed by current-user-mfa modals) are unrelated and untouched.

* Drop dead login_2fa* and login_mfaSecurityCodeMessage from nl, hr, tr

Same pattern as 26bc6211d27 (bs/cy cleanup), surfaced by re-running devops/localization/compare-languages.js after the previous pass:

- nl had both legacy 'login_2fa*' AND the canonical 'login_mfa*' (added in commit 1) sitting side by side after the auth.* → login.* port. Six true duplicates dropped, login_mfa* kept.
- hr and tr shipped legacy 'login_2fa*' that have no en counterpart, no consumer in src/, and no mfa pair locally. Dropped to align with en (the source of truth — every other locale should match it).

- All three files also still carried 'login_mfaSecurityCodeMessage' from the same family of server-side email-template keys cleaned up in 53ad52702e0; removed too.

user_2fa* / member_2fa keys are unrelated and untouched (consumed by current-user-mfa modals).

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-05-11 11:59:58 +01:00
8a73d713cd Login: Consume sibling Umbraco.Web.UI.Client by source for v18 parity (#22591)
* Login: Consume sibling Umbraco.Web.UI.Client by source for v18 parity

The Login project previously depended on the published `@umbraco-cms/backoffice@^17.3.4` npm package for types, while at runtime the importmap served the in-repo v18 backoffice. The version mismatch forced `as any` workarounds and masked real API drift. Since v18 (with UUI 2.0) isn't on npm yet, switch Login to consume the sibling Client via a local `file:` dep so types and runtime align on v18.

Changes:
- Login `package.json`: `@umbraco-cms/backoffice` → `file:../Umbraco.Web.UI.Client`; added `pre{build,dev,watch}` hooks that run a guard script to fail fast when Client's `dist-cms/` is missing.
- Login `scripts/ensure-client-built.mjs`: new guard with a clear "build the Client first" message.
- Login `CLAUDE.md`: documents the contract and build ordering.
- StaticAssets `.csproj`: `BuildLogin` now depends on `BuildBackoffice` so MSBuild (and therefore the Azure pipeline) builds Client before Login automatically.
- Client `src/tsconfig.build.json`: `declaration: true` so `dist-cms/` ships `.d.ts`.
- Client `package.json`: new `build:types` step (`tsc --emitDeclarationOnly --incremental false && tsc-alias`) wired into `build:for:cms` after `build:workspaces`. Vite workspaces wipe their output dirs before rebuilding JS, stripping the tsc-emitted declarations; re-emitting after workspaces restores them. `tsc-alias` rewrites Client-internal path aliases (e.g. `@umbraco-cms/backoffice/external/lit`) to relative paths so sibling consumers can resolve them.
- `copy-to-cms.js`: filter `.d.ts` and `.tsbuildinfo` from the copy to `wwwroot/umbraco/backoffice` — they're only needed by sibling projects consuming `dist-cms` for types, not at runtime.
- `src/external/uui/vite.config.ts`: set `treeshake: false` so per-component `defineElement()` side-effect calls (used by UUI 2.0 for custom-element registration) are preserved in the bundle. Without this, `<uui-button>` etc. never register and the login screen renders empty controls.
- `src/external/uui/index.ts`: bare `import '@umbraco-ui/uui'` to make the side-effect intent explicit.
- Small v18-compat fixes for `Object.groupBy` (TS 8 types): removed stale `@ts-expect-error`, switched to `Object.entries` + `?? []` to satisfy the `Partial<Record>` return type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address review feedback and fix CI

- Add `ignoreDeprecations: "6.0"` to `tsconfig.json` and the tsconfig generator to silence the TS 6.0 warning about the implicit baseUrl that TypeScript assigns when `paths` is declared. This was the CI `build` failure. The generator is also synced with the user's es2022 → es2024 bump.
- Drop the now-redundant `--declaration` flag from `build:for:npm` (tsconfig.build.json now has `declaration: true`, so the flag was duplicating intent).
- Align Login's `engines` with the Client's (`node >=24.13`, `npm >=11`) so `file:` install doesn't trip EBADENGINE.
- Guard script: hardcode the relative "../Umbraco.Web.UI.Client" path in the error message instead of interpolating the absolute path, which overflowed the ASCII box in CI logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Login: update CLAUDE.md Node/npm versions to match engines

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* check-path-length: skip .d.ts/.tsbuildinfo and directory paths

The 120-char Windows MAX_PATH guard protects files that actually ship to
CMS installs. `.d.ts` and `.tsbuildinfo` live in `dist-cms/` for sibling
projects to consume as types and are filtered out by `copy-to-cms.js`
before reaching `wwwroot/umbraco/backoffice` — they never land on a
Windows CMS install. Directories on their own also don't trigger
MAX_PATH; only files within them do, and those are still checked.

Unblocks CI after enabling `declaration: true` in the Client build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* check-path-length: extract exceedsPathLimit helper (CodeScene)

Decomposes the complex conditional flagged by CodeScene into a named
predicate with a docstring, clarifying when a path is reported.

* Login: switch to generated tsconfig paths; revert dist-cms type machinery

PR #22591 originally aligned Login's TypeScript types with the in-repo v18
backoffice by emitting `.d.ts` into Client's `dist-cms/` and consuming it
via a `file:` dep. That layered six side-effects across the Client build
(declaration: true, build:types step, tsc-alias in postbuild, copy-to-cms
filter, check:paths skip, MSBuild ordering). Reviewers pushed back.

This rework moves the type contract from "ship .d.ts in dist-cms" to
"point Login's tsconfig paths at Client's TypeScript source" — Login's
runtime behaviour is unchanged (vite still externalises /^@umbraco-cms/,
host importmap still serves the JS), only the type-resolution mechanism
swaps.

What's reverted (back to the pre-PR shape):
- src/Umbraco.Web.UI.Client/src/tsconfig.build.json: declaration: false
- src/Umbraco.Web.UI.Client/package.json: drops `build:types` script,
  reverts `postbuild` to global-types only, drops `--declaration` from
  the tsc CLI in `build:for:cms` and restores it in `build:for:npm`
- src/Umbraco.Web.UI.Client/devops/build/copy-to-cms.js: simple cpSync
- src/Umbraco.Web.UI.Client/devops/build/check-path-length.js: original
- src/Umbraco.Web.UI.Client/tsconfig.json + devops/tsconfig/index.js:
  drops `ignoreDeprecations` (not needed once baseUrl is gone)
- src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj:
  `BuildLogin` no longer depends on `BuildBackoffice`

What's new on the Login side:
- src/Umbraco.Web.UI.Login/devops/tsconfig/index.js: generator that
  reads Client's `package.json` exports and emits a full `tsconfig.json`
  with `paths` mapping every `@umbraco-cms/backoffice/<sub>` to
  `../Umbraco.Web.UI.Client/src/.../index.ts`. Mirrors Client's existing
  generator pattern (DON'T EDIT header, JSON.stringify with tabs).
- src/Umbraco.Web.UI.Login/tsconfig.json: regenerated; standalone `tsc`
  works (no `--project` needed) and 140 path aliases resolve types
  directly from Client's source.
- src/Umbraco.Web.UI.Login/package.json: drops `@umbraco-cms/backoffice`
  npm dep entirely (file: was only nominal — types come via paths,
  runtime via importmap, transitives via Client's own `node_modules`
  which is `npm install`-ed by CI's backoffice-install.yml). Replaces
  the `ensure-client-built` guard with the generator on `pre*` hooks
  and adds `generate:tsconfig` for ad-hoc invocation.
- src/Umbraco.Web.UI.Login/CLAUDE.md: documents the new layered
  contract (paths/externalisation/importmap) and the install-Client-
  before-Login prerequisite.
- src/Umbraco.Web.UI.Login/scripts/ensure-client-built.mjs: deleted.

What stays from the original PR (independent fixes):
- src/Umbraco.Web.UI.Client/src/external/uui/{vite.config.ts,index.ts}:
  `treeshake: false` + bare side-effect import — keeps UUI 2.0
  per-component `defineElement` calls in the bundle so `<uui-button>`
  etc. actually register.
- Object.groupBy cleanups in 6 element files (TS 8 type narrowing).
- Client tsconfig generator: target/lib bumped to ES2024, `baseUrl`
  removed.

Verified locally:
- `cd Client && rm -rf dist-cms && cd ../Login && npx tsc` → clean
  (proves Login compiles without Client's dist-cms)
- `cd Client && npm run build:for:cms` → 0 emitted .d.ts (back to
  pre-PR shape), `check:paths` passes
- Login `npm run build` → 64 KB bundle (unchanged)
- Browser at https://localhost:44339/umbraco: UUI 2.0 components
  render, login with `test@umbraco.com`/`test123456` succeeds and
  redirects to /umbraco/section/content

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Login: address review — idempotent generator + correct MSBuild ordering

- StaticAssets.csproj: BuildLogin now depends on RestoreBackoffice (not
  BuildBackoffice — Login doesn't need dist-cms types). Login's tsc walks
  Client source via tsconfig path aliases and resolves transitive deps
  (lit, rxjs, …) from Client's node_modules. Without this dependency a
  fresh local `dotnet build` could run BuildLogin before Client is
  installed; CI was already safe via backoffice-install.yml's npm ci.

- devops/tsconfig/index.js: skip rewrite when content is unchanged. Pre-
  hooks ran the generator on every npm command and bumped tsconfig.json
  mtime even when nothing changed, which can invalidate caches and rattle
  watchers downstream. Read-then-compare-then-write makes the generator
  truly idempotent.

- devops/tsconfig/index.js: derive the alias prefix from
  `clientPkg.name` instead of hardcoding `@umbraco-cms/backoffice` so a
  package rename can't silently break paths.

azure-pipelines.yml needs no changes — backoffice-install.yml already
runs `npm ci` in Client before dotnet build kicks in MSBuild.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Login: postinstall + dev-mode Vite alias + theme CSS path

Audit cleanup pass on the rework:

- Login package.json: collapse predev/prebuild/prewatch into a single
  postinstall hook. The generator runs whenever npm install/ci runs
  (locally + in CI via RestoreLogin's npm i + the dotnet build chain).
  Removes the per-command "tsconfig.json already up to date" noise.

- Login vite.config.ts: in dev mode (`vite serve`), read `paths` from
  the generated tsconfig.json and apply them as `resolve.alias` so Vite
  can resolve `@umbraco-cms/backoffice/*` to Client source. Vite doesn't
  honor tsconfig `paths` natively — without this `npm run dev` failed
  with "Failed to resolve import @umbraco-cms/backoffice/utils ...".
  Build mode (`vite build`) still externalises the namespace via the
  unchanged rollupOptions.external regex; alias is dev-only.

- Login index.html: UUI 2.0 reorganised CSS — the old
  `@umbraco-ui/uui-css/dist/uui-css.css` path no longer exists. Point
  at `@umbraco-ui/uui/dist/themes/light.css` which is what Client now
  ships. Path is relative through Client's node_modules since Login no
  longer declares a UUI dep itself.

- Client input-entity-user-permission.element.ts: prettier flagged a
  multi-line .map() arrow that should be inline; collapse to one line.

- Login CLAUDE.md: document the postinstall-driven generator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Use Vite 8 native tsconfigPaths; drop helper plugin and trim comments

- Both vite.config.ts files use `resolve.tsconfigPaths: true` instead of the
  `vite-tsconfig-paths` plugin. Plugin and dep removed.
- Trim explanatory comments on csproj target, generator, UUI vite config and
  external/uui/index.ts to conclusions only.

* Login: tsconfig generator fails fast on unsupported exports shapes

Distinguish between the legitimate `.` self-reference (target === null) and
unexpected non-string targets (e.g., conditional exports objects). The latter
now throw with a clear message instead of being silently dropped from `paths`,
which would otherwise produce confusing 'Cannot find module' errors at tsc
time later.

* Login: allow Vite dev server to serve Client's UUI assets

The light.css imported from Client's node_modules pulls Lato fonts via
relative URL, which Vite refuses by default since they sit outside
Login's project root. Extend server.fs.allow to the parent directory
(both sibling projects).

* Client: regenerate tsconfig on postinstall

* Login: keep UUI registrations in dev mode

Vite 8's esbuild dep pre-bundle drops the per-component
`customElements.define()` side-effects in @umbraco-ui/uui (a known UUI
issue with Vite 8). Exclude UUI from optimizeDeps so it's served
unbundled in dev. Re-add the bare side-effect import in external/uui
so the entry module evaluates the chain. Production build is unaffected
(workspace's `treeshake: false` already preserves registrations).

Also document the new MSBuild Login targets in StaticAssets CLAUDE.md.

* Login: clarify why optimizeDeps.exclude is needed for UUI

Tested treeshake.moduleSideEffects: true in optimizeDeps.rollupOptions
on Vite 8 / Rolldown 1.0.0-rc.17 — registrations still get stripped.
Excluding the package from the pre-bundle is the only reliable workaround
until UUI's own Vite 8 upgrade lands. Comment captures the conclusion.

* Roll back Vite 8 → 7 in Client and Login

Vite 8.0.10 ships Rolldown 1.0.0-rc.17 which strips UUI 2.0
`customElements.define()` side-effects during dep pre-bundle, leaving
elements unregistered in dev mode. Rather than ship a v18 release tied
to a non-final Rolldown RC, revert the Vite bump and pick it up again
once Rolldown 1.0 final lands.

Changes:
- Client: vite ^8.0.10 → ^7.3.2; vite-plugin-static-copy ^4.1.0 → ^3.2.0;
  re-add vite-tsconfig-paths plugin; drop native `resolve.tsconfigPaths`.
- Login: vite ^8.0.10 → ^7.3.2; add vite-tsconfig-paths; configure plugin
  with `projects: ['./tsconfig.json', '../Umbraco.Web.UI.Client/tsconfig.json']`
  so it can resolve `@umbraco-cms/backoffice/*` imports inside Client
  source files (which would otherwise lack a discoverable tsconfig in
  Login's project tree). Drop `optimizeDeps.exclude` (no longer needed
  without Rolldown). Keep `server.fs.allow` for the cross-project font.

TypeScript 6 + ES2024 + tsconfig path generator + Login architectural
pivot all stay — those are independent of the Vite version.

Verified:
- Production https://localhost:44339/umbraco — login works
- Login dev http://localhost:5191/ — UUI registers, all custom elements defined
- Client dev http://localhost:5192/ — page loads, navigates to /section/content

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address Copilot review

- vite.config.ts (Login): narrow server.fs.allow from the parent dir to
  Login + Client only, reducing the dev server's read scope.
- external/uui/vite.config.ts (Client): replace blanket `treeshake: false`
  with `moduleSideEffects: (id) => id.includes('@umbraco-ui/uui')` so
  Rollup keeps UUI's per-component registration calls but tree-shakes the
  rest. Bundle stays at 516 KB / 96 registered tags.

* fix merge overwrites

* update package lock

* fix: do not autogenerate tsconfig on postinstall

* removes postinstall script

* chore: generates tsconfig

* chore: update lockfile

* docs: updates claude.md

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-11 09:42:28 +00:00
Andreas ZerbstandGitHub f6ac750d09 E2E: QA: Add missing helpers for Content Versioning (#22788)
* Added missing rollback helpers

* Updated helper to match locator
2026-05-11 06:33:44 +02:00
Niels Lyngsø 4b66c114c4 Revert "fix validation filter"
This reverts commit 0bdb1bb1ed.
2026-05-10 20:17:36 +02:00
Niels Lyngsø 0bdb1bb1ed fix validation filter 2026-05-10 20:16:23 +02:00
Niels Lyngsø 3142691e4f Update architecture.md 2026-05-08 15:13:47 +02:00
Niels Lyngsø c813481b4e update UUI for icon manager 2026-05-08 15:11:53 +02:00
Andy Butland 5c1e7e9167 Merge branch 'release/18.0' of https://github.com/umbraco/Umbraco-CMS into release/18.0 2026-05-08 15:06:26 +02:00
2e16b0d38a Tests: Fix PublishedValueFallbackTests after ILocalizationService removal (#22772)
* fix(tests): replace removed ILocalizationService with ILanguageService in PublishedValueFallbackTests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-08 15:05:55 +02:00
Laura NetoandGitHub ab821e0519 Open API: Skip operation ID generation for non-controller endpoints (#22760)
Skip operation ID generation for non-controller endpoints

UmbracoOperationIdTransformer is registered globally for the default
OpenAPI document, so any minimal API endpoint that lands there ran
through it. The transformer threw "This handler operates only on
ControllerActionDescriptor" because its conventions (route prefix
stripping, MapToApiVersion lookup) only make sense for MVC actions.

Return null from the generator and skip the assignment when the action
descriptor isn't a ControllerActionDescriptor. The framework's default
operation ID applies in that case.
2026-05-08 15:05:11 +02:00
11ff2c8039 Tests: Fix PublishedValueFallbackTests after ILocalizationService removal (#22772)
* fix(tests): replace removed ILocalizationService with ILanguageService in PublishedValueFallbackTests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-08 15:02:58 +02:00
Jacob Overgaard 80098706cf chore: ignores default log message for MSW 2026-05-08 13:05:50 +02:00
leekelleher dff3941e1f Merge branch 'v18/dev' 2026-05-08 10:02:00 +01:00
leekelleher 072e362284 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/variant-state.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-table-column-property-value.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document-search-result-item.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/variant-state.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts
2026-05-08 10:01:33 +01:00
b243940ab5 Auth: Un-deprecate getLatestToken and route per-request fetches through it (#22736)
* Auth: un-deprecates getLatestToken and routes per-request fetches through it

getLatestToken is the only public API for "wait for any in-flight refresh,
trigger one if the access token has expired, then return". External and
internal consumers were warned off it without an equivalent replacement:
configureClient only helps @hey-api/openapi-ts clients, and consumers using
axios/ky/native fetch had no other gate.

- Removes the @deprecated JSDoc + UmbDeprecation.warn() call so the public
  surface no longer prints a console warning per call.
- Uses getLatestToken.bind(this) for the auth callback inside configureClient
  and the token callback inside getOpenApiConfiguration so both paths share
  the same #ensureTokenReady gate.
- Replaces the hard-coded `Authorization: Bearer [redacted]` in unlinkLogin
  and #makeLinkTokenRequest with `Bearer ${await getLatestToken()}` so those
  fetches participate in the refresh coordination rather than firing with a
  potentially-revoked cookie.

Also wires the UmbracoExtension template's entrypoint to call
authContext.configureClient(client), matching the v18 template change. The
framework awaits onInit, so this guarantees the API client is fully
configured before any element in the extension can use it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Auth: tightens UmbAuthContext correctness and accepts any hey-api client

Pulls in a batch of non-breaking improvements to UmbAuthContext that came
out of an audit on the back of the un-deprecation work in this PR:

Public surface:
- configureClient(client) now accepts a new structural UmbApiClient type
  (exported from @umbraco-cms/backoffice/http-client). Each @hey-api/openapi-ts
  generation produces a fully-bound Client<…>; the backoffice's umbHttpClient
  and an extension's regenerated client are structurally identical but TS
  treats them as distinct generic instantiations. The widened parameter lets
  extensions wire their own client without `as never` casts at call sites.
  bindDefaultInterceptors keeps its strict typeof umbHttpClient parameter
  (preserving autocomplete inside interceptor callbacks); the cast happens
  once, internally.

Correctness:
- The auth context now holds a single UmbApiInterceptorController, lazy-
  initialised on first configureClient() call. Previously each call
  instantiated a new controller, which re-provided the UmbAuthSignalerContext
  on the host and stacked listeners — visible the moment an extension also
  called configureClient. One controller for the lifetime of the host, all
  configured clients share it.
- completeAuthorizationRequest checks sessionStorage before asking
  window.opener for the PKCE verifier. The previous order hung for the full
  postMessage timeout whenever oauth_complete loaded with a non-OAuth
  window.opener (which is set for ANY window.open target). The opener
  postMessage timeout is also dropped from 5s to 1.5s — a real popup parent
  responds within milliseconds; longer is just wait time for the unrelated-
  opener case.
- The cross-tab 'authorized' BroadcastChannel handler now routes through
  #setSessionLocally so the timestamp math stays in one place. The
  'sessionUpdate' handler still applies pre-computed timestamps directly
  (peer broadcast already did the math) but does so inside the
  #inSessionUpdateCallback guard, so a synchronous session$ observer can no
  longer trigger a spurious /token refresh on top of a peer's update.
- #ensureTokenReady drops its query-then-request pattern. Now always queues
  behind the umb:token-refresh lock with a no-op callback — if the lock is
  free it acquires immediately, if held it waits. Eliminates the race window
  between query() and request().
- destroy() invokes #popupCleanup before tearing down so an in-flight popup
  flow's window-level message listener and closed-poll interval don't leak
  past the context's lifetime. The cleanup helper itself now resolves the
  popup-flow Promise — every termination path (authorized, popup closed,
  superseded by a new flow, context destroyed) is observable to the awaiter
  instead of hanging forever.

Cleanup:
- makeAuthorizationRequest is annotated Promise<void> so the redirect and
  popup branches share an explicit return type.
- unlinkLogin wraps the parsed problem-details payload in a real Error (with
  the original payload exposed on `.cause`) so callers using `instanceof
  Error` or expecting a stack trace get sane behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:59:23 +02:00
1c1787c445 Auth: Un-deprecate getLatestToken and route per-request fetches through it (#22736)
* Auth: un-deprecates getLatestToken and routes per-request fetches through it

getLatestToken is the only public API for "wait for any in-flight refresh,
trigger one if the access token has expired, then return". External and
internal consumers were warned off it without an equivalent replacement:
configureClient only helps @hey-api/openapi-ts clients, and consumers using
axios/ky/native fetch had no other gate.

- Removes the @deprecated JSDoc + UmbDeprecation.warn() call so the public
  surface no longer prints a console warning per call.
- Uses getLatestToken.bind(this) for the auth callback inside configureClient
  and the token callback inside getOpenApiConfiguration so both paths share
  the same #ensureTokenReady gate.
- Replaces the hard-coded `Authorization: Bearer [redacted]` in unlinkLogin
  and #makeLinkTokenRequest with `Bearer ${await getLatestToken()}` so those
  fetches participate in the refresh coordination rather than firing with a
  potentially-revoked cookie.

Also wires the UmbracoExtension template's entrypoint to call
authContext.configureClient(client), matching the v18 template change. The
framework awaits onInit, so this guarantees the API client is fully
configured before any element in the extension can use it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Auth: tightens UmbAuthContext correctness and accepts any hey-api client

Pulls in a batch of non-breaking improvements to UmbAuthContext that came
out of an audit on the back of the un-deprecation work in this PR:

Public surface:
- configureClient(client) now accepts a new structural UmbApiClient type
  (exported from @umbraco-cms/backoffice/http-client). Each @hey-api/openapi-ts
  generation produces a fully-bound Client<…>; the backoffice's umbHttpClient
  and an extension's regenerated client are structurally identical but TS
  treats them as distinct generic instantiations. The widened parameter lets
  extensions wire their own client without `as never` casts at call sites.
  bindDefaultInterceptors keeps its strict typeof umbHttpClient parameter
  (preserving autocomplete inside interceptor callbacks); the cast happens
  once, internally.

Correctness:
- The auth context now holds a single UmbApiInterceptorController, lazy-
  initialised on first configureClient() call. Previously each call
  instantiated a new controller, which re-provided the UmbAuthSignalerContext
  on the host and stacked listeners — visible the moment an extension also
  called configureClient. One controller for the lifetime of the host, all
  configured clients share it.
- completeAuthorizationRequest checks sessionStorage before asking
  window.opener for the PKCE verifier. The previous order hung for the full
  postMessage timeout whenever oauth_complete loaded with a non-OAuth
  window.opener (which is set for ANY window.open target). The opener
  postMessage timeout is also dropped from 5s to 1.5s — a real popup parent
  responds within milliseconds; longer is just wait time for the unrelated-
  opener case.
- The cross-tab 'authorized' BroadcastChannel handler now routes through
  #setSessionLocally so the timestamp math stays in one place. The
  'sessionUpdate' handler still applies pre-computed timestamps directly
  (peer broadcast already did the math) but does so inside the
  #inSessionUpdateCallback guard, so a synchronous session$ observer can no
  longer trigger a spurious /token refresh on top of a peer's update.
- #ensureTokenReady drops its query-then-request pattern. Now always queues
  behind the umb:token-refresh lock with a no-op callback — if the lock is
  free it acquires immediately, if held it waits. Eliminates the race window
  between query() and request().
- destroy() invokes #popupCleanup before tearing down so an in-flight popup
  flow's window-level message listener and closed-poll interval don't leak
  past the context's lifetime. The cleanup helper itself now resolves the
  popup-flow Promise — every termination path (authorized, popup closed,
  superseded by a new flow, context destroyed) is observable to the awaiter
  instead of hanging forever.

Cleanup:
- makeAuthorizationRequest is annotated Promise<void> so the redirect and
  popup branches share an explicit return type.
- unlinkLogin wraps the parsed problem-details payload in a real Error (with
  the original payload exposed on `.cause`) so callers using `instanceof
  Error` or expecting a stack trace get sane behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:58:19 +02:00
6c5873047b Auth: Un-deprecate getLatestToken and route per-request fetches through it (#22736)
* Auth: un-deprecates getLatestToken and routes per-request fetches through it

getLatestToken is the only public API for "wait for any in-flight refresh,
trigger one if the access token has expired, then return". External and
internal consumers were warned off it without an equivalent replacement:
configureClient only helps @hey-api/openapi-ts clients, and consumers using
axios/ky/native fetch had no other gate.

- Removes the @deprecated JSDoc + UmbDeprecation.warn() call so the public
  surface no longer prints a console warning per call.
- Uses getLatestToken.bind(this) for the auth callback inside configureClient
  and the token callback inside getOpenApiConfiguration so both paths share
  the same #ensureTokenReady gate.
- Replaces the hard-coded `Authorization: Bearer [redacted]` in unlinkLogin
  and #makeLinkTokenRequest with `Bearer ${await getLatestToken()}` so those
  fetches participate in the refresh coordination rather than firing with a
  potentially-revoked cookie.

Also wires the UmbracoExtension template's entrypoint to call
authContext.configureClient(client), matching the v18 template change. The
framework awaits onInit, so this guarantees the API client is fully
configured before any element in the extension can use it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Auth: tightens UmbAuthContext correctness and accepts any hey-api client

Pulls in a batch of non-breaking improvements to UmbAuthContext that came
out of an audit on the back of the un-deprecation work in this PR:

Public surface:
- configureClient(client) now accepts a new structural UmbApiClient type
  (exported from @umbraco-cms/backoffice/http-client). Each @hey-api/openapi-ts
  generation produces a fully-bound Client<…>; the backoffice's umbHttpClient
  and an extension's regenerated client are structurally identical but TS
  treats them as distinct generic instantiations. The widened parameter lets
  extensions wire their own client without `as never` casts at call sites.
  bindDefaultInterceptors keeps its strict typeof umbHttpClient parameter
  (preserving autocomplete inside interceptor callbacks); the cast happens
  once, internally.

Correctness:
- The auth context now holds a single UmbApiInterceptorController, lazy-
  initialised on first configureClient() call. Previously each call
  instantiated a new controller, which re-provided the UmbAuthSignalerContext
  on the host and stacked listeners — visible the moment an extension also
  called configureClient. One controller for the lifetime of the host, all
  configured clients share it.
- completeAuthorizationRequest checks sessionStorage before asking
  window.opener for the PKCE verifier. The previous order hung for the full
  postMessage timeout whenever oauth_complete loaded with a non-OAuth
  window.opener (which is set for ANY window.open target). The opener
  postMessage timeout is also dropped from 5s to 1.5s — a real popup parent
  responds within milliseconds; longer is just wait time for the unrelated-
  opener case.
- The cross-tab 'authorized' BroadcastChannel handler now routes through
  #setSessionLocally so the timestamp math stays in one place. The
  'sessionUpdate' handler still applies pre-computed timestamps directly
  (peer broadcast already did the math) but does so inside the
  #inSessionUpdateCallback guard, so a synchronous session$ observer can no
  longer trigger a spurious /token refresh on top of a peer's update.
- #ensureTokenReady drops its query-then-request pattern. Now always queues
  behind the umb:token-refresh lock with a no-op callback — if the lock is
  free it acquires immediately, if held it waits. Eliminates the race window
  between query() and request().
- destroy() invokes #popupCleanup before tearing down so an in-flight popup
  flow's window-level message listener and closed-poll interval don't leak
  past the context's lifetime. The cleanup helper itself now resolves the
  popup-flow Promise — every termination path (authorized, popup closed,
  superseded by a new flow, context destroyed) is observable to the awaiter
  instead of hanging forever.

Cleanup:
- makeAuthorizationRequest is annotated Promise<void> so the redirect and
  popup branches share an explicit return type.
- unlinkLogin wraps the parsed problem-details payload in a real Error (with
  the original payload exposed on `.cause`) so callers using `instanceof
  Error` or expecting a stack trace get sane behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:57:32 +02:00
7248f01292 Auth: Un-deprecate getLatestToken and route per-request fetches through it (#22736)
* Auth: un-deprecates getLatestToken and routes per-request fetches through it

getLatestToken is the only public API for "wait for any in-flight refresh,
trigger one if the access token has expired, then return". External and
internal consumers were warned off it without an equivalent replacement:
configureClient only helps @hey-api/openapi-ts clients, and consumers using
axios/ky/native fetch had no other gate.

- Removes the @deprecated JSDoc + UmbDeprecation.warn() call so the public
  surface no longer prints a console warning per call.
- Uses getLatestToken.bind(this) for the auth callback inside configureClient
  and the token callback inside getOpenApiConfiguration so both paths share
  the same #ensureTokenReady gate.
- Replaces the hard-coded `Authorization: Bearer [redacted]` in unlinkLogin
  and #makeLinkTokenRequest with `Bearer ${await getLatestToken()}` so those
  fetches participate in the refresh coordination rather than firing with a
  potentially-revoked cookie.

Also wires the UmbracoExtension template's entrypoint to call
authContext.configureClient(client), matching the v18 template change. The
framework awaits onInit, so this guarantees the API client is fully
configured before any element in the extension can use it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Auth: tightens UmbAuthContext correctness and accepts any hey-api client

Pulls in a batch of non-breaking improvements to UmbAuthContext that came
out of an audit on the back of the un-deprecation work in this PR:

Public surface:
- configureClient(client) now accepts a new structural UmbApiClient type
  (exported from @umbraco-cms/backoffice/http-client). Each @hey-api/openapi-ts
  generation produces a fully-bound Client<…>; the backoffice's umbHttpClient
  and an extension's regenerated client are structurally identical but TS
  treats them as distinct generic instantiations. The widened parameter lets
  extensions wire their own client without `as never` casts at call sites.
  bindDefaultInterceptors keeps its strict typeof umbHttpClient parameter
  (preserving autocomplete inside interceptor callbacks); the cast happens
  once, internally.

Correctness:
- The auth context now holds a single UmbApiInterceptorController, lazy-
  initialised on first configureClient() call. Previously each call
  instantiated a new controller, which re-provided the UmbAuthSignalerContext
  on the host and stacked listeners — visible the moment an extension also
  called configureClient. One controller for the lifetime of the host, all
  configured clients share it.
- completeAuthorizationRequest checks sessionStorage before asking
  window.opener for the PKCE verifier. The previous order hung for the full
  postMessage timeout whenever oauth_complete loaded with a non-OAuth
  window.opener (which is set for ANY window.open target). The opener
  postMessage timeout is also dropped from 5s to 1.5s — a real popup parent
  responds within milliseconds; longer is just wait time for the unrelated-
  opener case.
- The cross-tab 'authorized' BroadcastChannel handler now routes through
  #setSessionLocally so the timestamp math stays in one place. The
  'sessionUpdate' handler still applies pre-computed timestamps directly
  (peer broadcast already did the math) but does so inside the
  #inSessionUpdateCallback guard, so a synchronous session$ observer can no
  longer trigger a spurious /token refresh on top of a peer's update.
- #ensureTokenReady drops its query-then-request pattern. Now always queues
  behind the umb:token-refresh lock with a no-op callback — if the lock is
  free it acquires immediately, if held it waits. Eliminates the race window
  between query() and request().
- destroy() invokes #popupCleanup before tearing down so an in-flight popup
  flow's window-level message listener and closed-poll interval don't leak
  past the context's lifetime. The cleanup helper itself now resolves the
  popup-flow Promise — every termination path (authorized, popup closed,
  superseded by a new flow, context destroyed) is observable to the awaiter
  instead of hanging forever.

Cleanup:
- makeAuthorizationRequest is annotated Promise<void> so the redirect and
  popup branches share an explicit return type.
- unlinkLogin wraps the parsed problem-details payload in a real Error (with
  the original payload exposed on `.cause`) so callers using `instanceof
  Error` or expecting a stack trace get sane behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:54:04 +01:00
Laura NetoandGitHub 317e9b4e69 Elements: Disable inaccessible parent folders in element tree (#22749)
Disable inaccessible parent folders in element tree

When an element start node is configured to a child folder, the backend
returns ancestor folders flagged with NoAccess so they show as breadcrumbs.
The element folder tree item used the default tree item element, which
does not observe noAccess, so parent folders rendered as enabled and
clickable in the Library section tree. Added a custom
element-folder-tree-item element that observes the context's noAccess and
forwards it to the base, which already handles disabling the menu item.
2026-05-08 09:51:24 +01:00
leekelleher 4fab629ee3 Documents: Alias DocumentVariantStateModel API model for backoffice client (#22716)
* Client: Aliased `DocumentVariantStateModel` for documents and document-blueprints packages

Hoist `UmbDocumentVariantState` and `UmbDocumentBlueprintVariantState` aliases (re-exporting `DocumentVariantStateModel`) into dedicated `variant-state.ts` leaf files. Internal package modules, mocks and the core split-view selector now consume the alias instead of referencing `DocumentVariantStateModel` directly, mirroring the structure on `v18/dev` to reduce upstream-merge conflicts.

* Revert mock data changes

to prevent importing the whole "document" module.

* Tweaked the `DocumentVariantStateModel` import for mock data

Otherwise this is problematic for cherry-picked commits for v18.0.

* Missed one!
# Conflicts:
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/document-blueprint.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/kitchen-sink/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/user-permissions/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document-blueprint.db.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document-publishing.manager.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document.db.ts
#	src/Umbraco.Web.UI.Client/mocks/tools/sqlite-to-mock/transform-documents.ts
#	src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/types.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/types.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-table-column-property-value.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/shared/document-variant-language-picker.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document-search-result-item.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/variant-state.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts
2026-05-08 09:49:55 +01:00
Andy Butland 221531b614 Published Content: Fix Fallback.ToAncestors with no match throwing exception at property level (closes #22759) (#22763)
* Fix Fallback.ToAncestors regression at property level.

* Further unit tests.
2026-05-08 10:32:18 +02:00
leekelleher 8495405927 Documents: Alias DocumentVariantStateModel API model for backoffice client (#22716)
* Client: Aliased `DocumentVariantStateModel` for documents and document-blueprints packages

Hoist `UmbDocumentVariantState` and `UmbDocumentBlueprintVariantState` aliases (re-exporting `DocumentVariantStateModel`) into dedicated `variant-state.ts` leaf files. Internal package modules, mocks and the core split-view selector now consume the alias instead of referencing `DocumentVariantStateModel` directly, mirroring the structure on `v18/dev` to reduce upstream-merge conflicts.

* Revert mock data changes

to prevent importing the whole "document" module.

* Tweaked the `DocumentVariantStateModel` import for mock data

Otherwise this is problematic for cherry-picked commits for v18.0.

* Missed one!
# Conflicts:
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/document-blueprint.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/kitchen-sink/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/user-permissions/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document-blueprint.db.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document-publishing.manager.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document.db.ts
#	src/Umbraco.Web.UI.Client/mocks/tools/sqlite-to-mock/transform-documents.ts
#	src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/types.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/types.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-table-column-property-value.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/shared/document-variant-language-picker.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document-search-result-item.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/variant-state.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts
2026-05-08 09:23:36 +01:00
Andy Butland a434ad7b33 Published Content: Fix Fallback.ToAncestors with no match throwing exception at property level (closes #22759) (#22763)
* Fix Fallback.ToAncestors regression at property level.

* Further unit tests.
2026-05-08 10:19:00 +02:00
Andy Butland 3714ebbb29 Published Content: Fix Fallback.ToAncestors with no match throwing exception at property level (closes #22759) (#22763)
* Fix Fallback.ToAncestors regression at property level.

* Further unit tests.
2026-05-08 10:18:09 +02:00
Andy Butland 58b047bf7e Published Content: Fix Fallback.ToAncestors with no match throwing exception at property level (closes #22759) (#22763)
* Fix Fallback.ToAncestors regression at property level.

* Further unit tests.
2026-05-08 10:17:20 +02:00
Andy ButlandandGitHub 1a74aa53c9 Published Content: Fix Fallback.ToAncestors with no match throwing exception at property level (closes #22759) (#22763)
* Fix Fallback.ToAncestors regression at property level.

* Further unit tests.
2026-05-08 10:15:28 +02:00
Andy Butland 1214771847 Bump version to 17.6.0-rc. 2026-05-08 10:07:34 +02:00
Lee KelleherandGitHub 396497a921 Documents: Alias DocumentVariantStateModel API model for backoffice client (#22716)
* Client: Aliased `DocumentVariantStateModel` for documents and document-blueprints packages

Hoist `UmbDocumentVariantState` and `UmbDocumentBlueprintVariantState` aliases (re-exporting `DocumentVariantStateModel`) into dedicated `variant-state.ts` leaf files. Internal package modules, mocks and the core split-view selector now consume the alias instead of referencing `DocumentVariantStateModel` directly, mirroring the structure on `v18/dev` to reduce upstream-merge conflicts.

* Revert mock data changes

to prevent importing the whole "document" module.

* Tweaked the `DocumentVariantStateModel` import for mock data

Otherwise this is problematic for cherry-picked commits for v18.0.

* Missed one!
2026-05-08 08:04:07 +00:00
Jacob OvergaardandClaude Sonnet 4.6 80cc752b3d Backoffice Mocks: Add missing element start node fields to documents mock set
`elementStartNodeIds` and `hasElementRootAccess` were added to
`UmbCurrentUserModel` by the Global Elements PR but the documents mock
data set was created without them, causing `undefined.map()` errors in
the document workspace CRUD tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 09:08:40 +02:00
Jacob Overgaard 39125da978 Merge remote-tracking branch 'origin/main' into v18/dev 2026-05-08 09:03:36 +02:00
Niels Lyngsø bf32f9e5a6 update package-lock 2026-05-08 09:01:20 +02:00
Niels Lyngsø 3220739faa upgrade to UI LIbrary 1.17.3 2026-05-08 08:59:49 +02:00
Niels Lyngsø ff565b95e0 update package-lock 2026-05-08 08:54:28 +02:00
Niels Lyngsø 93a1f82b05 Merge branch 'release/17.4.0'
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
#	version.json
2026-05-08 08:53:57 +02:00
Jacob OvergaardandGitHub ae4ac2a4b9 build(deps): bumps @umbraco-ui/uui to 1.17.3 (#22753) 2026-05-08 08:52:11 +02:00
Andreas ZerbstandGitHub 54ded689e8 E2E: QA: Add .prettierrc.json to acceptance tests for formatting consistency (#22751)
Add .prettierrc.json to acceptance tests for formatting consistency
2026-05-08 09:19:26 +07:00
Jacob Overgaard 1c32829883 chore: fixes to use correct import of api types in mock data 2026-05-07 21:26:53 +02:00
Andy ButlandandJacob Overgaard dc446c0e4a Color Picker: Refresh stored label when data type label changes (closes #22741) (#22761)
* Update stored color label if changed on save of document with color picker.

* Clarify intent of change event dispatch in label sync

* Make comparison case insensitive.

* Added unit tests for new behaviour.
2026-05-07 21:17:00 +02:00
Jacob Overgaard 1e59af34ff Merge remote-tracking branch 'origin/main' into v18/dev 2026-05-07 21:16:24 +02:00
Andy ButlandandJacob Overgaard 17e73eee28 Color Picker: Refresh stored label when data type label changes (closes #22741) (#22761)
* Update stored color label if changed on save of document with color picker.

* Clarify intent of change event dispatch in label sync

* Make comparison case insensitive.

* Added unit tests for new behaviour.
2026-05-07 21:14:52 +02:00
Andy ButlandandGitHub 2292b7479d Color Picker: Refresh stored label when data type label changes (closes #22741) (#22761)
* Update stored color label if changed on save of document with color picker.

* Clarify intent of change event dispatch in label sync

* Make comparison case insensitive.

* Added unit tests for new behaviour.
2026-05-07 21:13:05 +02:00
Andy Butland ab1be601f5 Dictionary: Order SQL before FetchOneToMany to prevent duplicate items in collection view (closes #22640) (#22750)
* Order SQL before FetchOneToMany in dictionary entry retrieval to prevent duplicate items in collection view.

* Used PrimaryKey instead of UniqueId to take advantage of the clustered index.
2026-05-07 18:45:25 +02:00
Andy Butland 1baf4e5a5c Merge branch 'main' into v18/dev 2026-05-07 18:43:56 +02:00
Andy ButlandandGitHub 9b1fc50de3 Dictionary: Order SQL before FetchOneToMany to prevent duplicate items in collection view (closes #22640) (#22750)
* Order SQL before FetchOneToMany in dictionary entry retrieval to prevent duplicate items in collection view.

* Used PrimaryKey instead of UniqueId to take advantage of the clustered index.
2026-05-07 14:29:43 +00:00
Jacob Overgaard bcf9bf3f3a Merge branch 'release/18.0' into v18/dev 2026-05-07 16:23:19 +02:00
Niels Lyngsø e4dce93b79 Merge branch 'main' into v18/dev
# Conflicts:
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/ContentUiHelper.ts
2026-05-07 14:06:36 +02:00
Niels Lyngsø b07e908ce3 Document Workspace: Add CRUD and property value tests for document workspace context (#22621)
* temp mock set

* test getPropertyValue

* Extend document workspace context tests to cover read/write property values

* move context files into context folder

* Add document CRUD tests, mock handler & interceptor

* temp mock error interceptor

* Return 404 when document not found

* Use undefined for entity unique state until initialized

* Fix import paths for document workspace editor

* Add test utils and extend document workspace tests

* Update document-workspace-context.test-utils.ts

* Match invariant variant when variantId missing

* Ensure finishPropertyValueChange runs on exit

Wrap setPropertyValue implementation in a try/finally and move finishPropertyValueChange into the finally block so cleanup always runs even if an error is thrown. No other functional changes — code was re-indented and organized but behavior remains the same except for guaranteed cleanup on error.

* Require variantId for culture/segment-variant props

* fix types

* fix mock modal typescript error

* Distinguish unloaded vs root entity unique

* use the real current user context

* hide mock set in UI

* rename mock set

* Move initiatePropertyValueChange into try

* Use 'satisfies' for UmbMockDataSet assertions

* Preserve requested unique on failed load

* Treat missing variantId as invariant

* Reset update lock on destroy

* remove unused group + user

* Guard _current.unmute and remove destroy override

* Add tests for element data manager

* Guard subject access and add destroy test

* Throw when calling methods after destroy
2026-05-07 14:00:34 +02:00
3052b57203 E2E: QA: add acceptance tests for content versioning (#22702)
* Added tests

* Updated

* Cleaned up

* Fixes based on comments

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Added helpers for verifying document

* Removed redundant method

* Cleaned up

* Reverted deletion of constants

* Undo revert

* Fixes based on comments

* updated command

* Added removed method

* Update smokeTest command in package.json

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-07 10:13:29 +00:00
aba8e3eb7a Icons: developer icon manager (#22437)
* extend icons with information from theseaurus

* implement new icon search logic

* clean-up data

* icon manager

* sorting with a backup of the name

* refactor into a controller

* improve multi word group search

* embed lucide data

* rename tech into technology

* remove paper from dollar

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* improve search

* related should not show up in search

* update threshold

* separate name words

* also consider full icon name match

* better comment

* other approach for full name matches

* full icon name search if query contains a -

* fix test

* remove related code

* updates to related

* make its own package

* revert changes

* update tsconfig

* package-lock

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-07 10:11:05 +00:00
Lee KelleherandGitHub 41a9133c58 V18: Reverts removal of property-value-change event listeners (#22734)
* Reverts removal of `property-value-change` event listeners

* Adds `UmbDeprecation` warning

for `property-value-change` events.
2026-05-07 09:27:05 +00:00
4953855dda Build: Upgrade @hey-api/openapi-ts to 0.97 (#22735)
* build(deps): updates @hey-api/openapi-ts to latest and regenerates APi types

* build(deps): updates @hey-api/openapi-ts to latest and regenerates APi types (login)

* fix(backoffice): avoid invalid status 0 when synthesizing responses

Default to a 500 fallback status when no upstream Response is provided
to #createResponse, preventing a RangeError from the Response constructor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(deps): updates UmbracoExtension template to @hey-api/openapi-ts 0.97

- Bumps @hey-api/openapi-ts to ^0.97.0 in the extension template.
- Simplifies the generate-openapi.js plugin config: spread @hey-api defaults
  and only override @hey-api/sdk with responseStyle: 'fields' so call sites
  keep the { data, error } destructuring shape. Removes the redundant
  @hey-api/client-fetch redeclaration that triggered duplicate-plugin warnings.
- Drops the hey-api.ts runtime config file in favour of wiring the generated
  client through UMB_AUTH_CONTEXT.configureClient() from the entrypoint, so
  extensions inherit the same auth callback and default response interceptors
  (401 retry, error notifications) as the core backoffice.
- Regenerates the pre-bundled SDK against the template's canonical
  Umbraco.Extension scaffold so it matches what `npm run generate-client`
  produces on first run; default hey-api output is flat function exports.
- Updates dashboard.element.ts call sites to match the new SDK shape and
  renames the user model usage to Iuser to follow the new schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(git): marks UmbracoExtension template generated SDK as linguist-generated

So GitHub diffs collapse the regenerated *.gen.ts files in PRs, matching what
we already do for the backoffice client and Login app SDKs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(template): addresses review feedback on PR #22735

- Restores the regenerated SDK's hard-coded baseUrl to https://localhost:44339/
  so the SiteDomain template token in the .template.config still substitutes
  it at scaffold time. The 5443 port leaked in from the local host I used to
  regenerate; that domain is replaced by the user's chosen SiteDomain on
  scaffold.
- Stops marking onInit as `async`. The UmbEntryPointOnInit signature returns
  void; making the hook async is harmless under TS's bivariant void-return
  assignability but is misleading. Kicks the context resolution + client
  configuration off via .then() and logs a warning when UMB_AUTH_CONTEXT is
  not present (instead of silently optional-chaining).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(template): keeps onInit async — the framework awaits it

The previous tweak was based on Copilot's claim that UmbEntryPointOnInit
returns void. The signature does declare void, but the entry-point
initializer in app-entry-point-extension-initializer.ts and
backoffice-entry-point-extension-initializer.ts both `await
moduleInstance.onInit(...)`, so an async onInit is awaited end-to-end.
Reverting to async ensures configureClient runs to completion before any
element in the extension can hit the API client.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:11:59 +00:00
Mads RasmussenandGitHub 040a0d5e49 Document Workspace: Add CRUD and property value tests for document workspace context (#22621)
* temp mock set

* test getPropertyValue

* Extend document workspace context tests to cover read/write property values

* move context files into context folder

* Add document CRUD tests, mock handler & interceptor

* temp mock error interceptor

* Return 404 when document not found

* Use undefined for entity unique state until initialized

* Fix import paths for document workspace editor

* Add test utils and extend document workspace tests

* Update document-workspace-context.test-utils.ts

* Match invariant variant when variantId missing

* Ensure finishPropertyValueChange runs on exit

Wrap setPropertyValue implementation in a try/finally and move finishPropertyValueChange into the finally block so cleanup always runs even if an error is thrown. No other functional changes — code was re-indented and organized but behavior remains the same except for guaranteed cleanup on error.

* Require variantId for culture/segment-variant props

* fix types

* fix mock modal typescript error

* Distinguish unloaded vs root entity unique

* use the real current user context

* hide mock set in UI

* rename mock set

* Move initiatePropertyValueChange into try

* Use 'satisfies' for UmbMockDataSet assertions

* Preserve requested unique on failed load

* Treat missing variantId as invariant

* Reset update lock on destroy

* remove unused group + user

* Guard _current.unmute and remove destroy override

* Add tests for element data manager

* Guard subject access and add destroy test

* Throw when calling methods after destroy
2026-05-07 09:36:05 +02:00
Laura Neto 6201c3dc40 Bump version to 18.1.0-rc 2026-05-06 19:15:46 +02:00
Laura Neto 0c08d522a2 Adjust Umbraco.Tests.AcceptanceTest version to 18.0.0-beta1 2026-05-06 19:05:48 +02:00
Laura Neto 10a656c067 Merge branch 'main' into v18/dev 2026-05-06 18:55:54 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Jacob Overgaard
ef01edb46a Bump lodash from 4.17.21 to 4.18.1 in /src/Umbraco.Web.UI.Client (#22723)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-05-06 16:47:05 +00:00
Laura NetoandGitHub 6067d428d0 Delivery API: Fix broken discriminator mapping refs for polymorphic schemas (#22733)
* Delivery API: Fix broken discriminator mapping refs for polymorphic schemas

Microsoft.AspNetCore.OpenApi's MapPolymorphismOptionsToDiscriminator builds each ref as callback(base) + callback(derived), but our typed-schema flow registers the derived schemas without the base prefix. The auto-built mapping refs end up pointing at non-existent schemas, which crashes strict client generators like orval.

Strip the base schema id from the front of each broken ref to recover the registration key the derived schema actually uses.

* Delivery API: Add integration test coverage for the polymorphic discriminator mapping fix

Adds a test-only property editor whose Delivery API value type is a polymorphic interface declared with [JsonDerivedType], wired into the existing typed-schema integration test fixture. The OpenApiContract_HasExpectedSchemas test verifies that the auto-built discriminator mapping refs resolve to the registered derived schema names, providing end-to-end regression coverage for the fix.

Also extends AssertSchemaIsPolymorphicUnion to accept either oneOf (used by our typed schema unions) or anyOf (used by framework-built unions for [JsonDerivedType] interfaces).

* Use a captured schemas local in FixAutoBuiltDiscriminatorMapping

Move the null check for document.Components.Schemas into the top-of-method guard and use the captured non-null local in the loop body. Avoids both the null-conditional ?. operators and the null-forgiving ! operator at the use sites.
2026-05-06 17:09:44 +02:00
Andy Butland bf5f82607e Merge branch 'main' into v18/dev 2026-05-06 16:25:30 +02:00
8ab68b574f Backoffice: Add localize.htmlString() helper to prevent XSS in HTML-rendered translations (#22731)
* docs(claude): document how unsafeHTML should be used together with escapeHTML()

* fix: adds escapeHTML where appropriate in order not to render html directly

* chore: removes small nitpick fallback

* docs(claude): fixes incorrect using of unsafeHTML

* feat(localization): add localize.htmlString() and convert call sites

Adds a new `htmlString()` method on UmbLocalizationController that escapes
interpolated args via escapeHTML and returns a Lit unsafeHTML directive.
This is the safe replacement for the manual `unsafeHTML(this.localize.string(...))`
pattern, which leaves user-controlled args un-escaped (XSS hazard).

Converts all direct `unsafeHTML(localize.string|term(...))` call sites
across modals, rollback views, packager, property editors, and entity
actions. Also fixes the latent XSS in `trash.action.ts` (sibling of the
previously-fixed `delete.action.ts`).

Updates docs/security.md with guidance on `string()` vs `htmlString()`
and the modal-content wrapping pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(eslint): add no-unsafe-localize rule to flag unsafeHTML(localize.string|term(...))

Catches the XSS pattern this PR's helper replaces, so future regressions
are caught at lint time instead of in review (or in a security advisory).
Suggests `localize.htmlString(...)` as the safe replacement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(localization): stringify htmlString args before escaping

Addresses review feedback on PR #22731. escapeHTML() short-circuits on
non-strings (returns the value unchanged), so an arg like
{ toString: () => '<script>...</script>' } would bypass the escape and
render unescaped via unsafeHTML.

Stringifies args before escaping while preserving `undefined` so
string()'s placeholder semantics are unchanged. Adds a regression test
covering the toString() bypass.

Also adds the missing html/unsafeHTML imports to the security.md
example so the snippet is self-contained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(installer-consent-element): sanitise content before rendering it

* fix(dashboard-telem-element): sanitise html before rendering

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: LLaverty <liamlaverty@gmail.com>
2026-05-06 16:11:47 +02:00
7c6b755ca5 Backoffice: Add localize.htmlString() helper to prevent XSS in HTML-rendered translations (#22731)
* docs(claude): document how unsafeHTML should be used together with escapeHTML()

* fix: adds escapeHTML where appropriate in order not to render html directly

* chore: removes small nitpick fallback

* docs(claude): fixes incorrect using of unsafeHTML

* feat(localization): add localize.htmlString() and convert call sites

Adds a new `htmlString()` method on UmbLocalizationController that escapes
interpolated args via escapeHTML and returns a Lit unsafeHTML directive.
This is the safe replacement for the manual `unsafeHTML(this.localize.string(...))`
pattern, which leaves user-controlled args un-escaped (XSS hazard).

Converts all direct `unsafeHTML(localize.string|term(...))` call sites
across modals, rollback views, packager, property editors, and entity
actions. Also fixes the latent XSS in `trash.action.ts` (sibling of the
previously-fixed `delete.action.ts`).

Updates docs/security.md with guidance on `string()` vs `htmlString()`
and the modal-content wrapping pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(eslint): add no-unsafe-localize rule to flag unsafeHTML(localize.string|term(...))

Catches the XSS pattern this PR's helper replaces, so future regressions
are caught at lint time instead of in review (or in a security advisory).
Suggests `localize.htmlString(...)` as the safe replacement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(localization): stringify htmlString args before escaping

Addresses review feedback on PR #22731. escapeHTML() short-circuits on
non-strings (returns the value unchanged), so an arg like
{ toString: () => '<script>...</script>' } would bypass the escape and
render unescaped via unsafeHTML.

Stringifies args before escaping while preserving `undefined` so
string()'s placeholder semantics are unchanged. Adds a regression test
covering the toString() bypass.

Also adds the missing html/unsafeHTML imports to the security.md
example so the snippet is self-contained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(installer-consent-element): sanitise content before rendering it

* fix(dashboard-telem-element): sanitise html before rendering

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: LLaverty <liamlaverty@gmail.com>
2026-05-06 16:11:14 +02:00
Sven GeusensandGitHub b2ba4abd7e Code Tidy: Remove obsolete MoveEventInfo.NewParent (#22728)
* Removed obsoleted property

Updated methods that were still using it
Obsoleted constructors that were still setting the value.

* Updated code that were using the now obsoleted constructors

* More obsoleted constructor fixes

* Update unittests

Removed obsolete (parentId) cases and updated constructors

* DRY up constructor
2026-05-06 13:21:22 +00:00
2ac2b3e4fa Fix main branch after merge issue (#22729)
* Revert "MD files for Design knowledge (#22725)"

This reverts commit 212f3183c1.

* Revert "Backoffice Mocks: Derive user language access from user groups (#22721)"

This reverts commit 9671fec9ad.

* Revert "File-system Services: Complete child scopes on read-miss and validation-failure paths (#22717)"

This reverts commit 489d9ebc2e.

* Revert "manual revert of merge gone wrong"

This reverts commit a443f8ba08.

* Revert "fix(installer-user): added min length message for installer user elem… (#21829)"

This reverts commit 6789d7e757.

* Reapply "Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into claude/keen-nightingale-5ef5bd"

This reverts commit daecbd02b8.

* fix(installer-user): added min length message for installer user elem… (#21829)

* fix(installer-user): added min length message for installer user element.

* Update src/Umbraco.Web.UI.Client/src/apps/installer/user/installer-user.element.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix password minlength message binding syntax

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Emma L Garland <1649855+emmagarland@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>

* File-system Services: Complete child scopes on read-miss and validation-failure paths (#22717)

* Ensure scopes in FolderServiceOperationBase are completed.

* Added integration tests to verify the fixes.

* Backoffice Mocks: Derive user language access from user groups (#22721)

fix(mocks): derive user language access from user groups

Previously hasAccessToAllLanguages was hardcoded to true and languages to
an empty array. Now both are derived from the user's user group memberships.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* MD files for Design knowledge (#22725)

* Fix issues following merge.

* Fixed linting errors.

* Fix linter errors (2).

* Restore current-user.context.ts

* Restore block-list-entry.element.ts.

* Removed failing webhook repository test files.

---------

Co-authored-by: Yari Mariën <75362020+Yinzy00@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Emma L Garland <1649855+emmagarland@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 15:01:05 +02:00
fec0cac557 Delivery API: Generate typed OpenAPI schemas per content type (#22666)
* Delivery API: Generate typed OpenAPI schemas per content type

* Honour Delivery API allow/deny list in typed OpenAPI schemas

ContentTypeSchemaTransformer now filters DocumentTypes through
DeliveryApiSettings.IsAllowedContentType so document types blocked
by AllowedContentTypeAliases / DisallowedContentTypeAliases no longer
leak into the polymorphic union or discriminator mapping.

* Stop registering media derived types in the JSON resolver

ContentJsonTypeResolverBase.GetDerivedTypes goes back to returning
empty. Previously it registered ApiMediaWithCrops and
ApiMediaWithCropsResponse as derived types of their interfaces, which
made every consumer of the resolver (the Delivery API and webhooks)
emit a $type discriminator on media payloads, even when the typed
schema feature was disabled.

The Delivery API still needs a base schema for the typed media
schemas to extend via allOf. Since the concrete media classes are
internal to Umbraco.Infrastructure and cannot be referenced from
[JsonDerivedType] in Core, ContentTypeSchemaTransformer now builds
that base from the interface's own properties when the interface has
no [JsonDerivedType] entries. Content/element interfaces are
unaffected and keep using their declared concrete derived types.

Snapshots regenerated.

* Drop default JsonDerivedType registrations from Delivery API interfaces

Removes the [JsonDerivedType] attributes from IApiContent,
IApiContentResponse, and IApiElement. Without them System.Text.Json
configures no polymorphism by default, so wire payloads stop carrying
$type fields and the OpenAPI spec stops emitting a discriminator on
the generic schemas - matching v17 Delivery API behaviour. Consumers
that need polymorphic serialization can still register derived types
via ContentJsonTypeResolverBase.

Snapshots regenerated.

* Allows nulls at property reference sites without mutating any shared component schema.
Avoid unnecessary re-get of the JsonTypeInfo for the default case.

* Updated expected contracts following code adjustments

* Drop additionalProperties: false from typed schemas

JSON Schema 2020-12 (mandated by OpenAPI 3.1) does not let additionalProperties look through allOf, so a strict validator rejects every inherited field on the composed *ResponseModel/*Model/*PropertiesModel schemas. Most code generators silently ignore it, but the document is technically invalid and the constraint would be a lie anyway since Umbraco can grow new properties in non-major releases.

Removed from all four schema construction sites (response, content type, properties, and the interface-based fallback) and regenerated the affected snapshots.

* Preserve casing of content type aliases in OpenAPI schema IDs

Replaces the legacy ModelsBuilder-style ToCleanString tokenizer with
ToFirstUpperInvariant. The tokenizer split aliases on case boundaries
and mangled capital-letter runs (e.g. "xMLSitemap" -> "XMlsitemap"),
making the typed schema names harder to read for OpenAPI consumers.
Since content type aliases are already valid identifiers, only the
first character needs uppercasing.

Also adds an "xMLSitemap" sample type to the integration tests to
cover the casing-preservation behavior.

* Qualify properties model schema IDs by item type

Document, element, and media types share the same alias namespace
across content/media (a doc-type and a media-type can use the same
alias), so a "{Schema}PropertiesModel" naming scheme could collide.

Properties model schemas now follow the same Content/Element/Media
suffix as their parent *Model schema:

- Document type: ArticlePageContentPropertiesModel
- Element type:  TestElementElementPropertiesModel
- Media type:    VideoMediaPropertiesModel

Composition references look up each composition's own IsElement so
that a doc-type composing an element-type (allowed in the UI) still
references the correct ElementPropertiesModel schema.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-06 12:37:15 +00:00
1bda1c0ef6 Global Elements: User permissions for Element Folders (#22274)
* feat(elements): add granular user permissions for element folders

Add element-folder entity type to applicable entityUserPermission
manifests (Create, Read, Update, Delete, Move) and register a
separate userGranularPermission with a folder-only picker component.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(elements): separate element folder permissions into own directory

Move element-folder entityUserPermission and userGranularPermission
manifests into folder/user-permissions/ with dedicated component.
Revert element manifests to element-only forEntityTypes. Also adds
permission condition to folder update entity action and filters
permission names by entity type.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Corrects the type-safety of the "selected" event

* Commented out `userGranularPermission` manifest for Element Folders

* Added specific permission verbs for Element Folders

* Added Element Folder User Permission condition

* Updated entity-action manifest conditions

for Element Folder permissions

* Updated permission prefixes

from `Umb.ElementFolder.` to match the server `Umb.ElementContainer.`

* Add explicit element folder permission handling

* The ElementPermissionService should not authorize against element containers anymore

* More granular read permission handling for trees

* Rename ElementFolder to ElementContainer

* Export element folder user permission constants from @umbraco-cms/backoffice/element

The 6 new element folder permission constants were not re-exported
through the element package barrel, causing the export-consts test
to fail.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Updated manifest conditions for Element Folder delete permission

* Enforce update permission on element folder name field

Added nameWriteGuard rule to the element folder workspace context
that blocks renaming when the user lacks the
Umb.ElementContainer.Update permission.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix casing

* Fix incorrect condition aliases on element folder actions

- Remove trashed condition from folderCreateOption (create options modal
  already handles this via the parent create action's conditions)
- Use folder-specific permission condition alias on recycle-bin folder
  trash action instead of the generic element permission condition

* Renamed to `ElementContainerPermissionPresentationModel`

to match the server's future naming of this model.

* refactor(elements): apply review feedback for folder permissions

- Switch nameWriteGuard to fallbackToNotPermitted policy, so the rename
  guard expresses intent as "default deny, allow when permitted" rather
  than relying on a permitted:false rule cleared by the condition.
- Rename #enforceUpdatePermission to #setupNameWritePermissions for
  clarity (the method now manages a positive-grant rule).
- Make condition's #elementFolderPermissions and #fallbackPermissions
  optional so "not loaded" is distinguishable from "loaded empty";
  bail out early in #checkPermissions until both have populated, to
  avoid evaluating permissions against incomplete data.
- Drop constructor consumption of UMB_MODAL_MANAGER_CONTEXT in the
  granular permission input element; resolve the modal manager via
  getContext at call time inside the two action methods that need it.

* Add missing using to fix the failing build

* updates server api types

* Fix build error after clean-ups

* Fixes FE build error

Temporarily defines the `IPermissionPresentationModelElementContainerPermissionPresentationModel` type,
for future use.

* Remove duplicate migration

* Remove another duplicate migration

* Add performance improvements from #22405 to ElementContainerPermissionService and add unit tests to prove it

* Fix element permission authorization for descendants

* Test for descendant element delete permissions before deleting an element container

* Update tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ElementPermissionServiceTests.cs

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Co-authored-by: kjac <kja@umbraco.dk>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-06 11:30:52 +00:00
Andy ButlandandGitHub 179a3c8c5a Code Tidy: Clean up further obsoleted code scheduled for removal in Umbraco 18 (IFileService) (#22675)
* Remove the obsolete IFileService, the implementation and update all callers.

* Extend ServiceContext to include replacement service.

* Restore fallback behaviour for resolved users.

* Make TrySetTemplate async to avoid sync-over-async with new services.

* Addressed code review feedback.

* Reverted updates to stylesheet properties.

* Add helper and tests for path splitting.

* Ensure create of directory path on package data import.

* Verification with integration test.
2026-05-06 19:38:33 +09:00
Sven GeusensandGitHub d40eded264 Clarified BackOfficeTokenCookieSettings obsoletion message (#22727)
* Clarify obsoletion message

* Update obsoletion message with better templating/language
2026-05-06 10:29:35 +00:00
35fbc75f8d Typeloader: Comply with public obsoletion by making the Properties internal (#22726)
* Comply with public obsoletion by makng the Properties internal

* Tidied up XML header comments.

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Fixed indents.

Co-authored-by: Andy Butland <abutland73@gmail.com>

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-06 09:54:03 +00:00
Andy Butland 626f0a9ee1 Bump version to 17.4.0-rc3. 2026-05-06 11:39:17 +02:00
Niels LyngsøandGitHub 212f3183c1 MD files for Design knowledge (#22725) 2026-05-06 09:26:28 +00:00
Niels Lyngsø 38c68ef384 Merge branch 'v17/hotfix/22472' 2026-05-06 10:39:09 +02:00
9671fec9ad Backoffice Mocks: Derive user language access from user groups (#22721)
fix(mocks): derive user language access from user groups

Previously hasAccessToAllLanguages was hardcoded to true and languages to
an empty array. Now both are derived from the user's user group memberships.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 09:29:53 +02:00
Andy ButlandandGitHub 489d9ebc2e File-system Services: Complete child scopes on read-miss and validation-failure paths (#22717)
* Ensure scopes in FolderServiceOperationBase are completed.

* Added integration tests to verify the fixes.
2026-05-06 13:32:39 +09:00
Lan Nguyen ThuyandNguyenThuyLan 417e63028d update custom property editor setup for acceptance test 2026-05-06 10:17:28 +07:00
Mads Rasmussen 9e34b76bf0 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-05-05 22:29:56 +02:00
Mads Rasmussen a443f8ba08 manual revert of merge gone wrong 2026-05-05 22:29:35 +02:00
6789d7e757 fix(installer-user): added min length message for installer user elem… (#21829)
* fix(installer-user): added min length message for installer user element.

* Update src/Umbraco.Web.UI.Client/src/apps/installer/user/installer-user.element.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix password minlength message binding syntax

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Emma L Garland <1649855+emmagarland@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-05-05 22:26:40 +02:00
Mads Rasmussen daecbd02b8 Revert "Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into claude/keen-nightingale-5ef5bd"
This reverts commit 0c57e304f8, reversing
changes made to 7c7073428d.
2026-05-05 22:12:39 +02:00
Mads Rasmussen 0c57e304f8 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into claude/keen-nightingale-5ef5bd 2026-05-05 22:10:55 +02:00
ede972f711 Radio button list: Not saving value on keyboard navigation (closes #22698) (#22699)
Fix radio button list not saving value on keyboard navigation

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-05 19:50:59 +00:00
Mads RasmussenandGitHub 2c88f2ae3c Current User: Fix reload not fetching fresh data when entity events fire (#22719)
* Ensure current-user reloads fetch fresh data

* Update current-user.context.test.ts
2026-05-05 21:32:35 +02:00
d40c959be7 Dashboard: Browser title + Hints (#22517)
* View Contexts for Dashboards + Section Views to support Browser Title and Hints

* fix code

* use alias for observe ctrl alias

* remove test code

* Position badge in section icon slot

---------

Co-authored-by: engjlr <enl@umbraco.dk>
2026-05-05 21:26:37 +02:00
77ded81eff Languages: Sort the global content language selector (closes #22628) (#22711)
* Align sorting of content language selector with variant selector.

* Hoist sortLanguages helpers to module scope.

---------

Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-05-05 18:33:07 +02:00
Sven GeusensandGitHub 34569e48f0 Update SupportsBlockLayoutAlias obsoletion timeframe (#22715) 2026-05-05 15:49:41 +00:00
2c9acb38f0 Management API: Override document-level security on AllowAnonymous endpoints (#22712)
* Management API: Override document-level security on AllowAnonymous endpoints

Operations on controllers/actions decorated with [AllowAnonymous] inherit the
document-level Bearer security requirement in OpenAPI 3.x unless they explicitly
declare an empty security array. Without that override, the generated SDK
attaches an Authorization: Bearer header to anonymous endpoints (server/status,
server/configuration, install/*, manifest/manifest/public, etc.), which forces
a /security/back-office/token refresh during the very first page load.

On v18/dev this manifests as a 500 from /server/status during a fresh install:
the Authorization header triggers OpenIddict, which resolves UmbracoDbContext
from DI, which throws because the connection string is empty in the install
state.

The transformer now sets operation.Security = [] on AllowAnonymous endpoints so
they correctly opt out of the document-level security. The committed OpenApi.json
and the regenerated sdk.gen.ts reflect this.

* Management API: Fix unit tests for AllowAnonymous security override

The transformer now sets operation.Security = [] (empty list) on
[AllowAnonymous] endpoints to override document-level security, instead
of leaving it null. Update the two affected tests to assert the new
behaviour and rename them to reflect that the transformer overrides
rather than skips security on anonymous operations.

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-05-05 15:17:38 +00:00
b3a9f86fe0 SignalR: Add configurable transport settings for load-balanced deployments without sticky sessions (#22700)
* WIP

* Cleanup and type generation

* Improve obsoletions

* Fix removed constructor

* Simplify logic because of SignalR's JS limitations

* Apply suggestions from code review

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Add SignalRSettings to Schema

* Abstrack SignalRRoutes class

* Fix bool to observable<bool>

* Refactor base class: pull down common service property, make abstract with protected constructor.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-05 14:51:45 +00:00
Andy ButlandandGitHub c7d055a6e2 Caching: Invalidate published content type cache for element types (#22704)
* Ensure content type cache is correctly invalidated for element types.

* Clear key to Id map on clear all.

* Refactor and update tests for additional coverage and naming alignment.

* Updates from code review.
2026-05-05 13:20:37 +02:00
9e930739fb Tags: Close suggestion dropdown on blur and escape (closes #22636) (#22650)
* Close suggestion dropdown on blur and escape, fix suggestion selection

* Fix code complex

* Fix to tab and complexity

* Fix to tab and complexity

* Fix to tab and complexity

* Clear matches on add/escape and remove focus rule

---------

Co-authored-by: engjlr <enl@umbraco.dk>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-05-05 09:39:41 +00:00
727581b88b State System: more tests, MD updates and a tiny bit more consistency (#22673)
* unit test for boolean state

* improve umb class state set value identical check

* consistent ability to make a observablePart

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
2026-05-05 09:33:24 +00:00
b49929af97 Backoffice: Introduce Value Type and Value Summary extensions (#22481)
* Add table collection view and manifests

* Use table kind in collection example

* Update entity-name-table-column-layout.element.ts

* Recompute table rows when item hrefs change

* define and render columns from manifest

* wip language implementation

* map to unique field

* rename to label

* test implementation for users table

* experiment: value minimal display extension

* register as workspace context

* add boolean display

* clean up

* add example entity actions

* add example description

* Update table-collection-view.element.ts

* Omit base 'meta' and relax table meta type

* Hardcode description column when present

* localize column names

* Update table-collection-view.element.ts

* Type manifest on collection view elements

* Use UmbLitElement instead of LitElement

* fix types

* Update entity-name-table-column-layout.element.ts

* provide entity context for each table row

* fix breaking change and introduce a deprecation warning

* Add status column to example collection view + localize column labels

* implement the UmbTableColumnLayoutElement interface

* add tests for the table collection view

* Make host element optional; add table docs/types

* Update controller-host.mixin.ts

* Update src/Umbraco.Web.UI.Client/src/packages/core/entity-action/global-components/entity-actions-table-column-view/entity-actions-table-column-view.element.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add language collection context

* introduction of value type and rename to value summary

* Add core DateTime value summary; migrate user last-login

* remove unused

* Rename user group value type to References

* Remove the component's standalone resolution path

* Add start-node value summaries & sections for user group table

* Guard resolver and render when start node missing

* refactor value-summary resolver, coordinator, and API

* introduce default kind

* remove $ in variable name

* make extension element name more specific to not collide with interface name

* add element base

* move to section module

* return as observable from resolver

* use extension item repository

* prefix start node feature with user

* add value type and value summary for date-time-with-time-zone property editor

* render timezone

* Add fallback render if no extensions can be found

* Add color-picker value summary and types

* add summary for slider + align types

* make manifest prop name more explicit

* align element name with class name

* reorganize

* manually combine imports to decrease the number of dynamic imports

* export as valueResolver instead of api

* Inline default value-summary kind manifest

* Use single raw value in value-summary coordinator

* Render summaries on Document Collection cards

* format date the same way as the property editor

* first iteration of docs and skills

* updates to docs + skills

* render icon for language collection items

* remove test collection manifest

* delete local language table collection view implementation

* implement the get hrefs method in the user group collection context

* Update controller-host.mixin.ts

* Update entity-name-table-column-layout.element.ts

* Update entity-actions-table-column-view.element.ts

* Handle undefined row element in table rendering

Allow onRowRendered to accept an undefined element and clean up row contexts when a row is unmounted. Update the callback signature in table.element.ts and handle the undefined case in table-collection-view.element.ts by destroying the host and removing the stored context for the item to avoid memory leaks when rows are removed.

* Update controller-host.mixin.ts

* remove test registration

* Prefix type in value key generation

* Skip render when boolean value is undefined

* Add JSDoc and reorder imports in coordinator

* fix lint errors

* Update icons.ts

* valueResolver to class in tests

* Update index.ts

* Add value-summary and value-type Vite entries

* Cache table config and column cell elements

* Use localization for user state labels

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-05-05 09:07:39 +00:00
Niels Lyngsø f61adcc1c0 improve acceptance test 2026-05-01 23:04:23 +02:00
Niels Lyngsø 64525c201f specify app loader + acceptance test queries 2026-05-01 22:13:49 +02:00
Laura NetoandGitHub ef5d95a4c2 Merge branch 'release/17.4.0' into v17/hotfix/22472 2026-05-01 15:49:57 +02:00
Niels Lyngsø 06fb49fe7a make unit test only test output 2026-05-01 11:49:33 +02:00
Niels Lyngsø ab8e59a43f remove unused import 2026-05-01 11:44:30 +02:00
Niels Lyngsø 6037f7664c remove trash context for blocks 2026-05-01 11:36:16 +02:00
Niels Lyngsø 41ab7cbbab remove type cast 2026-05-01 11:22:06 +02:00
Niels LyngsøandCopilot 93a2d65702 JSDocs for INVARIANT umbVariantId
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 11:21:37 +02:00
Niels Lyngsø 62436a4c7f resolve load promise feedback 2026-05-01 11:20:48 +02:00
Niels LyngsøandCopilot 424a5060f8 fix typescript typings
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 11:19:23 +02:00
Niels Lyngsø 715831ae2a remove unused import 2026-05-01 11:09:03 +02:00
Niels Lyngsø afa0fab3fb back out if not available 2026-05-01 11:09:02 +02:00
Andreas Zerbst 1845a610a3 Makes helpers more robust by adding a hover step 2026-05-01 11:05:14 +02:00
Niels LyngsøandGitHub 59432bbbed Merge branch 'release/17.4.0' into v17/hotfix/22472 2026-05-01 10:06:03 +02:00
Niels LyngsøandCopilot a00d38eb04 readonly prop for grid,rte,single
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 09:55:15 +02:00
Niels Lyngsø 2a4cdcf884 readonly as view prop 2026-05-01 09:53:44 +02:00
Niels LyngsøandCopilot efc862d301 read-only as view prop for block list
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 09:53:23 +02:00
Niels LyngsøandCopilot 1568589576 is-trashed context + observation
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 09:23:38 +02:00
Niels LyngsøandCopilot e61e0b6f51 revert language readonly rules
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 09:23:27 +02:00
Niels Lyngsø 68b19a506c assign symbol for is-trashed observer 2026-05-01 08:37:09 +02:00
Niels Lyngsø 151d96f127 load user at the end of loading all package modules 2026-04-30 12:51:30 +02:00
Niels Lyngsø 5cd048fe67 block language access tests 2026-04-30 12:31:06 +02:00
Niels Lyngsø db5bd9ec50 destroy consumer if existing 2026-04-30 10:58:24 +02:00
Niels Lyngsø c1f7a37d2a comments and todos 2026-04-30 10:17:09 +02:00
Niels LyngsøandCopilot a1620c9a31 make sure load only calls once
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 10:00:45 +02:00
Niels LyngsøandCopilot b08e23d5ef comment
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 09:53:25 +02:00
Niels LyngsøandCopilot e27c16e1a9 enable routes to be undefined
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 09:45:34 +02:00
Niels Lyngsø 22d12449ac revert 2026-04-29 15:46:15 +02:00
Niels Lyngsø 2174b5f690 Merge remote-tracking branch 'origin/release/17.4.0' into v17/hotfix/22472 2026-04-29 15:34:03 +02:00
Niels Lyngsø 172ea1af59 remove lazy loads from dataSourceDataMapper 2026-04-29 15:32:27 +02:00
Niels Lyngsø d56c57cf2f embed umbraco-packages 2026-04-29 15:31:20 +02:00
Niels LyngsøandCopilot e65bacbdc8 app loader
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 15:23:33 +02:00
Niels Lyngsø ce0f5e77e8 base extension initializer is loaded update 2026-04-29 15:23:27 +02:00
Niels LyngsøandCopilot 69258aadea rename comment
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 14:35:28 +02:00
Niels Lyngsø 4d6b4b187b clean up imports 2026-04-29 14:32:45 +02:00
Niels Lyngsø 402e5dfa90 refactor backoffice -> app 2026-04-29 14:22:47 +02:00
Niels Lyngsø fc93fed936 remove unused imports 2026-04-29 14:14:54 +02:00
Mads Rasmussen 6306f3d4fd Merge branch 'v17/hotfix/22472' of https://github.com/umbraco/Umbraco-CMS into v17/hotfix/22472 2026-04-29 14:11:28 +02:00
Mads Rasmussen 586052bab1 Debounce extension updates and set loaded flag 2026-04-29 14:11:18 +02:00
Niels Lyngsø 87d8cab843 remove await on load for extension initializers 2026-04-29 14:11:08 +02:00
Mads Rasmussen 48973739aa Batch register extensions with validation 2026-04-29 13:58:34 +02:00
Mads Rasmussen 22c7e498d3 move initializer to app element 2026-04-29 13:54:46 +02:00
Niels LyngsøandCopilot 044950e0a4 await load all bundles
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 10:32:32 +02:00
Niels Lyngsø 2572f6f0b5 leave unregistere out 2026-04-29 09:44:09 +02:00
Niels Lyngsø 89f5e49293 package name for code editor 2026-04-29 09:41:57 +02:00
Niels Lyngsø 323a731ed1 refactor package registration logic 2026-04-29 08:59:26 +02:00
Niels Lyngsø 24177dc62d add comment 2026-04-28 16:22:48 +02:00
Niels Lyngsø 7b351b199c do not react to not existing user-data or missing context 2026-04-28 16:22:38 +02:00
Niels Lyngsø f30178ebc5 import directly 2026-04-28 16:21:41 +02:00
Niels Lyngsø cd0a8b2478 null ctrl alias for constructor initiated observations 2026-04-28 14:56:28 +02:00
Andreas Zerbst 9bdc0709cc Updated tests to make them less fragile 2026-04-28 13:05:58 +02:00
Andreas Zerbst 632b0ae099 Updated locator to use new data-mark 2026-04-28 13:05:32 +02:00
Niels Lyngsø 4df3fc7867 layout-headline 2026-04-28 12:44:02 +02:00
Niels Lyngsø b4e4a6db25 apply entity-type to the workspace data-mark 2026-04-28 10:32:00 +02:00
Andreas Zerbst e4c89092e2 Block Workspace: Add data-mark for acceptance test locator 2026-04-27 13:07:33 +02:00
Niels Lyngsø f292972078 offset condition 2026-04-27 12:27:47 +02:00
Niels Lyngsø cfe5ea4a5f improve switch condition 2026-04-27 12:24:25 +02:00
Niels Lyngsø de01efe718 fix test 2026-04-27 12:24:16 +02:00
Mads Rasmussen c364d0b629 Update base-extension-initializer.controller.ts 2026-04-27 11:12:32 +02:00
Mads Rasmussen 427b32fbd4 move block language access controller to block package 2026-04-27 10:11:31 +02:00
Niels Lyngsø 0f2ffb96a8 more variantId tests 2026-04-24 20:28:21 +02:00
Niels Lyngsø d6e5ff11d0 more guard unit tests 2026-04-24 20:22:12 +02:00
Niels Lyngsø 2415d72651 unit test for reactive fallback feature 2026-04-24 19:38:27 +02:00
Niels Lyngsø 831593c740 remove as const 2026-04-24 19:08:38 +02:00
Niels Lyngsø 83dd3e258e mark as readonly and make js-const 2026-04-24 19:08:03 +02:00
Niels Lyngsø c8b08f76ab remove style import 2026-04-24 19:07:54 +02:00
Niels Lyngsø 41a6bf3c83 add comment for clarification 2026-04-24 19:07:46 +02:00
Niels Lyngsø ca73e81b3c revert removal of || this._isReadOnly check for component rendering 2026-04-24 18:47:11 +02:00
Niels Lyngsø 03a63364ef prevent cancelled context get to cause problems 2026-04-24 17:25:11 +02:00
Niels Lyngsø d127289031 observe fallback for property + name guards 2026-04-24 17:02:32 +02:00
Niels Lyngsø 0c55587c7e no if sentence 2026-04-24 17:02:10 +02:00
Niels Lyngsø ba80e12f6c observe readOnly languages 2026-04-24 16:51:19 +02:00
Niels Lyngsø a2187801ce make guard fallback reactive 2026-04-24 16:51:04 +02:00
Niels Lyngsø 7de9a853c0 read-only tag for Block Workspace 2026-04-24 16:05:21 +02:00
Niels Lyngsø cccfc33977 inherit readOnly state when block workspace is invariant 2026-04-24 15:13:36 +02:00
Niels Lyngsø 9dcc2e9c15 set fallback on readOnly 2026-04-24 14:57:15 +02:00
Niels Lyngsø dee32a4171 RTE: set manager readOnly 2026-04-24 14:56:53 +02:00
Niels Lyngsø fbd6c61225 rename file in manifest 2026-04-24 13:19:24 +02:00
Niels Lyngsø c5425fe641 Revert "transform access context into local controller"
This reverts commit 1a83d9586b.
2026-04-24 13:18:09 +02:00
Niels Lyngsø 409c098acd strict compare on config object level, to cover multiple conditions of the same alias. 2026-04-24 11:39:00 +02:00
Niels Lyngsø 570597b78e update js docs 2026-04-24 11:37:28 +02:00
Niels Lyngsø 443b50b2eb simplify match 2026-04-24 11:36:19 +02:00
Niels Lyngsø 7b613a35fc re-introduce submit create button 2026-04-24 11:35:21 +02:00
Niels Lyngsø 1a83d9586b transform access context into local controller 2026-04-23 20:43:58 +02:00
Niels Lyngsø 52c29e3105 revert logic 2026-04-22 22:36:57 +02:00
Niels LyngsøandGitHub 5117e1ee24 Merge branch 'main' into v17/hotfix/22472 2026-04-22 22:34:23 +02:00
Niels Lyngsø 21dd725bc2 clean up 2026-04-22 22:31:07 +02:00
Niels Lyngsø 2811758e3f clean up 2026-04-22 22:29:02 +02:00
Niels Lyngsø 3878bb2009 unit test for the actual problem 2026-04-22 22:27:51 +02:00
Niels Lyngsø d3526d3448 clean up 2026-04-22 22:27:29 +02:00
Niels Lyngsø e9f85e1569 fix and clean-up 2026-04-22 22:10:51 +02:00
Niels Lyngsø cffac815f1 improve life cycle for extension initializer 2026-04-22 21:38:21 +02:00
Niels Lyngsø 8f1d4c49fc revert 2026-04-22 17:37:14 +02:00
Niels Lyngsø 2ff73f55a4 make isPermittedForObservableVariant return undefined in bad case 2026-04-22 17:36:03 +02:00
Niels Lyngsø 4679d9df77 simplify document-block-property-level-permissions 2026-04-22 17:35:14 +02:00
Niels Lyngsø 2cb015f42b Merge branch 'main' into v17/hotfix/22472 2026-04-22 12:32:21 +02:00
Niels Lyngsø 651574db44 setup read only state based on user permissions 2026-04-22 12:31:50 +02:00
Niels Lyngsø e49387cb35 no need for async 2026-04-22 12:31:26 +02:00
Niels Lyngsø 7351409b35 stop inheriting read only 2026-04-22 12:31:13 +02:00
Niels Lyngsø 49f8aab7ae parse readonly state, without variant ids as origin is the property read-only state 2026-04-22 08:16:26 +02:00
Niels Lyngsø dc2b471e4c INVARIANT variant id as static 2026-04-22 08:15:46 +02:00
Niels Lyngsø f67135a30f keep rendering edit in read-only mode 2026-04-21 13:52:51 +02:00
Mads RasmussenandClaude Sonnet 4.6 7c7073428d qa(backoffice): add client-side tests for UmbWebhookCollectionRepository
Covers requestCollection with shape validation and pagination behaviour
(take, skip, consistent total) using the kitchen sink mock set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 20:45:55 +02:00
Mads RasmussenandClaude Sonnet 4.6 e79f05e8f5 qa(backoffice): add client-side tests for UmbWebhookDetailRepository
Covers createScaffold, requestByUnique, create, save, and delete using
the kitchen sink mock set and MSW-intercepted webhook endpoints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 20:32:36 +02:00
Mads RasmussenandClaude Sonnet 4.6 e1567d6c20 qa(backoffice): add client-side tests for UmbWebhookItemRepository
Uses the kitchen sink mock set to test requestItems and items against
the MSW-intercepted webhook item endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 19:12:48 +02:00
Niels Lyngsø 50c5d4eabb remove inheritance of readonly state 2026-04-17 16:36:46 +02:00
1432 changed files with 48457 additions and 18522 deletions
+1
View File
@@ -59,4 +59,5 @@
# Generated files - hidden by default in GitHub diffs
src/Umbraco.Web.UI.Client/src/packages/core/backend-api/** linguist-generated
src/Umbraco.Web.UI.Login/src/api/** linguist-generated
templates/UmbracoExtension/Client/src/api/** linguist-generated
src/Umbraco.Cms.Api.Management/OpenApi.json linguist-generated
+1 -1
View File
@@ -7,7 +7,7 @@ body:
id: "version"
attributes:
label: "Which Umbraco version are you using?"
description: "Please write the *exact* version, example: `10.1.0`. Use the help icon in the Umbraco backoffice to find the version you're using"
description: "Please write the *exact* version, example: `10.1.0`. Click the Umbraco logo in the top left corner of the backoffice to find the version you're using."
validations:
required: true
- type: textarea
+1
View File
@@ -120,3 +120,4 @@ trace.zip
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
/src/Umbraco.Cms/appsettings-schema.json
.playwright-mcp/
+16
View File
@@ -448,6 +448,14 @@ When a PR changes Management API controllers or models, the `OpenApi.json` file
The backoffice is published to npm as `@umbraco-cms/backoffice`. Runtime dependencies are provided via importmap; npm peerDependencies provide types only. For full details on dependency hoisting, version range logic, and plugin development, see `/src/Umbraco.Web.UI.Client/CLAUDE.md` → "npm Package Publishing".
### SQL Server 2100-parameter limit
Any `WHERE IN (@0, @1, ...)` built from a runtime-sized collection risks hitting SQL Server's 2100-parameter ceiling and throwing `SqlException` 8003 in production.
Batch with `IEnumerable<T>.InGroupsOf(Constants.Sql.MaxParameterCount)` or `Database.FetchByGroups(...)` whenever the collection size is driven by user data — not just when it currently fits. Watch for products of two scaling dimensions (documents × languages, properties × versions) and config-tunable batch sizes whose defaults are safe but ceilings aren't.
Full guidance, safe patterns and decision rule: see `/src/Umbraco.Infrastructure/CLAUDE.md` → "Avoiding the SQL Server 2100-parameter limit".
### Known Limitations
1. **Circular Dependencies**: Avoided via `Lazy<T>` or event notifications
@@ -544,6 +552,14 @@ Allowed, but cheap to write and cheaper to leave behind. Keep them short and tra
---
## 9. Testing Practices
### Tests for a bug fix must fail before the fix
Verify any test you add for a bug fix actually catches the bug: either write the failing test first (TDD), or temporarily revert the production change and confirm the test fails before re-applying. A test that passes both ways proves nothing. Watch for coincidental passes — default seed/sort orders can make a buggy path produce the right answer for the test's specific inputs; construct inputs so the broken and fixed behaviours give visibly different results.
---
## Quick Reference
### Essential Commands
+11 -11
View File
@@ -45,7 +45,7 @@ parameters:
- name: integrationNonReleaseTestFilter
displayName: TestFilter used for non-release type builds
type: string
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
default: "TestCategory!=LongRunning&TestCategory!=NonCritical"
- name: integrationReleaseTestFilter
displayName: TestFilter used for release type builds
type: string
@@ -53,7 +53,7 @@ parameters:
- name: nonWindowsIntegrationNonReleaseTestFilter
displayName: TestFilter used for non-release type builds on non Windows agents
type: string
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
default: "TestCategory!=LongRunning&TestCategory!=NonCritical"
- name: nonWindowsIntegrationReleaseTestFilter
displayName: TestFilter used for release type builds on non Windows agents
type: string
@@ -455,13 +455,13 @@ stages:
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
testRunTitle: Integration Tests SQLite - $(Agent.OS)
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
arguments: '--filter "$(testFilter) & ${{parameters.integrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
${{ else }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
arguments: '--filter "$(testFilter) & ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
# Integration Tests (SQL Server)
- job:
timeoutInMinutes: 180
@@ -569,13 +569,13 @@ stages:
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
arguments: '--filter "$(testFilter) & ${{parameters.integrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
${{ else }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
arguments: '--filter "$(testFilter) & ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
# Stop SQL Server
- pwsh: docker stop mssql
@@ -905,10 +905,10 @@ stages:
- job: WaitForApproval
displayName: Wait for manual approval
pool: server
timeoutInMinutes: 4320 # 3 days
steps:
- task: ManualValidation@0
displayName: Manual approval to push to NuGet
timeoutInMinutes: 4320 # 3 days
inputs:
notifyUsers: ''
instructions: 'Approve to push the NuGet release.'
+3 -3
View File
@@ -4,11 +4,11 @@ pr: none
trigger: none
schedules:
- cron: '0 6 * * *'
displayName: Daily 6AM build (v18/dev)
- cron: '0 0 * * *'
displayName: Daily 0AM build (main)
branches:
include:
- v18/dev
- main
parameters:
- name: skipIntegrationTests
@@ -1,4 +1,3 @@
using System.Text.Json.Serialization.Metadata;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
@@ -69,7 +68,7 @@ internal abstract class ConfigureUmbracoOpenApiOptionsBase : IConfigureNamedOpti
});
options.ShouldInclude = ShouldInclude;
options.CreateSchemaReferenceId = CreateSchemaReferenceId;
options.CreateSchemaReferenceId = UmbracoSchemaIdGenerator.CreateSchemaReferenceId;
options.AddOperationTransformer<UmbracoOperationIdTransformer>();
@@ -80,33 +79,6 @@ internal abstract class ConfigureUmbracoOpenApiOptionsBase : IConfigureNamedOpti
.AddDocumentTransformer<SortTagsAndPathsTransformer>();
}
/// <summary>
/// Creates a schema reference ID for the given JSON type info.
/// Returns null for types that should be inlined, the default schema ID for non-Umbraco types,
/// or a generated schema ID for Umbraco types.
/// </summary>
/// <param name="jsonTypeInfo">The JSON type info to create a schema reference ID for.</param>
/// <returns>The schema reference ID, or null if the type should be inlined.</returns>
internal static string? CreateSchemaReferenceId(JsonTypeInfo jsonTypeInfo)
{
// Ensure that only types that would normally be included in the schema generation are given a schema reference ID.
// Otherwise, we should return null to inline them.
var defaultSchemaReferenceId = OpenApiOptions.CreateDefaultSchemaReferenceId(jsonTypeInfo);
if (defaultSchemaReferenceId is null)
{
return null;
}
Type targetType = Nullable.GetUnderlyingType(jsonTypeInfo.Type) ?? jsonTypeInfo.Type;
if (targetType.Namespace?.StartsWith("Umbraco.Cms") is not true)
{
return defaultSchemaReferenceId;
}
return UmbracoSchemaIdGenerator.Generate(targetType);
}
/// <summary>
/// Determines whether the specified API description should be included in this OpenAPI document.
/// </summary>
@@ -30,6 +30,27 @@ internal static class OpenApiSchemaServiceExtensions
this IServiceCollection services,
string documentName,
string jsonOptionsName)
=> services.ReplaceOpenApiSchemaService(
documentName,
sp => sp.GetRequiredService<IOptionsMonitor<JsonOptions>>().Get(jsonOptionsName));
/// <summary>
/// Replaces the internal Microsoft <c>OpenApiSchemaService</c> registration for the specified document so that schema
/// generation uses the <see cref="JsonOptions"/> instance produced by the supplied factory. Use this overload when
/// the options need to be resolved from the service provider, computed at the last moment, or built in a way that
/// doesn't fit the named-options lookup.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="documentName">The OpenAPI document key.</param>
/// <param name="jsonOptionsFactory">Factory invoked when the schema service is first resolved. Receives the resolving <see cref="IServiceProvider"/> and returns the <see cref="JsonOptions"/> to use.</param>
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
/// <remarks>
/// Workaround for <see href="https://github.com/dotnet/aspnetcore/issues/66340">dotnet/aspnetcore#66340</see>.
/// </remarks>
public static IServiceCollection ReplaceOpenApiSchemaService(
this IServiceCollection services,
string documentName,
Func<IServiceProvider, JsonOptions> jsonOptionsFactory)
{
ServiceDescriptor descriptor = services.FirstOrDefault(sd =>
sd.ServiceType.FullName == OpenApiSchemaServiceFullName
@@ -47,7 +68,7 @@ internal static class OpenApiSchemaServiceExtensions
sp,
descriptor.ServiceType,
key,
Options.Create(sp.GetRequiredService<IOptionsMonitor<JsonOptions>>().Get(jsonOptionsName))));
Options.Create(jsonOptionsFactory(sp))));
return services;
}
@@ -23,12 +23,26 @@ public static class OpenApiServiceCollectionExtensions
this IServiceCollection services,
string documentName,
string? documentTitle = null)
=> services.AddOpenApiDocumentToUi(documentName, () => documentTitle);
/// <summary>
/// Adds an OpenAPI document to the OpenAPI UI document selector dropdown, resolving the title lazily so
/// callers (such as builder-pattern helpers) can defer it until SwaggerUI options are resolved.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> instance.</param>
/// <param name="documentName">The name/identifier of the OpenAPI document.</param>
/// <param name="documentTitleFactory">Factory invoked when SwaggerUI options are resolved. Returning <c>null</c> falls back to <paramref name="documentName"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> instance.</returns>
internal static IServiceCollection AddOpenApiDocumentToUi(
this IServiceCollection services,
string documentName,
Func<string?> documentTitleFactory)
{
services.AddOptions<SwaggerUIOptions>()
.Configure<IOptions<UmbracoOpenApiOptions>>((swaggerUiOptions, openApiOptions) =>
{
var openApiRoute = openApiOptions.Value.RouteTemplate.Replace("{documentName}", documentName).EnsureStartsWith("/");
swaggerUiOptions.SwaggerEndpoint(openApiRoute, documentTitle ?? documentName);
swaggerUiOptions.SwaggerEndpoint(openApiRoute, documentTitleFactory() ?? documentName);
swaggerUiOptions.ConfigObject.Urls = swaggerUiOptions.ConfigObject.Urls.OrderBy(x => x.Name);
});
@@ -0,0 +1,169 @@
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Fluent builder for configuring a custom OpenAPI document.
/// </summary>
public sealed class BackOfficeOpenApiDocumentBuilder
{
private readonly List<Action<OpenApiOptions>> _configurations = [];
private string? _title;
private string? _uiTitle;
private bool _includedInUi = true;
private Func<IServiceProvider, JsonOptions>? _httpJsonOptionsFactory;
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeOpenApiDocumentBuilder"/> class.
/// </summary>
/// <param name="documentName">The name of the OpenAPI document being configured.</param>
internal BackOfficeOpenApiDocumentBuilder(string documentName)
=> DocumentName = documentName;
/// <summary>
/// Gets the name of the OpenAPI document being configured.
/// </summary>
public string DocumentName { get; }
/// <summary>
/// Sets the document's <c>Info.Title</c>. Also used as the UI dropdown label unless overridden via
/// <see cref="WithUiTitle"/>.
/// </summary>
/// <param name="title">The title to display.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder WithTitle(string title)
{
_title = title;
return this;
}
/// <summary>
/// Overrides the UI dropdown label for this document.
/// </summary>
/// <param name="uiTitle">The label to display.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder WithUiTitle(string uiTitle)
{
_uiTitle = uiTitle;
return this;
}
/// <summary>
/// Excludes this document from the UI dropdown.
/// </summary>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder ExcludeFromUi()
{
_includedInUi = false;
return this;
}
/// <summary>
/// Adds an <see cref="OpenApiOptions"/> configuration callback. Multiple calls compose.
/// </summary>
/// <param name="configure">Callback to configure the options.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder ConfigureOpenApiOptions(Action<OpenApiOptions> configure)
{
_configurations.Add(configure);
return this;
}
/// <summary>
/// Sets the named <see cref="JsonOptions">Microsoft.AspNetCore.Http.Json.JsonOptions</see> used when
/// generating this document's schema. Use this to match the serialization conventions of the API
/// endpoints the document describes.
/// </summary>
/// <param name="jsonOptionsName">The name of the registered HTTP <see cref="JsonOptions"/> to apply.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder WithJsonOptions(string jsonOptionsName)
=> WithJsonOptions(sp => sp.GetRequiredService<IOptionsMonitor<JsonOptions>>().Get(jsonOptionsName));
/// <summary>
/// Sets the <see cref="JsonOptions">Microsoft.AspNetCore.Http.Json.JsonOptions</see> used when
/// generating this document's schema. Use this to match the serialization conventions of the API
/// endpoints the document describes.
/// </summary>
/// <param name="jsonOptions">The HTTP JSON options to apply.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder WithJsonOptions(JsonOptions jsonOptions)
=> WithJsonOptions(_ => jsonOptions);
/// <summary>
/// Sets a factory that produces the <see cref="JsonOptions">Microsoft.AspNetCore.Http.Json.JsonOptions</see>
/// used when generating this document's schema. Use this to match the serialization conventions of the
/// API endpoints the document describes.
/// </summary>
/// <param name="jsonOptionsFactory">Factory invoked when the schema service is first resolved.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder WithJsonOptions(Func<IServiceProvider, JsonOptions> jsonOptionsFactory)
{
_httpJsonOptionsFactory = jsonOptionsFactory;
return this;
}
/// <summary>
/// Applies the accumulated configuration to the supplied <see cref="IUmbracoBuilder"/>'s service
/// collection. Called by <c>AddBackOfficeOpenApiDocument</c> once the user-supplied callback returns.
/// </summary>
/// <param name="builder">The Umbraco builder to register services against.</param>
internal void Build(IUmbracoBuilder builder)
{
builder.Services.AddOpenApi(
DocumentName,
options =>
{
options.ShouldInclude = apiDescription =>
apiDescription.ActionDescriptor.HasMapToApiAttribute(DocumentName);
options.CreateSchemaReferenceId = UmbracoSchemaIdGenerator.CreateSchemaReferenceId;
if (_title is not null)
{
options.AddDocumentTransformer((document, _, _) =>
{
document.Info.Title = _title;
return Task.CompletedTask;
});
}
// Generate operation IDs using Umbraco's naming conventions.
options.AddOperationTransformer<UmbracoOperationIdTransformer>();
// Trim redundant JSON-equivalent MIME types (e.g. text/json, application/*+json, text/plain)
// that ASP.NET Core adds alongside application/json.
options.AddOperationTransformer<MimeTypesTransformer>();
// Mark non-nullable properties as required so generated SDKs reflect the C# nullability.
options.AddSchemaTransformer<RequireNonNullablePropertiesSchemaTransformer>();
// Tag actions by group name and cleanup unused tags (caused by the tag changes).
options
.AddOperationTransformer<TagActionsByGroupNameTransformer>()
.AddDocumentTransformer<TagActionsByGroupNameTransformer>()
.AddDocumentTransformer<SortTagsAndPathsTransformer>();
foreach (Action<OpenApiOptions> configure in _configurations)
{
configure(options);
}
});
if (_includedInUi)
{
builder.Services.AddOpenApiDocumentToUi(DocumentName, _uiTitle ?? _title);
}
if (_httpJsonOptionsFactory is not null)
{
builder.Services.ReplaceOpenApiSchemaService(DocumentName, _httpJsonOptionsFactory);
}
}
}
@@ -1,5 +1,5 @@
using System.Net.Mime;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Umbraco.Extensions;
@@ -7,11 +7,31 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Removes unwanted MIME types from OpenAPI operations, keeping only the content types
/// declared by <c>[Consumes]</c> for request bodies or <c>application/json</c> as the default.
/// Trims redundant JSON-equivalent media types from OpenAPI operations.
/// </summary>
/// <remarks>
/// <para>
/// ASP.NET Core's content negotiation populates operations with several media types that all serialize to JSON
/// (<c>text/json</c>, <c>application/*+json</c>, and <c>text/plain</c> alongside <c>application/json</c>).
/// When <c>application/json</c> is present on a response or request body, this transformer strips those
/// equivalents so OpenAPI consumers and generated SDKs aren't burdened with variants that produce identical
/// payloads. Non-JSON media types (e.g. <c>application/xml</c>, <c>application/octet-stream</c>) are preserved.
/// </para>
/// <para>
/// Request bodies additionally honour <c>[Consumes]</c>: when the attribute is present, the request content is
/// replaced entirely with the declared content types, taking precedence over the
/// JSON-equivalent stripping above.
/// </para>
/// </remarks>
internal class MimeTypesTransformer : IOpenApiOperationTransformer
{
private static readonly string[] _jsonEquivalentMimeTypes =
[
MediaTypeNames.Text.Plain,
"application/*+json",
"text/json"
];
/// <inheritdoc/>
public Task TransformAsync(
OpenApiOperation operation,
@@ -40,29 +60,29 @@ internal class MimeTypesTransformer : IOpenApiOperationTransformer
}
else
{
RemoveNonJsonMimeTypes(requestContent);
RemoveJsonEquivalentMimeTypes(requestContent);
}
}
// For responses, always keep only application/json.
// For responses, drop JSON-equivalent media types when application/json is present.
foreach (IOpenApiResponse response in (operation.Responses ?? []).Values)
{
if (response is OpenApiResponse openApiResponse)
{
RemoveNonJsonMimeTypes(openApiResponse.Content);
RemoveJsonEquivalentMimeTypes(openApiResponse.Content);
}
}
return Task.CompletedTask;
}
private static void RemoveNonJsonMimeTypes(IDictionary<string, OpenApiMediaType>? content)
private static void RemoveJsonEquivalentMimeTypes(IDictionary<string, OpenApiMediaType>? content)
{
if (content?.ContainsKey("application/json") != true)
if (content?.ContainsKey(MediaTypeNames.Application.Json) != true)
{
return;
}
content.RemoveAll(r => r.Key != "application/json");
content.RemoveAll(r => _jsonEquivalentMimeTypes.Contains(r.Key, StringComparer.OrdinalIgnoreCase));
}
}
@@ -0,0 +1,81 @@
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder"/> to register custom OpenAPI documents.
/// </summary>
public static class UmbracoBuilderOpenApiExtensions
{
/// <summary>
/// Registers a custom OpenAPI document with Umbraco's defaults applied.
/// </summary>
/// <param name="builder">The Umbraco builder.</param>
/// <param name="documentName">The document name. Matches the <c>[MapToApi]</c> value on controllers to include.</param>
/// <param name="configure">Optional callback to customize the document.</param>
/// <returns>The same <see cref="IUmbracoBuilder"/> for chaining.</returns>
/// <remarks>
/// <para>
/// The following defaults are applied to the document and can be customized or overridden via the
/// <paramref name="configure"/> callback:
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// Endpoints are filtered by <c>[MapToApi(documentName)]</c>; only matching endpoints appear in the document.
/// </description>
/// </item>
/// <item>
/// <description>
/// Schema reference IDs are generated by <see cref="UmbracoSchemaIdGenerator.CreateSchemaReferenceId"/>, applying
/// Umbraco naming conventions to types under the <c>Umbraco.Cms</c> namespace and falling back to the framework
/// default for everything else. Register your own <c>CreateSchemaReferenceId</c> delegate via
/// <see cref="BackOfficeOpenApiDocumentBuilder.ConfigureOpenApiOptions"/> to override.
/// </description>
/// </item>
/// <item>
/// <description>
/// Operation IDs are generated by <see cref="UmbracoOperationIdTransformer"/>. Register your own
/// <see cref="Microsoft.AspNetCore.OpenApi.IOpenApiOperationTransformer"/> via
/// <see cref="BackOfficeOpenApiDocumentBuilder.ConfigureOpenApiOptions"/> to override.
/// </description>
/// </item>
/// <item>
/// <description>
/// Operations are tagged by their controller's API group name, and the resulting tags and paths are sorted
/// for stable, diffable document output.
/// </description>
/// </item>
/// <item>
/// <description>
/// Redundant JSON-equivalent media types (such as <c>text/json</c>, <c>application/*+json</c>, and
/// <c>text/plain</c>) are stripped from request and response content when <c>application/json</c> is present,
/// so the document doesn't list spurious media types that ASP.NET Core adds by default.
/// </description>
/// </item>
/// <item>
/// <description>
/// Non-nullable properties are marked as <c>required</c> in the schema so generated client SDKs reflect
/// C# nullability. Override via <see cref="BackOfficeOpenApiDocumentBuilder.ConfigureOpenApiOptions"/>
/// if your types don't follow this convention.
/// </description>
/// </item>
/// <item>
/// <description>
/// The document is registered in the OpenAPI UI document selector dropdown. Call
/// <see cref="BackOfficeOpenApiDocumentBuilder.ExcludeFromUi"/> to opt out.
/// </description>
/// </item>
/// </list>
/// </remarks>
public static IUmbracoBuilder AddBackOfficeOpenApiDocument(
this IUmbracoBuilder builder,
string documentName,
Action<BackOfficeOpenApiDocumentBuilder>? configure = null)
{
var documentBuilder = new BackOfficeOpenApiDocumentBuilder(documentName);
configure?.Invoke(documentBuilder);
documentBuilder.Build(builder);
return builder;
}
}
@@ -29,16 +29,23 @@ public class UmbracoOperationIdTransformer : IOpenApiOperationTransformer
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
operation.OperationId = GenerateOperationId(context);
var operationId = GenerateOperationId(context);
if (operationId is not null)
{
operation.OperationId = operationId;
}
return Task.CompletedTask;
}
private static string GenerateOperationId(OpenApiOperationTransformerContext context)
private static string? GenerateOperationId(OpenApiOperationTransformerContext context)
{
ApiDescription apiDescription = context.Description;
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
{
throw new ArgumentException($"This handler operates only on {nameof(ControllerActionDescriptor)}.");
// Minimal APIs and other non-MVC endpoints don't carry a ControllerActionDescriptor; leave their
// operation ID untouched so the framework's default applies.
return null;
}
ApiVersion defaultVersion = context.ApplicationServices.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.DefaultApiVersion;
@@ -1,4 +1,6 @@
using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.OpenApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
@@ -29,6 +31,32 @@ public static class UmbracoSchemaIdGenerator
return Regex.Replace(name, @"[^\w]", string.Empty);
}
/// <summary>
/// Creates a schema reference ID for the given JSON type info, applying Umbraco's naming conventions to
/// types in the <c>Umbraco.Cms</c> namespace and falling back to the framework default for other types.
/// </summary>
/// <param name="jsonTypeInfo">The JSON type info to create a schema reference ID for.</param>
/// <returns>The schema reference ID, or <c>null</c> if the type should be inlined.</returns>
internal static string? CreateSchemaReferenceId(JsonTypeInfo jsonTypeInfo)
{
// Ensure that only types that would normally be included in the schema generation are given a schema reference ID.
// Otherwise, we should return null to inline them.
var defaultSchemaReferenceId = OpenApiOptions.CreateDefaultSchemaReferenceId(jsonTypeInfo);
if (defaultSchemaReferenceId is null)
{
return null;
}
Type targetType = Nullable.GetUnderlyingType(jsonTypeInfo.Type) ?? jsonTypeInfo.Type;
if (targetType.Namespace?.StartsWith("Umbraco.Cms") is not true)
{
return defaultSchemaReferenceId;
}
return Generate(targetType);
}
private static string SanitizedTypeName(Type t) => t.Name
// first grab the "non-generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
.Split('`').First()
@@ -1,7 +1,9 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Api.Delivery.Configuration;
@@ -10,6 +12,17 @@ namespace Umbraco.Cms.Api.Delivery.Configuration;
/// </summary>
internal class ConfigureUmbracoDeliveryApiOpenApiOptions : ConfigureUmbracoOpenApiOptionsBase
{
private readonly DeliveryApiSettings _deliveryApiSettings;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureUmbracoDeliveryApiOpenApiOptions"/> class.
/// </summary>
/// <param name="deliveryApiSettings">The Delivery API settings.</param>
public ConfigureUmbracoDeliveryApiOpenApiOptions(IOptions<DeliveryApiSettings> deliveryApiSettings)
{
_deliveryApiSettings = deliveryApiSettings.Value;
}
/// <inheritdoc />
protected override string ApiName => DeliveryApiConfiguration.ApiName;
@@ -38,5 +51,12 @@ internal class ConfigureUmbracoDeliveryApiOpenApiOptions : ConfigureUmbracoOpenA
options.AddOperationTransformer<MimeTypesTransformer>();
options.AddOperationTransformer<ContentApiTransformer>();
options.AddOperationTransformer<MediaApiTransformer>();
if (_deliveryApiSettings.OpenApi.GenerateContentTypeSchemas)
{
options
.AddSchemaTransformer<ContentTypeSchemaTransformer>()
.AddDocumentTransformer<ContentTypeSchemaTransformer>();
}
}
}
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Api.Common.DependencyInjection;
@@ -171,6 +172,12 @@ public static class UmbracoBuilderExtensions
builder.Services.AddUnique<IDeliveryApiOutputCacheRequestFilter, DefaultDeliveryApiOutputCacheRequestFilter>();
builder.Services.AddUnique<IDeliveryApiOutputCacheManager, DeliveryApiOutputCacheManager>();
// Signal that Umbraco has enabled output caching so the application builder registers
// the output cache middleware. Gated via a marker rather than IOutputCacheStore so that
// applications calling services.AddOutputCache(...) for their own purposes are not
// affected by Umbraco's automatic middleware registration.
builder.Services.TryAddSingleton<IUmbracoManagedOutputCacheMarker, UmbracoManagedOutputCacheMarker>();
return builder;
}
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Extensions;
/// <summary>
/// Provides extension methods for <see cref="OpenApiSchemaTransformerContext"/>.
/// </summary>
internal static class OpenApiSchemaTransformerContextExtensions
{
/// <summary>
/// Gets the OpenAPI document from the context, throwing if it is null.
/// </summary>
/// <param name="context">The schema transformer context.</param>
/// <returns>The OpenAPI document.</returns>
/// <exception cref="InvalidOperationException">Thrown when the document is null.</exception>
public static OpenApiDocument GetRequiredDocument(this OpenApiSchemaTransformerContext context)
=> context.Document ?? throw new InvalidOperationException("OpenAPI document context is required for schema registration.");
}
@@ -0,0 +1,729 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Delivery.OpenApi.Extensions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
/// <summary>
/// Transforms the OpenAPI document to add schemas for the instance's document types.
/// </summary>
/// <remarks>
/// <para>
/// This transformer implements both <see cref="IOpenApiSchemaTransformer"/> and <see cref="IOpenApiDocumentTransformer"/>
/// to handle schema generation in two phases:
/// </para>
/// <para>
/// <b>Phase 1 - Schema Transformation:</b> When the schema transformer encounters types like
/// <see cref="IApiContentResponse"/> or <see cref="IApiMediaWithCrops"/>, it generates content-type-specific
/// schemas (e.g., "ArticleContentResponseModel") and registers them as components in the OpenAPI document.
/// </para>
/// <para>
/// <b>Circular Reference Handling:</b> Content type schemas can reference each other (e.g., a "Page"
/// might have a property of type "Article", which might reference "Page" again). To prevent infinite recursion
/// during schema generation, we use a placeholder pattern:
/// <list type="bullet">
/// <item>When generating a schema, we track its ID in <c>_handledSchemas</c></item>
/// <item>If we encounter the same schema ID again (circular reference), we return a temporary placeholder
/// schema with metadata marking it for later replacement</item>
/// <item>The placeholder contains a <c>x-recursive-ref</c> metadata key with the target schema ID</item>
/// </list>
/// </para>
/// <para>
/// <b>Phase 2 - Document Transformation:</b> After all schemas are generated, the document transformer
/// resolves inline schemas into proper <c>$ref</c> references. This handles two cases:
/// <list type="bullet">
/// <item>Circular reference placeholders (marked with <c>x-recursive-ref</c>) created during Phase 1</item>
/// <item>Componentized schemas (marked with <c>x-schema-id</c>) that the framework did not automatically
/// resolve to <c>$ref</c> — this can happen for schemas reached through properties or composition
/// rather than as direct API response types</item>
/// </list>
/// This is done by <see cref="ResolveSchemaReferences(OpenApiDocument, IOpenApiSchema)"/> which recursively walks
/// through all schemas and substitutes matching entries with <see cref="OpenApiSchemaReference"/> instances.
/// </para>
/// </remarks>
public sealed class ContentTypeSchemaTransformer : IOpenApiSchemaTransformer, IOpenApiDocumentTransformer
{
// Metadata keys
private const string RecursiveRefMetadataKey = "x-recursive-ref";
private const string SchemaIdMetadataKey = "x-schema-id";
// Schema ID suffixes
private const string ResponseModelSuffix = "ResponseModel";
private const string ModelSuffix = "Model";
private const string ContentSuffix = "Content";
private const string ElementSuffix = "Element";
private const string MediaSuffix = "Media";
private const string MediaWithCropsSuffix = "MediaWithCrops";
private const string PropertiesModelSuffix = "PropertiesModel";
private readonly IContentTypeSchemaService _contentTypeSchemaService;
private readonly IOptionsMonitor<DeliveryApiSettings> _deliveryApiSettings;
private readonly ILogger<ContentTypeSchemaTransformer> _logger;
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
/// <summary>
/// Tracks schema IDs that have been or are being generated to detect circular references.
/// When a schema ID is encountered a second time, a placeholder is returned instead of recursing infinitely.
/// </summary>
private readonly HashSet<string> _handledSchemas = [];
private readonly JsonSerializerOptions _serializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ContentTypeSchemaTransformer"/> class.
/// </summary>
/// <param name="contentTypeSchemaService">The content type info service.</param>
/// <param name="jsonOptionsMonitor">The JSON options monitor.</param>
/// <param name="deliveryApiSettings">The Delivery API settings, used to honour the allow/deny content type list.</param>
/// <param name="logger">The logger.</param>
public ContentTypeSchemaTransformer(
IContentTypeSchemaService contentTypeSchemaService,
IOptionsMonitor<JsonOptions> jsonOptionsMonitor,
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettings,
ILogger<ContentTypeSchemaTransformer> logger)
{
_contentTypeSchemaService = contentTypeSchemaService;
_deliveryApiSettings = deliveryApiSettings;
_logger = logger;
_serializerOptions = jsonOptionsMonitor
.Get(Constants.JsonOptionsNames.DeliveryApi)
.SerializerOptions;
_jsonTypeInfoResolver = _serializerOptions.TypeInfoResolver
?? throw new InvalidOperationException("The JSON serializer options must have a TypeInfoResolver configured.");
}
private IReadOnlyCollection<ContentTypeSchemaInfo> DocumentTypes
=> field ??= FilterAllowedDocumentTypes(_contentTypeSchemaService.GetDocumentTypes());
private IReadOnlyCollection<ContentTypeSchemaInfo> MediaTypes
=> field ??= _contentTypeSchemaService.GetMediaTypes();
/// <inheritdoc />
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
if (document.Components?.Schemas is not { Count: > 0 })
{
return Task.CompletedTask;
}
foreach ((var schemaId, IOpenApiSchema componentsSchema) in document.Components.Schemas)
{
ResolveSchemaReferences(document, componentsSchema);
FixAutoBuiltDiscriminatorMapping(document, schemaId, componentsSchema);
}
return Task.CompletedTask;
}
/// <summary>
/// Repairs broken discriminator mapping refs auto-built by the framework for polymorphic types.
/// </summary>
/// <remarks>
/// The framework prefixes each ref with the base schema id, but the derived schemas are
/// registered without that prefix. Stripping the prefix recovers the correct ref.
/// </remarks>
private static void FixAutoBuiltDiscriminatorMapping(OpenApiDocument document, string parentSchemaId, IOpenApiSchema schema)
{
if (schema is not OpenApiSchema concrete
|| concrete.Discriminator?.Mapping is not { } mapping
|| document.Components?.Schemas is not { } schemas)
{
return;
}
foreach ((var key, OpenApiSchemaReference currentRef) in mapping.ToList())
{
var targetId = currentRef.Reference.Id;
if (string.IsNullOrEmpty(targetId) || schemas.ContainsKey(targetId))
{
continue;
}
if (targetId.StartsWith(parentSchemaId, StringComparison.Ordinal) is false)
{
continue;
}
var stripped = targetId[parentSchemaId.Length..];
if (schemas.ContainsKey(stripped))
{
mapping[key] = new OpenApiSchemaReference(stripped, document);
}
}
}
/// <inheritdoc />
public async Task TransformAsync(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
switch (context.JsonTypeInfo.Type)
{
case var type when type == typeof(IApiContentResponse):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Content,
DocumentTypes.Where(c => !c.IsElement),
async (contentType, derivedTypeSchemas) =>
{
var schemaIdPrefix = $"{contentType.SchemaId}{ContentSuffix}";
return await CreateContentTypeResponseSchema(
schemaIdPrefix,
derivedTypeSchemas,
context);
},
cancellationToken);
await CreateSchema(GetJsonTypeInfo(typeof(IApiContent)), context, cancellationToken);
return;
case var type when type == typeof(IApiContent):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Content,
DocumentTypes.Where(c => !c.IsElement),
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{ContentSuffix}{ModelSuffix}";
return await CreateContentTypeSchema(
schemaId,
PublishedItemType.Content,
contentType,
derivedTypeSchemas,
context,
cancellationToken);
},
cancellationToken);
await CreateSchema(GetJsonTypeInfo(typeof(IApiElement)), context, cancellationToken);
return;
case var type when type == typeof(IApiElement):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Content,
DocumentTypes.Where(c => c.IsElement),
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{ElementSuffix}{ModelSuffix}";
return await CreateContentTypeSchema(
schemaId,
PublishedItemType.Content,
contentType,
derivedTypeSchemas,
context,
cancellationToken);
},
cancellationToken);
return;
case var type when type == typeof(IApiMediaWithCropsResponse):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Media,
MediaTypes,
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{MediaWithCropsSuffix}";
return await CreateContentTypeResponseSchema(
schemaId,
derivedTypeSchemas,
context);
},
cancellationToken);
await CreateSchema(GetJsonTypeInfo(typeof(IApiMediaWithCrops)), context, cancellationToken);
return;
case var type when type == typeof(IApiMediaWithCrops):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Media,
MediaTypes,
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{MediaWithCropsSuffix}{ModelSuffix}";
return await CreateContentTypeSchema(
schemaId,
PublishedItemType.Media,
contentType,
derivedTypeSchemas,
context,
cancellationToken);
},
cancellationToken);
return;
default:
// HACK: Some types with circular references (e.g. ApiBlockGridItem) get left
// inlined by the framework, breaking $ref resolution. Register them explicitly.
if (GetSchemaId(context.JsonTypeInfo) is not { } schemaId || !_handledSchemas.Add(schemaId))
{
return;
}
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, schema);
return;
}
}
private async Task ApplyPolymorphicContentType(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
PublishedItemType itemType,
IEnumerable<ContentTypeSchemaInfo> contentTypes,
Func<ContentTypeSchemaInfo, List<IOpenApiSchema>, Task<OpenApiSchema>> contentTypeSchemaFactory,
CancellationToken cancellationToken)
{
List<IOpenApiSchema> derivedTypeSchemas = await ResolveDerivedTypeSchemas(
schema,
context,
cancellationToken);
OpenApiDocument document = context.GetRequiredDocument();
var typePropertyName = GetTypePropertyName(itemType);
schema.Discriminator = new OpenApiDiscriminator
{
PropertyName = typePropertyName,
Mapping = new Dictionary<string, OpenApiSchemaReference>(),
};
schema.OneOf ??= new List<IOpenApiSchema>();
foreach (ContentTypeSchemaInfo contentType in contentTypes)
{
OpenApiSchema contentTypeSchema = await contentTypeSchemaFactory(contentType, derivedTypeSchemas);
var schemaId = (string)contentTypeSchema.Metadata![SchemaIdMetadataKey];
schema.Discriminator.Mapping[contentType.Alias] = new OpenApiSchemaReference(schemaId, document);
schema.OneOf.Add(contentTypeSchema);
}
// Remove all schema properties that are now handled by the derived types
schema.AnyOf = null;
schema.Properties = null;
schema.Required = new HashSet<string> { typePropertyName };
}
/// <summary>
/// Creates and adds a schema to the OpenAPI document if it does not already exist.
/// </summary>
/// <remarks>A placeholder schema is added first to avoid recursion issues when generating schemas that reference themselves.</remarks>
private async Task<IOpenApiSchema> CreateSchema(
JsonTypeInfo jsonTypeInfo,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
if (jsonTypeInfo.Type.IsArray || jsonTypeInfo.Kind == JsonTypeInfoKind.Enumerable)
{
Type elementType = jsonTypeInfo.ElementType ?? jsonTypeInfo.Type.GetElementType() ?? typeof(object);
JsonTypeInfo elementJsonTypeInfo = GetJsonTypeInfo(elementType);
IOpenApiSchema itemSchema = await CreateSchema(elementJsonTypeInfo, context, cancellationToken);
return new OpenApiSchema
{
Type = JsonSchemaType.Array,
Items = itemSchema,
};
}
var schemaId = GetSchemaId(jsonTypeInfo);
// If this is one of the types we handle, and we already started generating it, return a placeholder
// to avoid circular reference issues.
// In the document transformer, these placeholders will be replaced with the actual schemas.
if (schemaId is not null && !_handledSchemas.Add(schemaId))
{
return GetPlaceholderSchema(schemaId);
}
OpenApiSchema schema;
try
{
schema = await context.GetOrCreateSchemaAsync(
jsonTypeInfo.Type,
cancellationToken: cancellationToken);
}
catch (Exception ex)
{
// Log the error but continue with a fallback schema to avoid failing the entire document generation.
// The fallback schema includes a description indicating the failure, making it visible to API consumers.
_logger.LogError(ex, "Failed to create OpenAPI schema for type {TypeName}", jsonTypeInfo.Type.FullName);
schema = new OpenApiSchema
{
Description = $"[Schema generation failed for type '{jsonTypeInfo.Type.FullName}'. See server logs for details.]",
};
}
if (schemaId is null)
{
return schema;
}
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, schema);
return new OpenApiSchemaReference(schemaId, document);
}
/// <summary>
/// Allows null at a property reference site without mutating any shared component schema.
/// Inline schemas have <c>null</c> OR-ed into their <c>type</c> flags; schema references and
/// recursive-ref placeholders are wrapped in a <c>oneOf</c> with an explicit null branch so the
/// shared component is left unchanged.
/// </summary>
private static IOpenApiSchema AsNullable(IOpenApiSchema schema)
{
if (schema is OpenApiSchema inline
&& inline.Metadata?.ContainsKey(RecursiveRefMetadataKey) is not true)
{
inline.Type |= JsonSchemaType.Null;
return inline;
}
return new OpenApiSchema
{
OneOf =
[
schema,
new OpenApiSchema { Type = JsonSchemaType.Null },
],
};
}
private static Task<OpenApiSchema> CreateContentTypeResponseSchema(
string schemaIdPrefix,
List<IOpenApiSchema> derivedTypeSchemas,
OpenApiSchemaTransformerContext context)
{
var schemaId = $"{schemaIdPrefix}{ResponseModelSuffix}";
OpenApiDocument document = context.GetRequiredDocument();
var schema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
AllOf = [..derivedTypeSchemas, new OpenApiSchemaReference($"{schemaIdPrefix}{ModelSuffix}", document)],
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId },
};
document.AddComponent(schemaId, schema);
return Task.FromResult(schema);
}
private async Task<OpenApiSchema> CreateContentTypeSchema(
string schemaId,
PublishedItemType itemType,
ContentTypeSchemaInfo contentType,
List<IOpenApiSchema> derivedTypeSchemas,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
var typePropertyName = GetTypePropertyName(itemType);
var schema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
Properties = new Dictionary<string, IOpenApiSchema>
{
[typePropertyName] = new OpenApiSchema { Const = contentType.Alias },
["properties"] = await CreatePropertiesSchema(contentType, itemType, context, cancellationToken),
},
Required = new HashSet<string> { typePropertyName },
AllOf = derivedTypeSchemas.Count > 0 ? derivedTypeSchemas : null,
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId, },
};
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, schema);
return schema;
}
private async Task<OpenApiSchemaReference> CreatePropertiesSchema(
ContentTypeSchemaInfo contentType,
PublishedItemType itemType,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
var schemaId = GetPropertiesModelSchemaId(contentType, itemType);
var propertiesSchema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
AllOf =
[
..contentType.CompositionSchemaIds.Select(compositionSchemaId
=> GetPlaceholderSchema(GetCompositionPropertiesModelSchemaId(compositionSchemaId, itemType)))
],
Properties = await CreateContentTypeProperties(contentType, context, cancellationToken),
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId },
};
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, propertiesSchema);
return new OpenApiSchemaReference(schemaId, document);
}
private static string GetPropertiesModelSchemaId(ContentTypeSchemaInfo contentType, PublishedItemType itemType) =>
$"{contentType.SchemaId}{GetItemTypeSuffix(itemType, contentType.IsElement)}{PropertiesModelSuffix}";
private string GetCompositionPropertiesModelSchemaId(string compositionSchemaId, PublishedItemType itemType)
{
// Look up the composition's own IsElement so its reference points at the right
// generated schema (element-type compositions live under the Element suffix).
IReadOnlyCollection<ContentTypeSchemaInfo> candidates = itemType == PublishedItemType.Media ? MediaTypes : DocumentTypes;
ContentTypeSchemaInfo? composition = candidates.FirstOrDefault(c => c.SchemaId == compositionSchemaId);
var suffix = GetItemTypeSuffix(itemType, composition?.IsElement ?? false);
return $"{compositionSchemaId}{suffix}{PropertiesModelSuffix}";
}
private static string GetItemTypeSuffix(PublishedItemType itemType, bool isElement) =>
itemType switch
{
PublishedItemType.Media => MediaSuffix,
PublishedItemType.Content => isElement ? ElementSuffix : ContentSuffix,
_ => throw new NotSupportedException($"Unsupported PublishedItemType: {itemType}"),
};
private async Task<Dictionary<string, IOpenApiSchema>> CreateContentTypeProperties(
ContentTypeSchemaInfo contentType,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
var properties = new Dictionary<string, IOpenApiSchema>();
foreach (ContentTypePropertySchemaInfo propertyInfo in contentType.Properties.Where(p => !p.Inherited))
{
IOpenApiSchema schema = await CreateSchema(
GetJsonTypeInfo(propertyInfo.DeliveryApiClrType),
context,
cancellationToken);
// Properties may be null (e.g. property added after content was last published).
// Nullability is applied at the reference site, never on a shared component schema.
properties[propertyInfo.Alias] = AsNullable(schema);
}
return properties;
}
private JsonTypeInfo GetJsonTypeInfo(Type type)
{
JsonTypeInfo? jsonTypeInfo = _jsonTypeInfoResolver.GetTypeInfo(type, _serializerOptions);
return jsonTypeInfo ?? throw new InvalidOperationException("Could not get JsonTypeInfo for type " + type.FullName);
}
private string GetTypePropertyName(PublishedItemType itemType)
{
var propertyName = itemType switch
{
PublishedItemType.Content => nameof(IApiElement.ContentType),
PublishedItemType.Media => nameof(IApiMedia.MediaType),
_ => throw new NotSupportedException($"Unsupported PublishedItemType: {itemType}"),
};
return _serializerOptions.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName;
}
private static string? GetSchemaId(JsonTypeInfo type)
=> UmbracoSchemaIdGenerator.CreateSchemaReferenceId(type);
/// <summary>
/// Creates a temporary placeholder schema to break circular reference chains during schema generation.
/// </summary>
/// <remarks>
/// The placeholder contains metadata with the target schema ID. During the document transformation phase,
/// <see cref="ResolveSchemaReferences(OpenApiDocument, IOpenApiSchema)"/> will replace these placeholders with actual schema references.
/// </remarks>
/// <param name="schemaId">The ID of the schema this placeholder represents.</param>
/// <returns>A placeholder schema with metadata indicating the target schema reference.</returns>
private static OpenApiSchema GetPlaceholderSchema(string schemaId)
=> new()
{
Metadata = new Dictionary<string, object>
{
[RecursiveRefMetadataKey] = schemaId,
},
};
/// <summary>
/// Recursively resolves inline schemas into proper <c>$ref</c> references.
/// </summary>
/// <remarks>
/// This method is called during the document transformation phase (after all schemas have been generated).
/// It walks through all schema properties, allOf, oneOf, and anyOf collections, resolving two types of
/// inline schemas:
/// <list type="bullet">
/// <item>Circular reference placeholders created by <see cref="GetPlaceholderSchema"/> (marked with <c>x-recursive-ref</c>)</item>
/// <item>Componentized schemas that should be references (marked with <c>x-schema-id</c>)</item>
/// </list>
/// Each match is replaced with an <see cref="OpenApiSchemaReference"/> pointing to the actual schema in the document's components.
/// </remarks>
/// <param name="document">The OpenAPI document containing the registered schema components.</param>
/// <param name="schema">The schema to process (will be modified in place).</param>
private static void ResolveSchemaReferences(OpenApiDocument document, IOpenApiSchema schema)
{
// Replace in allOf, oneOf, anyOf
ResolveSchemaReferences(document, schema.AllOf);
ResolveSchemaReferences(document, schema.OneOf);
ResolveSchemaReferences(document, schema.AnyOf);
// Process array items
if (schema is OpenApiSchema { Items: OpenApiSchema itemsSchema } parentSchema)
{
parentSchema.Items = GetActualSchemaOrReference(document, itemsSchema, out var itemsReplaced);
if (!itemsReplaced)
{
ResolveSchemaReferences(document, itemsSchema);
}
}
if (schema.Properties is not { Count: > 0 })
{
return;
}
// Process properties
foreach (var propertyKey in schema.Properties.Keys)
{
IOpenApiSchema propertySchema = schema.Properties[propertyKey];
if (propertySchema is not OpenApiSchema innerSchema)
{
continue;
}
schema.Properties[propertyKey] = GetActualSchemaOrReference(document, innerSchema, out var replaced);
if (replaced)
{
continue;
}
// Recursive call to handle the property schema
ResolveSchemaReferences(document, innerSchema);
}
}
private static void ResolveSchemaReferences(OpenApiDocument document, IList<IOpenApiSchema>? schemas)
{
if (schemas is null || schemas.Count == 0)
{
return;
}
for (var i = 0; i < schemas.Count; i++)
{
IOpenApiSchema allOfSchema = schemas[i];
schemas[i] = GetActualSchemaOrReference(document, allOfSchema, out var replaced);
if (!replaced)
{
ResolveSchemaReferences(document, schemas[i]);
}
}
}
[return: NotNullIfNotNull(nameof(schema))]
private static IOpenApiSchema? GetActualSchemaOrReference(
OpenApiDocument document,
IOpenApiSchema? schema,
out bool replaced)
{
if (schema is not OpenApiSchema openApiSchema)
{
replaced = false;
return schema;
}
// Check if this is a placeholder schema (circular reference)
if (openApiSchema.Metadata?.TryGetValue(RecursiveRefMetadataKey, out var recursiveRefIdObj) == true
&& recursiveRefIdObj is string recursiveRefId)
{
replaced = true;
return new OpenApiSchemaReference(recursiveRefId, document);
}
// Check if this is a componentized schema that should be a $ref
// Only resolve if the component actually exists — the framework also sets x-schema-id on
// schemas that may not end up as components.
if (openApiSchema.Metadata?.TryGetValue(SchemaIdMetadataKey, out var schemaIdObj) == true
&& schemaIdObj is string schemaId
&& !string.IsNullOrEmpty(schemaId)
&& document.Components?.Schemas?.ContainsKey(schemaId) == true)
{
replaced = true;
return new OpenApiSchemaReference(schemaId, document);
}
replaced = false;
return schema;
}
private IReadOnlyCollection<ContentTypeSchemaInfo> FilterAllowedDocumentTypes(IReadOnlyCollection<ContentTypeSchemaInfo> documentTypes)
{
DeliveryApiSettings settings = _deliveryApiSettings.CurrentValue;
return documentTypes
.Where(c => settings.IsAllowedContentType(c.Alias))
.ToList();
}
/// <summary>
/// Returns the schemas to use as the <c>allOf</c> bases for each typed content type
/// schema in a polymorphic union. Prefers concrete derived types declared on the
/// interface via <c>[JsonDerivedType]</c>; when none are advertised, falls back to a
/// schema built from the interface's own properties.
/// </summary>
/// <remarks>
/// The fallback exists for media interfaces, whose concrete classes are internal in
/// Umbraco.Infrastructure and therefore cannot be referenced via <c>[JsonDerivedType]</c>
/// from Umbraco.Core.
/// </remarks>
private async Task<List<IOpenApiSchema>> ResolveDerivedTypeSchemas(
OpenApiSchema interfaceSchema,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
List<IOpenApiSchema> derivedTypeSchemas = [];
foreach (JsonDerivedType derivedType in context.JsonTypeInfo.PolymorphismOptions?.DerivedTypes ?? [])
{
IOpenApiSchema derivedTypeSchema = await CreateSchema(
GetJsonTypeInfo(derivedType.DerivedType),
context,
cancellationToken);
derivedTypeSchemas.Add(derivedTypeSchema);
}
if (derivedTypeSchemas.Count == 0)
{
derivedTypeSchemas.Add(CreateBaseSchemaFromInterface(interfaceSchema, context));
}
return derivedTypeSchemas;
}
private static IOpenApiSchema CreateBaseSchemaFromInterface(
OpenApiSchema interfaceSchema,
OpenApiSchemaTransformerContext context)
{
// Append a "Base" marker so this schema stays distinct from the polymorphic union
// schema for the same interface (e.g. IApiMediaWithCropsResponseBaseModel vs.
// IApiMediaWithCropsResponseModel).
var baseSchemaId = $"{context.JsonTypeInfo.Type.Name}Base{ModelSuffix}";
OpenApiDocument document = context.GetRequiredDocument();
var baseSchema = new OpenApiSchema
{
Type = interfaceSchema.Type,
Properties = interfaceSchema.Properties,
Required = interfaceSchema.Required,
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = baseSchemaId },
};
document.AddComponent(baseSchemaId, baseSchema);
return new OpenApiSchemaReference(baseSchemaId, document);
}
}
@@ -1,42 +0,0 @@
using Microsoft.AspNetCore.OpenApi;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Management.DependencyInjection;
using Umbraco.Cms.Api.Management.OpenApi;
using Umbraco.Cms.Api.Management.OpenApi.Transformers;
namespace Umbraco.Cms.Api.Management.Configuration;
/// <summary>
/// Configures the OpenAPI options for the Umbraco Management API.
/// </summary>
internal class ConfigureUmbracoManagementApiOpenApiOptions : ConfigureUmbracoOpenApiOptionsBase
{
/// <inheritdoc />
protected override string ApiName => ManagementApiConfiguration.ApiName;
/// <inheritdoc />
protected override string ApiTitle => ManagementApiConfiguration.ApiTitle;
/// <inheritdoc />
protected override string ApiVersion => "Latest";
/// <inheritdoc />
protected override string ApiDescription =>
"This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility";
/// <inheritdoc />
protected override void ConfigureOpenApi(OpenApiOptions options)
{
base.ConfigureOpenApi(options);
// Sets Security requirement on backoffice apis
options.AddBackofficeSecurityRequirements();
options.AddSchemaTransformer<RequireNonNullablePropertiesSchemaTransformer>();
options.AddSchemaTransformer<FixFileReturnTypesTransformer>();
options.AddOperationTransformer<MimeTypesTransformer>();
options.AddOperationTransformer<ResponseHeaderTransformer>();
options.AddOperationTransformer<NotificationHeaderTransformer>();
}
}
@@ -53,11 +53,14 @@ public class SearchDataTypeItemController : DatatypeItemControllerBase
return Ok(new PagedModel<DataTypeItemResponseModel> { Total = searchResult.Total });
}
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(searchResult.Items.Select(item => item.Key).ToArray());
Guid[] keys = searchResult.Items.Select(x => x.Key).ToArray();
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(keys);
IEnumerable<IDataType> orderedDataTypes = OrderByRequestedIds(dataTypes, keys);
var result = new PagedModel<DataTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(dataTypes),
Total = searchResult.Total
Items = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(orderedDataTypes),
Total = searchResult.Total,
};
return Ok(result);
@@ -49,8 +49,8 @@ public class ByKeyElementFolderController : ElementFolderControllerBase
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.WithKeys(ActionElementBrowse.ActionLetter, id),
AuthorizationPolicies.ElementPermissionByResource);
ElementContainerPermissionResource.WithKeys(ActionElementContainerBrowse.ActionLetter, id),
AuthorizationPolicies.ElementFolderPermissionByResource);
if (!authorizationResult.Succeeded)
{
@@ -50,8 +50,8 @@ public class CreateElementFolderController : ElementFolderControllerBase
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.WithKeys(ActionElementNew.ActionLetter, createFolderRequestModel.Parent?.Id),
AuthorizationPolicies.ElementPermissionByResource);
ElementContainerPermissionResource.WithKeys(ActionElementContainerNew.ActionLetter, createFolderRequestModel.Parent?.Id),
AuthorizationPolicies.ElementFolderPermissionByResource);
if (!authorizationResult.Succeeded)
{
@@ -49,7 +49,18 @@ public class DeleteElementFolderController : ElementFolderControllerBase
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.WithKeys(ActionElementDelete.ActionLetter, id),
ElementContainerPermissionResource.WithKeys(ActionElementContainerDelete.ActionLetter, id),
AuthorizationPolicies.ElementFolderPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
// Also authorize deletion of all descendant elements.
authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.Branch(ActionElementDelete.ActionLetter, id),
AuthorizationPolicies.ElementPermissionByResource);
if (!authorizationResult.Succeeded)
@@ -60,8 +60,8 @@ public class MoveElementFolderController : ElementFolderControllerBase
// Check Move permission on source folder
AuthorizationResult sourceAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.WithKeys(ActionElementMove.ActionLetter, id),
AuthorizationPolicies.ElementPermissionByResource);
ElementContainerPermissionResource.WithKeys(ActionElementContainerMove.ActionLetter, id),
AuthorizationPolicies.ElementFolderPermissionByResource);
if (!sourceAuthorizationResult.Succeeded)
{
@@ -71,8 +71,8 @@ public class MoveElementFolderController : ElementFolderControllerBase
// Check Create permission on target (where we're moving to)
AuthorizationResult targetAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.WithKeys(ActionElementNew.ActionLetter, moveFolderRequestModel.Target?.Id),
AuthorizationPolicies.ElementPermissionByResource);
ElementContainerPermissionResource.WithKeys(ActionElementContainerNew.ActionLetter, moveFolderRequestModel.Target?.Id),
AuthorizationPolicies.ElementFolderPermissionByResource);
if (!targetAuthorizationResult.Succeeded)
{
@@ -57,8 +57,8 @@ public class MoveToRecycleBinElementFolderController : ElementFolderControllerBa
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.WithKeys(ActionElementDelete.ActionLetter, id),
AuthorizationPolicies.ElementPermissionByResource);
ElementContainerPermissionResource.WithKeys(ActionElementContainerDelete.ActionLetter, id),
AuthorizationPolicies.ElementFolderPermissionByResource);
if (!authorizationResult.Succeeded)
{
@@ -51,8 +51,8 @@ public class UpdateElementFolderController : ElementFolderControllerBase
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.WithKeys(ActionElementUpdate.ActionLetter, id),
AuthorizationPolicies.ElementPermissionByResource);
ElementContainerPermissionResource.WithKeys(ActionElementContainerUpdate.ActionLetter, id),
AuthorizationPolicies.ElementFolderPermissionByResource);
if (!authorizationResult.Succeeded)
{
@@ -61,8 +61,9 @@ public class SearchElementItemController : ElementItemControllerBase
.GetAll(UmbracoObjectTypes.Element, keys)
.OfType<IElementEntitySlim>()
.ToArray();
List<IElementEntitySlim> orderedElements = OrderByRequestedIds(elements, keys);
ElementItemResponseModel[] items = await Task.WhenAll(elements.Select(_elementPresentationFactory.CreateItemResponseModelAsync));
ElementItemResponseModel[] items = await Task.WhenAll(orderedElements.Select(_elementPresentationFactory.CreateItemResponseModelAsync));
return Ok(
new PagedModel<ElementItemResponseModel>
@@ -63,7 +63,18 @@ public class DeleteElementFolderRecycleBinController : ElementRecycleBinControll
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.RecycleBin(ActionElementDelete.ActionLetter),
ElementContainerPermissionResource.RecycleBin(ActionElementContainerDelete.ActionLetter),
AuthorizationPolicies.ElementFolderPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
// Also authorize deletion of all descendant elements.
authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.Branch(ActionElementDelete.ActionLetter, id),
AuthorizationPolicies.ElementPermissionByResource);
if (!authorizationResult.Succeeded)
@@ -59,8 +59,8 @@ public class OriginalParentElementFolderRecycleBinController : ElementRecycleBin
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.RecycleBin(ActionElementBrowse.ActionLetter),
AuthorizationPolicies.ElementPermissionByResource);
ElementContainerPermissionResource.RecycleBin(ActionElementContainerBrowse.ActionLetter),
AuthorizationPolicies.ElementFolderPermissionByResource);
if (!authorizationResult.Succeeded)
{
@@ -64,8 +64,8 @@ public class RestoreElementFolderRecycleBinController : ElementRecycleBinControl
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ElementPermissionResource.RecycleBin(ActionElementMove.ActionLetter),
AuthorizationPolicies.ElementPermissionByResource);
ElementContainerPermissionResource.RecycleBin(ActionElementContainerMove.ActionLetter),
AuthorizationPolicies.ElementFolderPermissionByResource);
if (!authorizationResult.Succeeded)
{
@@ -54,11 +54,14 @@ public class SearchMediaTypeItemController : MediaTypeItemControllerBase
return Task.FromResult<IActionResult>(Ok(new PagedModel<MediaTypeItemResponseModel> { Total = searchResult.Total }));
}
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
Guid[] keys = searchResult.Items.Select(item => item.Key).ToArray();
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(keys.EmptyNull());
IEnumerable<IMediaType> orderedMediaTypes = OrderByRequestedIds(mediaTypes, keys);
var result = new PagedModel<MediaTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(mediaTypes),
Total = searchResult.Total
Items = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(orderedMediaTypes),
Total = searchResult.Total,
};
return Task.FromResult<IActionResult>(Ok(result));
@@ -32,6 +32,14 @@ public class SearchMemberTypeItemController : MemberTypeItemControllerBase
_mapper = mapper;
}
/// <summary>
/// Searches for member type items matching the specified query, with support for pagination.
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <param name="query">The search query used to filter member type items.</param>
/// <param name="skip">The number of items to skip before starting to collect the result set (used for pagination).</param>
/// <param name="take">The maximum number of items to return in the result set (used for pagination).</param>
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a <see cref="PagedModel{MemberTypeItemResponseModel}"/> containing the search results.</returns>
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<MemberTypeItemResponseModel>), StatusCodes.Status200OK)]
@@ -45,11 +53,14 @@ public class SearchMemberTypeItemController : MemberTypeItemControllerBase
return Task.FromResult<IActionResult>(Ok(new PagedModel<MemberTypeItemResponseModel> { Total = searchResult.Total }));
}
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray());
Guid[] keys = searchResult.Items.Select(item => item.Key).ToArray();
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(keys);
IEnumerable<IMemberType> orderedMemberTypes = OrderByRequestedIds(memberTypes, keys);
var result = new PagedModel<MemberTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(memberTypes),
Total = searchResult.Total
Items = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(orderedMemberTypes),
Total = searchResult.Total,
};
return Task.FromResult<IActionResult>(Ok(result));
@@ -8,67 +8,38 @@ using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Api.Management.Controllers.RedirectUrlManagement;
/// <summary>
/// Controller for setting the redirect URL tracking status.
/// Controller for setting the redirect URL tracking status. Retained for backwards compatibility only;
/// the endpoint no longer modifies any configuration.
/// </summary>
[ApiVersion("1.0")]
[Obsolete("This controller is deprecated and no longer modifies the configuration. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
public class SetStatusRedirectUrlManagementController : RedirectUrlManagementControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IConfigManipulator _configManipulator;
/// <summary>
/// Initializes a new instance of the <see cref="SetStatusRedirectUrlManagementController"/> class.
/// </summary>
/// <param name="backOfficeSecurityAccessor">The back office security accessor.</param>
/// <param name="configManipulator">The configuration manipulator.</param>
/// <param name="backOfficeSecurityAccessor">Ignored. Retained for binary compatibility.</param>
/// <param name="configManipulator">Ignored. Retained for binary compatibility.</param>
public SetStatusRedirectUrlManagementController(
#pragma warning disable IDE0060 // Remove unused parameter
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IConfigManipulator configManipulator)
#pragma warning restore IDE0060 // Remove unused parameter
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_configManipulator = configManipulator;
}
// TODO: Consider if we should even allow this, or only allow using the appsettings
// We generally don't want to edit the appsettings from our code.
// But maybe there is a valid use case for doing it on the fly.
/// <summary>
/// Sets the redirect URL tracking status.
/// Deprecated. Returns an OK response without modifying any configuration. To toggle redirect URL tracking,
/// set the <c>Umbraco:CMS:WebRouting:DisableRedirectUrlTracking</c> configuration key instead.
/// </summary>
/// <param name="cancellationToken">The cancellation token for the HTTP request.</param>
/// <param name="status">The redirect status to set.</param>
/// <returns>An OK result if successful.</returns>
/// <param name="status">The redirect status (ignored).</param>
/// <returns>An OK result.</returns>
[HttpPost("status")]
[EndpointSummary("Sets the redirect URL tracking status.")]
[EndpointDescription("Updates the redirect URL tracking configuration according to the provided status.")]
[EndpointSummary("Deprecated. No longer changes the redirect URL tracking status.")]
[EndpointDescription("This endpoint is deprecated and no longer modifies the configuration. To toggle redirect URL tracking, set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead.")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
{
// TODO: uncomment this when auth is implemented.
// var userIsAdmin = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.IsAdmin();
// if (userIsAdmin is null or false)
// {
// return Unauthorized();
// }
var enable = status switch
{
RedirectStatus.Enabled => true,
RedirectStatus.Disabled => false,
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unknown redirect status")
};
// For now I'm not gonna change this to limit breaking, but it's weird to have a "disabled" switch,
// since you're essentially negating the boolean from the get go,
// it's much easier to reason with enabled = false == disabled.
await _configManipulator.SaveDisableRedirectUrlTrackingAsync(!enable);
// Taken from the existing implementation in RedirectUrlManagementController
// TODO this is ridiculous, but we need to ensure the configuration is reloaded, before this request is ended.
// otherwise we can read the old value in GetEnableState.
// The value is equal to JsonConfigurationSource.ReloadDelay
Thread.Sleep(250);
return Ok();
}
[Obsolete("This endpoint is deprecated and no longer modifies the configuration. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
public Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
=> Task.FromResult<IActionResult>(Ok());
}
@@ -28,6 +28,7 @@ public class ConfigurationServerController : ServerControllerBase
private readonly GlobalSettings _globalSettings;
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly SignalRSettings _signalRSettings;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationServerController"/> class.
@@ -36,13 +37,38 @@ public class ConfigurationServerController : ServerControllerBase
/// <param name="globalSettings">The global settings options.</param>
/// <param name="externalLoginProviders">The external login providers for back office.</param>
/// <param name="hostingEnvironment">The hosting environment.</param>
/// <param name="signalRSettings">The SignalR settings options.</param>
[ActivatorUtilitiesConstructor]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders, IHostingEnvironment hostingEnvironment)
public ConfigurationServerController(
IOptions<SecuritySettings> securitySettings,
IOptions<GlobalSettings> globalSettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
IHostingEnvironment hostingEnvironment,
IOptions<SignalRSettings> signalRSettings)
{
_securitySettings = securitySettings.Value;
_globalSettings = globalSettings.Value;
_externalLoginProviders = externalLoginProviders;
_hostingEnvironment = hostingEnvironment;
_signalRSettings = signalRSettings.Value;
}
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Server.ConfigurationServerController"/> class.
/// </summary>
/// <param name="securitySettings">The <see cref="SecuritySettings"/> options.</param>
/// <param name="globalSettings">The <see cref="GlobalSettings"/> options.</param>
/// <param name="externalLoginProviders">The external login providers used for back office authentication.</param>
/// <param name="hostingEnvironment">The hosting environment.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders, IHostingEnvironment hostingEnvironment)
: this(
securitySettings,
globalSettings,
externalLoginProviders,
hostingEnvironment,
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
{
}
/// <summary>
@@ -78,6 +104,10 @@ public class ConfigurationServerController : ServerControllerBase
VersionCheckPeriod = _globalSettings.VersionCheckPeriod,
AllowLocalLogin = _externalLoginProviders.HasDenyLocalLogin() is false,
UmbracoCssPath = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoCssPath),
SignalR = new SignalRClientSettingsResponseModel
{
SkipNegotiation = _signalRSettings.ClientShouldSkipNegotiation,
},
};
return Task.FromResult<IActionResult>(Ok(responseModel));
@@ -32,6 +32,14 @@ public class SearchTemplateItemController : TemplateItemControllerBase
_mapper = mapper;
}
/// <summary>
/// Searches for template items matching the specified query, with support for pagination.
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <param name="query">The search query used to filter template items.</param>
/// <param name="skip">The number of items to skip before starting to collect the result set (used for pagination).</param>
/// <param name="take">The maximum number of items to return in the result set (used for pagination).</param>
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a <see cref="PagedModel{TemplateItemResponseModel}"/> containing the search results.</returns>
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<TemplateItemResponseModel>), StatusCodes.Status200OK)]
@@ -45,11 +53,14 @@ public class SearchTemplateItemController : TemplateItemControllerBase
return Ok(new PagedModel<TemplateItemResponseModel> { Total = searchResult.Total });
}
IEnumerable<ITemplate> templates = await _templateService.GetAllAsync(searchResult.Items.Select(item => item.Key).ToArray());
Guid[] keys = searchResult.Items.Select(x => x.Key).ToArray();
IEnumerable<ITemplate> templates = await _templateService.GetAllAsync(keys);
IEnumerable<ITemplate> orderedTemplates = OrderByRequestedIds(templates, keys);
var result = new PagedModel<TemplateItemResponseModel>
{
Items = _mapper.MapEnumerable<ITemplate, TemplateItemResponseModel>(templates),
Total = searchResult.Total
Items = _mapper.MapEnumerable<ITemplate, TemplateItemResponseModel>(orderedTemplates),
Total = searchResult.Total,
};
return Ok(result);
@@ -26,6 +26,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
builder.Services.AddSingleton<IAuthorizationHandler, DenyLocalLoginHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, DictionaryPermissionHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, ElementPermissionHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, ElementContainerPermissionHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, FeatureAuthorizeHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, MediaPermissionHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, UserGroupPermissionHandler>();
@@ -150,6 +151,12 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
policy.Requirements.Add(new ElementPermissionRequirement());
});
options.AddPolicy(AuthorizationPolicies.ElementFolderPermissionByResource, policy =>
{
policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
policy.Requirements.Add(new ElementContainerPermissionRequirement());
});
options.AddPolicy(AuthorizationPolicies.MediaPermissionByResource, policy =>
{
policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Web.Common.Hosting;
using Umbraco.Cms.Web.Common.Middleware;
namespace Umbraco.Extensions;
@@ -68,6 +69,10 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<IBackOfficeEnabledMarker, BackOfficeEnabledMarker>();
builder.Services.AddUnique<IBackOfficePathGenerator, UmbracoBackOfficePathGenerator>();
// Registered here rather than in AddWebComponents because the middleware depends on
// IBackOfficePathGenerator (registered just above). DI scope validation would otherwise
// fail in Delivery-only/Website-only bootstraps that never call AddBackOffice().
builder.Services.AddSingleton<UmbracoBackOfficeCacheHeadersMiddleware>();
builder.Services.AddUnique<IPhysicalFileSystem>(factory =>
{
var path = "~/";
@@ -1,9 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Api.Management.Configuration;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Management.DependencyInjection;
using Umbraco.Cms.Api.Management.Middleware;
using Umbraco.Cms.Api.Management.OpenApi;
using Umbraco.Cms.Api.Management.OpenApi.Transformers;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Serialization;
using Umbraco.Cms.Api.Management.Services;
@@ -100,10 +102,25 @@ public static partial class UmbracoBuilderExtensions
// Configures the JSON options for the Open API schema generation (based on the back-office MVC JSON options)
builder.Services.ConfigureOptions<ConfigureUmbracoBackofficeHttpJsonOptions>();
builder.AddUmbracoOpenApiDocument<ConfigureUmbracoManagementApiOpenApiOptions>(
builder.AddBackOfficeOpenApiDocument(
ManagementApiConfiguration.ApiName,
ManagementApiConfiguration.ApiTitle,
Constants.JsonOptionsNames.BackOffice);
document => document
.WithTitle(ManagementApiConfiguration.ApiTitle)
.WithBackOfficeAuthentication()
.WithJsonOptions(Constants.JsonOptionsNames.BackOffice)
.ConfigureOpenApiOptions(options =>
{
options.AddDocumentTransformer((doc, _, _) =>
{
doc.Info.Version = "Latest";
doc.Info.Description = "This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility";
doc.Servers?.Clear();
return Task.CompletedTask;
});
options.AddSchemaTransformer<FixFileReturnTypesTransformer>();
options.AddOperationTransformer<ResponseHeaderTransformer>();
options.AddOperationTransformer<NotificationHeaderTransformer>();
}));
services.Configure<UmbracoPipelineOptions>(options =>
{
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
using Umbraco.Cms.Api.Common.OpenApi;
namespace Umbraco.Cms.Api.Management.OpenApi;
/// <summary>
/// Management-specific extension methods for <see cref="BackOfficeOpenApiDocumentBuilder"/>.
/// </summary>
public static class BackOfficeOpenApiDocumentBuilderExtensions
{
/// <summary>
/// Adds backoffice authentication requirements to the document.
/// </summary>
/// <param name="documentBuilder">The document builder.</param>
/// <returns>The same builder for chaining.</returns>
public static BackOfficeOpenApiDocumentBuilder WithBackOfficeAuthentication(
this BackOfficeOpenApiDocumentBuilder documentBuilder)
=> documentBuilder.ConfigureOpenApiOptions(options => options.AddBackofficeSecurityRequirements());
}
@@ -26,11 +26,20 @@ internal class BackOfficeSecurityRequirementsTransformer : IOpenApiOperationTran
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
if (context.Description.ActionDescriptor is not ControllerActionDescriptor description ||
description.MethodInfo.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) ||
if (context.Description.ActionDescriptor is not ControllerActionDescriptor description)
{
return Task.CompletedTask;
}
if (description.MethodInfo.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) ||
description.MethodInfo.DeclaringType?.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) ==
true)
{
// Explicitly clear security on anonymous operations so they override the document-level
// security requirement added below. Without this, OpenAPI consumers (including the
// generated backoffice SDK) treat these endpoints as authenticated and attach a Bearer
// token, which triggers a /token refresh before the user has logged in.
operation.Security = [];
return Task.CompletedTask;
}
@@ -1,8 +1,12 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Controllers.Security;
using Umbraco.Cms.Api.Management.ServerEvents;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Routing;
using Umbraco.Extensions;
@@ -12,26 +16,36 @@ namespace Umbraco.Cms.Api.Management.Routing;
/// <summary>
/// Creates routes for the back office area.
/// </summary>
public sealed class BackOfficeAreaRoutes : IAreaRoutes
public sealed class BackOfficeAreaRoutes : SignalRRoutesBase, IAreaRoutes
{
private readonly IRuntimeState _runtimeState;
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeAreaRoutes" /> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public BackOfficeAreaRoutes(IRuntimeState runtimeState)
: this(
runtimeState,
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeAreaRoutes" /> class.
/// </summary>
public BackOfficeAreaRoutes(IRuntimeState runtimeState)
=> _runtimeState = runtimeState;
public BackOfficeAreaRoutes(IRuntimeState runtimeState, IOptions<SignalRSettings> signalRSettings)
: base(runtimeState, signalRSettings)
{
}
/// <inheritdoc />
public void CreateRoutes(IEndpointRouteBuilder endpoints)
{
if (_runtimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
if (RuntimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
{
MapMinimalBackOffice(endpoints);
endpoints.MapHub<BackofficeHub>(Constants.System.UmbracoPathSegment + Constants.Web.BackofficeSignalRHub);
endpoints.MapHub<ServerEventHub>(Constants.System.UmbracoPathSegment + Constants.Web.ServerEventSignalRHub);
endpoints.MapHub<BackofficeHub>(Constants.System.UmbracoPathSegment + Constants.Web.BackofficeSignalRHub, ConfigureHubEndpoint);
endpoints.MapHub<ServerEventHub>(Constants.System.UmbracoPathSegment + Constants.Web.ServerEventSignalRHub, ConfigureHubEndpoint);
}
}
@@ -1,7 +1,11 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Preview;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Routing;
@@ -10,16 +14,29 @@ namespace Umbraco.Cms.Api.Management.Routing;
/// <summary>
/// Creates routes for the preview hub
/// </summary>
public sealed class PreviewRoutes : IAreaRoutes
public sealed class PreviewRoutes : SignalRRoutesBase, IAreaRoutes
{
private readonly IRuntimeState _runtimeState;
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Routing.PreviewRoutes"/> class, configuring preview routing based on the application's runtime state.
/// Initializes a new instance of the <see cref="PreviewRoutes"/> class, configuring preview routing based on the application's runtime state.
/// </summary>
/// <param name="runtimeState">An instance representing the current runtime state of the Umbraco application.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public PreviewRoutes(IRuntimeState runtimeState)
=> _runtimeState = runtimeState;
: this(
runtimeState,
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PreviewRoutes"/> class, configuring preview routing based on the application's runtime state.
/// </summary>
/// <param name="runtimeState">An instance representing the current runtime state of the Umbraco application.</param>
/// <param name="signalRSettings">The SignalR settings options.</param>
public PreviewRoutes(IRuntimeState runtimeState, IOptions<SignalRSettings> signalRSettings)
: base(runtimeState, signalRSettings)
{
}
/// <summary>
/// Creates the preview routes on the specified endpoint route builder.
@@ -27,9 +44,9 @@ public sealed class PreviewRoutes : IAreaRoutes
/// <param name="endpoints">The endpoint route builder to add routes to.</param>
public void CreateRoutes(IEndpointRouteBuilder endpoints)
{
if (_runtimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
if (RuntimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
{
endpoints.MapHub<PreviewHub>(GetPreviewHubRoute());
endpoints.MapHub<PreviewHub>(GetPreviewHubRoute(), ConfigureHubEndpoint);
}
}
@@ -41,3 +58,4 @@ public sealed class PreviewRoutes : IAreaRoutes
/// </returns>
public string GetPreviewHubRoute() => $"/{Constants.System.UmbracoPathSegment}/{nameof(PreviewHub)}";
}
@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Routing;
/// <summary>
/// Base class for route definitions that map SignalR hub endpoints,
/// applying shared transport configuration from <see cref="SignalRSettings"/>.
/// </summary>
public abstract class SignalRRoutesBase
{
private readonly SignalRSettings _signalRSettings;
/// <summary>
/// Initializes a new instance of the <see cref="SignalRRoutesBase"/> class.
/// </summary>
/// <param name="runtimeState">The current runtime state of the Umbraco application.</param>
/// <param name="signalRSettings">The SignalR settings options.</param>
protected SignalRRoutesBase(IRuntimeState runtimeState, IOptions<SignalRSettings> signalRSettings)
{
RuntimeState = runtimeState;
_signalRSettings = signalRSettings.Value;
}
/// <summary>
/// Gets the current runtime state of the Umbraco application.
/// </summary>
protected IRuntimeState RuntimeState { get; }
/// <summary>
/// Configures the transport options for a SignalR hub endpoint.
/// When <see cref="SignalRSettings.ClientShouldSkipNegotiation"/> is enabled,
/// restricts the endpoint to WebSocket transport only so clients can skip the negotiate round-trip.
/// </summary>
/// <param name="options">The hub endpoint dispatcher options to configure.</param>
protected void ConfigureHubEndpoint(HttpConnectionDispatcherOptions options)
{
if (_signalRSettings.ClientShouldSkipNegotiation)
{
options.Transports = HttpTransportType.WebSockets;
}
}
}
@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security.Authorization;
namespace Umbraco.Cms.Api.Management.Security.Authorization.Element;
/// <summary>
/// Authorizes that the current user has the correct permission access to the element container item(s) specified in the request.
/// </summary>
public class ElementContainerPermissionHandler : MustSatisfyRequirementAuthorizationHandler<ElementContainerPermissionRequirement, ElementContainerPermissionResource>
{
private readonly IElementContainerPermissionAuthorizer _elementContainerPermissionAuthorizer;
private readonly IAuthorizationHelper _authorizationHelper;
/// <summary>
/// Initializes a new instance of the <see cref="ElementContainerPermissionHandler" /> class.
/// </summary>
/// <param name="elementContainerPermissionAuthorizer">Authorizer for element container access.</param>
/// <param name="authorizationHelper">The authorization helper.</param>
public ElementContainerPermissionHandler(IElementContainerPermissionAuthorizer elementContainerPermissionAuthorizer, IAuthorizationHelper authorizationHelper)
{
_elementContainerPermissionAuthorizer = elementContainerPermissionAuthorizer;
_authorizationHelper = authorizationHelper;
}
/// <inheritdoc />
protected override async Task<bool> IsAuthorized(
AuthorizationHandlerContext context,
ElementContainerPermissionRequirement requirement,
ElementContainerPermissionResource resource)
{
var result = true;
IUser user = _authorizationHelper.GetUmbracoUser(context.User);
if (resource.CheckRoot)
{
result &= await _elementContainerPermissionAuthorizer.IsDeniedAtRootLevelAsync(user, resource.PermissionsToCheck) is false;
}
if (resource.CheckRecycleBin)
{
result &= await _elementContainerPermissionAuthorizer.IsDeniedAtRecycleBinLevelAsync(user, resource.PermissionsToCheck) is false;
}
if (resource.ContainerKeys.Any())
{
result &= await _elementContainerPermissionAuthorizer.IsDeniedAsync(user, resource.ContainerKeys, resource.PermissionsToCheck) is false;
}
return result;
}
}
@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Authorization;
namespace Umbraco.Cms.Api.Management.Security.Authorization.Element;
/// <summary>
/// Authorization requirement for the <see cref="ElementContainerPermissionHandler" />.
/// </summary>
public class ElementContainerPermissionRequirement : IAuthorizationRequirement
{
}
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,6 @@
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
@@ -25,7 +26,7 @@ internal sealed class DocumentPermissionFilterService : PermissionFilterServiceB
=> _contentPermissionService = contentPermissionService;
/// <inheritdoc/>
protected override string BrowseActionLetter => ActionBrowse.ActionLetter;
protected override string BrowseActionLetter(IEntitySlim entity) => ActionBrowse.ActionLetter;
/// <inheritdoc/>
protected override Task<IEnumerable<NodePermissions>> GetPermissionsAsync(IUser user, IEnumerable<Guid> entityKeys)
@@ -1,5 +1,7 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
@@ -25,7 +27,10 @@ internal sealed class ElementPermissionFilterService : PermissionFilterServiceBa
=> _elementPermissionService = elementPermissionService;
/// <inheritdoc/>
protected override string BrowseActionLetter => ActionElementBrowse.ActionLetter;
protected override string BrowseActionLetter(IEntitySlim entity)
=> entity.NodeObjectType == Constants.ObjectTypes.Element
? ActionElementBrowse.ActionLetter
: ActionElementContainerBrowse.ActionLetter;
/// <inheritdoc/>
protected override Task<IEnumerable<NodePermissions>> GetPermissionsAsync(IUser user, IEnumerable<Guid> entityKeys)
@@ -22,7 +22,7 @@ internal abstract class PermissionFilterServiceBase
/// <summary>
/// Gets the browse action letter used to check permissions.
/// </summary>
protected abstract string BrowseActionLetter { get; }
protected abstract string BrowseActionLetter(IEntitySlim entity);
/// <summary>
/// Filters entities based on the current user's browse permissions.
@@ -106,5 +106,5 @@ internal abstract class PermissionFilterServiceBase
private bool HasBrowsePermission(IEntitySlim entity, Dictionary<Guid, NodePermissions> permissionsByNodeKey)
=> permissionsByNodeKey.TryGetValue(entity.Key, out NodePermissions? nodePermissions)
&& nodePermissions.Permissions.Contains(BrowseActionLetter);
&& nodePermissions.Permissions.Contains(BrowseActionLetter(entity));
}
@@ -21,4 +21,9 @@ public class ServerConfigurationResponseModel
/// Gets or sets the relative or absolute path to the Umbraco CSS file used by the application.
/// </summary>
public string UmbracoCssPath { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the client-side SignalR settings.
/// </summary>
public SignalRClientSettingsResponseModel SignalR { get; set; } = new();
}
@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Server;
/// <summary>
/// Represents client-side SignalR settings returned by the server configuration endpoint.
/// </summary>
public class SignalRClientSettingsResponseModel
{
/// <summary>Gets or sets a value indicating whether the client should skip the SignalR negotiate round-trip.</summary>
public bool SkipNegotiation { get; set; }
}
+13 -9
View File
@@ -119,8 +119,7 @@ BackofficeProjectDirectory = ../Umbraco.Web.UI.Client/
BackofficeAssetsPath = wwwroot/umbraco/backoffice
```
**Login Build** (lines 94-148):
**Login Build** (lines 102-148):
```
LoginProjectDirectory = ../Umbraco.Web.UI.Login/
LoginAssetsPath = wwwroot/umbraco/login
@@ -128,13 +127,18 @@ LoginAssetsPath = wwwroot/umbraco/login
### Build Targets
| Target | Purpose |
| -------------------------------- | -------------------------------------------- |
| `BuildStaticAssetsPreconditions` | Checks if build needed (Visual Studio only) |
| `RestoreBackoffice` | Runs `npm i` if package-lock changed |
| `BuildBackoffice` | Runs `npm run build:for:cms` |
| `DefineBackofficeAssets` | Registers assets with StaticWebAssets system |
| `CleanBackoffice` | Removes built assets on `dotnet clean` |
| Target | Purpose |
|--------|---------|
| `BuildBackofficeStaticAssetsPreconditions` | Checks if Backoffice build needed (Visual Studio only) |
| `RestoreBackoffice` | Runs `npm i` if package-lock changed |
| `BuildBackoffice` | Runs `npm run build:for:cms` |
| `DefineBackofficeAssets` | Registers Backoffice assets with StaticWebAssets system |
| `CleanBackoffice` | Removes built Backoffice assets on `dotnet clean` |
| `BuildLoginStaticAssetsPreconditions` | Checks if Login build needed (Visual Studio only) |
| `RestoreLogin` | Runs `npm i` if Login's package-lock changed |
| `BuildLogin` | Runs `npm run build` in Login. Depends on `RestoreBackoffice` because Login's `tsc` walks Client's `src/` via tsconfig path aliases and needs Client's `node_modules` populated for transitive `lit`/`rxjs`/UUI resolution |
| `DefineLoginAssets` | Registers Login assets with StaticWebAssets system |
| `CleanLogin` | Removes built Login assets on `dotnet clean` |
### Build Conditions
@@ -113,7 +113,7 @@
<Exec Command="npm i --no-fund --no-audit" WorkingDirectory="$(LoginProjectDirectory)" />
</Target>
<Target Name="BuildLogin" DependsOnTargets="RestoreLogin">
<Target Name="BuildLogin" DependsOnTargets="RestoreLogin;RestoreBackoffice">
<Message Importance="high" Text="Executing Login NPM build script..." />
<Exec Command="npm run build" WorkingDirectory="$(LoginProjectDirectory)" />
<ItemGroup>
@@ -21,7 +21,7 @@
var backOfficeAssetsPath = BackOfficePathGenerator.BackOfficeAssetsPath;
var loginLogoImageAlternative = Url.RouteUrl(BackOfficeGraphicsController.LoginLogoAlternativeRouteName, new {Version= "1"});
}<!doctype html>
<html lang="@GlobalSettings.Value.DefaultUILanguage">
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -61,7 +61,7 @@
<p>Here are the <a href="https://www.enable-javascript.com/" target="_blank" rel="noopener" style="text-decoration: underline;">instructions how to enable JavaScript in your web browser</a>.</p>
</div>
</noscript>
<umb-app @(SecuritySettings.Value.KeepUserLoggedIn ? "keep-user-logged-in" : "")></umb-app>
<umb-app lang="@GlobalSettings.Value.DefaultUILanguage" @(SecuritySettings.Value.KeepUserLoggedIn ? "keep-user-logged-in" : "")></umb-app>
@if (isDebug)
{
@@ -35,7 +35,7 @@
}
<!DOCTYPE html>
<html lang="@GlobalSettings.Value.DefaultUILanguage">
<html lang="en">
<head>
<meta charset="UTF-8"/>
<base href="@backOfficePath.EnsureEndsWith('/')" />
@@ -83,6 +83,7 @@
</noscript>
<umb-auth
lang="@GlobalSettings.Value.DefaultUILanguage"
return-url="@backOfficePath"
logo-image="@loginLogoImage"
logo-image-alternative="@loginLogoImageAlternative"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 176 KiB

@@ -16,21 +16,16 @@
</ItemGroup>
<!--
The Razor editor in VS2026 and the C# extension for VS Code uses the Razor source generator
The Razor editor in modern Visual Studio and the C# extension for VS Code use the Razor source generator
for IDE functionality. We need to add some things to make sure it works correctly, but we
only do them for design time builds, so that we don't impact regular builds or CI.
We also have an escape hatch in case it does cause issues, users can set the appropriate property
We also have an escape hatch in case it does cause issues, users can set EnableCohostEditorCompatibility=false
in their project file to disable this.
CompilerVisibleProperty is surfaced to generators via AnalyzerConfigOptionsProvider, not as a source-generator input file,
so it doesn't enter the hintName-collision codepath that AdditionalFiles does. Keeping it at evaluation time is safe.
-->
<ItemGroup Condition="'$(DesignTimeBuild)' == 'true' and '$(EnableCohostEditorCompatibility)' != 'false'">
<!--
We have to make sure the source generator can see the .cshtml files, so make them AdditionalFiles.
-->
<AdditionalFiles Include="**\*.cshtml" />
<!--
Make sure the source generator knows where the project is, so it can compute target paths.
-->
<CompilerVisibleProperty Include="MSBuildProjectDirectory" />
</ItemGroup>
</Project>
@@ -49,4 +49,39 @@
<ContentWithTargetPath Include="@(_UmbracoFolderFiles)" Exclude="@(ContentWithTargetPath)" TargetPath="%(Identity)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Target>
<!--
The Razor source generator needs .cshtml files in @(AdditionalFiles). The Razor SDK adds them
via @(RazorGenerate), but only inside a target that runs during the build — so during cohost
design-time builds they may not be present yet, which is what PR #21861 worked around.
Doing the include at evaluation time (as PR #21861 did) causes duplicates with the SDK during
dotnet watch / hot reload design-time builds: the SDK adds the same .cshtml under a different
item Identity (slash form / relative vs absolute) and the generator then sees two inputs that
derive the same hintName, which crashes it with CS8785 (see issue #22773).
Run as a target before CoreCompile (hot-reload path) and CompileDesignTime (IDE design-time path)
so the SDK's contribution is visible in both cases. Then add only the .cshtml files that are not already
present. Both sides are normalized to %(FullPath) so items with different Identity forms still compare equal.
Set EnableCohostEditorCompatibility=false in a project to opt out entirely.
-->
<Target Name="_UmbracoEnsureRazorAdditionalFilesForCohostEditor"
BeforeTargets="CoreCompile;CompileDesignTime"
Condition="'$(DesignTimeBuild)' == 'true' and '$(EnableCohostEditorCompatibility)' != 'false'">
<ItemGroup>
<_UmbracoCshtmlCandidate Include="**\*.cshtml" />
<_UmbracoCshtmlCandidateFull Include="@(_UmbracoCshtmlCandidate->'%(FullPath)')" />
<_UmbracoExistingAdditionalCshtmlFull
Include="@(AdditionalFiles->'%(FullPath)')"
Condition="'%(Extension)' == '.cshtml'" />
<_UmbracoCshtmlMissingFromAdditional
Include="@(_UmbracoCshtmlCandidateFull)"
Exclude="@(_UmbracoExistingAdditionalCshtmlFull)" />
<AdditionalFiles Include="@(_UmbracoCshtmlMissingFromAdditional)" />
</ItemGroup>
</Target>
</Project>
@@ -0,0 +1,33 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
namespace Umbraco.Cms.Core.Actions;
/// <summary>
/// This action is used as a security constraint that grants a user the ability to view element containers in a tree
/// that has permissions applied to it.
/// </summary>
/// <remarks>
/// This action should not be invoked. It is used as the minimum required permission to view element containers in the element tree.
/// By granting a user this permission, the user is able to see the element container in the tree but not edit it.
/// </remarks>
public class ActionElementContainerBrowse : IAction
{
/// <inheritdoc cref="IAction.ActionLetter" />
public const string ActionLetter = "Umb.ElementContainer.Read";
/// <inheritdoc cref="IAction.ActionAlias" />
public const string ActionAlias = "elementcontainerbrowse";
/// <inheritdoc/>
public string Letter => ActionLetter;
/// <inheritdoc/>
public string Alias => ActionAlias;
/// <inheritdoc />
public bool ShowInNotifier => false;
/// <inheritdoc />
public bool CanBePermissionAssigned => true;
}
@@ -0,0 +1,28 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
namespace Umbraco.Cms.Core.Actions;
/// <summary>
/// This action is used as a security constraint that grants a user the ability to delete element containers.
/// </summary>
public class ActionElementContainerDelete : IAction
{
/// <inheritdoc cref="IAction.ActionLetter" />
public const string ActionLetter = "Umb.ElementContainer.Delete";
/// <inheritdoc cref="IAction.ActionAlias" />
public const string ActionAlias = "elementcontainerdelete";
/// <inheritdoc/>
public string Letter => ActionLetter;
/// <inheritdoc/>
public string Alias => ActionAlias;
/// <inheritdoc />
public bool ShowInNotifier => true;
/// <inheritdoc />
public bool CanBePermissionAssigned => true;
}
@@ -0,0 +1,28 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
namespace Umbraco.Cms.Core.Actions;
/// <summary>
/// This action is used as a security constraint that grants a user the ability to move element containers.
/// </summary>
public class ActionElementContainerMove : IAction
{
/// <inheritdoc cref="IAction.ActionLetter" />
public const string ActionLetter = "Umb.ElementContainer.Move";
/// <inheritdoc cref="IAction.ActionAlias" />
public const string ActionAlias = "elementcontainermove";
/// <inheritdoc/>
public string Letter => ActionLetter;
/// <inheritdoc/>
public string Alias => ActionAlias;
/// <inheritdoc />
public bool ShowInNotifier => true;
/// <inheritdoc />
public bool CanBePermissionAssigned => true;
}
@@ -0,0 +1,28 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
namespace Umbraco.Cms.Core.Actions;
/// <summary>
/// This action is used as a security constraint that grants a user the ability to create new element containers.
/// </summary>
public class ActionElementContainerNew : IAction
{
/// <inheritdoc cref="IAction.ActionLetter" />
public const string ActionLetter = "Umb.ElementContainer.Create";
/// <inheritdoc cref="IAction.ActionAlias" />
public const string ActionAlias = "elementcontainercreate";
/// <inheritdoc/>
public string Letter => ActionLetter;
/// <inheritdoc/>
public string Alias => ActionAlias;
/// <inheritdoc />
public bool ShowInNotifier => true;
/// <inheritdoc />
public bool CanBePermissionAssigned => true;
}
@@ -0,0 +1,28 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
namespace Umbraco.Cms.Core.Actions;
/// <summary>
/// This action is used as a security constraint that grants a user the ability to update element containers.
/// </summary>
public class ActionElementContainerUpdate : IAction
{
/// <inheritdoc cref="IAction.ActionLetter" />
public const string ActionLetter = "Umb.ElementContainer.Update";
/// <inheritdoc cref="IAction.ActionAlias" />
public const string ActionAlias = "elementcontainerupdate";
/// <inheritdoc/>
public string Letter => ActionLetter;
/// <inheritdoc/>
public string Alias => ActionAlias;
/// <inheritdoc />
public bool ShowInNotifier => true;
/// <inheritdoc />
public bool CanBePermissionAssigned => true;
}
+2
View File
@@ -306,6 +306,8 @@ public class MyEntityCacheRefresher : CacheRefresherBase<MyEntityCacheRefresher>
- `Attempt.Succeed(value)` / `Attempt.Fail<T>()`
- `Attempt<Content, ContentEditingOperationStatus>` - typed result with status
> Writing or reviewing a query with a `WHERE IN` on a runtime-sized collection? See "Avoiding the SQL Server 2100-parameter limit" in `/src/Umbraco.Infrastructure/CLAUDE.md` — that's where the full helper list (`Constants.Sql.MaxParameterCount`, `InGroupsOf`, NPoco's `FetchByGroups`) and the decision rules live.
### Configuration
Configuration models in `/Configuration/Models`:
@@ -26,9 +26,21 @@ public interface IRepositoryCacheVersionAccessor
/// Notifies of a version change on a given cache key.
/// </summary>
/// <param name="cacheKey">Key of the changed version.</param>
[Obsolete("Use version that takes newVersion, scheduled for removal in V19")]
void VersionChanged(string cacheKey)
{ }
/// <summary>
/// Notifies of a version change on a given cache key, providing the new version so internal caches
/// can be updated in-place without a database round-trip.
/// </summary>
/// <param name="cacheKey">Key of the changed version.</param>
/// <param name="newVersion">The new version GUID that was just written to the database.</param>
void VersionChanged(string cacheKey, Guid newVersion)
{
VersionChanged(cacheKey);
}
/// <summary>
/// Notifies the accessor that caches have been synchronized.
/// </summary>
@@ -0,0 +1,12 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
namespace Umbraco.Cms.Core.Cache;
/// <summary>
/// Defines an asynchronous handler for a <typeparamref name="TNotification" /> that should be invoked when notifications are dispatched in a distributed cache scope (e.g. to trigger a distributed cache refresher).
/// </summary>
/// <typeparam name="TNotification">The type of the notification.</typeparam>
public interface IDistributedCacheAsyncNotificationHandler<in TNotification> : INotificationAsyncHandler<TNotification>, IDistributedCacheNotificationHandler
where TNotification : INotification
{ }
@@ -428,15 +428,15 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
{
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
_documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
_documentUrlAliasService.CreateOrUpdateAliasesAsync(key).GetAwaiter().GetResult();
_documentUrlService.UpdateUrlSegmentCacheAsync(key).GetAwaiter().GetResult();
_documentUrlAliasService.UpdateAliasCacheAsync(key).GetAwaiter().GetResult();
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
{
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
_documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
_documentUrlAliasService.CreateOrUpdateAliasesWithDescendantsAsync(key).GetAwaiter().GetResult();
_documentUrlService.UpdateUrlSegmentCacheWithDescendantsAsync(key).GetAwaiter().GetResult();
_documentUrlAliasService.UpdateAliasCacheWithDescendantsAsync(key).GetAwaiter().GetResult();
}
}
@@ -131,8 +131,8 @@ public sealed class ElementCacheRefresher : PayloadCacheRefresherBase<ElementCac
// By INT Id
isolatedCache.Clear(RepositoryCacheKeys.GetKey<IElement, int>(payload.Id));
// By GUID Key
isolatedCache.Clear(RepositoryCacheKeys.GetKey<IElement, Guid?>(payload.Key));
// By GUID Key (GUID-keyed read repository uses a separate "uRepoGuid_" prefix)
isolatedCache.Clear(RepositoryCacheKeys.GetGuidKey<IElement>(payload.Key));
HandleMemoryCache(payload);
HandlePublishStatusAsync(payload, CancellationToken.None).GetAwaiter().GetResult();
@@ -78,8 +78,10 @@ public sealed class UserCacheRefresher : PayloadCacheRefresherBase<UserCacheRefr
userCache.Result?.Clear(RepositoryCacheKeys.GetKey<IUser, int>(p.Id));
userCache.Result?.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + p.Key);
userCache.Result?.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + p.Key);
userCache.Result?.ClearByKey(CacheKeys.UserElementStartNodePathsPrefix + p.Key);
userCache.Result?.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + p.Key);
userCache.Result?.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + p.Key);
userCache.Result?.ClearByKey(CacheKeys.UserAllElementStartNodesPrefix + p.Key);
}
}
}
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Collections;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
@@ -14,6 +15,7 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
private readonly ILogger<RepositoryCacheVersionService> _logger;
private readonly IRepositoryCacheVersionAccessor _repositoryCacheVersionAccessor;
private readonly ConcurrentDictionary<string, Guid> _cacheVersions = new();
private readonly ConcurrentDictionary<Guid, ConcurrentHashSet<string>> _writtenKeysByScope = new();
/// <summary>
/// Initializes a new instance of the <see cref="RepositoryCacheVersionService" /> class.
@@ -44,7 +46,6 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
var cacheKey = GetCacheKey<TEntity>();
// The cache version accessor will take a read lock if the version is not in request cache, so we don't need to take one here.
RepositoryCacheVersion? databaseVersion = await _repositoryCacheVersionAccessor.GetAsync(cacheKey);
if (databaseVersion?.Version is null)
@@ -84,18 +85,23 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
public async Task SetCacheUpdatedAsync<TEntity>()
where TEntity : class
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
string cacheKey = GetCacheKey<TEntity>();
// We have to take a write lock to ensure the cache is not being read while we update the version.
ConcurrentHashSet<string>? writtenKeys = GetOrRegisterScopeWrittenKeys();
if (writtenKeys?.TryAdd(cacheKey) is false)
{
_logger.LogDebug("Cache version for {EntityType} already written in this scope, skipping", typeof(TEntity).Name);
return;
}
using ICoreScope scope = _scopeProvider.CreateCoreScope();
scope.WriteLock(Constants.Locks.CacheVersion);
var cacheKey = GetCacheKey<TEntity>();
var newVersion = Guid.NewGuid();
_logger.LogDebug("Setting cache for {EntityType} to version {Version}", typeof(TEntity).Name, newVersion);
await _repositoryCacheVersionRepository.SaveAsync(new RepositoryCacheVersion { Identifier = cacheKey, Version = newVersion.ToString() });
_cacheVersions[cacheKey] = newVersion;
_repositoryCacheVersionAccessor.VersionChanged(cacheKey);
_repositoryCacheVersionAccessor.VersionChanged(cacheKey, newVersion);
scope.Complete();
}
@@ -104,7 +110,6 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
public async Task SetCachesSyncedAsync()
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
scope.ReadLock(Constants.Locks.CacheVersion);
// We always sync all caches versions, so it's safe to assume all caches are synced at this point.
IEnumerable<RepositoryCacheVersion> cacheVersions = await _repositoryCacheVersionRepository.GetAllAsync();
@@ -131,4 +136,22 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
internal string GetCacheKey<TEntity>()
where TEntity : class =>
typeof(TEntity).FullName ?? typeof(TEntity).Name;
private ConcurrentHashSet<string>? GetOrRegisterScopeWrittenKeys()
{
IScopeContext? context = _scopeProvider.Context;
if (context is null)
{
return null;
}
Guid contextId = context.InstanceId;
ConcurrentHashSet<string> writtenKeys = _writtenKeysByScope.GetOrAdd(contextId, _ => new ConcurrentHashSet<string>());
context.Enlist(
$"RepositoryCacheVersionService_{contextId}",
completed => _writtenKeysByScope.TryRemove(contextId, out _));
return writtenKeys;
}
}
+5 -9
View File
@@ -49,7 +49,7 @@ public sealed class TypeLoader
public ITypeFinder TypeFinder { get; }
/// <summary>
/// Gets or sets the set of assemblies to scan.
/// Gets the set of assemblies to scan.
/// </summary>
/// <remarks>
/// <para>
@@ -58,19 +58,15 @@ public sealed class TypeLoader
/// assemblies
/// for example.
/// </para>
/// <para>This is for unit tests.</para>
/// <para>Marked as internal as used only for unit tests.</para>
/// </remarks>
// internal for tests
[Obsolete("Scheduled for removal in Umbraco 18.")]
public IEnumerable<Assembly> AssembliesToScan => _assemblies ??= TypeFinder.AssembliesToScan;
internal IEnumerable<Assembly> AssembliesToScan => _assemblies ??= TypeFinder.AssembliesToScan;
/// <summary>
/// Gets the type lists.
/// </summary>
/// <remarks>For unit tests.</remarks>
// internal for tests
[Obsolete("Scheduled for removal in Umbraco 18.")]
public IEnumerable<TypeList> TypeLists => _types.Values;
/// <remarks>Marked as internal as used only for unit tests.</remarks>
internal IEnumerable<TypeList> TypeLists => _types.Values;
#region Get Assembly Attributes
@@ -36,6 +36,7 @@ public interface IConfigManipulator
/// </summary>
/// <param name="disable">The value to save.</param>
/// <returns></returns>
[Obsolete("This method is no longer used by Umbraco. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
Task SaveDisableRedirectUrlTrackingAsync(bool disable);
/// <summary>
@@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.Configuration.Models;
/// Typed configuration options for back-office token cookie settings.
/// </summary>
[UmbracoOptions(Constants.Configuration.ConfigBackOfficeTokenCookie)]
[Obsolete("This will be replaced with a different authentication scheme. Scheduled for removal in Umbraco 18.")]
[Obsolete("This will be replaced with a different authentication scheme when the BFF project is complete. Scheduled for removal in Umbraco 19.")]
public class BackOfficeTokenCookieSettings
{
private const string StaticSameSite = "Strict";
@@ -83,6 +83,11 @@ public class DeliveryApiSettings
/// </summary>
public OutputCacheSettings OutputCache { get; set; } = new ();
/// <summary>
/// Gets or sets the settings for the Delivery API OpenAPI document.
/// </summary>
public OpenApiSettings OpenApi { get; set; } = new ();
/// <summary>
/// Gets a value indicating if any member authorization type is enabled for the Delivery API.
/// </summary>
@@ -254,4 +259,27 @@ public class DeliveryApiSettings
/// <value>The client secret.</value>
public string ClientSecret { get; set; } = string.Empty;
}
/// <summary>
/// Typed configuration options for the OpenAPI document of the Delivery API.
/// </summary>
public class OpenApiSettings
{
private const bool StaticGenerateContentTypeSchemas = false;
/// <summary>
/// Gets or sets a value indicating whether the Delivery API OpenAPI document should include
/// schemas for the instance's content types (document types, element types, and media types).
/// </summary>
/// <value>
/// <c>true</c> to generate content-type-specific schemas in the OpenAPI document;
/// <c>false</c> to use only the base interface schemas.
/// </value>
/// <remarks>
/// When enabled, the OpenAPI document will contain content-type-specific schemas with their
/// specific properties. When disabled (default), only the base interface schemas will be used.
/// </remarks>
[DefaultValue(StaticGenerateContentTypeSchemas)]
public bool GenerateContentTypeSchemas { get; set; } = StaticGenerateContentTypeSchemas;
}
}
@@ -0,0 +1,38 @@
using System.ComponentModel;
namespace Umbraco.Cms.Core.Configuration.Models;
/// <summary>
/// Typed configuration options for SignalR settings.
/// </summary>
/// <remarks>
/// <para>
/// When <see cref="ClientShouldSkipNegotiation"/> is enabled, all hub endpoints are restricted
/// to WebSocket transport and the client skips the negotiate round-trip. The setting is forwarded
/// to the client via the <c>/umbraco/management/api/v1/server/configuration</c> endpoint.
/// </para>
/// <para>
/// Downstream packages (e.g. Umbraco Cloud) can configure these settings via
/// <c>IConfigureOptions&lt;SignalRSettings&gt;</c> or <c>appsettings.json</c>
/// under <c>Umbraco:CMS:SignalR</c>.
/// </para>
/// </remarks>
[UmbracoOptions(Constants.Configuration.ConfigSignalR)]
public class SignalRSettings
{
internal const bool StaticClientShouldSkipNegotiation = false;
/// <summary>
/// Gets or sets a value indicating whether the client should skip the SignalR negotiate
/// round-trip and connect directly via WebSockets.
/// </summary>
/// <remarks>
/// When <c>true</c>, the server restricts all hub endpoints to the WebSocket transport only
/// (via <c>HttpConnectionDispatcherOptions.Transports</c>) and the client is instructed to
/// set <c>skipNegotiation = true</c> with <c>transport = WebSockets</c>. This eliminates the
/// negotiate HTTP request that causes failures in load-balanced deployments without sticky sessions.
/// This is safe for self-hosted SignalR but must <b>not</b> be used with Azure SignalR Service.
/// </remarks>
[DefaultValue(StaticClientShouldSkipNegotiation)]
public bool ClientShouldSkipNegotiation { get; set; } = StaticClientShouldSkipNegotiation;
}
@@ -16,6 +16,7 @@ public class UnattendedSettings
private const bool StaticInstallUnattended = false;
private const bool StaticUpgradeUnattended = false;
private const TelemetryLevel StaticTelemetryLevel = TelemetryLevel.Detailed;
private const string StaticMigrationClaimTimeout = "02:00:00";
/// <summary>
/// Gets or sets a value indicating whether unattended installs are enabled.
@@ -45,6 +46,17 @@ public class UnattendedSettings
/// </remarks>
public bool PackageMigrationsUnattended { get; set; } = true;
/// <summary>
/// Gets or sets the maximum time a migration leadership claim is considered valid before
/// another server may take over. Protects against a leader crashing mid-migration.
/// </summary>
/// <remarks>
/// Only relevant in load-balanced deployments with <see cref="UpgradeUnattended"/> enabled.
/// Default is 2 hours, which should exceed the longest reasonable migration run time.
/// </remarks>
[DefaultValue(StaticMigrationClaimTimeout)]
public TimeSpan MigrationClaimTimeout { get; set; } = TimeSpan.Parse(StaticMigrationClaimTimeout);
/// <summary>
/// Gets or sets a value to use for creating a user with a name for Unattended Installs
/// </summary>
@@ -302,6 +302,11 @@ public static partial class Constants
/// </summary>
public const string ConfigWebsite = ConfigPrefix + "Website";
/// <summary>
/// The configuration key for SignalR settings.
/// </summary>
public const string ConfigSignalR = ConfigPrefix + "SignalR";
/// <summary>
/// Contains constants for named options used in configuration.
/// </summary>
@@ -36,6 +36,14 @@ public static partial class Constants
/// The key used to store the Umbraco pre-migrations upgrade plan state.
/// </summary>
public const string UmbracoUpgradePlanPremigrationsKey = KeyValuePrefix + UmbracoUpgradePlanPremigrationsName;
/// <summary>
/// The key used to coordinate migration leadership across servers in a load-balanced
/// environment. The value is either empty (no active leader) or
/// <c>"{machineIdentifier}|{claimedAtUtc:O}"</c> when a server holds the claim,
/// where <c>machineIdentifier</c> is the value returned by <see cref="Umbraco.Cms.Core.Factories.IMachineInfoFactory.GetMachineIdentifier"/>.
/// </summary>
public const string UpgradeLockKey = "Umbraco.Core.Upgrader.Lock";
}
/// <summary>
@@ -0,0 +1,15 @@
namespace Umbraco.Cms.Core.DependencyInjection;
/// <summary>
/// Marker interface indicating that Umbraco itself has enabled ASP.NET Core output caching
/// (via Website template caching or Delivery API caching configuration).
/// Used to gate Umbraco's automatic registration of the output cache middleware so that
/// applications calling <c>services.AddOutputCache(...)</c> for their own purposes do not
/// inadvertently trigger a duplicate <c>UseOutputCache()</c> registration.
/// </summary>
public interface IUmbracoManagedOutputCacheMarker { }
/// <summary>
/// Marker class implementation for <see cref="IUmbracoManagedOutputCacheMarker"/>.
/// </summary>
public sealed class UmbracoManagedOutputCacheMarker : IUmbracoManagedOutputCacheMarker { }
@@ -101,7 +101,8 @@ public static partial class UmbracoBuilderExtensions
.AddUmbracoOptions<SystemDateMigrationSettings>()
.AddUmbracoOptions<DistributedJobSettings>()
.AddUmbracoOptions<BackOfficeTokenCookieSettings>()
.AddUmbracoOptions<WebsiteSettings>();
.AddUmbracoOptions<WebsiteSettings>()
.AddUmbracoOptions<SignalRSettings>();
// Configure connection string and ensure it's updated when the configuration changes
builder.Services.AddSingleton<IConfigureOptions<ConnectionStrings>, ConfigureConnectionStrings>();
@@ -312,6 +312,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<IContentPermissionService, ContentPermissionService>();
Services.AddUnique<IDictionaryPermissionService, DictionaryPermissionService>();
Services.AddUnique<IElementPermissionService, ElementPermissionService>();
Services.AddUnique<IElementContainerPermissionService, ElementContainerPermissionService>();
Services.AddUnique<IContentService, ContentService>();
Services.AddUnique<IElementService, ElementService>();
Services.AddUnique<IElementVersionService, ElementVersionService>();
@@ -336,7 +337,6 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<IMediaTypeService, MediaTypeService>();
Services.AddUnique<IContentTypeEditingService, ContentTypeEditingService>();
Services.AddUnique<IMediaTypeEditingService, MediaTypeEditingService>();
Services.AddUnique<IFileService, FileService>();
Services.AddUnique<ITemplateService, TemplateService>();
Services.AddUnique<IScriptService, ScriptService>();
Services.AddUnique<IStylesheetService, StylesheetService>();
@@ -452,6 +452,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddSingleton<IContentPermissionAuthorizer, ContentPermissionAuthorizer>();
Services.AddSingleton<IDictionaryPermissionAuthorizer, DictionaryPermissionAuthorizer>();
Services.AddSingleton<IElementPermissionAuthorizer, ElementPermissionAuthorizer>();
Services.AddSingleton<IElementContainerPermissionAuthorizer, ElementContainerPermissionAuthorizer>();
Services.AddSingleton<IFeatureAuthorizer, FeatureAuthorizer>();
Services.AddSingleton<IMediaPermissionAuthorizer, MediaPermissionAuthorizer>();
Services.AddSingleton<IUserGroupPermissionAuthorizer, UserGroupPermissionAuthorizer>();
@@ -475,6 +476,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<IDocumentUrlAliasService, DocumentUrlAliasService>();
Services.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, DocumentUrlAliasServiceInitializerNotificationHandler>();
Services.AddNotificationAsyncHandler<ContentTypeChangedNotification, DocumentUrlServiceContentTypeChangedNotificationHandler>();
Services.AddNotificationAsyncHandler<ContentTreeChangeNotification, DocumentUrlServiceContentTreeChangeNotificationHandler>();
}
}
}
+16 -9
View File
@@ -13,11 +13,10 @@ public class MoveEventInfo<TEntity> : MoveEventInfoBase<TEntity>
/// <param name="originalPath">The original path of the entity.</param>
/// <param name="newParentId">The identifier of the new parent.</param>
/// <param name="newParentKey">The unique identifier of the new parent.</param>
[Obsolete("Use the overload without the newParentId parameter instead. Scheduled for removal in v19.")]
public MoveEventInfo(TEntity entity, string originalPath, int newParentId, Guid? newParentKey)
: base(entity, originalPath)
: this(entity, originalPath, newParentKey)
{
NewParentId = newParentId;
NewParentKey = newParentKey;
}
/// <summary>
@@ -26,15 +25,23 @@ public class MoveEventInfo<TEntity> : MoveEventInfoBase<TEntity>
/// <param name="entity">The entity being moved.</param>
/// <param name="originalPath">The original path of the entity.</param>
/// <param name="newParentId">The identifier of the new parent.</param>
public MoveEventInfo(TEntity entity, string originalPath, int newParentId) : this(entity, originalPath, newParentId, null)
[Obsolete("Use the overload with the newParentKey parameter instead. Scheduled for removal in v19.")]
public MoveEventInfo(TEntity entity, string originalPath, int newParentId)
: this(entity, originalPath, null)
{
}
/// <summary>
/// Gets or sets the identifier of the new parent.
/// Initializes a new instance of the <see cref="MoveEventInfo{TEntity}" /> class.
/// </summary>
[Obsolete("Please use NewParentKey instead. Scheduled for removal in Umbraco 18.")]
public int NewParentId { get; set; }
/// <param name="entity">The entity being moved.</param>
/// <param name="originalPath">The original path of the entity.</param>
/// <param name="newParentKey">The unique identifier of the new parent.</param>
public MoveEventInfo(TEntity entity, string originalPath, Guid? newParentKey)
: base(entity, originalPath)
{
NewParentKey = newParentKey;
}
/// <summary>
/// Gets the unique identifier of the new parent.
@@ -57,7 +64,7 @@ public class MoveEventInfo<TEntity> : MoveEventInfoBase<TEntity>
/// </summary>
/// <param name="other">The other instance to compare.</param>
/// <returns><c>true</c> if the instances are equal; otherwise, <c>false</c>.</returns>
public bool Equals(MoveEventInfo<TEntity>? other) => NewParentId == other?.NewParentId && NewParentKey == other.NewParentKey && base.Equals(other);
public bool Equals(MoveEventInfo<TEntity>? other) => NewParentKey == other?.NewParentKey && base.Equals(other);
/// <inheritdoc />
public override int GetHashCode()
@@ -67,7 +74,7 @@ public class MoveEventInfo<TEntity> : MoveEventInfoBase<TEntity>
var hashCode = Entity is not null
? EqualityComparer<TEntity>.Default.GetHashCode(Entity)
: base.GetHashCode();
hashCode = (hashCode * 397) ^ NewParentId;
hashCode = (hashCode * 397) ^ NewParentKey.GetHashCode();
hashCode = (hashCode * 397) ^ OriginalPath.GetHashCode();
return hashCode;
}
@@ -196,14 +196,14 @@ public static class PublishedContentExtensions
/// Returns the current template Alias
/// </summary>
/// <returns>Empty string if none is set.</returns>
public static string GetTemplateAlias(this IPublishedContent content, IFileService fileService)
public static string GetTemplateAlias(this IPublishedContent content, ITemplateService templateService)
{
if (content.TemplateId.HasValue == false)
{
return string.Empty;
}
ITemplate? template = fileService.GetTemplate(content.TemplateId.Value);
ITemplate? template = templateService.GetAsync(content.TemplateId.Value).GetAwaiter().GetResult();
return template?.Alias ?? string.Empty;
}
@@ -253,15 +253,15 @@ public static class PublishedContentExtensions
/// Determines whether a specific template is allowed for the content item by template alias.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="fileService">The file service.</param>
/// <param name="templateService">The template service.</param>
/// <param name="contentTypeService">The content type service.</param>
/// <param name="disableAlternativeTemplates">Whether alternative templates are disabled.</param>
/// <param name="validateAlternativeTemplates">Whether to validate alternative templates against allowed templates.</param>
/// <param name="templateAlias">The template alias.</param>
/// <returns><c>true</c> if the template is allowed; otherwise, <c>false</c>.</returns>
public static bool IsAllowedTemplate(this IPublishedContent content, IFileService fileService, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, string templateAlias)
public static bool IsAllowedTemplate(this IPublishedContent content, ITemplateService templateService, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, string templateAlias)
{
ITemplate? template = fileService.GetTemplate(templateAlias);
ITemplate? template = templateService.GetAsync(templateAlias).GetAwaiter().GetResult();
return template != null && content.IsAllowedTemplate(contentTypeService, disableAlternativeTemplates, validateAlternativeTemplates, template.Id);
}
@@ -2141,9 +2141,9 @@ public static class PublishedContentExtensions
// with a non-existing published node, will get cache misses and call the DB
// making it a very slow operation.
return publishedStatusFilteringService
.FilterAvailable(childrenKeys, culture)
.OrderBy(x => x.SortOrder);
// INavigationQueryService.TryGetChildrenKeys returns keys already ordered by SortOrder
// and FilterAvailable preserves enumeration order, so no further OrderBy is needed.
return publishedStatusFilteringService.FilterAvailable(childrenKeys, culture);
}
private static IEnumerable<IPublishedContent> EnumerateDescendantsOrSelfInternal(
@@ -106,6 +106,7 @@ public static class UdiGetterExtensions
return entity switch
{
IContent content => content.GetUdi(),
IElement element => element.GetUdi(),
IMedia media => media.GetUdi(),
IMember member => member.GetUdi(),
_ => throw new NotSupportedException($"Content base type {entity.GetType().FullName} is not supported."),
@@ -128,6 +129,20 @@ public static class UdiGetterExtensions
return new GuidUdi(entityType, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IElement entity)
{
ArgumentNullException.ThrowIfNull(entity);
return new GuidUdi(Constants.UdiEntityType.Element, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
@@ -106,7 +106,7 @@ public interface IHostingEnvironment
/// content root are the same, however
/// in netcore the web root is /www therefore this will Map to a physical path within www.
/// </remarks>
[Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead")]
[Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead. Scheduled for removal in Umbraco 20.")]
string MapPathWebRoot(string path);
/// <summary>
@@ -118,7 +118,7 @@ public interface IHostingEnvironment
/// in netcore the web root is /www therefore this will Map to a physical path within www.
/// </remarks>
[Obsolete(
"Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead")]
"Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead. Scheduled for removal in Umbraco 20.")]
string MapPathContentRoot(string path);
/// <summary>
+63 -32
View File
@@ -36,7 +36,14 @@ internal sealed partial class ShadowFileSystem : IFileSystem
/// <summary>
/// Gets the dictionary of shadow nodes tracking file and directory changes.
/// </summary>
private Dictionary<string, ShadowNode> Nodes => _nodes ??= new Dictionary<string, ShadowNode>();
/// <remarks>
/// Uses <see cref="StringComparer.OrdinalIgnoreCase"/> so the shadow exposes case-insensitive
/// path semantics (matching Windows file system behavior) while preserving the original case
/// of paths. Preserving case is required for <see cref="Complete"/>: the stored key is also
/// used to locate the shadow file via <c>_sfs.GetFullPath</c>, which on case-sensitive
/// file systems (e.g. Linux) must match the case the file was actually written with.
/// </remarks>
private Dictionary<string, ShadowNode> Nodes => _nodes ??= new Dictionary<string, ShadowNode>(StringComparer.OrdinalIgnoreCase);
/// <inheritdoc />
public IEnumerable<string> GetDirectories(string path)
@@ -66,7 +73,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
var normPath = NormPath(path);
if (recursive)
{
Nodes[normPath] = new ShadowNode(true, true);
Nodes[normPath] = new ShadowNode(true, true, normPath);
var remove = Nodes.Where(x => IsDescendant(normPath, x.Key)).ToList();
foreach (KeyValuePair<string, ShadowNode> kvp in remove)
{
@@ -84,7 +91,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
throw new InvalidOperationException("Directory is not empty.");
}
Nodes[path] = new ShadowNode(true, true);
Nodes[normPath] = new ShadowNode(true, true, normPath);
var remove = Nodes.Where(x => IsChild(normPath, x.Key)).ToList();
foreach (KeyValuePair<string, ShadowNode> kvp in remove)
{
@@ -131,7 +138,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
if (sd.IsDelete)
{
Nodes[dirPath] = new ShadowNode(false, true);
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
}
}
else
@@ -146,12 +153,13 @@ internal sealed partial class ShadowFileSystem : IFileSystem
throw new InvalidOperationException("Invalid path.");
}
Nodes[dirPath] = new ShadowNode(false, true);
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
}
}
_sfs.AddFile(path, stream, overrideIfExists);
Nodes[normPath] = new ShadowNode(false, false);
var canonicalPath = sf?.CanonicalPath ?? path;
_sfs.AddFile(canonicalPath, stream, overrideIfExists);
Nodes[normPath] = new ShadowNode(false, false, canonicalPath);
}
/// <inheritdoc />
@@ -178,7 +186,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
{
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf))
{
return sf.IsDir || sf.IsDelete ? Stream.Null : _sfs.OpenFile(path);
return sf.IsDir || sf.IsDelete ? Stream.Null : _sfs.OpenFile(sf.CanonicalPath);
}
return Inner.OpenFile(path);
@@ -192,7 +200,8 @@ internal sealed partial class ShadowFileSystem : IFileSystem
return;
}
Nodes[NormPath(path)] = new ShadowNode(true, false);
var normPath = NormPath(path);
Nodes[normPath] = new ShadowNode(true, false, normPath);
}
/// <inheritdoc />
@@ -226,7 +235,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
if (sd.IsDelete)
{
Nodes[dirPath] = new ShadowNode(false, true);
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
}
}
else
@@ -241,13 +250,15 @@ internal sealed partial class ShadowFileSystem : IFileSystem
throw new InvalidOperationException("Invalid path.");
}
Nodes[dirPath] = new ShadowNode(false, true);
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
}
}
_sfs.MoveFile(normSource, normTarget, overrideIfExists);
Nodes[normSource] = new ShadowNode(true, false);
Nodes[normTarget] = new ShadowNode(false, false);
var sourceCanonical = sf?.CanonicalPath ?? normSource;
var targetCanonical = tf?.CanonicalPath ?? normTarget;
_sfs.MoveFile(sourceCanonical, targetCanonical, overrideIfExists);
Nodes[normSource] = new ShadowNode(true, false, sourceCanonical);
Nodes[normTarget] = new ShadowNode(false, false, targetCanonical);
}
/// <inheritdoc />
@@ -269,7 +280,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
{
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf))
{
return sf.IsDir || sf.IsDelete ? string.Empty : _sfs.GetFullPath(path);
return sf.IsDir || sf.IsDelete ? string.Empty : _sfs.GetFullPath(sf.CanonicalPath);
}
return Inner.GetFullPath(path);
@@ -291,7 +302,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
throw new InvalidOperationException("Invalid path.");
}
return _sfs.GetLastModified(path);
return _sfs.GetLastModified(sf.CanonicalPath);
}
/// <inheritdoc />
@@ -307,7 +318,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
throw new InvalidOperationException("Invalid path.");
}
return _sfs.GetCreated(path);
return _sfs.GetCreated(sf.CanonicalPath);
}
/// <inheritdoc />
@@ -323,7 +334,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
throw new InvalidOperationException("Invalid path.");
}
return _sfs.GetSize(path);
return _sfs.GetSize(sf.CanonicalPath);
}
/// <inheritdoc />
@@ -348,7 +359,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
if (sd.IsDelete)
{
Nodes[dirPath] = new ShadowNode(false, true);
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
}
}
else
@@ -363,12 +374,13 @@ internal sealed partial class ShadowFileSystem : IFileSystem
throw new InvalidOperationException("Invalid path.");
}
Nodes[dirPath] = new ShadowNode(false, true);
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
}
}
_sfs.AddFile(path, physicalPath, overrideIfExists, copy);
Nodes[normPath] = new ShadowNode(false, false);
var canonicalPath = sf?.CanonicalPath ?? path;
_sfs.AddFile(canonicalPath, physicalPath, overrideIfExists, copy);
Nodes[normPath] = new ShadowNode(false, false, canonicalPath);
}
/// <summary>
@@ -393,11 +405,11 @@ internal sealed partial class ShadowFileSystem : IFileSystem
{
if (Inner.CanAddPhysical)
{
Inner.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move
Inner.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Value.CanonicalPath)); // overwrite, move
}
else
{
using (Stream stream = _sfs.OpenFile(kvp.Key))
using (Stream stream = _sfs.OpenFile(kvp.Value.CanonicalPath))
{
Inner.AddFile(kvp.Key, stream, true);
}
@@ -441,11 +453,15 @@ internal sealed partial class ShadowFileSystem : IFileSystem
}
/// <summary>
/// Normalizes a path to lowercase with forward slashes.
/// Normalizes a path's directory separators to forward slashes.
/// </summary>
/// <param name="path">The path to normalize.</param>
/// <returns>The normalized path.</returns>
private static string NormPath(string path) => path.ToLowerInvariant().Replace("\\", "/");
/// <remarks>
/// Case is preserved. Case-insensitive matching is handled by <see cref="Nodes"/>'s
/// <see cref="StringComparer.OrdinalIgnoreCase"/> comparer.
/// </remarks>
private static string NormPath(string path) => path.Replace("\\", "/");
/// <summary>
/// Determines whether the input path is a direct child of the specified path.
@@ -456,7 +472,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
/// <remarks>Values can be "" (root), "foo", "foo/bar"...</remarks>
private static bool IsChild(string path, string input)
{
if (input.StartsWith(path) == false || input.Length < path.Length + 2)
if (input.StartsWith(path, StringComparison.OrdinalIgnoreCase) == false || input.Length < path.Length + 2)
{
return false;
}
@@ -466,7 +482,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
return false;
}
var pos = input.IndexOf("/", path.Length + 1, StringComparison.OrdinalIgnoreCase);
var pos = input.IndexOf('/', path.Length + 1);
return pos < 0;
}
@@ -478,7 +494,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
/// <returns><c>true</c> if input is a descendant of path; otherwise, <c>false</c>.</returns>
private static bool IsDescendant(string path, string input)
{
if (input.StartsWith(path) == false || input.Length < path.Length + 2)
if (input.StartsWith(path, StringComparison.OrdinalIgnoreCase) == false || input.Length < path.Length + 2)
{
return false;
}
@@ -495,12 +511,14 @@ internal sealed partial class ShadowFileSystem : IFileSystem
{
foreach (var file in Inner.GetFiles(path))
{
Nodes[NormPath(file)] = new ShadowNode(true, false);
var normFile = NormPath(file);
Nodes[normFile] = new ShadowNode(true, false, normFile);
}
foreach (var dir in Inner.GetDirectories(path))
{
Nodes[NormPath(dir)] = new ShadowNode(true, true);
var normDir = NormPath(dir);
Nodes[normDir] = new ShadowNode(true, true, normDir);
if (recurse)
{
Delete(dir, true);
@@ -612,10 +630,12 @@ internal sealed partial class ShadowFileSystem : IFileSystem
/// </summary>
/// <param name="isDelete">Whether this node represents a deletion.</param>
/// <param name="isdir">Whether this node represents a directory.</param>
public ShadowNode(bool isDelete, bool isdir)
/// <param name="canonicalPath">The original-case path tracked by this node.</param>
public ShadowNode(bool isDelete, bool isdir, string canonicalPath)
{
IsDelete = isDelete;
IsDir = isdir;
CanonicalPath = canonicalPath;
}
/// <summary>
@@ -628,6 +648,17 @@ internal sealed partial class ShadowFileSystem : IFileSystem
/// </summary>
public bool IsDir { get; }
/// <summary>
/// Gets the original-case path tracked by this node. For existing-file nodes this is
/// the path used the first time the file was staged in the current shadow scope.
/// </summary>
/// <remarks>
/// All operations against the inner shadow file system (<c>_sfs</c>) must use this
/// path so that re-staging the same logical path with a different case still reaches
/// the same on-disk file on case-sensitive file systems (e.g. Linux).
/// </remarks>
public string CanonicalPath { get; }
/// <summary>
/// Gets a value indicating whether this node represents an existing item (not deleted).
/// </summary>
@@ -64,4 +64,8 @@ public class BlockGridLayoutItem : BlockLayoutItemBase
/// <inheritdoc />
public override bool ReferencesSetting(Guid key)
=> SettingsKey == key || Areas.Any(area => area.ContainsSetting(key));
/// <inheritdoc />
public override IEnumerable<IBlockLayoutItem> GetContainedLayouts()
=> Areas.SelectMany(area => area.Items);
}
@@ -5,12 +5,18 @@ namespace Umbraco.Cms.Core.Models.Blocks;
/// </summary>
public abstract class BlockLayoutItemBase : IBlockLayoutItem
{
/// <inheritdoc />
public Guid Key { get; set; }
/// <inheritdoc />
public Guid ContentKey { get; set; }
/// <inheritdoc />
public Guid? SettingsKey { get; set; }
/// <inheritdoc />
public bool IsExternalContent { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BlockLayoutItemBase" /> class.
/// </summary>
@@ -44,4 +50,7 @@ public abstract class BlockLayoutItemBase : IBlockLayoutItem
/// <inheritdoc />
public virtual bool ReferencesSetting(Guid key)
=> SettingsKey == key;
/// <inheritdoc />
public virtual IEnumerable<IBlockLayoutItem> GetContainedLayouts() => [];
}
+1 -1
View File
@@ -55,7 +55,7 @@ public abstract class BlockValue
/// <returns>
/// <c>true</c> if the specified block layout alias is supported; otherwise, <c>false</c>.
/// </returns>
[Obsolete("Scheduled for removal in Umbraco 18.")]
[Obsolete("Scheduled for removal in Umbraco 19.")]
public virtual bool SupportsBlockLayoutAlias(string alias) => alias.Equals(PropertyEditorAlias);
}
@@ -8,6 +8,18 @@ namespace Umbraco.Cms.Core.Models.Blocks;
/// </summary>
public interface IBlockLayoutItem
{
/// <summary>
/// Gets or sets the layout item key.
/// </summary>
/// <value>
/// The layout item key.
/// </value>
/// <remarks>
/// Uniquely identifies a layout item. Previously the <see cref="ContentKey"/> could be used for this, but
/// with reusable elements, the same <see cref="ContentKey"/> can appear multiple times in one layout.
/// </remarks>
public Guid Key { get; set; }
/// <summary>
/// Gets or sets the content key.
/// </summary>
@@ -24,6 +36,11 @@ public interface IBlockLayoutItem
/// </value>
public Guid? SettingsKey { get; set; }
/// <summary>
/// Indicates if the content source is local or originates from the element service.
/// </summary>
public bool IsExternalContent { get; set; }
/// <summary>
/// Determines whether this layout item references the specified content key.
/// </summary>
@@ -41,4 +58,10 @@ public interface IBlockLayoutItem
/// <c>true</c> if this layout item references the specified settings key; otherwise, <c>false</c>.
/// </returns>
public bool ReferencesSetting(Guid key) => SettingsKey == key;
/// <summary>
/// Returns any nested layouts for this layout (e.g. area layouts for the Block Grid).
/// </summary>
/// <returns>The nested layouts.</returns>
public IEnumerable<IBlockLayoutItem> GetContainedLayouts();
}
@@ -454,7 +454,24 @@ public static class ContentRepositoryExtensions
/// Clears all publish culture information from the content item.
/// </summary>
/// <param name="content">The content item to clear publish information from.</param>
public static void ClearPublishInfos(this IPublishableContentBase content) => content.PublishCultureInfos = null;
public static void ClearPublishInfos(this IPublishableContentBase content)
{
if (content.PublishCultureInfos is null)
{
return;
}
// Pass each published culture through ClearPublishInfo([culture]) to ensure correct change tracking.
var cultures = content.PublishCultureInfos.Values.Select(c => c.Culture).ToArray();
foreach (var culture in cultures)
{
content.ClearPublishInfo(culture);
}
// Following #22799 the explicit calls to `ClearPublishInfo` for each culture cause the unpublish in all cultures.
// `PublishCultureInfos` is set to null purely to retain previous behaviour at a property level.
content.PublishCultureInfos = null;
}
/// <summary>
/// Returns false if the culture is already unpublished
@@ -8,7 +8,25 @@ namespace Umbraco.Cms.Core.Models.Navigation;
/// </summary>
public sealed class NavigationNode
{
private ConcurrentHashSet<Guid> _children;
private static readonly Comparison<(Guid Key, int SortOrder)> _sortBySortOrder =
static (a, b) => a.SortOrder.CompareTo(b.SortOrder);
private readonly ConcurrentHashSet<Guid> _children;
/// <summary>
/// Cached snapshot of <see cref="Children"/> ordered by each child's <c>SortOrder</c>.
/// </summary>
/// <remarks>
/// Built lazily by <see cref="GetOrderedChildren"/> on first access and invalidated
/// (set to <c>null</c>) by <see cref="AddChild"/> / <see cref="RemoveChild"/> /
/// <see cref="InvalidateOrderedChildren"/>. Reads are lock-free on the fast path; the
/// build and invalidation paths take <see cref="_orderedChildrenLock"/> so concurrent
/// first-access threads agree on a single canonical array and an in-flight build
/// cannot finish after a concurrent invalidation has cleared it.
/// </remarks>
private Guid[]? _orderedChildren;
private readonly Lock _orderedChildrenLock = new();
/// <summary>
/// Gets the unique key of this navigation node.
@@ -53,6 +71,17 @@ public sealed class NavigationNode
/// Updates the sort order of this node.
/// </summary>
/// <param name="newSortOrder">The new sort order value.</param>
/// <remarks>
/// The parent node's cached ordered-children list (if any) is now stale because it sorts
/// by child <c>SortOrder</c>. Callers that hold a reference to the parent should call
/// <see cref="InvalidateOrderedChildren"/> on it; <see cref="NavigationNode"/> does not
/// hold a reference to its parent <see cref="NavigationNode"/> so cannot invalidate it
/// itself.
/// </remarks>
// TODO (V19): Make internal. The contract requires the caller to invalidate the parent's
// ordered-children cache (InvalidateOrderedChildren is internal, so external callers cannot
// satisfy that contract and would silently observe stale ordering on subsequent reads).
// Internal callers in ContentNavigationServiceBase already do the invalidation correctly.
public void UpdateSortOrder(int newSortOrder) => SortOrder = newSortOrder;
/// <summary>
@@ -74,6 +103,8 @@ public sealed class NavigationNode
child.SortOrder = _children.Count;
_children.Add(childKey);
InvalidateOrderedChildren();
}
/// <summary>
@@ -91,5 +122,91 @@ public sealed class NavigationNode
_children.Remove(childKey);
child.Parent = null;
InvalidateOrderedChildren();
}
/// <summary>
/// Returns this node's children ordered by <c>SortOrder</c>.
/// </summary>
/// <param name="navigationStructure">The navigation structure dictionary containing all nodes; needed to look up each child's current <c>SortOrder</c>.</param>
/// <returns>An immutable, sort-order-presorted snapshot of the children. The result is cached and reused across calls until the children set or a child's <c>SortOrder</c> is mutated.</returns>
/// <remarks>
/// Lock-free fast path: a non-null cached array is returned without acquiring the lock.
/// If the cache is empty, <see cref="BuildOrderedChildren"/> is called under the lock to
/// build (with double-checked re-read) and store the canonical array.
/// </remarks>
internal IReadOnlyList<Guid> GetOrderedChildren(ConcurrentDictionary<Guid, NavigationNode> navigationStructure)
{
// Volatile.Read provides the acquire fence that pairs with the release fence on the
// lock-protected stores in BuildOrderedChildren / InvalidateOrderedChildren. On weak
// memory architectures (e.g. ARM64) a plain read can observe writes out of order with
// the lock release, so without this barrier a reader could in principle see a torn or
// unpublished reference; on x86/x64 the TSO model already gives acquire semantics so
// this compiles to a normal load. Matches the lock-free read idiom in System.Lazy<T>
// and LazyInitializer.EnsureInitialized.
Guid[]? cached = Volatile.Read(ref _orderedChildren);
if (cached is not null)
{
return cached;
}
return BuildOrderedChildren(navigationStructure);
}
/// <summary>
/// Invalidates the cached ordered-children snapshot.
/// </summary>
/// <remarks>
/// Called by <see cref="AddChild"/> and <see cref="RemoveChild"/> automatically. Must be
/// called externally when a child's <c>SortOrder</c> changes (the parent's cache sorts by
/// child <c>SortOrder</c> and so is stale after such an update).
/// </remarks>
internal void InvalidateOrderedChildren()
{
lock (_orderedChildrenLock)
{
_orderedChildren = null;
}
}
private Guid[] BuildOrderedChildren(ConcurrentDictionary<Guid, NavigationNode> navigationStructure)
{
lock (_orderedChildrenLock)
{
// Double-check under the lock — another thread may have built the cache while we
// were waiting to acquire it.
Guid[]? cached = _orderedChildren;
if (cached is not null)
{
return cached;
}
if (_children.Count == 0)
{
_orderedChildren = [];
return _orderedChildren;
}
var sorted = new List<(Guid Key, int SortOrder)>(_children.Count);
foreach (Guid childKey in _children)
{
if (navigationStructure.TryGetValue(childKey, out NavigationNode? childNode))
{
sorted.Add((childKey, childNode.SortOrder));
}
}
sorted.Sort(_sortBySortOrder);
var result = new Guid[sorted.Count];
for (var i = 0; i < sorted.Count; i++)
{
result[i] = sorted[i].Key;
}
_orderedChildren = result;
return result;
}
}
}
@@ -74,6 +74,10 @@ public class PublishedValueFallback : IPublishedValueFallback
}
break;
case Fallback.Ancestors:
// Ancestors fallback only applies at IPublishedContent level (tree-aware).
// Skip silently here so chained fallbacks still work and direct element calls don't throw.
continue;
default:
throw NotSupportedFallbackMethod(f, "property");
}
@@ -127,6 +131,10 @@ public class PublishedValueFallback : IPublishedValueFallback
}
break;
case Fallback.Ancestors:
// Ancestors fallback only applies at IPublishedContent level (tree-aware).
// Skip silently here so chained fallbacks still work and direct element calls don't throw.
continue;
default:
throw NotSupportedFallbackMethod(f, "element");
}
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// A notification that is used to trigger the IFileService when the DeleteScript method is called in the API, after the script has been deleted.
/// A notification that is used to trigger the <see cref="Services.IScriptService"/> after a script has been deleted.
/// </summary>
public class ScriptDeletedNotification : DeletedNotification<IScript>
{
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// A notification that is used to trigger the IFileService when the DeleteScript method is called in the API.
/// A notification that is used to trigger the <see cref="Services.IScriptService"/> when a script is being deleted.
/// </summary>
public class ScriptDeletingNotification : DeletingNotification<IScript>
{
@@ -10,7 +10,7 @@ namespace Umbraco.Cms.Core.Notifications;
/// Notification that is published after a script file has been saved.
/// </summary>
/// <remarks>
/// This notification is published by the <see cref="Services.IFileService"/> after the script has been persisted.
/// This notification is published by the <see cref="Services.IScriptService"/> after the script has been persisted.
/// It is not cancelable since the save operation has already completed.
/// </remarks>
public class ScriptSavedNotification : SavedNotification<IScript>
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Notifications;
/// </summary>
/// <remarks>
/// This notification is cancelable, allowing handlers to prevent the save operation.
/// The notification is published by the <see cref="Services.IFileService"/> before the script is persisted.
/// The notification is published by the <see cref="Services.IScriptService"/> before the script is persisted.
/// </remarks>
public class ScriptSavingNotification : SavingNotification<IScript>
{
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// A notification that is used to trigger the IFileService when the DeleteStylesheet method is called in the API, after the stylesheet has been deleted.
/// A notification that is used to trigger the <see cref="Services.IStylesheetService"/> after a stylesheet has been deleted.
/// </summary>
public class StylesheetDeletedNotification : DeletedNotification<IStylesheet>
{
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// A notification that is used to trigger the IFileService when the DeleteStylesheet method is called in the API.
/// A notification that is used to trigger the <see cref="Services.IStylesheetService"/> when a stylesheet is being deleted.
/// </summary>
public class StylesheetDeletingNotification : DeletingNotification<IStylesheet>
{

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