* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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.
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* Use direct imports in core manifests
* Extract theme aliases into constants file
---------
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
* 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).
* 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>
* 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>
* Use direct imports in core manifests
* Extract theme aliases into constants file
---------
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
* 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>
* 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>
* Support anchor fragments that are included in the data attribute but missing in the href when migrating local links.
* Addressed code review feedback.
* Support anchor fragments that are included in the data attribute but missing in the href when migrating local links.
* Addressed code review feedback.
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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
- 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>
* 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>
* 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>
* 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.
* 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>
* 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>
* 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.
* 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.
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.
* 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.
* 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.
* 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>
* 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>
* 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>
* 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>
* 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>
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.
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.
* 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)
* 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>
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.
* 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>
* 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
* 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
* 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
* 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)
* 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>
* 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)
* 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
* 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>
* 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>
* 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>
* 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>
* 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!
`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>
* 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.
* 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.
* 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>
* 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
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."
@@ -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.
@@ -343,12 +341,6 @@ public sealed class ContentTypeSchemaTransformer : IOpenApiSchemaTransformer, IO
varschemaId=GetSchemaId(jsonTypeInfo);
// Types that produce 'true' in JSON Schema (unconstrained: JsonNode, object, custom-converter types) should be inline {} rather than named components.
@@ -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>
/// 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.")]
// 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.")]
// 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);
returnOk();
}
[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.")]
@@ -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>
"summary":"Sets the redirect URL tracking status.",
"description":"Updates the redirect URL tracking configuration according to the provided status.",
"summary":"Deprecated. No longer changes the redirect URL tracking status.",
"description":"This endpoint is deprecated and no longer modifies the configuration. To toggle redirect URL tracking, set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead.",
@@ -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.
@@ -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.")]
<keyalias="umbracoApplicationUrlCheckResultTrue"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' je postavljen na <strong>%0%</strong>.]]></key>
<keyalias="umbracoApplicationUrlCheckResultFalse"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen, pa će se URL aplikacije automatski otkriti iz dolaznih zahtjeva. Preporučuje se da ga postavite izričito.]]></key>
<keyalias="umbracoApplicationUrlCheckResultError"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen, a automatsko otkrivanje URL-a aplikacije je onemogućeno ('Umbraco:CMS:WebRouting:ApplicationUrlDetection' je 'None'). Značajke koje zahtijevaju apsolutni URL, poput e-pošte za poništavanje lozinke i pozivnica, neće raditi. Postavite URL aplikacije izričito ili omogućite automatsko otkrivanje.]]></key>
<keyalias="umbracoApplicationUrlCheckResultFalse">AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen.</key>
<!-- The following key get these tokens passed in:
<keyalias="httpsCheckConfigurationRectifyNotPossible">Mae gosodiad ap 'Umbraco:CMS:Global:UseHttps' wedi'i osod i 'false' yn eich ffeil appSettings.json. Unwaith y byddwch yn cyrchu'r wefan hon gan ddefnyddio'r cynllun HTTPS, dylid gosod hwnnw i 'true'.</key>
<keyalias="httpsCheckConfigurationCheckResult">Mae'r gosodiad ap 'Umbraco:CMS:Global:UseHttps' wedi'i osod i '%0%' yn eich ffeil appSettings.json, mae eich cwcis %1% wedi'u marcio'n ddiogel.</key>
<keyalias="umbracoApplicationUrlCheckResultTrue">Mae gosodiad yr ap 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' wedi'i osod i <strong>%0%</strong>.</key>
<keyalias="umbracoApplicationUrlCheckResultFalse"><![CDATA[Nid yw gosodiad ap 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' wedi'i osod, felly bydd URL y rhaglen yn cael ei ganfod yn awtomatig o geisiadau sy'n dod i mewn. Argymhellir ei osod yn benodol.]]></key>
<keyalias="umbracoApplicationUrlCheckResultError"><![CDATA[Nid yw gosodiad ap 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' wedi'i osod ac mae canfod URL y rhaglen yn awtomatig wedi'i analluogi (mae 'Umbraco:CMS:WebRouting:ApplicationUrlDetection' yn 'None'). Ni fydd nodweddion sydd angen URL absoliwt, fel e-byst ailosod cyfrinair a gwahoddiadau, yn gweithio. Gosodwch URL y rhaglen yn benodol, neu galluogwch ganfod yn awtomatig.]]></key>
<keyalias="umbracoApplicationUrlCheckResultFalse">Nid yw gosodiad ap 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' wedi'i osod.</key>
<keyalias="smtpMailSettingsNotFound">Nid oedd modd dod o hyd i'r ffurfweddiad 'Umbraco:CMS:Global:Smtp'.</key>
<keyalias="smtpMailSettingsHostNotConfigured">Nid oedd modd dod o hyd i'r ffurfweddiad 'Umbraco:CMS:Global:Smtp:Host'.</key>
<keyalias="smtpMailSettingsConnectionFail">Methwyd cyrraedd y gweinydd SMTP a ffurfweddwyd gyda gwesteiwr '%0%' a phorth '%1%'. Gwiriwch i sicrhau bod y gosodiadau SMTP yn y ffurfweddiad 'Umbraco:CMS:Global:Smtp' yn gywir.</key>
<keyalias="umbracoApplicationUrlCheckResultTrue"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is set to <strong>%0%</strong>.]]></key>
<keyalias="umbracoApplicationUrlCheckResultFalse"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set, so the application URL will be auto-detected from incoming requests. Setting it explicitly is recommended.]]></key>
<keyalias="umbracoApplicationUrlCheckResultError"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set and application URL auto-detection is disabled ('Umbraco:CMS:WebRouting:ApplicationUrlDetection' is 'None'). Features that require an absolute URL, such as password reset and invitation emails, will not work. Set the application URL explicitly, or enable auto-detection.]]></key>
<keyalias="umbracoApplicationUrlCheckResultFalse">The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set.</key>
<!-- The following key get these tokens passed in:
<keyalias="umbracoApplicationUrlCheckResultTrue"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is set to <strong>%0%</strong>.]]></key>
<keyalias="umbracoApplicationUrlCheckResultFalse"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set, so the application URL will be auto-detected from incoming requests. Setting it explicitly is recommended.]]></key>
<keyalias="umbracoApplicationUrlCheckResultError"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set and application URL auto-detection is disabled ('Umbraco:CMS:WebRouting:ApplicationUrlDetection' is 'None'). Features that require an absolute URL, such as password reset and invitation emails, will not work. Set the application URL explicitly, or enable auto-detection.]]></key>
<keyalias="umbracoApplicationUrlCheckResultFalse">The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set.</key>
<keyalias="clickJackingCheckHeaderFound">
<![CDATA[The header or meta-tag <strong>X-Frame-Options</strong> used to control whether a site can be IFRAMEd by another was found.]]></key>
<keyalias="umbracoApplicationUrlCheckResultTrue"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' je postavljen na <strong>%0%</strong>.]]></key>
<keyalias="umbracoApplicationUrlCheckResultFalse"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen, pa će se URL aplikacije automatski otkriti iz dolaznih zahtjeva. Preporučuje se da ga postavite izričito.]]></key>
<keyalias="umbracoApplicationUrlCheckResultError"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen, a automatsko otkrivanje URL-a aplikacije je onemogućeno ('Umbraco:CMS:WebRouting:ApplicationUrlDetection' je 'None'). Značajke koje zahtijevaju apsolutni URL, poput e-pošte za poništavanje lozinke i pozivnica, neće raditi. Postavite URL aplikacije izričito ili omogućite automatsko otkrivanje.]]></key>
<keyalias="umbracoApplicationUrlCheckResultFalse">AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen.</key>
<!-- The following key get these tokens passed in:
@@ -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.")]
stringMapPathWebRoot(stringpath);
/// <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.")]
/// Creates an <see cref="IPublishedElement"/> instance from <see cref="BlockItemData"/>.
/// </summary>
/// <param name="owner">The <see cref="IPublishedElement"/> that contains the block property which is the origin to the <see cref="BlockItemData"/>.</param>
/// <param name="blockItemData">The <see cref="BlockItemData"/> containing the data to convert into an <see cref="IPublishedElement"/>.</param>
/// <param name="preview">Whether to perform the conversion for preview.</param>
/// <returns>The created <see cref="IPublishedElement"/>, or null if an element could not be created from the <see cref="BlockItemData"/>.</returns>
@@ -55,7 +55,9 @@ public class LuceneIndexDiagnostics : IIndexDiagnostics
DirectoryluceneDir=Index.GetLuceneDirectory();
vard=newDictionary<string,object?>
{
#pragmawarningdisableCS0618// CommitCount is obsolete and reported unused, but retained to avoid any risk of change to existing behaviour. Remove this entry when a future Examine upgrade removes the underlying field.
@@ -384,6 +384,57 @@ using (ICoreScope scope = ScopeProvider.CreateCoreScope())
3.**Lazy loading outside scope** - NPoco relationships must load within scope
4.**Large migrations** - Split into multiple steps if > 1000 lines
5.**Repository logic in services** - Keep repos thin, logic in services
6.**Unbatched `WHERE IN` on user-sized collections** - See "Avoiding the SQL Server 2100-parameter limit" below
### Avoiding the SQL Server 2100-parameter limit
SQL Server caps a single statement at 2100 parameters. When an `IN` clause is built from a collection sized by user data, that cap can be hit — and the symptom is a runtime `SqlException` (error 8003) on customer installs that nobody hit in dev.
**The constant and helpers**:
-`Constants.Sql.MaxParameterCount = 2000` (in `Umbraco.Core`, `Constants-Sql.cs`) — the ceiling we target (2100 minus headroom for joined predicates already in the SQL).
-`IEnumerable<T>.InGroupsOf(groupSize)` (in `Umbraco.Core`, `Extensions/EnumerableExtensions.cs`) — extension method to batch a collection.
-`Database.FetchByGroups<TResult, TSource>(source, groupSize, sqlFactory)` (in `Umbraco.Infrastructure`, `Persistence/NPocoDatabaseExtensions.cs`) — NPoco helper that batches a fetch.
**The safe patterns** (use one of these any time the collection size is user-driven):
```csharp
// Pattern 1: batch a DeleteMany / Execute / Fetch by looping.
// statement uses entityIds + userGroupIds, so subtract the other predicate's parameter count from the budget
}
```
**Decision rule when writing or reviewing a `WHERE IN`-style query**:
Look at what drives the size of the collection feeding the `IN`. Ask: *could this realistically exceed 2000 on a large install?* Risky drivers — batch any query backed by these:
- All content / media / member nodes (or descendants of a deep tree).
- A product of two scaling dimensions, e.g. `documents × languages`, `properties × versions`, `relations × endpoints`.
- Configuration-tunable batch sizes (`CacheSettings.DocumentSeedBatchSize`, `NuCacheSettings.SqlPageSize`, etc.). The default may be safe but the customer can raise it.
- Anything that scans property data, version history, relations, or audit logs across many nodes.
Safe drivers — don't bother batching:
- Languages / content types / member groups / user groups — bounded by install configuration, typically <100.
- "Per single content item" collections — properties on one document, versions of one document, tokens for one external login.
- IDs supplied directly by a user action through the UI (picker selections, bulk actions on a page of results).
If you're not sure, batch — the cost is one loop and an `IEnumerable<T>` allocation per batch; the cost of being wrong is a SqlException on a customer's biggest site.
**For new public APIs** that take an `IEnumerable<int>`/`IEnumerable<Guid>` and feed it into a query, batch internally even if no current caller is large — package authors and future callers will not know about the 2000-limit ceiling.
**Don't** rely on `if (ids.Length > MaxParameterCount) throw` as a substitute for batching. Throwing only moves the problem; the caller has no obvious way to recover and will most likely just fail in production.
@@ -104,6 +104,7 @@ internal sealed class JsonConfigManipulator : IConfigManipulator
}
/// <inheritdoc />
[Obsolete("This method is no longer used by Umbraco. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
@@ -18,7 +19,9 @@ internal sealed class RichTextBlockPropertyValueCreator : BlockPropertyValueCrea
/// </summary>
/// <param name="blockEditorConverter">The <see cref="BlockEditorConverter"/> used to convert block editor values.</param>
/// <param name="variationContextAccessor">The <see cref="IVariationContextAccessor"/> providing access to the variation context.</param>
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
/// <param name="blockEditorVarianceHandler">The <see cref="BlockEditorVarianceHandler"/> that handles block editor variance.</param>
/// <param name="elementCacheService">The cache for elements.</param>
/// <param name="jsonSerializer">The <see cref="IJsonSerializer"/> used for JSON serialization and deserialization.</param>
/// <param name="constructorCache">The <see cref="RichTextBlockPropertyValueConstructorCache"/> used to cache rich text block property value constructors.</param>
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
@@ -27,10 +30,11 @@ internal sealed class RichTextBlockPropertyValueCreator : BlockPropertyValueCrea
// Serilog.Formatting.Compact.Reader uses Newtonsoft.Json internally and surfaces
// its exceptions (Umbraco's own serialization is on System.Text.Json, but that
// doesn't apply here — we have to catch what the reader actually throws).
// JsonException covers parse failures (e.g. an unterminated string in a truncated
// entry); InvalidDataException covers structurally-valid JSON that isn't a valid
// Serilog Compact event. Either way the offending line has been consumed from the
// underlying StreamReader and the next TryRead call advances. Anything else
// (IOException, decoder failures, etc.) is propagated to the file-level catch in
// GetLogs so we don't risk a tight loop or silently swallow a more serious failure.
errorCount++;
firstError??=ex;
continue;
}
// LogEventReader may return true with a null event for a benign skip.
if(evtisnull)
{
continue;
}
if(logFilter.TakeLogEvent(evt))
{
logs.Add(evt);
}
}
if(errorCount>0)
{
_logger.LogWarning(
firstError,
"Encountered {ErrorCount} unreadable line(s) while reading log file {FilePath}. The file may contain partially-written or corrupt entries; affected lines were skipped.",
@@ -256,6 +257,8 @@ ASP.NET Core Identity sign-in manager for members.
### Middleware
**Convention**: middleware lives in `Middleware/` as a class implementing `IMiddleware`, registered as a singleton next to its dependencies' registration (generic middleware in `AddWebComponents`; feature-specific middleware where the feature's services are added, e.g. backoffice middleware in `AddBackOfficeCore`), and wired into the pipeline via `app.UseMiddleware<TMiddleware>()`. Companion `IApplicationBuilder` extension methods are thin one-line `UseMiddleware<T>()` wrappers — inline `builder.Use(async …)` lambdas bypass DI and are harder to test; `CspNonceExtensions` and `Web.UI/WebApplicationExtensions` are tiny pre-existing exceptions, not a precedent for new work.
**BootFailedMiddleware** (lines 17-81):
- Intercepts requests when `RuntimeLevel == BootFailed`
- Debug mode: Rethrows exception for stack trace
@@ -266,6 +269,11 @@ ASP.NET Core Identity sign-in manager for members.
- Sets `Cache-Control: public, max-age=31536000, immutable` on responses under the cache-busted backoffice asset prefix (`/umbraco/backoffice/<hash>/…`); `no-cache` in debug mode
- Runs before `UseUmbracoBackOfficeRewrites` so the original (hash-bearing) path can be matched
- Non-destructive: uses `Response.OnStarting` + `ContainsKey` guard so any consumer override wins
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.