Compare commits

...
Author SHA1 Message Date
Ronald Barendse 1d4d3842b3 Allow Element and ElementContainer object types in RelationService 2026-06-26 16:13:33 +02:00
Laura NetoandGitHub df16c114b6 OpenApi: Lowercase document name on registration to match AddOpenApi internal behaviour (closes #23210) (#23212)
* Lowercase OpenAPI document name on registration to match AddOpenApi internal behaviour

AddOpenApi lowercases the document name when registering its keyed services, so
ReplaceOpenApiSchemaService must receive the same lowercased key or the lookup
throws. BackOfficeOpenApiDocumentBuilder now computes a normalised registration
name and uses it for all DI calls, while keeping DocumentName in its original
casing. ShouldInclude matches [MapToApi] case-insensitively to align with how
documents are registered, and the UI dropdown label falls back to DocumentName
(original casing) rather than the lowercased registration key.
AddUmbracoOpenApiDocument applies the same normalisation for its apiName parameter.

* Add regression tests for mixed-case OpenAPI document name registration

Covers the bug scenario where AddBackOfficeOpenApiDocument with a mixed-case
name and WithJsonOptions threw InvalidOperationException at startup, and verifies
that ShouldInclude matches [MapToApi] case-insensitively.
2026-06-26 13:59:27 +02:00
fb1e16ff36 Table dates and User dates (#22169)
User-collection-table didn´t format and if you have da backoffice the time is still Am/pm

Co-authored-by: Lucas Bach Bisgaard <lucas.bisgaard@kraftvaerk.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-06-26 09:23:21 +02:00
Niels Lyngsø d889de6af0 Merge branch 'v17/dev' 2026-06-26 09:19:08 +02:00
774b2d4822 Fix detail data request manager failing when items hit 40, making document unusable (#23164)
Fix detail data request manager failing as soon as the number of items requested hits the UmbItemDataApiGetRequestController batch limit (40)

Co-authored-by: Paul Woodland <paul.woodland@pwnewmedia.com>
2026-06-26 09:08:14 +02:00
Laura NetoandGitHub ba6ec7abcf Re-enable package validation (#23197) 2026-06-25 08:43:56 +00:00
Laura Neto 5d4d3a51ea Merge branch 'release/18.0' 2026-06-25 07:16:30 +01:00
Jacob Overgaard c6c5a9e7e6 Merge remote-tracking branch 'origin/v17/dev' 2026-06-24 11:51:40 +02:00
Laura Neto c087ce9fb5 Bump the version of the Umbraco.TheStarterKit to 18.0.0 in the UmbracoProject template 2026-06-24 11:23:37 +02:00
Jacob OvergaardandClaude Opus 4.8 cda880bb62 test(hybridcache): use v18 IDatabaseCacheRepository method names in stale-set race test
DocumentHybridCacheStaleSetRaceTests came from the v17 PR #23169 and merged
forward into main (v18) unchanged, but on v18 IDatabaseCacheRepository renamed
GetContentSourceAsync -> GetDocumentSourceAsync and
GetContentSourceForPublishStatesAsync -> GetDocumentSourceForPublishStatesAsync.
The mock setups still referenced the v17 names, failing the build with CS1061.

Rename the two Moq setups to the v18 method names so the test compiles and the
mocks actually intercept the calls DocumentCacheService makes. v17 keeps the
Content* names and is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:06:16 +02:00
Jacob Overgaard b3e6477df9 Merge remote-tracking branch 'origin/v17/dev' 2026-06-23 14:33:28 +02:00
Jacob Overgaard 8f3ff26005 Merge remote-tracking branch 'origin/release/18.0' 2026-06-23 14:32:19 +02:00
Jacob Overgaard 8dd8820fa3 build(deps): bumps @umbraco-ui/uui to 2.0.0 2026-06-23 11:54:00 +02:00
Laura Neto 6b76230da5 Bump version to 18.0.0. 2026-06-23 09:38:59 +02:00
Andy Butland 2e3628dad3 Bump version to 18.0.0-rc4. 2026-06-19 18:50:40 +02:00
0e14ace89a Element Picker: Adds valueSummary display and value resolver (#23154)
* feat(elements): add value summary for Element Picker property editor

Adds a valueSummary extension so picked element names appear in collection
view columns. Includes a value-type constant, batch resolver, variant-aware
element, and a resolver unit test (11 cases).

* fix(elements): address PR review feedback on element picker value summary

- Call removeUmbControllerByAlias when removing a stale resolver so the
  named observer controller is released from the element's controller list
- Call setData on existing resolvers when _value refreshes so renames are
  reflected without recreating the resolver
- Remove redundant valueResolver re-export from resolver file (barrel handles it)


---------

Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-06-19 14:11:12 +00:00
c9059c7b07 UFM: Add umbElementName component (#23162)
* feat(ufm): add umbElementName UFM component

Adds a new UFM component that resolves Element display names from element
keys, mirroring the umbContentName component pattern. Uses the variant-aware
UmbElementItemDataResolver (via UmbElementItemRepository) for proper
culture/variant handling and (Untitled) fallback.

Also exports UmbElementItemDataResolver from the public
@umbraco-cms/backoffice/element entry point, matching the pattern used by
the documents package.

* fix(ufm): clear stale value and destroy resolvers in element-name element

Clear this.value when the render context produces no usable input, preventing
stale names from lingering when the context changes. Also destroy each
UmbElementItemDataResolver after getName() to avoid accumulating controller
registrations on the host element.

* Update src/Umbraco.Web.UI.Client/src/packages/ufm/components/element-name/element-name.element.ts

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

* test(ufm): add umbElementName parsing test to marked-ufm.test.ts

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-06-19 13:26:20 +00:00
Andy Butland 1ee24595d0 Fixed build error with integration tests. 2026-06-19 14:24:41 +02:00
Jacob Overgaard 4ac8a397d0 Merge branch 'v17/dev'
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/content/content/types.ts
2026-06-19 13:58:25 +02:00
Andy Butland 0f4b784c23 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-06-19 13:40:22 +02:00
Andy Butland ebc0ef36f5 Merge branch 'v17/dev' 2026-06-19 13:40:09 +02:00
Jacob OvergaardandClaude Opus 4.8 cb23c84c3d External login: wait for app-entry-points before the login provider decision (#23167)
* External login: wait for app-entry-points before the login provider decision

The backoffice boot stopped waiting for app-entry-point extensions to settle
before deciding which auth provider to use (regression introduced in #22522).
On a slow connection an externally registered authProvider (e.g. Umbraco ID)
is not registered yet when the login screen renders, so the user is dropped on
the local login instead of being redirected to the external provider.

- extension-initializer-base: `loaded` re-arms to `undefined` while a pass is in
  flight and resolves to `true` unconditionally (including zero extensions), so
  `.asPromise()` gates correctly and never hangs on a default install (which has
  no app-entry-points) — the reason the await was removed in the first place.
- app.element: restore the awaited boot gate before routing.

Tests:
- Unit test for the `loaded` signal contract (zero extensions resolves; a late,
  slow extension is awaited).
- Playwright acceptance test that deploys an app-entry-point registering an
  authProvider after a delay and asserts it is offered on the login screen.

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

* test(backoffice): guard the loaded-gate timing for permission loading

Add a test asserting the collection initializer's `loaded` does not open the
gate (`#loadedGuard` awaits it via `.asPromise()`, fronting private-extension
and user-permission loading) until the initially-registered extensions have
instantiated. Addresses the #22522 "user permissions resolved too late" concern
in writing; user-permission condition resolution itself lives in
UmbBaseExtensionInitializer (covered by base-extension-initializer.race.test.ts)
and is untouched by this change.

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

* fix(backoffice): harden loaded signal + narrow acceptance test glob (review)

Address PR review feedback:
- extension-initializer-base: only the latest processing pass settles `loaded`
  (monotonic pass id), so a slow earlier pass can't unblock waiters early when
  the async observer overlaps passes; and use `Promise.allSettled` so a throwing
  `instantiateExtension` can't leave `loaded` stuck at `undefined` (hanging the
  boot gate) — failures are logged rather than swallowed.
- playwright.config: narrow the project glob to `**/*.spec.ts` so Playwright
  doesn't try to load the App_Plugins `entry-point.js` ESM fixture as a test.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:28:23 +02:00
Niels Lyngsø fc3b13c85c Squashed commit of the following:
commit cd132f44b1
Author: Niels Lyngsø <niels.lyngso@gmail.com>
Date:   Fri Jun 19 13:18:07 2026 +0200

    correct to use display: block;

commit a9ffa9f90b
Author: Andreas Lykke Borg <72602768+andreaslborg@users.noreply.github.com>
Date:   Mon Jun 15 20:56:51 2026 +0200

    Added css styling to block list and single to respect custom width
2026-06-19 13:24:56 +02:00
Andy Butland 8bcd5990c2 Merge branch 'v17/dev' 2026-06-17 20:00:48 +02:00
Jesper MadsenandJacob Overgaard 1dfcd9332a Let the external login button show "sign in with {providername}" in languages (#23135) 2026-06-17 15:57:40 +02:00
Jesper MadsenandGitHub b2b45b016d Let the external login button show "sign in with {providername}" in languages (#23135) 2026-06-17 15:56:59 +02:00
Jacob Overgaard ef610c7d23 Merge remote-tracking branch 'origin/release/18.0' 2026-06-17 15:51:52 +02:00
Jacob OvergaardandClaude Opus 4.8 d84186061c fix(tests): point DomainCacheServiceTests mocks at GetAllAsync
IDomainService.GetAll was removed in #22629; DomainCacheServiceTests was
added later in #23084 against a stale base and still mocked the removed
method, breaking the Release build on release/18.0. Production
DomainCacheService already calls GetAllAsync, so update the four mock
setups to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:52:52 +02:00
Jacob Overgaard 87bc6522f1 build: deploy to npm through a template 2026-06-17 14:26:25 +02:00
Jacob OvergaardandClaude Opus 4.7 c300ebf94a Build: tag prerelease npm publishes with 'next' dist-tag (#22909)
* Build: tag prerelease npm publishes with 'next' dist-tag

Prereleases that flow through Deploy_Npm (e.g. 18.0.0-beta1) currently
land on the `latest` dist-tag, so a bare `npm install @umbraco-cms/backoffice`
resolves to an unstable build. Switch to `--tag next` when
NBGV_PrereleaseVersion is non-empty, leaving `latest` for stable releases.

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

* Build: address review feedback on npm prerelease dist-tag

- Add Build to Deploy_Npm dependsOn so stageDependencies.Build.A.outputs
  resolves explicitly (mirrors the Upload_API_Docs pattern).
- Pass npmPrereleaseVersion via env: instead of inline macro expansion in
  bash, so an unset variable won't be interpreted as command substitution.

* Build: source npmPrereleaseVersion via dependencies, not dependsOn

Switches the variable mapping from stageDependencies (which needs Build
in dependsOn) to dependencies.Build.outputs[...], matching the pattern
the stage's condition already uses on line 941. Avoids drawing a
redundant parallel arrow from Build to Deploy_Npm in the ADO stage
graph — Build is already in the ancestor chain via Deploy_NuGet.

* Build: align Deploy_Npm with Umbraco Deploy publish pattern

- Use stageDependencies form in variables: (dependencies.* only works in conditions).
- Source NBGV_PrereleaseVersionNoLeadingHyphen for a cleaner check.
- Replace echo >> .npmrc with npm config set --location=project.
- Collapse if/else into a tag=latest|next shell variable; single npm publish *.tgz.
- Drop unnecessary env: passthrough and npm init -y.

Per Ronald's feedback on PR #22909 — mirrors the Deploy pipeline's release stage.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-17 14:00:19 +02:00
7fdf9c12f3 Build: tag prerelease npm publishes with 'next' dist-tag (#22909)
* Build: tag prerelease npm publishes with 'next' dist-tag

Prereleases that flow through Deploy_Npm (e.g. 18.0.0-beta1) currently
land on the `latest` dist-tag, so a bare `npm install @umbraco-cms/backoffice`
resolves to an unstable build. Switch to `--tag next` when
NBGV_PrereleaseVersion is non-empty, leaving `latest` for stable releases.

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

* Build: address review feedback on npm prerelease dist-tag

- Add Build to Deploy_Npm dependsOn so stageDependencies.Build.A.outputs
  resolves explicitly (mirrors the Upload_API_Docs pattern).
- Pass npmPrereleaseVersion via env: instead of inline macro expansion in
  bash, so an unset variable won't be interpreted as command substitution.

* Build: source npmPrereleaseVersion via dependencies, not dependsOn

Switches the variable mapping from stageDependencies (which needs Build
in dependsOn) to dependencies.Build.outputs[...], matching the pattern
the stage's condition already uses on line 941. Avoids drawing a
redundant parallel arrow from Build to Deploy_Npm in the ADO stage
graph — Build is already in the ancestor chain via Deploy_NuGet.

* Build: align Deploy_Npm with Umbraco Deploy publish pattern

- Use stageDependencies form in variables: (dependencies.* only works in conditions).
- Source NBGV_PrereleaseVersionNoLeadingHyphen for a cleaner check.
- Replace echo >> .npmrc with npm config set --location=project.
- Collapse if/else into a tag=latest|next shell variable; single npm publish *.tgz.
- Drop unnecessary env: passthrough and npm init -y.

Per Ronald's feedback on PR #22909 — mirrors the Deploy pipeline's release stage.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-17 13:59:17 +02:00
Ronald BarendseandAndy Butland e2252a8634 Tests: Remove AutoFixture from the shipped Umbraco.Cms.Tests package (#23141)
Tests: Remove AutoFixture from Umbraco.Cms.Tests package
2026-06-17 09:50:01 +02:00
Ronald BarendseandGitHub 190e3d373a Tests: Remove AutoFixture from the shipped Umbraco.Cms.Tests package (#23141)
Tests: Remove AutoFixture from Umbraco.Cms.Tests package
2026-06-17 09:47:22 +02:00
Laura NetoandGitHub 84a1ca8335 SonarCloud: Improve workflow (#23080)
* SonarCloud: allow unit test failures without failing the analysis

Test failures should not block SonarCloud analysis - coverage data is
still collected by dotnet-coverage regardless of test outcome. The
regular CI pipeline is the correct gate for test pass/fail.

* SonarCloud: install Java 21 explicitly and skip JRE provisioning

- Add actions/setup-java@v5 (temurin-21) so JAVA_HOME always points to Java 21
- Pass sonar.scanner.skipJreProvisioning=true in the begin command since Java 21 is installed explicitly, removing the need for the scanner to download a JRE at runtime

* SonarCloud: clear SONARQUBE_SCANNER_PARAMS after begin

Prevents the End analysis step from re-applying sonar params that begin already wrote to the analysis config, eliminating the "Ignoring property from env variable" warning.

* SonarCloud: always cancel in-progress runs on new push

* TEMP: add failing test to verify pipeline resilience — revert before merge

* Revert "TEMP: add failing test to verify pipeline resilience — revert before merge"

This reverts commit 828a7510cb.

* Revert "SonarCloud: clear SONARQUBE_SCANNER_PARAMS after begin"

This reverts commit 1911b65db1.

* Make SonarCloud workflow resilient to build and test failures

* Fix inaccurate warning message when unit tests fail

* Revert build step resilience, keep test failure warning

* Improve test failure warning with coverage file check

* Temporary: add failing test to verify SonarCloud workflow resilience

* Revert "Temporary: add failing test to verify SonarCloud workflow resilience"

This reverts commit 71ecd61034.
2026-06-16 09:31:50 +00:00
Andy Butland b940b27ad2 Updated OpenApi.json. 2026-06-16 10:09:54 +02:00
Andy Butland d416d352e0 Merge branch 'v17/dev' 2026-06-16 09:26:33 +02:00
Nhu DinhandGitHub a15d340608 E2E: QA Added acceptance tests for published element extensions (#23030)
* Added api helper for creating element type with compositions

* Added api helper for creating template with displaying element picker

* Added tests for published element extensions

* Make tests run in the pipeline

* Fixed suggestions

* Fixed comment

* Reverted npm command
2026-06-16 03:29:45 +00:00
Andy Butland 37e2458475 Merge branch 'release/18.0' of https://github.com/umbraco/Umbraco-CMS into release/18.0 2026-06-15 16:20:46 +02:00
Andy Butland 2461853b11 Published Cache: Fix multi-site domains falling back to the first root node after restart (#23084)
* Prevent empty domain cache during concurrent initialization.

* Addressed code review comments and added further comment to the code.

* Use Lock object.
2026-06-15 16:20:11 +02:00
Nicklas KramerandGitHub 9fe9469e3a Integration Tests: Adjusting Status Codes for Failing Tests (#23125)
Adjusting expected status codes
2026-06-15 15:39:47 +02:00
16c97d613f Elements: Invalidate the id/key map when an element container is deleted (closes #23072) (#23074)
* Refresh the element container cache on delete to ensure the id/key map is invalidated.

* Rename and additional asserts in test.

* Use a dedicated refresher for element container id/key map eviction

Routing container-delete invalidation through ElementCacheRefresher cleared
the entire elements cache on every payload, so deleting a container triggered
a full clear even though no element data changed (and a second clear on top of
the ElementTreeChangeNotification refresh when the container held elements).

Add a dedicated ElementContainerCacheRefresher whose only job is to evict the
container's IIdKeyMap entry, and route EntityContainerDeletedNotification
through it instead.

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

* Addressed code review feedback.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:16:31 +02:00
Nhu DinhandGitHub 5a6b18307d E2E: QA Added acceptance tests for broken publish path in legacy routing (#23090)
* Added ui helper for does not contain text

* Added constant for cannot be routed in content

* Added ui helper for verify document does not contain link

* Added api helper for unpublish document and publish chain of document

* Added api helper for updating document type

* Added tests for handling broken publish path in backoffice

* Added tests for handling broken publish path in delivery API

* Make tests run in the pipeline

* Clean up

* Fixed comments

* Reverted npm command
2026-06-15 09:55:26 +00:00
Nhu DinhandGitHub 3ee7b142b3 E2E: Fixed failing acceptance tests for user group, backoffice search and content picker (#23108)
* Updated backoffice search element tests to match the test helper changes

* Added step to delete user group before deleting language

* Updated tests for element folder permission due to recent changes

* Fix failing tests for content picker and element with element picker due to UI changes
2026-06-15 08:57:13 +00:00
Andy Butland ae92b0b0a9 Fixed failing unit test. 2026-06-15 07:07:51 +02:00
Andy Butland 8503762431 Merge branch 'v17/dev' 2026-06-15 06:49:06 +02:00
Andy Butland 97a005e709 Merge branch 'release/18.0' 2026-06-15 06:42:11 +02:00
Andy Butland 30b4370044 Merge branch 'v17/dev' 2026-06-15 06:40:59 +02:00
Andy Butland cffac2a990 Dependencies: Update MessagePack to 3.1.7 to address security advisories (#23113)
Update MessagePack dependency to 3.1.7.
2026-06-15 06:37:13 +02:00
Kenn JacobsenandGitHub 6883c6fcfd Tags: Expand ITagService to handle Elements (#23117) 2026-06-15 06:28:52 +02:00
Andy Butland e8586493e2 Merge branch 'v17/dev' 2026-06-13 15:42:41 +02:00
d5d0ce68aa Property editors: Add configuration and validation of allowed types for element picker, document picker, media picker (#23026)
* add allowed type for element picker

* update validation and unit test

* remove redundant code

* update tests name

* revert code GetReferences

* remove un-using code and add more check value

* remove redundant param

* add allowed type for content picker, update validation

* add min max validation into element and its unit test

* update media picker validation

* split validation runner into other class

* update SystemTextJsonSerializerBase back to old code

* update unit tests

* Resolved some code warnings.

* Remove accidentally committed file

* Introduce ITypedValidator and obsolete ITypeJsonValidator to better reflect validators that may or may not contain JSON editor values.

* Extraced ParseAllowedContentTypeKeys into a common helper.

* Aligned parameters on AllowedTypeValidator.

* Align parsing of allowed type Ids on client between document and media pickers.

* Restored validation of media where provided key can't be retrieved.

* Aligned document, media and element configuration labels and weights.

* Added additional unit tests.

* Addressed code review comments.

* Resolved further code warnings and code tidy.

* Resolve potential binary breaking change concern with obsolete ITypedJsonValidator.

* Reuse shared DocumentTypePicker for picker allowed-types config.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-06-13 13:30:34 +00:00
de61491093 Tests: Update DomainCacheServiceTests to mock GetAllAsync instead of removed GetAll (#23097)
update unit tests for domain cache service

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2026-06-10 12:14:01 +02:00
Jacob Overgaard deafc20db9 Merge remote-tracking branch 'origin/v17/dev' 2026-06-09 13:20:53 +02:00
6d1487ef4e Global Search: Localize global search manifest labels and names (#23091)
* add localization for search manifest

* only localization for label

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2026-06-09 08:35:15 +00:00
Jacob Overgaard 46aa184e10 Merge remote-tracking branch 'origin/v17/dev' 2026-06-09 10:31:49 +02:00
Jacob Overgaard c1ba303fdc Merge remote-tracking branch 'origin/v17/dev' 2026-06-09 10:30:22 +02:00
Andy ButlandandGitHub 5dd28e7a13 Tests: Fix failing Management API integration tests (closes #23076) (#23081)
* Include currently missing management API tests in the CI build.

* Fixed failing tests.

* Revert the pipeline updates.

* Addressed code review feedback.
2026-06-07 08:40:23 +02:00
Niels LyngsøandGitHub 87b24912c7 V18: Beta UI adjustments (#22869) 2026-06-05 15:10:54 +02:00
a3424eb40d Dependencies: Upgrade Examine to 3.8.0 (#23075)
Upgrade Examine to 3.8.0

Co-authored-by: Simon Gibbs <sgibbs@qmu.ac.uk>
2026-06-05 06:58:20 +02:00
Andy Butland f6c70e8429 Bump version to 18.0.0-rc3. 2026-06-05 06:46:44 +02:00
Andy Butland 68d03ae7c4 Merge branch 'release/18.0' 2026-06-05 06:42:24 +02:00
Laura NetoandGitHub 1dbcf1037a Delivery API - Open API: return inline {} schema for unconstrained property types (#23066)
* Delivery API: return inline {} schema for unconstrained property types

ContentTypeSchemaTransformer now checks the raw STJ schema via JsonSchemaExporter before
calling GetOrCreateSchemaAsync. STJ generates boolean true for unconstrained types (JsonNode,
object, types with custom converters), which the pipeline converts to {}. When the raw schema
is true, an inline {} is returned without registering a named component - a named component
adds no value and misleads API consumers into thinking a concrete model shape exists.

* Delivery API: add Plain JSON property to contract test sample types

Adds a Plain JSON property to the sample article page content type used by the OpenAPI contract
tests. This exercises the unconstrained-type fix: the property should appear as inline {} in the
schema, not as a named JsonNode component. Updates the expected contract to reflect the new
property.

* Re-generate typed-schemas-with-sample-types.json

For some reason the previous change got formatted differently, so it was displaying more changes than it should.

* Simplify comments

* Delivery API: guard unconstrained type check with JsonTypeInfoKind.None
2026-06-04 17:01:49 +02:00
Andy ButlandandGitHub 27909ed18e Elements: Hide element actions from the document notifications dialog (closes #23053) (#23059)
Remove element actions from the notification dialog.
2026-06-04 14:57:32 +02:00
Jacob Overgaard 91c9b79926 Merge remote-tracking branch 'origin/v17/dev' 2026-06-04 10:41:08 +02:00
Erik-Jan WestendorpandGitHub 5ade6ae6ec Localization: Add Dutch translations for create and delete actions (#23039)
Update nl.ts
2026-06-04 08:37:12 +02:00
Andy Butland 7ca8d3d872 Merge branch 'v17/dev' 2026-06-04 07:59:53 +02:00
Andy Butland fceb2f421f Merge branch 'v17/dev' 2026-06-04 06:45:50 +02:00
Andy Butland 54827c4c92 Background Jobs: Resolve server role so recurring jobs run when no application URL is configured (#23033)
Resolve server role when no application URL is configured.
2026-06-04 06:44:51 +02:00
Andreas ZerbstandGitHub 245bc336a5 E2E: QA: Add acceptance test for backoffice search (#22730)
* Added tests

* Cleaned up

* Updated command

* Fixes based on comments

* Split tests

* Updated helpers

* Fixed constant helper after merge

* Use correct helper

* Cleaned up

* Update smokeTest command in package.json
2026-06-03 19:49:12 +00:00
Andy Butland c5efd65e23 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-06-03 18:49:28 +02:00
Andy Butland cb9ac904f6 Merge branch 'v17/dev' 2026-06-03 18:49:12 +02:00
Laura NetoandGitHub cc38e5724b SonarCloud: Simplify to single workflow, skip fork PRs (#23054) 2026-06-03 18:37:07 +02:00
Jacob Overgaard 7158aec145 Merge branch 'release/18.0' 2026-06-03 13:06:55 +02:00
Laura NetoandGitHub 214fd03241 OpenAPI: Disable XML documentation source generator (closes #23018) (#23045)
Disable OpenAPI XML documentation source generator

Due to the amount of XML documentation in this solution, the OpenAPI XML documentation source generator produces too many lines of code in a single method (GenerateCacheEntries), which causes a StackOverflowException when running on IIS. The fix disables the analyzer globally via Directory.Build.props.
2026-06-03 12:28:51 +02:00
a3f65b2920 Redirects: Adding notification for redirect save and deletion (#22985)
* Creating notification objects and status

* Adjusting service and tracker to support cancelable notifications

* Adding Operation Status Results to base controller.

* Adding notification support to the delete controller.

* Integration tests

* Changes in accordance to CR

* Changes in accordance to code review

* Fixed further use of obsolete methods in tests.

* Added comment explaining why messages on create or update cancellation are suppressed.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-06-03 09:37:31 +00:00
Jacob OvergaardandGitHub 787f66be3d Dependencies: Bumps @umbraco-ui/uui from 2.0.0-rc.1 to 2.0.0-rc.2 (#23052)
build(deps): bumps @umbraco-ui/uui from 2.0.0-rc.1 to 2.0.0-rc.2
2026-06-03 09:00:01 +00:00
Erik-Jan WestendorpandAndy Butland cd23fc75c5 Localisation: Translate the "Library" section header into other languages (#23043)
* Translate library to Dutch and Spanish

* Add translations for other cultures.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-06-03 09:36:43 +02:00
230b5db528 Localisation: Translate the "Library" section header into other languages (#23043)
* Translate library to Dutch and Spanish

* Add translations for other cultures.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-06-03 07:35:06 +00:00
Nhu DinhandGitHub 2372c40056 E2E: QA Added acceptance tests for element folder permission (#22905)
* Added constant setting for element folder permission

* Added api helper for element folder

* Added api helper for user group with element folder permission

* Added ui helper for element folder permission in user group

* Added tests for element folder permission

* Added locator for restore element

* Added api helper for Combined element + element folder permission methods

* Added more tests for restore element folder

* Make tests run in the pipeline

* Fixed comments

* Reverted npm command
2026-06-03 03:19:02 +00:00
75d2b0129d E2E: QA Added acceptance tests for Element Type settings (#23005)
* Updated createEmptyElementType

* Updated tests due to test helper changes

* Added ui helper for not applicable message for element type

* Added tests for showing message for non-applicable Element Type settings

* Added tests for preventing disabling isElement when elements of that type exist

* Make tests run in the pipeline

* Fixed comments

* Update tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeSettingsTab.spec.ts

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

* Reverted npm command

---------

Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
2026-06-03 03:13:52 +00:00
Jacob Overgaard b410e060b6 Merge remote-tracking branch 'origin/v17/dev' 2026-06-02 16:32:14 +02:00
Laura NetoandGitHub a86777a8f2 Build: Add SonarCloud CI workflow (#22960)
* Add SonarCloud CI workflow

Adds a manual-dispatch GitHub Actions workflow for SonarQube Cloud
analysis (build, unit test coverage, scan). Moves file_header_template
and SA1636/SA1633 suppression from .editorconfig comments and
.globalconfig into the active .editorconfig .NET language conventions
section, removing the duplicated suppression from .globalconfig.

* Remove branch filter from pull_request trigger in SonarCloud workflow

Runs analysis on all PRs regardless of target branch.

* Adjust sonarcloud gh action based on feedback

* Add .sonarqube to .gitignore

* Attempt to split build and analysis in order to be able to run in PRs from forks

* Adjust SonarCloud workflows

* Rename SonarCloud workflows to reflect their actual purpose

* Remove sonar.coverage.exclusions

* Include .github in sonar analysis

* Include build directory in sonar analysis

* Apply sonarcloud workflow fixes from test branch

* Remove setup-dotnet step from upload workflow

* Use default branch from context instead of hardcoded main in analysis workflow

* Update checkout action to v6 in upload workflow

* Add actions: read permission to upload workflow

* Enable SCM integration in upload workflow
2026-06-02 14:49:10 +02:00
Jacob Overgaard 4e815d7e9d Merge remote-tracking branch 'origin/v17/dev' 2026-06-02 12:46:33 +02:00
Nhu DinhandGitHub 318843793f E2E: QA Updated acceptance tests for removing the Element Picker from elements to reflect the recent changes (#23037)
* Added comments for the out-of-date tests

* Fixed element with element picker tests due to response changes
2026-06-02 10:20:40 +00:00
Andy Butland c10e23fd92 Merge branch 'v17/dev' 2026-06-01 14:41:53 +02:00
Sven GeusensandAndy Butland 0adb5d21f5 Developer Experience: Improve cohost editor polyfill to work with dotnet watch (closes #22773) (#22999)
* Improve cohost polyfill

* Apply suggestions from code review

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

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

---------

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

* Cleaned up

* Updated command

* Fixes based on comments

* Split tests

* Updated helpers

* Fixed constant helper after merge

* Use correct helper

* Added constant for element search

* Added ui helper for element backoffice search

* Added tests for element backoffice search

* Updated tests for finding element by name

* Apply suggestion from @andr317c

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

* Cleaned up

* Reverted npm command

* Fixed npm command

---------

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:59:42 +02:00
Andy ButlandandGitHub 446a3795b7 Elements: Clear user and user group start node references when deleting an element container (closes #23010) (#23011)
* Clear user and user group start nodes when deleting an element container.

* Assert user start node references cleared after container delete

Mirrors the post-delete assertion already present in the user group
sibling test so both tests confirm the reference was cleaned up, not
just that no FK exception was thrown.

* Guard against null entity in PersistDeletedItem override

Mirrors the ArgumentNullException guard in the base
EntityContainerRepository.PersistDeletedItem so a null argument throws
the same exception type.
2026-06-01 10:36:11 +02:00
Niels LyngsøandGitHub 0d73243b7c Property Editors: Add value summary extensions for collection views (#23027)
Squashed commit of the following:

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

    Delete package-lock.json

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

    Simplify content-picker resolved item shape

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

    content picker value summary add tests & observable support

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

    Inline markdown editor value-type constant

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

    Remove unused import

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

    Enable table text clipping; remove value-summary styles

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

    Add user-picker value-summary tests and mock

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

    Add tests for member-group value resolver

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

    Add member picker value-summary resolver tests

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

    Add media picker value summary resolver tests

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

    Update value-summary.resolver.test.ts

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

    add tests for the document picker value summary resolver

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

    Use check icon for non-empty value summaries

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

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

    align member picker

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

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

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

    Add value-summary manifests to packages

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

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

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

    Update imports

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

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

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

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

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

    Use item models for resolvers instead of raw string IDs

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

    Update the visual of tags and block list

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

    Render all the values in the checkbox list

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

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

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

    Remove undefined from UmbValueTypeMap declarations

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

    Add value summary to element picker

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

    Guard against raw string value in member group picker value summary

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

    Export value type constants from package indexes

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

    Fix imports

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

    Use relative repository imports in pickers

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

    Use DOM parsing for RTE value summary

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

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

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

    Truncate the fallback element

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

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

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

    Add checkbox-list value summary; truncate labels

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

    Add valueSummary components for editors

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

    Add value summaries for user and member-group pickers

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

    Add value summaries for content/member/document picker

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

    Add value summaries for media picker and cropper

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

    Add value-summary support for several editors

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

    Add value summary for multiple text string and tags

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

    Add value summary for Toggle and date/time editors
2026-06-01 10:00:37 +02:00
Andy ButlandandGitHub 3f0e0747fa Management API: Declare multipart/form-data on the Create Temporary File endpoint (closes #23017) (#23025)
Add explicit Consumes to management API endpoint that accepts IFormFile.
2026-06-01 09:32:48 +02:00
Andy Butland 84ad9a1443 Merge branch 'v17/dev' 2026-06-01 08:33:13 +02:00
Nhu DinhandGitHub 346a22aeca E2E: QA Added acceptance tests for audit log in element (#22972)
* Added constant variables for element audit trail message

* Added ui helper for history item of element

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

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

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

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

* Potential fix for pull request finding

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 16:57:06 +01:00
Andy Butland 70258066d4 Merge branch 'v17/dev' 2026-05-28 10:46:31 +02:00
Andy Butland 5cfbfe7cc3 Merge branch 'v17/dev' 2026-05-28 10:44:20 +02:00
Andy Butland 4b9c0eb667 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-05-28 09:56:39 +02:00
Laura Neto f3471e961f Bump version to 18.0.0-rc2 2026-05-28 09:56:34 +02:00
2581e3fdbc Code Quality: Fix CS0618 obsolete API warnings in Umbraco.Examine.Lucene project (#22978)
* Suppress CS0618 obsolete API warnings in Umbraco.Examine.Lucene

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

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

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

Fixes #15015

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Related to #21152, builds on #22995.

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

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

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

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

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

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

---------

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

* Extract theme aliases into constants file

---------

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

* keep umbraco-package.ts and embed manifests instead

* Inline package manifests into umbraco-package

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

Replace numerous relative/internal import paths with centralized '@umbraco-cms/backoffice' package entry points across core modules.This consolidates exports, simplifies import paths.
2026-05-27 14:31:29 +02:00
Jacob Overgaard 5a5902e1d4 Merge remote-tracking branch 'origin/v17/dev' 2026-05-27 14:28:44 +02:00
Engiber Lozadaandleekelleher 9cf3a7e296 Content Editor: Fix workspace footer breadcrumb overflow hiding (closes #20132) (#22323)
* Allow the breadcrumbs to collapse in the workspace view

* Remove redundant styles

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

* Remove redundant styles
2026-05-27 10:52:49 +00:00
Jacob Overgaard c2416429b7 Merge remote-tracking branch 'origin/v17/dev' 2026-05-27 09:45:15 +02:00
Andreas Zerbst 2fa7067803 Fixed required value 2026-05-27 09:27:13 +02:00
mole b0a825e6c0 Fix test filters 2026-05-26 12:31:44 +02:00
mole fc261d1ce4 Fix intergration tests 2026-05-26 09:40:12 +02:00
Jacob Overgaard 31944675c0 Merge remote-tracking branch 'origin/v17/dev' 2026-05-26 08:33:28 +02:00
Andy Butland 5a28f6e0f1 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-05-26 06:33:52 +02:00
Zeegaan 6e2ba699ee Merge remote-tracking branch 'origin/v17/dev' 2026-05-26 12:22:28 +09:00
Andy Butland c2b6210137 Merge branch 'v17/dev' 2026-05-25 10:21:31 +02:00
Andy Butland f7a45dc9d6 Merge branch 'v17/dev' 2026-05-25 10:13:58 +02:00
9cd2e6ecd2 Management API: ensure the order from the search endpoints taking a collection of keys is preserved (#22920)
* update order search result for element, member type, dictionary...

* undo dictionary search API

* reorder search value

* Apply OrderByRequestedIds

* add unit tests for search order

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

---------

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

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

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

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

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

* Clarify XML docs.

* Introduce helper for cancellation source rotate and cancel.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-25 08:02:14 +02:00
Jacob Overgaard d2e32d6fcb Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-05-23 10:11:21 +02:00
Jacob Overgaard bec333e37b Merge remote-tracking branch 'origin/v17/dev' 2026-05-23 10:11:10 +02:00
Andreas Lykke BorgandGitHub 4344fe9060 Accessibility: Added missing labels to webhook details and headers (#22918)
* Added missing labels to webhoot details and headers

* Changed toggle label to aria-label to remove visible text
2026-05-22 19:02:10 +02:00
Jacob Overgaard 485257a949 Merge branch 'v17/dev' 2026-05-22 11:23:07 +02:00
Jacob OvergaardandClaude Opus 4.7 cef6a467eb Workspace Actions: Restore waiting state for buttons with additional options (closes #18670, #20593) (#22554)
* Workspace Actions: Restore waiting state for buttons with additional options

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

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

Fixes #22551

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

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

Changes:

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

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

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

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

Fixes #22551

* Reduce cyclomatic complexity of #onClick and _handleSave

CodeScene Code Health Review flagged two complexity issues:

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

No behavioural change.

* Reduce cyclomatic complexity of #handleSaveAndPublish

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

No behavioural change.

* DRY: extract notifyWorkspaceActionStarting into a shared utility

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

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

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

No behavioural change.

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

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

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

Pure rename; no behavioural change.

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

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

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

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

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

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

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

Three follow-ups on top of c15eb2d0bc:

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

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

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

Code-review cleanup applied on the same pass:

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

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

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

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

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

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

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

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

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

---------

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

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

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

Fixes #22551

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

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

Changes:

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

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

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

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

Fixes #22551

* Reduce cyclomatic complexity of #onClick and _handleSave

CodeScene Code Health Review flagged two complexity issues:

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

No behavioural change.

* Reduce cyclomatic complexity of #handleSaveAndPublish

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

No behavioural change.

* DRY: extract notifyWorkspaceActionStarting into a shared utility

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

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

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

No behavioural change.

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

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

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

Pure rename; no behavioural change.

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

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

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

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

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

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

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

Three follow-ups on top of c15eb2d0bc:

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

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

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

Code-review cleanup applied on the same pass:

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:39:06 +02:00
Jacob Overgaard 767fafe06d Merge remote-tracking branch 'origin/v17/dev' 2026-05-22 09:38:31 +02:00
Andy Butland 216fee987b Added a TODO for a future major. 2026-05-22 06:44:36 +02:00
Andy Butland 36ea0a494d Bump version to 18.0.0-rc1. 2026-05-21 19:45:51 +02:00
Andy Butland ca6a32088a Merge branch 'v17/dev' 2026-05-21 19:41:41 +02:00
Andy ButlandandClaude Opus 4.6 d6f6e31c68 Background Jobs: Rewrite RecurringHostedServiceBase with SemaphoreSlim and add signalling support (#22331)
* Compute next delay to compensate for time drift

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

* Add RecurringBackgroundJobBase to contain default values and hide obsoleted method

* Add NextExecutionStrategy parameter to adjust the schedule after triggered executions

* Add TriggerExecution methods to RecurringBackgroundJobHostedServiceRunner

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

* Handle cancellation (application shutdown) and publish RecurringBackgroundJobCanceledNotification

* Match hosted services by Type instead of type name string

* Extract shared helper for TriggerExecution tests

* Clear trigger state when initial delay is interrupted

* Clear _nextExecutionSkipOnOvershoot unconditionally

* Combine ComputeNextDelay tests

* Consolidate trigger state into an immutable record for thread safety

* Use ConcurrentDictionary for thread-safe hosted service lookup

* Remove hosted services from dictionary on stop

* Fix API compatibility errors

* Removed unneeded using.

* Register RecurringBackgroundJobHostedServiceRunner as resolvable singleton

* Remove failed hosted service from dictionary when StartAsync throws

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

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

* Inject TimeProvider into RecurringHostedServiceBase for deterministic testing

Fix timeprovider

* Use DelayCalculator.GetDelay instead of RecurringHostedServiceBase.GetDelay

* Fix Exception_In_PerformExecuteAsync_Does_Not_Kill_Loop test

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

* Configure IEventMessagesFactory mock to return real EventMessages

* Clarify TriggerExecution(TimeSpan) docs and add ChangePeriod test

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

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

* Ensure PeriodChanged event is unsubscribed again

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

Fix trigger state

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

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

* Replace Task.Yield with semaphore timeouts in negative assertions

* Tidy RecurringBackgroundJobBase docs and runner error handling

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

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

* Register IRecurringBackgroundJobTrigger as open generic and drop AddTriggerableRecurringBackgroundJob

* Fix and add parameter validation

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

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

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

* Handle edge case of backoff via InfiniteTimeSpan.

* Refactored large method.

* Added clarifying documentation.

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

* Relocate Suppress ExecutionContext flow to avoid package validation error.

* Align IRecurringBackgroundJobTrigger generic type constraint with AddRecurringBackgroundJob

* Rename ApplyTriggerState to ComputeNextDelayFromTriggerState

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

* Fix generic type constraint

---------

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

* Disconnect layout resize observer on destroy

* Initialize ResizeObserver and simplify cleanup

* Remove optional chaining on layout observer disconnect
2026-05-21 18:13:10 +02:00
Andy Butland 0f357f595e Merge branch 'v17/dev' 2026-05-21 18:01:17 +02:00
Andy ButlandandLan Nguyen Thuy 3523fbbed2 Reset password: Add inline validation messaging for password pattern requirements (#22880)
* add custom validation for password input in reset password

* update remove invalid listeners in disconnectedCallback

---------

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

* update remove invalid listeners in disconnectedCallback

---------

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

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

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

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

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

* add js docs

* remove duplicated fallback logic

* wip unit tests of requestItemName method

* Use DocumentVariantStateModel in mock documents to fix compiler

* Update input-entity-data.context.ts

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

(cherry picked from commit c74a58246f)
2026-05-21 09:27:42 +01:00
nikolajlauridsen 60948d197a Merge branch 'v17/dev'
# Conflicts:
#	src/Umbraco.Core/Services/DocumentUrlService.cs
2026-05-21 10:15:13 +02:00
nikolajlauridsen 62048dc5a8 Merge branch 'release/17.4.2' into release/18.0
# Conflicts:
#	src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs
#	src/Umbraco.Core/Services/DocumentUrlService.cs
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	src/Umbraco.Web.UI.Client/src/external/uui/package.json
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
#	tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/PublishedContent/PublishedValueFallbackTests.cs
#	version.json
2026-05-21 09:25:44 +02:00
Andy Butland 337cd32258 Merge branch 'v17/dev' 2026-05-21 07:45:20 +02:00
Jacob OvergaardandClaude Opus 4.7 247935cc38 Tests: Fix unit tests broken by sync element fast-path and lazy property materialization
- ElementPickerValueConverterTests: also stub the new synchronous IPublishedElementCache.GetById,
  since Moq does not execute default interface implementations.
- PropertyCacheLevelTests.CacheUnknownTest: access a property inside Assert.Throws to trigger the
  now-lazy property wrapper materialization.

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

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

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

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

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

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

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

* Extension template: Tighten whoAmI 204 guard in dashboard

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

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

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

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

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

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

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

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

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

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

* update mock data and tests to include real compositions

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-05-20 10:53:46 +02:00
Andy Butland 09af8c044b Merge branch 'v17/dev' 2026-05-20 10:32:08 +02:00
Andy Butland 1dc47bf4a1 Merge branch 'release/18.0' 2026-05-19 18:44:42 +02:00
Andy Butland 5f26c16c8e Add synchronous fast path for retrieval of cached elements (aligning documents and media from the previous cherry-pick from 17). 2026-05-19 18:43:27 +02:00
Andy Butland 896f449343 Children/Descendants: improve traversal performance (closes #22646) (#22742)
* Add benchmark test for measuring improvements to children and descendant retrieval.

* Remove unnecessary sort from retrieval of children.

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

* Lazily build property wrappers when materializing IPublishedContent.

* Cache the ordered children list on NavigationNode.

* Cache descendants per parent on the navigation snapshot.

* Add synchronous fast path for retrieved of cached content.

* Additional unit tests.

* Add TODO to make UpdateSortOrder internal.

* Addressed code review feedback.

* Further unit tests.

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

* remove comment

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

* Update Comment

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

* mergeObservables approach

---------

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

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

Closes #22808.

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

Changes:

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

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

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

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

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

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

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

* Simplify: split setActiveLanguage from notifyLanguageChanged

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

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

* Restore deprecated UmbLocalizationManager.updateAll for backward compat

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

* Fix deprecation removal version to v20 + correct baseLocaleOf JSDoc

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

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

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

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

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

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

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

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

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

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

* Drop deprecated UmbLocalizationManager.updateAll

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

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

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

* Collapse setActiveLanguage + notifyLanguageChanged into one method

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

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

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

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

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

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

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

Closes #22808.

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

Changes:

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

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

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

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

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

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

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

* Simplify: split setActiveLanguage from notifyLanguageChanged

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

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

* Restore deprecated UmbLocalizationManager.updateAll for backward compat

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

* Fix deprecation removal version to v20 + correct baseLocaleOf JSDoc

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

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

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

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

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

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

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

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

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

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

* Drop deprecated UmbLocalizationManager.updateAll

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

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

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

* Collapse setActiveLanguage + notifyLanguageChanged into one method

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

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

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

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

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

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

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

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

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

* Remove UdiEntityTypeHelperTests

---------

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

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

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

* Add missing case for MemberTypeContainer.

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

---------

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:36:27 +02:00
Sebastiaan Janssen 136c494d82 Core: Preserve path case in ShadowFileSystem (#22838)
* Core: Preserve path case in ShadowFileSystem

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

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

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

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

* Rename regression test to follow Can_ naming convention

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

* Track canonical staged path per shadow node

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

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

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

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

* Make ShadowNode.CanonicalPath non-nullable

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
(cherry picked from commit abfa8cb144)
2026-05-14 10:42:07 +02:00
Andy Butland 60655da1c2 Bump version to 18.0.0-beta3. 2026-05-14 08:14:25 +02:00
Andy Butland 4b82828a23 Merge branch 'release/18.0' 2026-05-14 08:11:39 +02:00
Jacob OvergaardandGitHub 756510d9e7 Backoffice: Fix typedoc UI API docs generation (#22836)
The Generate API Docs CI step (npm run generate:ui-api-docs) has been
failing with 3284 TypeScript errors since the TS 5.9.3 -> 6.0.3 bump in
PR #22591. TS 6 stopped auto-loading @types/* under moduleResolution:
"bundler", so every .test.ts file in the program fails to find describe,
it, beforeEach, etc., and typedoc aborts before emitting anything.

Point typedoc at a dedicated tsconfig.typedoc.json that narrows include
to src/**/*.ts + index.ts and excludes *.test.ts and *.stories.ts.
Entry points come from package.json exports and all live under src/, so
the docs build no longer drags test files, stories, mocks, e2e specs,
or storybook stories through the TS program.

Verified locally: npm run generate:ui-api-docs exits 0 and writes
6533 files under src/Umbraco.Web.UI.Client/ui-api/.
2026-05-14 06:43:22 +02:00
Jacob OvergaardandAndy Butland 2954578386 Backoffice: Preserve prerelease tag when hoisting peer dependencies (#22841)
The publish cleanse step strips the prerelease suffix from hoisted dependency
ranges via `semver.minVersion(...).major/minor/patch`. For `^2.0.0-rc.1`
this produced `^2.0.0`, which no published `@umbraco-ui/uui` version
currently satisfies, breaking extension installs against
`@umbraco-cms/backoffice@18.0.0-beta1`+.

Use the full SemVer (including any prerelease) as the floor so
`^2.0.0-rc.1` stays satisfiable by the actual published rc.
2026-05-13 22:52:51 +02:00
Jacob OvergaardandGitHub 3e7c1fa8e4 Backoffice: Preserve prerelease tag when hoisting peer dependencies (#22841)
The publish cleanse step strips the prerelease suffix from hoisted dependency
ranges via `semver.minVersion(...).major/minor/patch`. For `^2.0.0-rc.1`
this produced `^2.0.0`, which no published `@umbraco-ui/uui` version
currently satisfies, breaking extension installs against
`@umbraco-cms/backoffice@18.0.0-beta1`+.

Use the full SemVer (including any prerelease) as the floor so
`^2.0.0-rc.1` stays satisfiable by the actual published rc.
2026-05-13 22:51:55 +02:00
Andy Butland 7b579cd021 Merge branch 'v17/dev' 2026-05-13 15:45:47 +02:00
Ronald BarendseandAndy Butland 6fef118a8f SignalR: Mark ServerEventSender as a distributed cache notification handler (#22818)
* Mark ServerEventSender as distributed cache notification handler

* Batch and deduplicate notifications in ServerEventSender

* Introduce IDistributedCacheAsyncNotificationHandler<T> and use it in ServerEventSender

* Add ServerEventSender unit tests and address PR review feedback
2026-05-13 14:44:30 +02:00
Jacob Overgaard 1edd6ec0bc Merge branch 'release/18.0' 2026-05-13 13:44:17 +02:00
Laura Neto c4a3b95195 Bump version to 18.0.0-beta2 2026-05-13 13:16:59 +02:00
d2d0d6d2d8 System information: Adds __uuiVersions to sysinfo output (#22831)
* feat: adds `__uuiVersions` to system information output

* avoid printet the array of version by handle single or multiple versions

* feat: ensures type safety of global variable

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-13 12:07:17 +02:00
f245bc00d0 UI: UI Library adjustments for v18 (#22824)
* transfer style to uui v2

* accordingly interactive state for document-links

* overflow clip for border radius appearance

* link style

* fix block grid area configuration

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-13 10:04:18 +00:00
Jacob OvergaardandGitHub c385991ffb build(deps): bumps @umbraco-ui/uui from 2.0.0-alpha.1 to 2.0.0-rc.0 (#22827) 2026-05-13 09:59:46 +00:00
Jacob Overgaard 14886a9425 build: updates acceptance test lockfile 2026-05-13 09:56:34 +02:00
Nhu DinhandGitHub 79aadd827a Build: Updated nightly E2E test pipeline schedule in v18 (#22802)
Update nightly e2e test pipeline
2026-05-13 06:33:02 +00:00
Nhu DinhandGitHub 4bb5864e20 E2E: QA Updated acceptance tests to match the recent changes (#22801)
* Updated locator for user group table

* Updated json builder for user groups permission due to element folder permission

* Updated api helper to match with element folder permission

* Updated tests and add comments for the failing tests
2026-05-13 12:15:43 +07:00
Ronald Barendseandleekelleher af03e1d30a User Permission: Re-export fallback condition config type and global augmentation (#22794)
(cherry picked from commit 55fec1dc2a)
2026-05-12 17:15:56 +01:00
Andy Butlandandleekelleher 50727c4bb8 Sort Children: Show loading state on Sort button (closes #22651) (#22813)
* Add submit button state to sort dialog.

* Guard against re-entrant submit in sort-children-of modal.

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

* Set failed button state when sort-children-of submit throws.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit dfe93c5639)
2026-05-12 17:07:59 +01:00
Andy ButlandandGitHub def5be0855 18.0-beta1: Recycle Bin: Fix empty action button in collection view (closes #22798) (#22811)
Fix empty action button in collection view.
2026-05-12 16:31:07 +01:00
Niels LyngsøandGitHub 0a3647e4f0 v18 login photo (#22814) 2026-05-12 16:04:48 +02:00
Kenn Jacobsen 3daf7275ff Content: Ensure correct variant change tracking when unpublishing variant content (#22799)
(cherry picked from commit 6766eb9411)
2026-05-12 13:30:04 +02:00
kjac 2a2252474f Merge remote-tracking branch 'origin/main' 2026-05-12 12:32:07 +02:00
Jacob Overgaard 9c15572a49 fix: reinstates ./element export 2026-05-12 12:13:39 +02:00
Jacob Overgaard 045db8d699 fix: reinstate ./library export 2026-05-12 12:13:05 +02:00
kjac ecd29d79ff Merge remote-tracking branch 'origin/main' 2026-05-12 11:56:45 +02:00
Jacob Overgaard 60104e2a0e chore: regenerates tsconfig.json 2026-05-12 11:02:23 +02:00
Jacob Overgaard 2d747c0b43 chore: set version back to 18.1.0 2026-05-12 10:43:05 +02:00
Kenn Jacobsen 6766eb9411 Content: Ensure correct variant change tracking when unpublishing variant content (#22799) 2026-05-12 10:21:41 +02:00
Jacob Overgaard c9c4704e1a fix: exports condition configs and fixes test imports 2026-05-12 10:12:46 +02:00
Jacob Overgaard 0b0fea04d9 chore: sets version in backoffice client and regen packagel ock 2026-05-12 10:08:01 +02:00
Laura NetoandGitHub f007855696 Open API: Add fluent builder for registering custom backoffice OpenAPI documents (#22774)
* Add helper for registering custom backoffice OpenAPI documents

Bundles AddOpenApi, the [MapToApi]-aware ShouldInclude predicate, the
Umbraco schema reference ID convention, and AddOpenApiDocumentToUi
behind a single IUmbracoBuilder.AddBackOfficeOpenApiDocument call.
Authors pass documentName, an optional title (used both as Info.Title
and the UI dropdown label), and an optional configure callback that
runs last so it can override anything the helper sets. An optional
jsonOptionsName is forwarded to ReplaceOpenApiSchemaService for
documents that need schema-time JSON serialization aligned to a named
JsonOptions.

Schema reference ID logic moves out of ConfigureUmbracoOpenApiOptionsBase
into UmbracoSchemaIdGenerator.CreateSchemaReferenceId so both the new
helper and the base class share one source of truth. The extension
template's composer collapses to a single AddBackOfficeOpenApiDocument
call, with document Info.Version, backoffice security, and the operation
ID transformer staying in the configure callback.

* Refactor backoffice OpenAPI helper into a fluent builder

Replace the parameter-list AddBackOfficeOpenApiDocument helper with a
callback-based form that yields a BackOfficeOpenApiDocumentBuilder. The
builder owns its state and applies it to the IUmbracoBuilder once the
user callback returns, so authors don't need to remember a terminal
Build call. Extension methods can layer on (e.g.
WithBackOfficeAuthentication in Umbraco.Cms.Api.Management) without the
core helper carrying every opinion.

Defaults stay sensible: filtering by [MapToApi(documentName)], the
Umbraco schema reference IDs, and the tag/sort transformers that v17's
global Swashbuckle pipeline applied. UI dropdown registration is
opt-out via ExcludeFromUi rather than opt-in. JSON options for schema
generation are an opt-in via WithHttpJsonOptions (instance or factory),
described purely in terms of the schema effect.

Move UmbracoSchemaIdGenerator's CreateSchemaReferenceId wrapper out of
ConfigureUmbracoOpenApiOptionsBase so both the base config class and
the new builder share one source of truth, and update the
ContentTypeSchemaTransformer / unit test callsites accordingly. Refresh
the extension template to use the new shape.

* Rename WithHttpJsonOptions to WithJsonOptions

The Http qualifier was naming the .NET type rather than the intent.
The parameter type carries the disambiguation; the method name is now
intent-focused and the XML doc explains the use case (matching the
serialization conventions of the API endpoints the document describes).

* Add WithJsonOptions(string) overload for named HTTP JsonOptions

Convenience overload that accepts the registered name and resolves the
matching Microsoft.AspNetCore.Http.Json.JsonOptions via IOptionsMonitor.
Documents on all three WithJsonOptions overloads now explicitly name
the HTTP JsonOptions type so consumers know which framework type they
are configuring.

* Migrate Management API OpenAPI registration to AddBackOfficeOpenApiDocument

Replaces the AddUmbracoOpenApiDocument<ConfigureUmbracoManagementApiOpenApiOptions>
call with the new fluent builder. The custom config class becomes dead
code and is deleted; all per-document opinions (Info metadata, security
requirements, transformers, JSON options) move into the configuration
callback alongside the document registration.

Behavior preserved: same ShouldInclude (now via [MapToApi]-only since
all Management controllers carry the attribute through their base class),
same schema reference IDs, same operation IDs via UmbracoOperationIdTransformer,
same backoffice security requirements, same schema/operation transformers,
same named JSON options for schema generation.

* Cleanup unused usings

* Address PR review feedback on AddBackOfficeOpenApiDocument

Make UmbracoOperationIdTransformer part of the builder's defaults instead of
the Management API adding it explicitly, and expand the XML docs on
AddBackOfficeOpenApiDocument to spell out the defaults a caller opts into.

Add tests covering the new builder and its defaults:
- Unit tests for BackOfficeOpenApiDocumentBuilder defaults (CreateSchemaReferenceId,
  ShouldInclude, ConfigureOpenApiOptions composition, WithTitle/WithUiTitle UI
  dropdown handling, ExcludeFromUi).
- Integration tests that register sample controllers, fetch the generated OpenAPI
  document and verify the defaults end-to-end: Info.Title from WithTitle,
  MapToApi filtering, Umbraco operation-id and schema-id conventions (including
  the version-suffix branch), tag-by-group-name and tag-first path sorting.
- Integration tests for the three WithJsonOptions overloads (instance, factory,
  named) confirming the configured JsonOptions reach schema generation.

* Remove redundant operation-id override from extension template

UmbracoOperationIdTransformer is now part of the AddBackOfficeOpenApiDocument
defaults, so the template's custom action-name transformer would only overwrite
the work the default just did. Drop it, and consolidate the documentation
pointer to a single link.

* Narrow MimeTypesTransformer to JSON-equivalent variants and register it in AddBackOfficeOpenApiDocument

Filter only removes redundant JSON-equivalent MIME types (text/json,
application/*+json, text/plain) when application/json is present.
Non-JSON types like application/xml are preserved. Register the
transformer as a default in AddBackOfficeOpenApiDocument so custom
backoffice documents get the same treatment as Umbraco's own APIs.

* Register RequireNonNullablePropertiesSchemaTransformer in AddBackOfficeOpenApiDocument

* Apply review notes

- Drop RequireNonNullablePropertiesSchemaTransformer and MimeTypesTransformer
  from the Management API's ConfigureOpenApiOptions block — both are now
  defaults on the builder.
- Expand MimeTypesTransformer XML docs to reflect its broader role (it now
  applies to every backoffice document, not just the Management API) and
  correct the response-side inline comment.
- Move MimeTypesTransformerTests from the Delivery test folder/namespace to
  the Api.Common test folder/namespace, since the transformer is no longer
  Delivery-specific.
- Rename BackOfficeOpenApiDocumentExtensionTests to
  UmbracoBuilderOpenApiExtensionsTests so the test fixture name matches the
  concrete class under test.
2026-05-12 09:55:08 +02:00
Sven Geusensandmole 4286a361d8 Distributed background jobs: Improve gracefull shutdown behaviour (#22796)
* Dont fail silently on missing ambientscope

This makes it in line with other methods in the repo

* Pass on Cancellationtoken to the job to support gracefull job shutdown

(cherry picked from commit 5ae17ace6a)
2026-05-12 09:52:41 +02:00
mole 91313fff0e Merge remote-tracking branch 'refs/remotes/origin/v17/dev'
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
#	tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/PublishedContent/PublishedValueFallbackTests.cs
#	version.json
2026-05-12 09:50:50 +02:00
Nhu DinhandGitHub 3dca735a38 E2E: QA Added acceptance tests for current user workspace (#22457)
* Updated acceptance tests for current user profile

* Updated locator for save button

* Reverted npm command
2026-05-11 14:59:03 +00:00
Andy Butlandandleekelleher 57b9a7ef80 Tiptap RTE: Ignore no-op transactions in onUpdate to prevent phantom dirty state (closes #22767) (#22781)
Ignore Tiptap no-op transactions in onUpdate to prevent phantom dirty state.

(cherry picked from commit cd476ab6ed)
2026-05-11 14:14:21 +01:00
Andy ButlandandGitHub 65a45a4ac6 18.0-beta1: User Management: Invalidate cached element start nodes on user save and fix access summary display (closes #22770) (#22779)
* Users: Invalidate cached element start nodes on user save (closes #22770)

* Fix display of selected element folders under the user's access summary.
2026-05-11 12:40:05 +01:00
Andy ButlandandGitHub 38b6752d24 18.0-beta1: Elements: Show trashed folder ancestor names in breadcrumb (closes #22768) (#22780)
Fix breadcrumb for trashed element within a folder.
2026-05-11 12:23:43 +01:00
Jacob Overgaardandleekelleher 097b9c11f6 Login: Reuse backoffice localization (closes #20082) (#22743)
* Login: Reuse backoffice localization for canonical login_* keys (closes #56402)

The login screen no longer ships its own localization tree. The slim backoffice controller registers the backoffice's built-in localization manifests, so all login screen text resolves from the same dictionary the in-backoffice auth view uses. Translators override one place; both screens reflect it.

All consumers in the Login project moved from auth_* to login_*. The Login project's localization/ directory is removed entirely. The auth.* keys it used to ship (form labels, mfa, invite, password reset) now live under login.* in the backoffice's en/da/de/nb/nl/sv lang files. Other backoffice languages fall back to en for these keys, automatically extending the login screen's language coverage.

* Backoffice localization: drop server-only email keys, add login.setPasswordInstruction in en/da/nb/sv

bottomText, resetPasswordEmailCopySubject, resetPasswordEmailCopyFormat, mfaSecurityCodeSubject and mfaSecurityCodeBody are read only by the server's own localization layer — they were dead weight in every backoffice lang dictionary that carried them. Removed across 23 lang files.

login.setPasswordInstruction is rendered on the new-password screen via the now-canonical login_* namespace; it was missing from en (the fallback), da, nb and sv. Added there using the same translation tone as the existing de/nl entries.

* Login: Honour legacy auth_greeting* overrides with UmbDeprecation warning

Translation packages still shipping 'auth_greeting0..6' overrides keep working on both welcome screens (the standalone login page and the in-backoffice umb-auth-view): when an auth_* greeting is registered the consumer prefers it, otherwise the canonical login_* key is used. Each legacy key triggers a one-time UmbDeprecation warning pointing at the canonical name. Scheduled for removal in v20.

* Fix Prettier formatting and correct issue references in deprecation message

Addresses Copilot review feedback on PR #22743:

- Run Prettier on the 6 backoffice lang files I added keys to (en/da/de/nb/nl/sv); the new entries used double quotes which violated the repo's singleQuote: true config and would have failed the format check.
- Update the UmbDeprecation 'solution' link and the inline source comments from #56402 (an ADO work item id) to #20082 (the actual GitHub issue tracking this work).

* Drop stale login_2fa* and login_mfaSecurityCodeMessage from bs.ts and cy.ts

Surfaced by 'devops/localization/compare-languages.js': bs and cy were the only lang files shipping these keys, and they have no en counterpart. The login_2fa* set is leftover from before the codebase renamed 2fa → mfa in the login flow (the live keys are login_mfa*). login_mfaSecurityCodeMessage is server-side only, like the other email-template keys cleaned up in 53ad52702e0. None of these are referenced anywhere in src/. The user-facing user_2fa* keys (consumed by current-user-mfa modals) are unrelated and untouched.

* Drop dead login_2fa* and login_mfaSecurityCodeMessage from nl, hr, tr

Same pattern as 26bc6211d27 (bs/cy cleanup), surfaced by re-running devops/localization/compare-languages.js after the previous pass:

- nl had both legacy 'login_2fa*' AND the canonical 'login_mfa*' (added in commit 1) sitting side by side after the auth.* → login.* port. Six true duplicates dropped, login_mfa* kept.
- hr and tr shipped legacy 'login_2fa*' that have no en counterpart, no consumer in src/, and no mfa pair locally. Dropped to align with en (the source of truth — every other locale should match it).

- All three files also still carried 'login_mfaSecurityCodeMessage' from the same family of server-side email-template keys cleaned up in 53ad52702e0; removed too.

user_2fa* / member_2fa keys are unrelated and untouched (consumed by current-user-mfa modals).

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
(cherry picked from commit def18e440f)
2026-05-11 12:01:23 +01:00
Jacob Overgaardandleekelleher 30d7161399 Login: Consume sibling Umbraco.Web.UI.Client by source for v18 parity (#22591)
* Login: Consume sibling Umbraco.Web.UI.Client by source for v18 parity

The Login project previously depended on the published `@umbraco-cms/backoffice@^17.3.4` npm package for types, while at runtime the importmap served the in-repo v18 backoffice. The version mismatch forced `as any` workarounds and masked real API drift. Since v18 (with UUI 2.0) isn't on npm yet, switch Login to consume the sibling Client via a local `file:` dep so types and runtime align on v18.

Changes:
- Login `package.json`: `@umbraco-cms/backoffice` → `file:../Umbraco.Web.UI.Client`; added `pre{build,dev,watch}` hooks that run a guard script to fail fast when Client's `dist-cms/` is missing.
- Login `scripts/ensure-client-built.mjs`: new guard with a clear "build the Client first" message.
- Login `CLAUDE.md`: documents the contract and build ordering.
- StaticAssets `.csproj`: `BuildLogin` now depends on `BuildBackoffice` so MSBuild (and therefore the Azure pipeline) builds Client before Login automatically.
- Client `src/tsconfig.build.json`: `declaration: true` so `dist-cms/` ships `.d.ts`.
- Client `package.json`: new `build:types` step (`tsc --emitDeclarationOnly --incremental false && tsc-alias`) wired into `build:for:cms` after `build:workspaces`. Vite workspaces wipe their output dirs before rebuilding JS, stripping the tsc-emitted declarations; re-emitting after workspaces restores them. `tsc-alias` rewrites Client-internal path aliases (e.g. `@umbraco-cms/backoffice/external/lit`) to relative paths so sibling consumers can resolve them.
- `copy-to-cms.js`: filter `.d.ts` and `.tsbuildinfo` from the copy to `wwwroot/umbraco/backoffice` — they're only needed by sibling projects consuming `dist-cms` for types, not at runtime.
- `src/external/uui/vite.config.ts`: set `treeshake: false` so per-component `defineElement()` side-effect calls (used by UUI 2.0 for custom-element registration) are preserved in the bundle. Without this, `<uui-button>` etc. never register and the login screen renders empty controls.
- `src/external/uui/index.ts`: bare `import '@umbraco-ui/uui'` to make the side-effect intent explicit.
- Small v18-compat fixes for `Object.groupBy` (TS 8 types): removed stale `@ts-expect-error`, switched to `Object.entries` + `?? []` to satisfy the `Partial<Record>` return type.

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

* Address review feedback and fix CI

- Add `ignoreDeprecations: "6.0"` to `tsconfig.json` and the tsconfig generator to silence the TS 6.0 warning about the implicit baseUrl that TypeScript assigns when `paths` is declared. This was the CI `build` failure. The generator is also synced with the user's es2022 → es2024 bump.
- Drop the now-redundant `--declaration` flag from `build:for:npm` (tsconfig.build.json now has `declaration: true`, so the flag was duplicating intent).
- Align Login's `engines` with the Client's (`node >=24.13`, `npm >=11`) so `file:` install doesn't trip EBADENGINE.
- Guard script: hardcode the relative "../Umbraco.Web.UI.Client" path in the error message instead of interpolating the absolute path, which overflowed the ASCII box in CI logs.

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

* Login: update CLAUDE.md Node/npm versions to match engines

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

* check-path-length: skip .d.ts/.tsbuildinfo and directory paths

The 120-char Windows MAX_PATH guard protects files that actually ship to
CMS installs. `.d.ts` and `.tsbuildinfo` live in `dist-cms/` for sibling
projects to consume as types and are filtered out by `copy-to-cms.js`
before reaching `wwwroot/umbraco/backoffice` — they never land on a
Windows CMS install. Directories on their own also don't trigger
MAX_PATH; only files within them do, and those are still checked.

Unblocks CI after enabling `declaration: true` in the Client build.

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

* check-path-length: extract exceedsPathLimit helper (CodeScene)

Decomposes the complex conditional flagged by CodeScene into a named
predicate with a docstring, clarifying when a path is reported.

* Login: switch to generated tsconfig paths; revert dist-cms type machinery

PR #22591 originally aligned Login's TypeScript types with the in-repo v18
backoffice by emitting `.d.ts` into Client's `dist-cms/` and consuming it
via a `file:` dep. That layered six side-effects across the Client build
(declaration: true, build:types step, tsc-alias in postbuild, copy-to-cms
filter, check:paths skip, MSBuild ordering). Reviewers pushed back.

This rework moves the type contract from "ship .d.ts in dist-cms" to
"point Login's tsconfig paths at Client's TypeScript source" — Login's
runtime behaviour is unchanged (vite still externalises /^@umbraco-cms/,
host importmap still serves the JS), only the type-resolution mechanism
swaps.

What's reverted (back to the pre-PR shape):
- src/Umbraco.Web.UI.Client/src/tsconfig.build.json: declaration: false
- src/Umbraco.Web.UI.Client/package.json: drops `build:types` script,
  reverts `postbuild` to global-types only, drops `--declaration` from
  the tsc CLI in `build:for:cms` and restores it in `build:for:npm`
- src/Umbraco.Web.UI.Client/devops/build/copy-to-cms.js: simple cpSync
- src/Umbraco.Web.UI.Client/devops/build/check-path-length.js: original
- src/Umbraco.Web.UI.Client/tsconfig.json + devops/tsconfig/index.js:
  drops `ignoreDeprecations` (not needed once baseUrl is gone)
- src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj:
  `BuildLogin` no longer depends on `BuildBackoffice`

What's new on the Login side:
- src/Umbraco.Web.UI.Login/devops/tsconfig/index.js: generator that
  reads Client's `package.json` exports and emits a full `tsconfig.json`
  with `paths` mapping every `@umbraco-cms/backoffice/<sub>` to
  `../Umbraco.Web.UI.Client/src/.../index.ts`. Mirrors Client's existing
  generator pattern (DON'T EDIT header, JSON.stringify with tabs).
- src/Umbraco.Web.UI.Login/tsconfig.json: regenerated; standalone `tsc`
  works (no `--project` needed) and 140 path aliases resolve types
  directly from Client's source.
- src/Umbraco.Web.UI.Login/package.json: drops `@umbraco-cms/backoffice`
  npm dep entirely (file: was only nominal — types come via paths,
  runtime via importmap, transitives via Client's own `node_modules`
  which is `npm install`-ed by CI's backoffice-install.yml). Replaces
  the `ensure-client-built` guard with the generator on `pre*` hooks
  and adds `generate:tsconfig` for ad-hoc invocation.
- src/Umbraco.Web.UI.Login/CLAUDE.md: documents the new layered
  contract (paths/externalisation/importmap) and the install-Client-
  before-Login prerequisite.
- src/Umbraco.Web.UI.Login/scripts/ensure-client-built.mjs: deleted.

What stays from the original PR (independent fixes):
- src/Umbraco.Web.UI.Client/src/external/uui/{vite.config.ts,index.ts}:
  `treeshake: false` + bare side-effect import — keeps UUI 2.0
  per-component `defineElement` calls in the bundle so `<uui-button>`
  etc. actually register.
- Object.groupBy cleanups in 6 element files (TS 8 type narrowing).
- Client tsconfig generator: target/lib bumped to ES2024, `baseUrl`
  removed.

Verified locally:
- `cd Client && rm -rf dist-cms && cd ../Login && npx tsc` → clean
  (proves Login compiles without Client's dist-cms)
- `cd Client && npm run build:for:cms` → 0 emitted .d.ts (back to
  pre-PR shape), `check:paths` passes
- Login `npm run build` → 64 KB bundle (unchanged)
- Browser at https://localhost:44339/umbraco: UUI 2.0 components
  render, login with `test@umbraco.com`/`test123456` succeeds and
  redirects to /umbraco/section/content

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

* Login: address review — idempotent generator + correct MSBuild ordering

- StaticAssets.csproj: BuildLogin now depends on RestoreBackoffice (not
  BuildBackoffice — Login doesn't need dist-cms types). Login's tsc walks
  Client source via tsconfig path aliases and resolves transitive deps
  (lit, rxjs, …) from Client's node_modules. Without this dependency a
  fresh local `dotnet build` could run BuildLogin before Client is
  installed; CI was already safe via backoffice-install.yml's npm ci.

- devops/tsconfig/index.js: skip rewrite when content is unchanged. Pre-
  hooks ran the generator on every npm command and bumped tsconfig.json
  mtime even when nothing changed, which can invalidate caches and rattle
  watchers downstream. Read-then-compare-then-write makes the generator
  truly idempotent.

- devops/tsconfig/index.js: derive the alias prefix from
  `clientPkg.name` instead of hardcoding `@umbraco-cms/backoffice` so a
  package rename can't silently break paths.

azure-pipelines.yml needs no changes — backoffice-install.yml already
runs `npm ci` in Client before dotnet build kicks in MSBuild.

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

* Login: postinstall + dev-mode Vite alias + theme CSS path

Audit cleanup pass on the rework:

- Login package.json: collapse predev/prebuild/prewatch into a single
  postinstall hook. The generator runs whenever npm install/ci runs
  (locally + in CI via RestoreLogin's npm i + the dotnet build chain).
  Removes the per-command "tsconfig.json already up to date" noise.

- Login vite.config.ts: in dev mode (`vite serve`), read `paths` from
  the generated tsconfig.json and apply them as `resolve.alias` so Vite
  can resolve `@umbraco-cms/backoffice/*` to Client source. Vite doesn't
  honor tsconfig `paths` natively — without this `npm run dev` failed
  with "Failed to resolve import @umbraco-cms/backoffice/utils ...".
  Build mode (`vite build`) still externalises the namespace via the
  unchanged rollupOptions.external regex; alias is dev-only.

- Login index.html: UUI 2.0 reorganised CSS — the old
  `@umbraco-ui/uui-css/dist/uui-css.css` path no longer exists. Point
  at `@umbraco-ui/uui/dist/themes/light.css` which is what Client now
  ships. Path is relative through Client's node_modules since Login no
  longer declares a UUI dep itself.

- Client input-entity-user-permission.element.ts: prettier flagged a
  multi-line .map() arrow that should be inline; collapse to one line.

- Login CLAUDE.md: document the postinstall-driven generator.

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

* Use Vite 8 native tsconfigPaths; drop helper plugin and trim comments

- Both vite.config.ts files use `resolve.tsconfigPaths: true` instead of the
  `vite-tsconfig-paths` plugin. Plugin and dep removed.
- Trim explanatory comments on csproj target, generator, UUI vite config and
  external/uui/index.ts to conclusions only.

* Login: tsconfig generator fails fast on unsupported exports shapes

Distinguish between the legitimate `.` self-reference (target === null) and
unexpected non-string targets (e.g., conditional exports objects). The latter
now throw with a clear message instead of being silently dropped from `paths`,
which would otherwise produce confusing 'Cannot find module' errors at tsc
time later.

* Login: allow Vite dev server to serve Client's UUI assets

The light.css imported from Client's node_modules pulls Lato fonts via
relative URL, which Vite refuses by default since they sit outside
Login's project root. Extend server.fs.allow to the parent directory
(both sibling projects).

* Client: regenerate tsconfig on postinstall

* Login: keep UUI registrations in dev mode

Vite 8's esbuild dep pre-bundle drops the per-component
`customElements.define()` side-effects in @umbraco-ui/uui (a known UUI
issue with Vite 8). Exclude UUI from optimizeDeps so it's served
unbundled in dev. Re-add the bare side-effect import in external/uui
so the entry module evaluates the chain. Production build is unaffected
(workspace's `treeshake: false` already preserves registrations).

Also document the new MSBuild Login targets in StaticAssets CLAUDE.md.

* Login: clarify why optimizeDeps.exclude is needed for UUI

Tested treeshake.moduleSideEffects: true in optimizeDeps.rollupOptions
on Vite 8 / Rolldown 1.0.0-rc.17 — registrations still get stripped.
Excluding the package from the pre-bundle is the only reliable workaround
until UUI's own Vite 8 upgrade lands. Comment captures the conclusion.

* Roll back Vite 8 → 7 in Client and Login

Vite 8.0.10 ships Rolldown 1.0.0-rc.17 which strips UUI 2.0
`customElements.define()` side-effects during dep pre-bundle, leaving
elements unregistered in dev mode. Rather than ship a v18 release tied
to a non-final Rolldown RC, revert the Vite bump and pick it up again
once Rolldown 1.0 final lands.

Changes:
- Client: vite ^8.0.10 → ^7.3.2; vite-plugin-static-copy ^4.1.0 → ^3.2.0;
  re-add vite-tsconfig-paths plugin; drop native `resolve.tsconfigPaths`.
- Login: vite ^8.0.10 → ^7.3.2; add vite-tsconfig-paths; configure plugin
  with `projects: ['./tsconfig.json', '../Umbraco.Web.UI.Client/tsconfig.json']`
  so it can resolve `@umbraco-cms/backoffice/*` imports inside Client
  source files (which would otherwise lack a discoverable tsconfig in
  Login's project tree). Drop `optimizeDeps.exclude` (no longer needed
  without Rolldown). Keep `server.fs.allow` for the cross-project font.

TypeScript 6 + ES2024 + tsconfig path generator + Login architectural
pivot all stay — those are independent of the Vite version.

Verified:
- Production https://localhost:44339/umbraco — login works
- Login dev http://localhost:5191/ — UUI registers, all custom elements defined
- Client dev http://localhost:5192/ — page loads, navigates to /section/content

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

* Address Copilot review

- vite.config.ts (Login): narrow server.fs.allow from the parent dir to
  Login + Client only, reducing the dev server's read scope.
- external/uui/vite.config.ts (Client): replace blanket `treeshake: false`
  with `moduleSideEffects: (id) => id.includes('@umbraco-ui/uui')` so
  Rollup keeps UUI's per-component registration calls but tree-shakes the
  rest. Bundle stays at 516 KB / 96 registered tags.

* fix merge overwrites

* update package lock

* fix: do not autogenerate tsconfig on postinstall

* removes postinstall script

* chore: generates tsconfig

* chore: update lockfile

* docs: updates claude.md

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
(cherry picked from commit 8a73d713cd)
2026-05-11 12:01:13 +01:00
def18e440f Login: Reuse backoffice localization (closes #20082) (#22743)
* Login: Reuse backoffice localization for canonical login_* keys (closes #56402)

The login screen no longer ships its own localization tree. The slim backoffice controller registers the backoffice's built-in localization manifests, so all login screen text resolves from the same dictionary the in-backoffice auth view uses. Translators override one place; both screens reflect it.

All consumers in the Login project moved from auth_* to login_*. The Login project's localization/ directory is removed entirely. The auth.* keys it used to ship (form labels, mfa, invite, password reset) now live under login.* in the backoffice's en/da/de/nb/nl/sv lang files. Other backoffice languages fall back to en for these keys, automatically extending the login screen's language coverage.

* Backoffice localization: drop server-only email keys, add login.setPasswordInstruction in en/da/nb/sv

bottomText, resetPasswordEmailCopySubject, resetPasswordEmailCopyFormat, mfaSecurityCodeSubject and mfaSecurityCodeBody are read only by the server's own localization layer — they were dead weight in every backoffice lang dictionary that carried them. Removed across 23 lang files.

login.setPasswordInstruction is rendered on the new-password screen via the now-canonical login_* namespace; it was missing from en (the fallback), da, nb and sv. Added there using the same translation tone as the existing de/nl entries.

* Login: Honour legacy auth_greeting* overrides with UmbDeprecation warning

Translation packages still shipping 'auth_greeting0..6' overrides keep working on both welcome screens (the standalone login page and the in-backoffice umb-auth-view): when an auth_* greeting is registered the consumer prefers it, otherwise the canonical login_* key is used. Each legacy key triggers a one-time UmbDeprecation warning pointing at the canonical name. Scheduled for removal in v20.

* Fix Prettier formatting and correct issue references in deprecation message

Addresses Copilot review feedback on PR #22743:

- Run Prettier on the 6 backoffice lang files I added keys to (en/da/de/nb/nl/sv); the new entries used double quotes which violated the repo's singleQuote: true config and would have failed the format check.
- Update the UmbDeprecation 'solution' link and the inline source comments from #56402 (an ADO work item id) to #20082 (the actual GitHub issue tracking this work).

* Drop stale login_2fa* and login_mfaSecurityCodeMessage from bs.ts and cy.ts

Surfaced by 'devops/localization/compare-languages.js': bs and cy were the only lang files shipping these keys, and they have no en counterpart. The login_2fa* set is leftover from before the codebase renamed 2fa → mfa in the login flow (the live keys are login_mfa*). login_mfaSecurityCodeMessage is server-side only, like the other email-template keys cleaned up in 53ad52702e0. None of these are referenced anywhere in src/. The user-facing user_2fa* keys (consumed by current-user-mfa modals) are unrelated and untouched.

* Drop dead login_2fa* and login_mfaSecurityCodeMessage from nl, hr, tr

Same pattern as 26bc6211d27 (bs/cy cleanup), surfaced by re-running devops/localization/compare-languages.js after the previous pass:

- nl had both legacy 'login_2fa*' AND the canonical 'login_mfa*' (added in commit 1) sitting side by side after the auth.* → login.* port. Six true duplicates dropped, login_mfa* kept.
- hr and tr shipped legacy 'login_2fa*' that have no en counterpart, no consumer in src/, and no mfa pair locally. Dropped to align with en (the source of truth — every other locale should match it).

- All three files also still carried 'login_mfaSecurityCodeMessage' from the same family of server-side email-template keys cleaned up in 53ad52702e0; removed too.

user_2fa* / member_2fa keys are unrelated and untouched (consumed by current-user-mfa modals).

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-05-11 11:59:58 +01:00
8a73d713cd Login: Consume sibling Umbraco.Web.UI.Client by source for v18 parity (#22591)
* Login: Consume sibling Umbraco.Web.UI.Client by source for v18 parity

The Login project previously depended on the published `@umbraco-cms/backoffice@^17.3.4` npm package for types, while at runtime the importmap served the in-repo v18 backoffice. The version mismatch forced `as any` workarounds and masked real API drift. Since v18 (with UUI 2.0) isn't on npm yet, switch Login to consume the sibling Client via a local `file:` dep so types and runtime align on v18.

Changes:
- Login `package.json`: `@umbraco-cms/backoffice` → `file:../Umbraco.Web.UI.Client`; added `pre{build,dev,watch}` hooks that run a guard script to fail fast when Client's `dist-cms/` is missing.
- Login `scripts/ensure-client-built.mjs`: new guard with a clear "build the Client first" message.
- Login `CLAUDE.md`: documents the contract and build ordering.
- StaticAssets `.csproj`: `BuildLogin` now depends on `BuildBackoffice` so MSBuild (and therefore the Azure pipeline) builds Client before Login automatically.
- Client `src/tsconfig.build.json`: `declaration: true` so `dist-cms/` ships `.d.ts`.
- Client `package.json`: new `build:types` step (`tsc --emitDeclarationOnly --incremental false && tsc-alias`) wired into `build:for:cms` after `build:workspaces`. Vite workspaces wipe their output dirs before rebuilding JS, stripping the tsc-emitted declarations; re-emitting after workspaces restores them. `tsc-alias` rewrites Client-internal path aliases (e.g. `@umbraco-cms/backoffice/external/lit`) to relative paths so sibling consumers can resolve them.
- `copy-to-cms.js`: filter `.d.ts` and `.tsbuildinfo` from the copy to `wwwroot/umbraco/backoffice` — they're only needed by sibling projects consuming `dist-cms` for types, not at runtime.
- `src/external/uui/vite.config.ts`: set `treeshake: false` so per-component `defineElement()` side-effect calls (used by UUI 2.0 for custom-element registration) are preserved in the bundle. Without this, `<uui-button>` etc. never register and the login screen renders empty controls.
- `src/external/uui/index.ts`: bare `import '@umbraco-ui/uui'` to make the side-effect intent explicit.
- Small v18-compat fixes for `Object.groupBy` (TS 8 types): removed stale `@ts-expect-error`, switched to `Object.entries` + `?? []` to satisfy the `Partial<Record>` return type.

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

* Address review feedback and fix CI

- Add `ignoreDeprecations: "6.0"` to `tsconfig.json` and the tsconfig generator to silence the TS 6.0 warning about the implicit baseUrl that TypeScript assigns when `paths` is declared. This was the CI `build` failure. The generator is also synced with the user's es2022 → es2024 bump.
- Drop the now-redundant `--declaration` flag from `build:for:npm` (tsconfig.build.json now has `declaration: true`, so the flag was duplicating intent).
- Align Login's `engines` with the Client's (`node >=24.13`, `npm >=11`) so `file:` install doesn't trip EBADENGINE.
- Guard script: hardcode the relative "../Umbraco.Web.UI.Client" path in the error message instead of interpolating the absolute path, which overflowed the ASCII box in CI logs.

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

* Login: update CLAUDE.md Node/npm versions to match engines

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

* check-path-length: skip .d.ts/.tsbuildinfo and directory paths

The 120-char Windows MAX_PATH guard protects files that actually ship to
CMS installs. `.d.ts` and `.tsbuildinfo` live in `dist-cms/` for sibling
projects to consume as types and are filtered out by `copy-to-cms.js`
before reaching `wwwroot/umbraco/backoffice` — they never land on a
Windows CMS install. Directories on their own also don't trigger
MAX_PATH; only files within them do, and those are still checked.

Unblocks CI after enabling `declaration: true` in the Client build.

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

* check-path-length: extract exceedsPathLimit helper (CodeScene)

Decomposes the complex conditional flagged by CodeScene into a named
predicate with a docstring, clarifying when a path is reported.

* Login: switch to generated tsconfig paths; revert dist-cms type machinery

PR #22591 originally aligned Login's TypeScript types with the in-repo v18
backoffice by emitting `.d.ts` into Client's `dist-cms/` and consuming it
via a `file:` dep. That layered six side-effects across the Client build
(declaration: true, build:types step, tsc-alias in postbuild, copy-to-cms
filter, check:paths skip, MSBuild ordering). Reviewers pushed back.

This rework moves the type contract from "ship .d.ts in dist-cms" to
"point Login's tsconfig paths at Client's TypeScript source" — Login's
runtime behaviour is unchanged (vite still externalises /^@umbraco-cms/,
host importmap still serves the JS), only the type-resolution mechanism
swaps.

What's reverted (back to the pre-PR shape):
- src/Umbraco.Web.UI.Client/src/tsconfig.build.json: declaration: false
- src/Umbraco.Web.UI.Client/package.json: drops `build:types` script,
  reverts `postbuild` to global-types only, drops `--declaration` from
  the tsc CLI in `build:for:cms` and restores it in `build:for:npm`
- src/Umbraco.Web.UI.Client/devops/build/copy-to-cms.js: simple cpSync
- src/Umbraco.Web.UI.Client/devops/build/check-path-length.js: original
- src/Umbraco.Web.UI.Client/tsconfig.json + devops/tsconfig/index.js:
  drops `ignoreDeprecations` (not needed once baseUrl is gone)
- src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj:
  `BuildLogin` no longer depends on `BuildBackoffice`

What's new on the Login side:
- src/Umbraco.Web.UI.Login/devops/tsconfig/index.js: generator that
  reads Client's `package.json` exports and emits a full `tsconfig.json`
  with `paths` mapping every `@umbraco-cms/backoffice/<sub>` to
  `../Umbraco.Web.UI.Client/src/.../index.ts`. Mirrors Client's existing
  generator pattern (DON'T EDIT header, JSON.stringify with tabs).
- src/Umbraco.Web.UI.Login/tsconfig.json: regenerated; standalone `tsc`
  works (no `--project` needed) and 140 path aliases resolve types
  directly from Client's source.
- src/Umbraco.Web.UI.Login/package.json: drops `@umbraco-cms/backoffice`
  npm dep entirely (file: was only nominal — types come via paths,
  runtime via importmap, transitives via Client's own `node_modules`
  which is `npm install`-ed by CI's backoffice-install.yml). Replaces
  the `ensure-client-built` guard with the generator on `pre*` hooks
  and adds `generate:tsconfig` for ad-hoc invocation.
- src/Umbraco.Web.UI.Login/CLAUDE.md: documents the new layered
  contract (paths/externalisation/importmap) and the install-Client-
  before-Login prerequisite.
- src/Umbraco.Web.UI.Login/scripts/ensure-client-built.mjs: deleted.

What stays from the original PR (independent fixes):
- src/Umbraco.Web.UI.Client/src/external/uui/{vite.config.ts,index.ts}:
  `treeshake: false` + bare side-effect import — keeps UUI 2.0
  per-component `defineElement` calls in the bundle so `<uui-button>`
  etc. actually register.
- Object.groupBy cleanups in 6 element files (TS 8 type narrowing).
- Client tsconfig generator: target/lib bumped to ES2024, `baseUrl`
  removed.

Verified locally:
- `cd Client && rm -rf dist-cms && cd ../Login && npx tsc` → clean
  (proves Login compiles without Client's dist-cms)
- `cd Client && npm run build:for:cms` → 0 emitted .d.ts (back to
  pre-PR shape), `check:paths` passes
- Login `npm run build` → 64 KB bundle (unchanged)
- Browser at https://localhost:44339/umbraco: UUI 2.0 components
  render, login with `test@umbraco.com`/`test123456` succeeds and
  redirects to /umbraco/section/content

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

* Login: address review — idempotent generator + correct MSBuild ordering

- StaticAssets.csproj: BuildLogin now depends on RestoreBackoffice (not
  BuildBackoffice — Login doesn't need dist-cms types). Login's tsc walks
  Client source via tsconfig path aliases and resolves transitive deps
  (lit, rxjs, …) from Client's node_modules. Without this dependency a
  fresh local `dotnet build` could run BuildLogin before Client is
  installed; CI was already safe via backoffice-install.yml's npm ci.

- devops/tsconfig/index.js: skip rewrite when content is unchanged. Pre-
  hooks ran the generator on every npm command and bumped tsconfig.json
  mtime even when nothing changed, which can invalidate caches and rattle
  watchers downstream. Read-then-compare-then-write makes the generator
  truly idempotent.

- devops/tsconfig/index.js: derive the alias prefix from
  `clientPkg.name` instead of hardcoding `@umbraco-cms/backoffice` so a
  package rename can't silently break paths.

azure-pipelines.yml needs no changes — backoffice-install.yml already
runs `npm ci` in Client before dotnet build kicks in MSBuild.

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

* Login: postinstall + dev-mode Vite alias + theme CSS path

Audit cleanup pass on the rework:

- Login package.json: collapse predev/prebuild/prewatch into a single
  postinstall hook. The generator runs whenever npm install/ci runs
  (locally + in CI via RestoreLogin's npm i + the dotnet build chain).
  Removes the per-command "tsconfig.json already up to date" noise.

- Login vite.config.ts: in dev mode (`vite serve`), read `paths` from
  the generated tsconfig.json and apply them as `resolve.alias` so Vite
  can resolve `@umbraco-cms/backoffice/*` to Client source. Vite doesn't
  honor tsconfig `paths` natively — without this `npm run dev` failed
  with "Failed to resolve import @umbraco-cms/backoffice/utils ...".
  Build mode (`vite build`) still externalises the namespace via the
  unchanged rollupOptions.external regex; alias is dev-only.

- Login index.html: UUI 2.0 reorganised CSS — the old
  `@umbraco-ui/uui-css/dist/uui-css.css` path no longer exists. Point
  at `@umbraco-ui/uui/dist/themes/light.css` which is what Client now
  ships. Path is relative through Client's node_modules since Login no
  longer declares a UUI dep itself.

- Client input-entity-user-permission.element.ts: prettier flagged a
  multi-line .map() arrow that should be inline; collapse to one line.

- Login CLAUDE.md: document the postinstall-driven generator.

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

* Use Vite 8 native tsconfigPaths; drop helper plugin and trim comments

- Both vite.config.ts files use `resolve.tsconfigPaths: true` instead of the
  `vite-tsconfig-paths` plugin. Plugin and dep removed.
- Trim explanatory comments on csproj target, generator, UUI vite config and
  external/uui/index.ts to conclusions only.

* Login: tsconfig generator fails fast on unsupported exports shapes

Distinguish between the legitimate `.` self-reference (target === null) and
unexpected non-string targets (e.g., conditional exports objects). The latter
now throw with a clear message instead of being silently dropped from `paths`,
which would otherwise produce confusing 'Cannot find module' errors at tsc
time later.

* Login: allow Vite dev server to serve Client's UUI assets

The light.css imported from Client's node_modules pulls Lato fonts via
relative URL, which Vite refuses by default since they sit outside
Login's project root. Extend server.fs.allow to the parent directory
(both sibling projects).

* Client: regenerate tsconfig on postinstall

* Login: keep UUI registrations in dev mode

Vite 8's esbuild dep pre-bundle drops the per-component
`customElements.define()` side-effects in @umbraco-ui/uui (a known UUI
issue with Vite 8). Exclude UUI from optimizeDeps so it's served
unbundled in dev. Re-add the bare side-effect import in external/uui
so the entry module evaluates the chain. Production build is unaffected
(workspace's `treeshake: false` already preserves registrations).

Also document the new MSBuild Login targets in StaticAssets CLAUDE.md.

* Login: clarify why optimizeDeps.exclude is needed for UUI

Tested treeshake.moduleSideEffects: true in optimizeDeps.rollupOptions
on Vite 8 / Rolldown 1.0.0-rc.17 — registrations still get stripped.
Excluding the package from the pre-bundle is the only reliable workaround
until UUI's own Vite 8 upgrade lands. Comment captures the conclusion.

* Roll back Vite 8 → 7 in Client and Login

Vite 8.0.10 ships Rolldown 1.0.0-rc.17 which strips UUI 2.0
`customElements.define()` side-effects during dep pre-bundle, leaving
elements unregistered in dev mode. Rather than ship a v18 release tied
to a non-final Rolldown RC, revert the Vite bump and pick it up again
once Rolldown 1.0 final lands.

Changes:
- Client: vite ^8.0.10 → ^7.3.2; vite-plugin-static-copy ^4.1.0 → ^3.2.0;
  re-add vite-tsconfig-paths plugin; drop native `resolve.tsconfigPaths`.
- Login: vite ^8.0.10 → ^7.3.2; add vite-tsconfig-paths; configure plugin
  with `projects: ['./tsconfig.json', '../Umbraco.Web.UI.Client/tsconfig.json']`
  so it can resolve `@umbraco-cms/backoffice/*` imports inside Client
  source files (which would otherwise lack a discoverable tsconfig in
  Login's project tree). Drop `optimizeDeps.exclude` (no longer needed
  without Rolldown). Keep `server.fs.allow` for the cross-project font.

TypeScript 6 + ES2024 + tsconfig path generator + Login architectural
pivot all stay — those are independent of the Vite version.

Verified:
- Production https://localhost:44339/umbraco — login works
- Login dev http://localhost:5191/ — UUI registers, all custom elements defined
- Client dev http://localhost:5192/ — page loads, navigates to /section/content

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

* Address Copilot review

- vite.config.ts (Login): narrow server.fs.allow from the parent dir to
  Login + Client only, reducing the dev server's read scope.
- external/uui/vite.config.ts (Client): replace blanket `treeshake: false`
  with `moduleSideEffects: (id) => id.includes('@umbraco-ui/uui')` so
  Rollup keeps UUI's per-component registration calls but tree-shakes the
  rest. Bundle stays at 516 KB / 96 registered tags.

* fix merge overwrites

* update package lock

* fix: do not autogenerate tsconfig on postinstall

* removes postinstall script

* chore: generates tsconfig

* chore: update lockfile

* docs: updates claude.md

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-11 09:42:28 +00:00
Andreas ZerbstandGitHub f6ac750d09 E2E: QA: Add missing helpers for Content Versioning (#22788)
* Added missing rollback helpers

* Updated helper to match locator
2026-05-11 06:33:44 +02:00
Niels Lyngsø 3142691e4f Update architecture.md 2026-05-08 15:13:47 +02:00
Niels Lyngsø c813481b4e update UUI for icon manager 2026-05-08 15:11:53 +02:00
Andy Butland 5c1e7e9167 Merge branch 'release/18.0' of https://github.com/umbraco/Umbraco-CMS into release/18.0 2026-05-08 15:06:26 +02:00
2e16b0d38a Tests: Fix PublishedValueFallbackTests after ILocalizationService removal (#22772)
* fix(tests): replace removed ILocalizationService with ILanguageService in PublishedValueFallbackTests

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

* Potential fix for pull request finding

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-08 15:05:55 +02:00
Laura NetoandGitHub ab821e0519 Open API: Skip operation ID generation for non-controller endpoints (#22760)
Skip operation ID generation for non-controller endpoints

UmbracoOperationIdTransformer is registered globally for the default
OpenAPI document, so any minimal API endpoint that lands there ran
through it. The transformer threw "This handler operates only on
ControllerActionDescriptor" because its conventions (route prefix
stripping, MapToApiVersion lookup) only make sense for MVC actions.

Return null from the generator and skip the assignment when the action
descriptor isn't a ControllerActionDescriptor. The framework's default
operation ID applies in that case.
2026-05-08 15:05:11 +02:00
11ff2c8039 Tests: Fix PublishedValueFallbackTests after ILocalizationService removal (#22772)
* fix(tests): replace removed ILocalizationService with ILanguageService in PublishedValueFallbackTests

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

* Potential fix for pull request finding

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-08 15:02:58 +02:00
Jacob Overgaard 80098706cf chore: ignores default log message for MSW 2026-05-08 13:05:50 +02:00
leekelleher dff3941e1f Merge branch 'v18/dev' 2026-05-08 10:02:00 +01:00
leekelleher 072e362284 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/variant-state.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-table-column-property-value.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document-search-result-item.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/variant-state.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts
2026-05-08 10:01:33 +01:00
b243940ab5 Auth: Un-deprecate getLatestToken and route per-request fetches through it (#22736)
* Auth: un-deprecates getLatestToken and routes per-request fetches through it

getLatestToken is the only public API for "wait for any in-flight refresh,
trigger one if the access token has expired, then return". External and
internal consumers were warned off it without an equivalent replacement:
configureClient only helps @hey-api/openapi-ts clients, and consumers using
axios/ky/native fetch had no other gate.

- Removes the @deprecated JSDoc + UmbDeprecation.warn() call so the public
  surface no longer prints a console warning per call.
- Uses getLatestToken.bind(this) for the auth callback inside configureClient
  and the token callback inside getOpenApiConfiguration so both paths share
  the same #ensureTokenReady gate.
- Replaces the hard-coded `Authorization: Bearer [redacted]` in unlinkLogin
  and #makeLinkTokenRequest with `Bearer ${await getLatestToken()}` so those
  fetches participate in the refresh coordination rather than firing with a
  potentially-revoked cookie.

Also wires the UmbracoExtension template's entrypoint to call
authContext.configureClient(client), matching the v18 template change. The
framework awaits onInit, so this guarantees the API client is fully
configured before any element in the extension can use it.

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

* Auth: tightens UmbAuthContext correctness and accepts any hey-api client

Pulls in a batch of non-breaking improvements to UmbAuthContext that came
out of an audit on the back of the un-deprecation work in this PR:

Public surface:
- configureClient(client) now accepts a new structural UmbApiClient type
  (exported from @umbraco-cms/backoffice/http-client). Each @hey-api/openapi-ts
  generation produces a fully-bound Client<…>; the backoffice's umbHttpClient
  and an extension's regenerated client are structurally identical but TS
  treats them as distinct generic instantiations. The widened parameter lets
  extensions wire their own client without `as never` casts at call sites.
  bindDefaultInterceptors keeps its strict typeof umbHttpClient parameter
  (preserving autocomplete inside interceptor callbacks); the cast happens
  once, internally.

Correctness:
- The auth context now holds a single UmbApiInterceptorController, lazy-
  initialised on first configureClient() call. Previously each call
  instantiated a new controller, which re-provided the UmbAuthSignalerContext
  on the host and stacked listeners — visible the moment an extension also
  called configureClient. One controller for the lifetime of the host, all
  configured clients share it.
- completeAuthorizationRequest checks sessionStorage before asking
  window.opener for the PKCE verifier. The previous order hung for the full
  postMessage timeout whenever oauth_complete loaded with a non-OAuth
  window.opener (which is set for ANY window.open target). The opener
  postMessage timeout is also dropped from 5s to 1.5s — a real popup parent
  responds within milliseconds; longer is just wait time for the unrelated-
  opener case.
- The cross-tab 'authorized' BroadcastChannel handler now routes through
  #setSessionLocally so the timestamp math stays in one place. The
  'sessionUpdate' handler still applies pre-computed timestamps directly
  (peer broadcast already did the math) but does so inside the
  #inSessionUpdateCallback guard, so a synchronous session$ observer can no
  longer trigger a spurious /token refresh on top of a peer's update.
- #ensureTokenReady drops its query-then-request pattern. Now always queues
  behind the umb:token-refresh lock with a no-op callback — if the lock is
  free it acquires immediately, if held it waits. Eliminates the race window
  between query() and request().
- destroy() invokes #popupCleanup before tearing down so an in-flight popup
  flow's window-level message listener and closed-poll interval don't leak
  past the context's lifetime. The cleanup helper itself now resolves the
  popup-flow Promise — every termination path (authorized, popup closed,
  superseded by a new flow, context destroyed) is observable to the awaiter
  instead of hanging forever.

Cleanup:
- makeAuthorizationRequest is annotated Promise<void> so the redirect and
  popup branches share an explicit return type.
- unlinkLogin wraps the parsed problem-details payload in a real Error (with
  the original payload exposed on `.cause`) so callers using `instanceof
  Error` or expecting a stack trace get sane behaviour.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:59:23 +02:00
7248f01292 Auth: Un-deprecate getLatestToken and route per-request fetches through it (#22736)
* Auth: un-deprecates getLatestToken and routes per-request fetches through it

getLatestToken is the only public API for "wait for any in-flight refresh,
trigger one if the access token has expired, then return". External and
internal consumers were warned off it without an equivalent replacement:
configureClient only helps @hey-api/openapi-ts clients, and consumers using
axios/ky/native fetch had no other gate.

- Removes the @deprecated JSDoc + UmbDeprecation.warn() call so the public
  surface no longer prints a console warning per call.
- Uses getLatestToken.bind(this) for the auth callback inside configureClient
  and the token callback inside getOpenApiConfiguration so both paths share
  the same #ensureTokenReady gate.
- Replaces the hard-coded `Authorization: Bearer [redacted]` in unlinkLogin
  and #makeLinkTokenRequest with `Bearer ${await getLatestToken()}` so those
  fetches participate in the refresh coordination rather than firing with a
  potentially-revoked cookie.

Also wires the UmbracoExtension template's entrypoint to call
authContext.configureClient(client), matching the v18 template change. The
framework awaits onInit, so this guarantees the API client is fully
configured before any element in the extension can use it.

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

* Auth: tightens UmbAuthContext correctness and accepts any hey-api client

Pulls in a batch of non-breaking improvements to UmbAuthContext that came
out of an audit on the back of the un-deprecation work in this PR:

Public surface:
- configureClient(client) now accepts a new structural UmbApiClient type
  (exported from @umbraco-cms/backoffice/http-client). Each @hey-api/openapi-ts
  generation produces a fully-bound Client<…>; the backoffice's umbHttpClient
  and an extension's regenerated client are structurally identical but TS
  treats them as distinct generic instantiations. The widened parameter lets
  extensions wire their own client without `as never` casts at call sites.
  bindDefaultInterceptors keeps its strict typeof umbHttpClient parameter
  (preserving autocomplete inside interceptor callbacks); the cast happens
  once, internally.

Correctness:
- The auth context now holds a single UmbApiInterceptorController, lazy-
  initialised on first configureClient() call. Previously each call
  instantiated a new controller, which re-provided the UmbAuthSignalerContext
  on the host and stacked listeners — visible the moment an extension also
  called configureClient. One controller for the lifetime of the host, all
  configured clients share it.
- completeAuthorizationRequest checks sessionStorage before asking
  window.opener for the PKCE verifier. The previous order hung for the full
  postMessage timeout whenever oauth_complete loaded with a non-OAuth
  window.opener (which is set for ANY window.open target). The opener
  postMessage timeout is also dropped from 5s to 1.5s — a real popup parent
  responds within milliseconds; longer is just wait time for the unrelated-
  opener case.
- The cross-tab 'authorized' BroadcastChannel handler now routes through
  #setSessionLocally so the timestamp math stays in one place. The
  'sessionUpdate' handler still applies pre-computed timestamps directly
  (peer broadcast already did the math) but does so inside the
  #inSessionUpdateCallback guard, so a synchronous session$ observer can no
  longer trigger a spurious /token refresh on top of a peer's update.
- #ensureTokenReady drops its query-then-request pattern. Now always queues
  behind the umb:token-refresh lock with a no-op callback — if the lock is
  free it acquires immediately, if held it waits. Eliminates the race window
  between query() and request().
- destroy() invokes #popupCleanup before tearing down so an in-flight popup
  flow's window-level message listener and closed-poll interval don't leak
  past the context's lifetime. The cleanup helper itself now resolves the
  popup-flow Promise — every termination path (authorized, popup closed,
  superseded by a new flow, context destroyed) is observable to the awaiter
  instead of hanging forever.

Cleanup:
- makeAuthorizationRequest is annotated Promise<void> so the redirect and
  popup branches share an explicit return type.
- unlinkLogin wraps the parsed problem-details payload in a real Error (with
  the original payload exposed on `.cause`) so callers using `instanceof
  Error` or expecting a stack trace get sane behaviour.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:54:04 +01:00
Laura NetoandGitHub 317e9b4e69 Elements: Disable inaccessible parent folders in element tree (#22749)
Disable inaccessible parent folders in element tree

When an element start node is configured to a child folder, the backend
returns ancestor folders flagged with NoAccess so they show as breadcrumbs.
The element folder tree item used the default tree item element, which
does not observe noAccess, so parent folders rendered as enabled and
clickable in the Library section tree. Added a custom
element-folder-tree-item element that observes the context's noAccess and
forwards it to the base, which already handles disabling the menu item.
2026-05-08 09:51:24 +01:00
leekelleher 4fab629ee3 Documents: Alias DocumentVariantStateModel API model for backoffice client (#22716)
* Client: Aliased `DocumentVariantStateModel` for documents and document-blueprints packages

Hoist `UmbDocumentVariantState` and `UmbDocumentBlueprintVariantState` aliases (re-exporting `DocumentVariantStateModel`) into dedicated `variant-state.ts` leaf files. Internal package modules, mocks and the core split-view selector now consume the alias instead of referencing `DocumentVariantStateModel` directly, mirroring the structure on `v18/dev` to reduce upstream-merge conflicts.

* Revert mock data changes

to prevent importing the whole "document" module.

* Tweaked the `DocumentVariantStateModel` import for mock data

Otherwise this is problematic for cherry-picked commits for v18.0.

* Missed one!
# Conflicts:
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/document-blueprint.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/kitchen-sink/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/user-permissions/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document-blueprint.db.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document-publishing.manager.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document.db.ts
#	src/Umbraco.Web.UI.Client/mocks/tools/sqlite-to-mock/transform-documents.ts
#	src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/types.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/types.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-table-column-property-value.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/shared/document-variant-language-picker.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document-search-result-item.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/variant-state.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts
2026-05-08 09:49:55 +01:00
Andy Butland 221531b614 Published Content: Fix Fallback.ToAncestors with no match throwing exception at property level (closes #22759) (#22763)
* Fix Fallback.ToAncestors regression at property level.

* Further unit tests.
2026-05-08 10:32:18 +02:00
leekelleher 8495405927 Documents: Alias DocumentVariantStateModel API model for backoffice client (#22716)
* Client: Aliased `DocumentVariantStateModel` for documents and document-blueprints packages

Hoist `UmbDocumentVariantState` and `UmbDocumentBlueprintVariantState` aliases (re-exporting `DocumentVariantStateModel`) into dedicated `variant-state.ts` leaf files. Internal package modules, mocks and the core split-view selector now consume the alias instead of referencing `DocumentVariantStateModel` directly, mirroring the structure on `v18/dev` to reduce upstream-merge conflicts.

* Revert mock data changes

to prevent importing the whole "document" module.

* Tweaked the `DocumentVariantStateModel` import for mock data

Otherwise this is problematic for cherry-picked commits for v18.0.

* Missed one!
# Conflicts:
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/document-blueprint.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/kitchen-sink/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/user-permissions/document.data.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document-blueprint.db.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document-publishing.manager.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document.db.ts
#	src/Umbraco.Web.UI.Client/mocks/tools/sqlite-to-mock/transform-documents.ts
#	src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/types.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/types.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-table-column-property-value.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/shared/document-variant-language-picker.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document-search-result-item.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/variant-state.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts
2026-05-08 09:23:36 +01:00
Andy ButlandandGitHub 1a74aa53c9 Published Content: Fix Fallback.ToAncestors with no match throwing exception at property level (closes #22759) (#22763)
* Fix Fallback.ToAncestors regression at property level.

* Further unit tests.
2026-05-08 10:15:28 +02:00
Jacob OvergaardandClaude Sonnet 4.6 80cc752b3d Backoffice Mocks: Add missing element start node fields to documents mock set
`elementStartNodeIds` and `hasElementRootAccess` were added to
`UmbCurrentUserModel` by the Global Elements PR but the documents mock
data set was created without them, causing `undefined.map()` errors in
the document workspace CRUD tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 09:08:40 +02:00
Jacob Overgaard 39125da978 Merge remote-tracking branch 'origin/main' into v18/dev 2026-05-08 09:03:36 +02:00
Jacob Overgaard 1c32829883 chore: fixes to use correct import of api types in mock data 2026-05-07 21:26:53 +02:00
Andy ButlandandJacob Overgaard dc446c0e4a Color Picker: Refresh stored label when data type label changes (closes #22741) (#22761)
* Update stored color label if changed on save of document with color picker.

* Clarify intent of change event dispatch in label sync

* Make comparison case insensitive.

* Added unit tests for new behaviour.
2026-05-07 21:17:00 +02:00
Jacob Overgaard 1e59af34ff Merge remote-tracking branch 'origin/main' into v18/dev 2026-05-07 21:16:24 +02:00
Andy Butland ab1be601f5 Dictionary: Order SQL before FetchOneToMany to prevent duplicate items in collection view (closes #22640) (#22750)
* Order SQL before FetchOneToMany in dictionary entry retrieval to prevent duplicate items in collection view.

* Used PrimaryKey instead of UniqueId to take advantage of the clustered index.
2026-05-07 18:45:25 +02:00
Andy Butland 1baf4e5a5c Merge branch 'main' into v18/dev 2026-05-07 18:43:56 +02:00
Jacob Overgaard bcf9bf3f3a Merge branch 'release/18.0' into v18/dev 2026-05-07 16:23:19 +02:00
Niels Lyngsø e4dce93b79 Merge branch 'main' into v18/dev
# Conflicts:
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/ContentUiHelper.ts
2026-05-07 14:06:36 +02:00
Niels Lyngsø b07e908ce3 Document Workspace: Add CRUD and property value tests for document workspace context (#22621)
* temp mock set

* test getPropertyValue

* Extend document workspace context tests to cover read/write property values

* move context files into context folder

* Add document CRUD tests, mock handler & interceptor

* temp mock error interceptor

* Return 404 when document not found

* Use undefined for entity unique state until initialized

* Fix import paths for document workspace editor

* Add test utils and extend document workspace tests

* Update document-workspace-context.test-utils.ts

* Match invariant variant when variantId missing

* Ensure finishPropertyValueChange runs on exit

Wrap setPropertyValue implementation in a try/finally and move finishPropertyValueChange into the finally block so cleanup always runs even if an error is thrown. No other functional changes — code was re-indented and organized but behavior remains the same except for guaranteed cleanup on error.

* Require variantId for culture/segment-variant props

* fix types

* fix mock modal typescript error

* Distinguish unloaded vs root entity unique

* use the real current user context

* hide mock set in UI

* rename mock set

* Move initiatePropertyValueChange into try

* Use 'satisfies' for UmbMockDataSet assertions

* Preserve requested unique on failed load

* Treat missing variantId as invariant

* Reset update lock on destroy

* remove unused group + user

* Guard _current.unmute and remove destroy override

* Add tests for element data manager

* Guard subject access and add destroy test

* Throw when calling methods after destroy
2026-05-07 14:00:34 +02:00
Lee KelleherandGitHub 41a9133c58 V18: Reverts removal of property-value-change event listeners (#22734)
* Reverts removal of `property-value-change` event listeners

* Adds `UmbDeprecation` warning

for `property-value-change` events.
2026-05-07 09:27:05 +00:00
4953855dda Build: Upgrade @hey-api/openapi-ts to 0.97 (#22735)
* build(deps): updates @hey-api/openapi-ts to latest and regenerates APi types

* build(deps): updates @hey-api/openapi-ts to latest and regenerates APi types (login)

* fix(backoffice): avoid invalid status 0 when synthesizing responses

Default to a 500 fallback status when no upstream Response is provided
to #createResponse, preventing a RangeError from the Response constructor.

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

* build(deps): updates UmbracoExtension template to @hey-api/openapi-ts 0.97

- Bumps @hey-api/openapi-ts to ^0.97.0 in the extension template.
- Simplifies the generate-openapi.js plugin config: spread @hey-api defaults
  and only override @hey-api/sdk with responseStyle: 'fields' so call sites
  keep the { data, error } destructuring shape. Removes the redundant
  @hey-api/client-fetch redeclaration that triggered duplicate-plugin warnings.
- Drops the hey-api.ts runtime config file in favour of wiring the generated
  client through UMB_AUTH_CONTEXT.configureClient() from the entrypoint, so
  extensions inherit the same auth callback and default response interceptors
  (401 retry, error notifications) as the core backoffice.
- Regenerates the pre-bundled SDK against the template's canonical
  Umbraco.Extension scaffold so it matches what `npm run generate-client`
  produces on first run; default hey-api output is flat function exports.
- Updates dashboard.element.ts call sites to match the new SDK shape and
  renames the user model usage to Iuser to follow the new schema.

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

* chore(git): marks UmbracoExtension template generated SDK as linguist-generated

So GitHub diffs collapse the regenerated *.gen.ts files in PRs, matching what
we already do for the backoffice client and Login app SDKs.

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

* fix(template): addresses review feedback on PR #22735

- Restores the regenerated SDK's hard-coded baseUrl to https://localhost:44339/
  so the SiteDomain template token in the .template.config still substitutes
  it at scaffold time. The 5443 port leaked in from the local host I used to
  regenerate; that domain is replaced by the user's chosen SiteDomain on
  scaffold.
- Stops marking onInit as `async`. The UmbEntryPointOnInit signature returns
  void; making the hook async is harmless under TS's bivariant void-return
  assignability but is misleading. Kicks the context resolution + client
  configuration off via .then() and logs a warning when UMB_AUTH_CONTEXT is
  not present (instead of silently optional-chaining).

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

* fix(template): keeps onInit async — the framework awaits it

The previous tweak was based on Copilot's claim that UmbEntryPointOnInit
returns void. The signature does declare void, but the entry-point
initializer in app-entry-point-extension-initializer.ts and
backoffice-entry-point-extension-initializer.ts both `await
moduleInstance.onInit(...)`, so an async onInit is awaited end-to-end.
Reverting to async ensures configureClient runs to completion before any
element in the extension can hit the API client.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:11:59 +00:00
Laura Neto 6201c3dc40 Bump version to 18.1.0-rc 2026-05-06 19:15:46 +02:00
Laura Neto 0c08d522a2 Adjust Umbraco.Tests.AcceptanceTest version to 18.0.0-beta1 2026-05-06 19:05:48 +02:00
Laura Neto 10a656c067 Merge branch 'main' into v18/dev 2026-05-06 18:55:54 +02:00
Laura NetoandGitHub 6067d428d0 Delivery API: Fix broken discriminator mapping refs for polymorphic schemas (#22733)
* Delivery API: Fix broken discriminator mapping refs for polymorphic schemas

Microsoft.AspNetCore.OpenApi's MapPolymorphismOptionsToDiscriminator builds each ref as callback(base) + callback(derived), but our typed-schema flow registers the derived schemas without the base prefix. The auto-built mapping refs end up pointing at non-existent schemas, which crashes strict client generators like orval.

Strip the base schema id from the front of each broken ref to recover the registration key the derived schema actually uses.

* Delivery API: Add integration test coverage for the polymorphic discriminator mapping fix

Adds a test-only property editor whose Delivery API value type is a polymorphic interface declared with [JsonDerivedType], wired into the existing typed-schema integration test fixture. The OpenApiContract_HasExpectedSchemas test verifies that the auto-built discriminator mapping refs resolve to the registered derived schema names, providing end-to-end regression coverage for the fix.

Also extends AssertSchemaIsPolymorphicUnion to accept either oneOf (used by our typed schema unions) or anyOf (used by framework-built unions for [JsonDerivedType] interfaces).

* Use a captured schemas local in FixAutoBuiltDiscriminatorMapping

Move the null check for document.Components.Schemas into the top-of-method guard and use the captured non-null local in the loop body. Avoids both the null-conditional ?. operators and the null-forgiving ! operator at the use sites.
2026-05-06 17:09:44 +02:00
Andy Butland bf5f82607e Merge branch 'main' into v18/dev 2026-05-06 16:25:30 +02:00
Sven GeusensandGitHub b2ba4abd7e Code Tidy: Remove obsolete MoveEventInfo.NewParent (#22728)
* Removed obsoleted property

Updated methods that were still using it
Obsoleted constructors that were still setting the value.

* Updated code that were using the now obsoleted constructors

* More obsoleted constructor fixes

* Update unittests

Removed obsolete (parentId) cases and updated constructors

* DRY up constructor
2026-05-06 13:21:22 +00:00
fec0cac557 Delivery API: Generate typed OpenAPI schemas per content type (#22666)
* Delivery API: Generate typed OpenAPI schemas per content type

* Honour Delivery API allow/deny list in typed OpenAPI schemas

ContentTypeSchemaTransformer now filters DocumentTypes through
DeliveryApiSettings.IsAllowedContentType so document types blocked
by AllowedContentTypeAliases / DisallowedContentTypeAliases no longer
leak into the polymorphic union or discriminator mapping.

* Stop registering media derived types in the JSON resolver

ContentJsonTypeResolverBase.GetDerivedTypes goes back to returning
empty. Previously it registered ApiMediaWithCrops and
ApiMediaWithCropsResponse as derived types of their interfaces, which
made every consumer of the resolver (the Delivery API and webhooks)
emit a $type discriminator on media payloads, even when the typed
schema feature was disabled.

The Delivery API still needs a base schema for the typed media
schemas to extend via allOf. Since the concrete media classes are
internal to Umbraco.Infrastructure and cannot be referenced from
[JsonDerivedType] in Core, ContentTypeSchemaTransformer now builds
that base from the interface's own properties when the interface has
no [JsonDerivedType] entries. Content/element interfaces are
unaffected and keep using their declared concrete derived types.

Snapshots regenerated.

* Drop default JsonDerivedType registrations from Delivery API interfaces

Removes the [JsonDerivedType] attributes from IApiContent,
IApiContentResponse, and IApiElement. Without them System.Text.Json
configures no polymorphism by default, so wire payloads stop carrying
$type fields and the OpenAPI spec stops emitting a discriminator on
the generic schemas - matching v17 Delivery API behaviour. Consumers
that need polymorphic serialization can still register derived types
via ContentJsonTypeResolverBase.

Snapshots regenerated.

* Allows nulls at property reference sites without mutating any shared component schema.
Avoid unnecessary re-get of the JsonTypeInfo for the default case.

* Updated expected contracts following code adjustments

* Drop additionalProperties: false from typed schemas

JSON Schema 2020-12 (mandated by OpenAPI 3.1) does not let additionalProperties look through allOf, so a strict validator rejects every inherited field on the composed *ResponseModel/*Model/*PropertiesModel schemas. Most code generators silently ignore it, but the document is technically invalid and the constraint would be a lie anyway since Umbraco can grow new properties in non-major releases.

Removed from all four schema construction sites (response, content type, properties, and the interface-based fallback) and regenerated the affected snapshots.

* Preserve casing of content type aliases in OpenAPI schema IDs

Replaces the legacy ModelsBuilder-style ToCleanString tokenizer with
ToFirstUpperInvariant. The tokenizer split aliases on case boundaries
and mangled capital-letter runs (e.g. "xMLSitemap" -> "XMlsitemap"),
making the typed schema names harder to read for OpenAPI consumers.
Since content type aliases are already valid identifiers, only the
first character needs uppercasing.

Also adds an "xMLSitemap" sample type to the integration tests to
cover the casing-preservation behavior.

* Qualify properties model schema IDs by item type

Document, element, and media types share the same alias namespace
across content/media (a doc-type and a media-type can use the same
alias), so a "{Schema}PropertiesModel" naming scheme could collide.

Properties model schemas now follow the same Content/Element/Media
suffix as their parent *Model schema:

- Document type: ArticlePageContentPropertiesModel
- Element type:  TestElementElementPropertiesModel
- Media type:    VideoMediaPropertiesModel

Composition references look up each composition's own IsElement so
that a doc-type composing an element-type (allowed in the UI) still
references the correct ElementPropertiesModel schema.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-06 12:37:15 +00:00
1bda1c0ef6 Global Elements: User permissions for Element Folders (#22274)
* feat(elements): add granular user permissions for element folders

Add element-folder entity type to applicable entityUserPermission
manifests (Create, Read, Update, Delete, Move) and register a
separate userGranularPermission with a folder-only picker component.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* refactor(elements): separate element folder permissions into own directory

Move element-folder entityUserPermission and userGranularPermission
manifests into folder/user-permissions/ with dedicated component.
Revert element manifests to element-only forEntityTypes. Also adds
permission condition to folder update entity action and filters
permission names by entity type.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Corrects the type-safety of the "selected" event

* Commented out `userGranularPermission` manifest for Element Folders

* Added specific permission verbs for Element Folders

* Added Element Folder User Permission condition

* Updated entity-action manifest conditions

for Element Folder permissions

* Updated permission prefixes

from `Umb.ElementFolder.` to match the server `Umb.ElementContainer.`

* Add explicit element folder permission handling

* The ElementPermissionService should not authorize against element containers anymore

* More granular read permission handling for trees

* Rename ElementFolder to ElementContainer

* Export element folder user permission constants from @umbraco-cms/backoffice/element

The 6 new element folder permission constants were not re-exported
through the element package barrel, causing the export-consts test
to fail.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Updated manifest conditions for Element Folder delete permission

* Enforce update permission on element folder name field

Added nameWriteGuard rule to the element folder workspace context
that blocks renaming when the user lacks the
Umb.ElementContainer.Update permission.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Fix casing

* Fix incorrect condition aliases on element folder actions

- Remove trashed condition from folderCreateOption (create options modal
  already handles this via the parent create action's conditions)
- Use folder-specific permission condition alias on recycle-bin folder
  trash action instead of the generic element permission condition

* Renamed to `ElementContainerPermissionPresentationModel`

to match the server's future naming of this model.

* refactor(elements): apply review feedback for folder permissions

- Switch nameWriteGuard to fallbackToNotPermitted policy, so the rename
  guard expresses intent as "default deny, allow when permitted" rather
  than relying on a permitted:false rule cleared by the condition.
- Rename #enforceUpdatePermission to #setupNameWritePermissions for
  clarity (the method now manages a positive-grant rule).
- Make condition's #elementFolderPermissions and #fallbackPermissions
  optional so "not loaded" is distinguishable from "loaded empty";
  bail out early in #checkPermissions until both have populated, to
  avoid evaluating permissions against incomplete data.
- Drop constructor consumption of UMB_MODAL_MANAGER_CONTEXT in the
  granular permission input element; resolve the modal manager via
  getContext at call time inside the two action methods that need it.

* Add missing using to fix the failing build

* updates server api types

* Fix build error after clean-ups

* Fixes FE build error

Temporarily defines the `IPermissionPresentationModelElementContainerPermissionPresentationModel` type,
for future use.

* Remove duplicate migration

* Remove another duplicate migration

* Add performance improvements from #22405 to ElementContainerPermissionService and add unit tests to prove it

* Fix element permission authorization for descendants

* Test for descendant element delete permissions before deleting an element container

* Update tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ElementPermissionServiceTests.cs

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Co-authored-by: kjac <kja@umbraco.dk>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-06 11:30:52 +00:00
Andy ButlandandGitHub 179a3c8c5a Code Tidy: Clean up further obsoleted code scheduled for removal in Umbraco 18 (IFileService) (#22675)
* Remove the obsolete IFileService, the implementation and update all callers.

* Extend ServiceContext to include replacement service.

* Restore fallback behaviour for resolved users.

* Make TrySetTemplate async to avoid sync-over-async with new services.

* Addressed code review feedback.

* Reverted updates to stylesheet properties.

* Add helper and tests for path splitting.

* Ensure create of directory path on package data import.

* Verification with integration test.
2026-05-06 19:38:33 +09:00
Sven GeusensandGitHub d40eded264 Clarified BackOfficeTokenCookieSettings obsoletion message (#22727)
* Clarify obsoletion message

* Update obsoletion message with better templating/language
2026-05-06 10:29:35 +00:00
35fbc75f8d Typeloader: Comply with public obsoletion by making the Properties internal (#22726)
* Comply with public obsoletion by makng the Properties internal

* Tidied up XML header comments.

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

* Fixed indents.

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-06 09:54:03 +00:00
Lan Nguyen ThuyandNguyenThuyLan 417e63028d update custom property editor setup for acceptance test 2026-05-06 10:17:28 +07:00
Sven GeusensandGitHub 34569e48f0 Update SupportsBlockLayoutAlias obsoletion timeframe (#22715) 2026-05-05 15:49:41 +00:00
2c9acb38f0 Management API: Override document-level security on AllowAnonymous endpoints (#22712)
* Management API: Override document-level security on AllowAnonymous endpoints

Operations on controllers/actions decorated with [AllowAnonymous] inherit the
document-level Bearer security requirement in OpenAPI 3.x unless they explicitly
declare an empty security array. Without that override, the generated SDK
attaches an Authorization: Bearer header to anonymous endpoints (server/status,
server/configuration, install/*, manifest/manifest/public, etc.), which forces
a /security/back-office/token refresh during the very first page load.

On v18/dev this manifests as a 500 from /server/status during a fresh install:
the Authorization header triggers OpenIddict, which resolves UmbracoDbContext
from DI, which throws because the connection string is empty in the install
state.

The transformer now sets operation.Security = [] on AllowAnonymous endpoints so
they correctly opt out of the document-level security. The committed OpenApi.json
and the regenerated sdk.gen.ts reflect this.

* Management API: Fix unit tests for AllowAnonymous security override

The transformer now sets operation.Security = [] (empty list) on
[AllowAnonymous] endpoints to override document-level security, instead
of leaving it null. Update the two affected tests to assert the new
behaviour and rename them to reflect that the transformer overrides
rather than skips security on anonymous operations.

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-05-05 15:17:38 +00:00
a50397f4e6 Elements: Clean up the remaining TODOs (#22689)
* Management API sweep

* Remove leftover comment from ContentService

* Clarify TODOs

* Use IPublishedElementCache instead of IElementCacheService in ElementPickerValueConverter

* Rename private helper for clarification

* Fix build error

* Remove "Create" from ElementService, as it was only ever used for tests

* Rename DocumentVariantStateModel to PublishableVariantStateModel in backoffice client

Refresh OpenApi.json and regenerate backend-api after the server-side enum rename, then update all client imports and usages to match.

* Client: Aliased `PublishableVariantStateModel` for each module package

* Client: Resolve circular dependencies for variant-state alias

Hoist the `UmbDocumentVariantState` and `UmbElementVariantState` aliases (re-exporting `PublishableVariantStateModel`) into dedicated `variant-state.ts` files. Internal modules now import the alias from this leaf file instead of the package's root `index.js`, breaking the 5 cycles reported by `npm run check:circular` while keeping the public API surface unchanged.

* Post-merge fixes

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-05-05 16:40:14 +02:00
c7ba6506aa E2E: QA Updated acceptance tests to reflect UI changes in v18 (#22709)
* Updated webhook tests since Change the default payload type to "minimal"

* Added .skip tag for block grid area tests due to the actual issues

* Update template tests due to test helpers changes

* Updated locator for rollback button due to UI changes

* Updated locator for block edit button due to UI changes

* Updated locator for delete block icon

---------

Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
2026-05-05 14:38:59 +00:00
Laura NetoandGitHub 96ae2f384f Delivery API: Drop $type discriminator from response payloads (#22710)
* Delivery API: Drop $type discriminator from response payloads

Removed [JsonDerivedType] from IApiContent and IApiContentResponse so
System.Text.Json stops emitting $type on collection endpoints and the
OpenAPI spec stops requiring a discriminator on the generic schemas,
restoring v17 behaviour. Consumers that need polymorphic responses can
still register derived types via ContentJsonTypeResolverBase.

Snapshot regenerated.

* Delivery API: Preserve cultures property order on collection responses

Added [JsonPropertyOrder(100)] to IApiContentResponse.Cultures so the
property is serialized last when the static type is the interface
(collection endpoints), matching the existing attribute on the concrete
ApiContentResponse class. Mirrors the [JsonPropertyOrder(-100)] pattern
already used for ContentType on IApiElement / ApiElement.

Snapshot regenerated.
2026-05-05 15:37:22 +02:00
f158e6601b Code Tidy: Remove unused obsoleted InstalledPackage mapping (#22713)
* Remove unused obseleted InstalledPackage mapping

* Fix up XML header documentation on PackageViewModelMapDefinition.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-05 12:18:25 +00:00
8e6e0e7f05 Code Tidy: Clean up further obsoleted code scheduled for removal in Umbraco 18 (ILocalizationService) (#22677)
* Remove the obsolete ILocalizationService and implementation and update all callers to non-obsolete alternatives.

* Addressed code review feedback.

* Fixed failing integration test.

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2026-05-05 11:50:45 +00:00
Jacob Overgaard d0648ac7df chore: updates references to renamed uui-css.css -> light.css file 2026-05-05 13:31:11 +02:00
c257b91443 Code Tidy: Make name non-nullable on content/element/media/member constructors (#22638)
* Remove overloads for creation of content that allow for null name.

* Add defensive null guard on create media.

* Remove unused publishedValueFallback parameter in published content models.

* Fixed failing integration tests.

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2026-05-05 12:10:47 +02:00
Laura NetoandGitHub 6866a964f6 Elements: Add elements to the backoffice global search (#22674)
* Backoffice Element Search: add global search provider for elements

Adds an "Elements" category to the backoffice global search, scoped to
the Library section.

Server: SearchElementItemController exposes
  GET /umbraco/management/api/v1/item/element/search
backed by IEntitySearchService (DB-backed name match, mirrors the
DataType search pattern). Maps results via IElementPresentationFactory.

Client: new src/packages/elements/search/ module with a search provider,
repository, server data source, search-result-item element, and
globalSearch manifest (alias Umb.GlobalSearch.Element). Wired into the
elements package manifests. Backend SDK regenerated from OpenApi.json.

* Backoffice Element Search: surface ancestors, trashed and draft state

- New ancestors endpoint at /item/element/ancestors so result items can
  render a parent breadcrumb (uses NamedItemResponseModel to cover
  element folder ancestors).
- ElementItemResponseModel.IsTrashed added and populated by the
  presentation factory, flowing through search and item responses.
- Frontend search result item renders breadcrumb, Trashed tag with
  strike-through, and Draft tag (mirrors document search result item).

* Address PR review feedback

- Add integration test for AncestorsElementItemController (mirrors
  AncestorsDocumentItemControllerTests).
- UmbElementSearchItemModel: declare `name: string` (the search result
  contract requires it; mirrors UmbDocumentSearchItemModel).
- Element search data source: drop the empty-string fallback on `name`
  and add the same TODO comment used in the document data source.
- Add JSDoc to UmbElementSearchProvider, UmbElementSearchRepository and
  UmbElementSearchServerDataSource (matches document equivalents).

* Backoffice Element Search: export search consts and unblock isTrashed on item endpoint

- Re-export ./search/constants.js from the elements package barrel so
  UMB_ELEMENT_SEARCH_PROVIDER_ALIAS and UMB_ELEMENT_GLOBAL_SEARCH_ALIAS
  are reachable as the export-consts test expects.
- element-item.server.data-source.ts: stop hardcoding isTrashed to false
  - now that ElementItemResponseModel exposes the flag, item-based UIs
  reflect the actual trashed state.
2026-05-05 10:08:37 +00:00
Andy ButlandandGitHub 233865a1e6 Code Tidy: Clean up obsoleted code scheduled for removal in Umbraco 18 (IDomainService, IContentTypeBaseService) (#22629)
* Fix naming warning in UmbracoIntegrationTestBase.

* Fixes a namespace.

* Removed TODO for removing registrations of UserPasswordConfigurationSettings and MemberPasswordConfigurationSettings. The inheritance hierarchy of UmbracoUserManager makes this difficult and unnecessary to unpick.

* Aligned TODO with obsoletion message.

* Remove obsoleted code from IDomainService and update all callers.

* Removed obsolete members from IContentTypeBaseService.

* Addressed code review feedback.

* Fix Setup on ThreadSafetyTests.
2026-05-05 10:02:18 +00:00
877e93aec2 Elements: Add the Library section to the admin group on upgrade (#22706)
* Add the Elements section to the admin group on upgrade

* Update src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/AddElementSectionForAdmins.cs

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-05-05 09:58:19 +00:00
Niels Lyngsø adf02910ab Merge branch 'main' into v18/dev 2026-05-05 10:30:57 +02:00
Niels Lyngsø 89e89a38ab add library package 2026-05-05 09:56:47 +02:00
Niels Lyngsø fa123444bd re-introduce element package 2026-05-05 09:48:59 +02:00
Niels Lyngsø 5833269196 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/ConstantHelper.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/ContentUiHelper.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/UserApiHelper.ts
2026-05-05 09:24:02 +02:00
Andy ButlandandGitHub 0c9b817372 Code Tidy: Clean up obsoleted code scheduled for removal in Umbraco 18 (IDataTypeService) (#22634)
* Remove obsolete methods from IDataTypeService and update callers.

* Fixed failing integration test and resolved code review feedback.

* Further code review feedback.

* Introduce shared helper for retrieving data type from property type.
2026-05-05 05:06:50 +00:00
Sven GeusensandGitHub 29ecd48cc1 Migrations: Removed obsoleted MigrationBase and all migrations between old and current LTS (13-17) (#22618)
* Change AddElements to a premigration

* Move AddAllowedInLibraryToContentType to premigration

* Remove old migrations and remove obsoleted migratiobase

Includes updating existing migrations and tests to AsyncMigrationBase

* Update claude files

* update xml comment

* Set correct initalstate

* More cleanup

* Remove old migration tests

* Put the test ignore on the right testcase 🙈

* cleanup async migrateasync calls without awaits in tests

* Updated InitialStateVersion
2026-05-05 06:48:16 +02:00
Andy ButlandandGitHub 6c2473aeb0 Code Tidy: Remove package validation suppression files (#22696)
Removed CompatibilitySuppressions.xml files.
2026-05-05 10:14:10 +09:00
Andy ButlandandGitHub 7e2edd4733 Dependencies: Bump selected NuGet packages to latest versions (#22693)
* Bumped selected dependencies to the latest versions.

* Resolved warning seen on dotnet restore.
2026-05-05 09:09:00 +09:00
Laura NetoandGitHub 0d6aedac7c Management API: Refactor element tree controllers to use start node filter service (#22598)
* Refactor element tree controllers to use start node filter service

Move user start node filtering logic from UserStartNodeFolderTreeControllerBase
and ElementTreeControllerBase into a dedicated ElementStartNodeTreeFilterService,
matching the pattern established for document and media trees in PR #22486.

Add a virtual TreeObjectTypes property to UserStartNodeTreeFilterService so
element trees can query both Element and ElementContainer object types.

* Address PR review feedback

* Apply PR review feedback

Replace TreeObjectType (singular) with abstract TreeObjectTypes (array).
Use static readonly arrays in concrete implementations to avoid
allocations.

Move multi-object-type test into UserStartNodeTreeFilterServiceTests
since it exercises base class behavior, not the element service
specifically.
2026-05-04 23:13:23 +02:00
7eb586f520 Code Tidy: Clean up further obsoleted code scheduled for removal in Umbraco 18 (IMemberService.GetMembersByPropertyValue) (#22678)
Removed obsolete methods on IMemberService, their implementations and the related tests.

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-05-04 14:59:49 +00:00
Andy ButlandandGitHub 138e29db58 Code Tidy: Remove obsolete code scheduled for removal in Umbraco 18 (UmbracoApiController and front-end API auto-routing) (#22692)
* Remove UmbracoApiController and associated code.

* Test naming and attributes.
2026-05-04 16:13:22 +02:00
Andy ButlandandGitHub 1a87feb84d Code Tidy: Remove obsolete code scheduled for removal in Umbraco 18 (UrlSegment extension method) (#22682)
* Remove obsolete UrlSegment extension methods and update callers. Clarify obsoletion of UrlSegment property for Umbraco 19.

* Use 20 for the new obsoletions.

* Align all existing obsolete and non-obsolete calls to retrieve a URL segment to use IDocumentUrlService.

* Fix failing tests.

* Revert incorrectly updated obsoletion removal version.
2026-05-04 13:36:58 +00:00
2434c3ec7b Global Elements: Implements auditLog and contentRollback kinds for History and Rollback (#22633)
* Implements `auditLog` kind for Elements

* Implements `contentRollback` kind for Elements

* Adds JSDoc comments to element rollback repository and data source

* Exports `UMB_ELEMENT_AUDIT_LOG_REPOSITORY_ALIAS` from `@umbraco-cms/backoffice/element`

Surfaces the constant through the element audit-log barrel so it's available on the public package entry, matching the documents audit-log pattern.

* Added `rollbackNotificationMessage` for Element Rollback

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-04 12:20:10 +00:00
c725881e67 Elements: Published element extensions (#22585)
* Add element-level extension methods for variance, culture, fallback support

Published Element Extensions now support the same culture, type-checking,
equality, and creator/writer methods that were previously only available
on Published Content Extensions. Content extensions delegate to the
element versions, preserving backwards compatibility.

New element extensions (Core):
- HasCulture, IsInvariantOrHasCulture, CultureDate
- IsDocumentType (both overloads)
- IsEqual, IsNotEqual
- GetCreatorName, GetWriterName
- HasValue with IPublishedValueFallback and Fallback support

New friendly element wrappers (Web.Common):
- Name, CultureDate, CreatorName, WriterName

New non-friendly element extensions (Web.Common):
- CreatorName, WriterName (with IUserService parameter)

* Add unit tests for PublishedElement extension methods

Tests for core extensions (HasCulture, IsInvariantOrHasCulture,
CultureDate, IsDocumentType, IsEqual/IsNotEqual, GetCreatorName,
GetWriterName, HasValue with fallback) and friendly wrappers (Name,
CultureDate, CreatorName, WriterName). All mocks use MockBehavior.Strict.

* Fix empty XML doc param tag for variationContextAccessor in CultureDate

* Delegate content CreatorName/WriterName to element friendly extensions, remove redundant UserService field

* Address PR review: restore StaticServiceProvider in TearDown, use case-insensitive culture dictionary in tests

* Add remarks note about Fallback.ToAncestors not being supported at element level

* Clarify the casting for readability

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-05-04 13:30:27 +02:00
Andy Butland 51892a840c Merge branch 'v18/dev' of https://github.com/umbraco/Umbraco-CMS into v18/dev 2026-05-04 13:15:20 +02:00
Andy Butland a2fa694553 Merge remote-tracking branch 'origin/main' into v18/dev 2026-05-04 13:15:00 +02:00
Laura NetoandGitHub 4e74e33dde Templates: Update Umbraco extension template for OpenAPI route changes (#22670)
* Update Umbraco extension template for OpenAPI route changes

Following the migration from Swashbuckle to Microsoft.AspNetCore.OpenApi
in #21058, the extension template still pointed at the old Swagger URL
pattern and used outdated terminology in code comments.

- generate-client npm script now points at /umbraco/openapi/{name}.json
  instead of /umbraco/swagger/{name}/swagger.json
- generate-openapi.js renames swaggerUrl to openApiUrl and updates the
  example URL in the missing-argument error message
- UmbracoExtensionApiComposer.cs comments updated from "Swagger" to
  "OpenAPI"

* Scope custom OpenAPI document to extension's own endpoints

Without an explicit ShouldInclude predicate, Microsoft.AspNetCore.OpenApi
only includes endpoints whose ApiExplorer GroupName equals the document
name. The template's controller declared a different group name, so the
custom document was created but stayed empty (paths: []), which in turn
made npm run generate-client produce an empty TypeScript SDK.

Filter by the [MapToApi] attribute already present on the extension's
controller base, mirroring the pattern used by the Management and
Delivery API options.

* Add Microsoft.AspNetCore.OpenApi reference to Central package management

The PerProject mode of the umbraco-extension template took a direct
dependency on Microsoft.AspNetCore.OpenApi (with a long comment
explaining why) but the Central mode did not, so default Central
scaffolds failed to build with the source-generator interceptors
error. Mirror the dependency in the Central csproj block and
Directory.Packages.props.
2026-05-04 12:16:48 +02:00
2d4418b36f Code Tidy: Clean up further obsoleted code scheduled for removal in Umbraco 18 (LogFiles, legacy permissions tables, LoggerConfigExtensions) (#22679)
* Remove obsolete logger configuration extensions.

* Removed obsolete database table DTO and constants.

* Removed obsolete LogFiles constant.

* Moved SuperUserId constants obsoletion to 19.

* Remove further reference to removed table.

* Comment out reference to removed table in migration that is also for removal for 18.

* Remove further obsolete methods from LoggerConfigExtensions

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-04 12:08:16 +02:00
Nhu DinhandGitHub b638fa0c73 E2E: Revert npm command for smokeTest (#22683)
* Revert npm command for smokeTest

* Updated audit trail for trash content
2026-05-04 15:48:20 +07:00
101acdd583 Templates: Rename "Master Template" to "Layout Template" (#21743)
* Rename "Master Template" to "Layout Template" throughout the codebase

Since Umbraco switched from WebForms to MVC, the "Master" template
terminology has been incorrect — in Razor/MVC the parent template is
called a "Layout", not a "Master page". This renames the concept across
C# models, services, repositories, the Management API, and the
backoffice frontend while preserving backward compatibility via
[Obsolete] members scheduled for removal in Umbraco 20.

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

* Remove dead template layout XML element code and rename test methods

The serializer code that wrote <Master>/<MasterAlias> elements was never
executed because Lazy<int>.IsValueCreated was always false after loading
from the database. The corresponding import code that read these elements
was equally dead since no package.xml ever contained them. Template
parent-child hierarchy is resolved from Razor Layout directives instead.

Also renames 4 test methods from "Master" to "Layout" to match the
updated terminology.

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

* Packaging: Suppress noisy log when imported Template has no Layout

A null Layout is legitimate for root layout files (e.g. `Layout = null;`)
and shouldn't be reported as "invalid". Only log when a non-null Layout
was referenced but couldn't be resolved in the import.

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

* Update acceptance tests and further references in comments.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Sebastiaan Janssen <sebastiaan@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-04 10:01:42 +02:00
Lee KelleherandGitHub ad904d6b05 Global Elements: Localize bulk publish/unpublish notifications (#22641)
* Localizes element bulk publish/unpublish notifications

* Removed the "visible on the website" part from localizations for Elements.
2026-05-04 04:35:30 +00:00
Andy ButlandandGitHub b499a0e121 Code Tidy: Clean up obsoleted code scheduled for removal in Umbraco 18 (IMemberGroupService) (#22632)
Remove obsolete methods on IMemberGroupService and update callers.
2026-05-03 09:47:38 +02:00
4543856ce1 Elements: Clear element entity cache on content type changes (#22362)
fix(core): clear element entity cache on content type changes

The ContentTypeCacheRefresher clears IContent isolated cache when a
content type changes, but did not clear IElement cache. This caused
stale element entities to be returned after modifying property type
variation settings (e.g. enabling vary-by-culture), leading to 500
errors when saving elements.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-05-01 13:09:09 +00:00
5a545fa122 Open API: Use Microsoft.AspNetCore.OpenApi for Open API document generation (#21058)
* Uninstall `Swashbuckle.AspNetCore` and install `Microsoft.AspNetCore.OpenApi`

Also installed `Swashbuckle.AspNetCore.SwaggerUI` for now to use as UI only.

* Registered UI and removed or commented out Swashbuckle specific code

* Started configuring the different Open API documents

* Started moving configuration

* Simplifying configuration

* Added missing configuration for the Delivery API

* Added missing configurations for Management API

Still missing polymorphism settings for both APIs

* Adjust Umbraco Extension template with OpenApi changes

* Handle sub types in open api document generation

* Renaming mime types transformer to align with others

* Added discriminator configuration

* Reference Umbraco.Cms.DevelopmentMode.Backoffice from integration tests project to avoid models mode exception being logged in tests

* Now configuring and using the HTTP json options instead of having custom transformers for handling enums and polymorphism

* Fixes to examples

* Update OpenAPI packages

* Mark most transformers as internal

* Simplify adding backoffice security requirements to your API

* Fix missing required properties

* Re-order transformers to fix missing notification headers

* Fix most build errors after regenerating client

* Fix mime types transformer being applied to Management API

* Additional fixes

* Additional fixes to file response types

* Configure Swagger UI documents

* Clear server list

* Sort APIs in UI

* Re-introduce schema handlers and fix issue with nullable enum schema name

* Simplify examples

* Small optimization

* Simplify nullability check in RequireNonNullablePropertiesSchemaTransformer

* Remove unused property

* Small fixes suggested by Claude

* Undo unintended space changes

* Add unit tests for OpenAPI transformers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Add unit tests for additional OpenAPI transformers

- RequireNonNullablePropertiesSchemaTransformer (7 tests)
- BackOfficeSecurityRequirementsTransformer (10 tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Rename SwaggerGen classes to OpenApi for consistency

- Rename ConfigureUmbracoDeliveryApiSwaggerGenOptions to ConfigureUmbracoDeliveryApiOpenApiOptions
- Rename ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions to ConfigureUmbracoMemberAuthenticationDeliveryApiOpenApiOptions
- Rename ConfigureUmbracoManagementApiSwaggerGenOptions to ConfigureUmbracoManagementApiOpenApiOptions
- Rename SwaggerRouteTemplatePipelineFilter to OpenApiRouteTemplatePipelineFilter
- Update DI registrations to use new class names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Update OpenAPI contract test for Microsoft.AspNetCore.OpenApi

Update expected Delivery API OpenAPI contract to reflect changes from
the migration to Microsoft.AspNetCore.OpenApi:

- OpenAPI version 3.0.4 → 3.1.1
- Nullable types now use type array format (OpenAPI 3.1 style)
- Polymorphic types use anyOf with discriminator
- Security moved from header parameter to securitySchemes
- Removed unnecessary oneOf wrappers around single $ref

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Disable Models Builder in integration tests by default

* Rename Swagger references to OpenApi for consistency

- Rename SwaggerIsEnabled to OpenApiIsEnabled
- Rename SwaggerRouteTemplate to OpenApiRouteTemplate
- Rename SwaggerUiRoutePrefix to OpenApiUiRoutePrefix
- Rename SwaggerUiConfiguration to ConfigureOpenApiUI
- Rename swaggerPipelineFilter variable to openApiPipelineFilter
- Update code comments from "Swagger JSON" to "OpenAPI specification"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Re-generate Management API open api doc and UI client after merge

* Add reference in comment to additional PR to fix file return types schema

* Fix Open API validation errors

* OpenAPI: Replace ISchemaIdHandler/ISchemaIdSelector with static UmbracoSchemaIdGenerator

Remove the DI-based schema ID handler/selector pattern and replace with a
static UmbracoSchemaIdGenerator utility class. This allows both Umbraco code
and external consumers to call the schema ID generation logic directly, which
is useful since the Microsoft OpenAPI package's schema selectors only apply
to Umbraco's own OpenAPI documents.

- Remove ISchemaIdHandler, ISchemaIdSelector interfaces and implementations
- Add static UmbracoSchemaIdGenerator.Generate() method
- Update ConfigureUmbracoOpenApiOptionsBase to use UmbracoSchemaIdGenerator directly
- Remove constructor dependencies from API options classes
- Add unit tests for UmbracoSchemaIdGenerator and CreateSchemaReferenceId

* Rename CustomOperationIdsTransformer to UmbracoOperationIdTransformer and make public

- Rename class to better reflect its purpose as Umbraco's operation ID transformer
- Change visibility from internal to public so it can be used by external consumers
- Update XML documentation to clarify usage for custom OpenAPI configurations

* OpenAPI: Update Delivery API contract test for new document format

Update expected OpenAPI output to include explicit empty values in
examples and consistent array formatting in security requirements.

* OpenAPI: Remove obsolete DocumentInclusionSelector abstraction

The document inclusion logic is now handled directly by
ConfigureUmbracoOpenApiOptionsBase.ShouldInclude(), making
the separate IDocumentInclusionSelector abstraction unnecessary.

* OpenAPI: Reorganize Management API OpenApi folder structure

- Move transformers to OpenApi/Transformers subfolder
- Move OpenApiOptionsExtensions from Extensions to OpenApi folder
- Update namespaces accordingly:
  - Umbraco.Cms.Api.Management.OpenApi.Transformers (transformers)
  - Umbraco.Cms.Api.Management.OpenApi (extensions)

* OpenAPI: Add ExcludeFromDefaultOpenApiDocument attribute

- Add [ExcludeFromDefaultOpenApiDocument] attribute for excluding controllers from the default OpenAPI document
- Make ShouldInclude method protected virtual in ConfigureUmbracoOpenApiOptionsBase for extensibility
- Override ShouldInclude in ConfigureDefaultApiOptions to check for the exclusion attribute

* OpenAPI: Add UmbracoOpenApiOptions for configuring OpenAPI routes

Add UmbracoOpenApiOptions configuration class to allow customizing:
- Enabled: Enable/disable OpenAPI and Swagger UI (default: non-production)
- RouteTemplate: Route template for OpenAPI JSON documents
- UiRoutePrefix: Route prefix for Swagger UI

Umbraco sets defaults via Configure, users can override via PostConfigure.
Simplify OpenApiRouteTemplatePipelineFilter to use options directly.

* Pipeline filters: Add OnPreMapEndpoints and rename OnEndpoints to OnPreEndpoints

- Add OnPreMapEndpoints method to IUmbracoPipelineFilter for registering
  endpoints inside UseEndpoints without calling UseEndpoints twice
- Rename OnEndpoints to OnPreEndpoints (with backward-compatible default)
- Add PreMapEndpoints and PreEndpoints properties to UmbracoPipelineFilter
- Mark OnEndpoints and Endpoints as obsolete (removal in Umbraco 19)
- Update UmbracoApplicationBuilder to call OnPreMapEndpoints inside UseEndpoints
- Remove redundant UseEndpoints() call from BackOfficeManagementApiFilter
- Update LoadTestController to use PreMapEndpoints instead of Endpoints

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

* OpenAPI: Move MapOpenApi to PreMapEndpoints hook

Move OpenAPI endpoint mapping from PostPipeline to PreMapEndpoints
to avoid calling UseEndpoints twice in the pipeline.

* OpenAPI: Rename URL paths from swagger to openapi

- Change OpenAPI UI and document URLs from /umbraco/swagger to /umbraco/openapi
- Rename OAuth client constant from Swagger to OpenApiUi (value kept as
  umbraco-swagger for backwards compatibility with existing DB registrations)
- Update display name to "Umbraco OpenAPI access"
- Add DefaultUiEnabled option to allow disabling the default UI while
  keeping OpenAPI documents available (enables use of alternative UIs)
- Update MiniProfiler ignored path

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

* OpenAPI: Update Microsoft.AspNetCore.OpenApi to 10.0.2

* OpenAPI: Add AddOpenApiDocumentToUi extension method

Adds a public extension method to simplify adding OpenAPI documents to the
UI document selector. This respects the configured UmbracoOpenApiOptions
route template, so users don't need to hardcode paths.

The documentTitle parameter is optional and defaults to the documentName.

Also updates the UmbracoExtension template to use the new method and
fixes the documentation URL reference.

* OpenAPI: Make OpenApiRouteTemplatePipelineFilter internal

The class has no extension points (all methods are private static) and
customization is now done via UmbracoOpenApiOptions instead.

* OpenAPI: Rename DeliveryApiSecurityFilter to DeliveryApiSecurityTransformer

Aligns naming with other OpenAPI transformers for consistency.

* OpenAPI: Simplify Delivery API member authentication configuration

Replace ConfigureUmbracoMemberAuthenticationDeliveryApiOpenApiOptions with
a simpler AddDeliveryApiOpenApiMemberAuthentication() extension method on
IServiceCollection. This hides implementation details and provides a cleaner
API for users to enable member authentication in the Delivery API OpenAPI document.

* OpenAPI: Add reference to proposal for custom JSON options support

* Move Delivery API transformers to OpenApi/Transformers folder

Aligns the folder structure with the Management API project.

* Update OpenAPI contract tests to use new URL format

Changed from /swagger/{name}/swagger.json to /openapi/{name}.json

* Apply suggestions from code review

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

* Update src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs

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

* Fix IAuthorizationService injection detection in BackOfficeSecurityRequirementsTransformer

- Fix bug where parameter.GetType() was used instead of parameter.ParameterType,
  causing the IAuthorizationService injection check to always return false
- Replace magic number with BaseAuthorizeAttributeCount constant
- Improve comments explaining the 403 response logic
- Add test for IAuthorizationService injection detection

* Remove unnecessary InterceptorsNamespaces from API projects

* Remove default implementations from IUmbracoPipelineFilter methods

* Update documentation for Microsoft.AspNetCore.OpenApi migration

- Update CLAUDE.md files to reflect the migration from Swashbuckle to Microsoft.AspNetCore.OpenApi for document generation
- Update URL paths from /umbraco/swagger/ to /umbraco/openapi/
- Rename swaggerPath variables to openApiPath in test files
- Update references to removed types (SchemaIdHandler, OperationIdHandler, etc.) with their new equivalents (UmbracoSchemaIdGenerator, UmbracoOperationIdTransformer)
- Remove outdated technical debt reference to deleted SwaggerDocumentationFilterBase

* Update Swashbuckle.AspNetCore.SwaggerUI to 10.1.2

Fixes browser caching behavior and document URL serialization issues.

* Refactor OpenAPI contract tests with validation

- Add OpenAPI spec validation for both Delivery and Management APIs
- Delivery API: Store expected contract in external JSON file for regression testing
- Management API: Compare generated contract against expected contract endpoint
- Organize Delivery API tests under OpenApi/ subdirectory
- Auto-generate Delivery API contract file if it doesn't exist

* Update ElementReferenceResponseModel type reference after OpenAPI regeneration

* Add discriminator values to Delivery API polymorphic JSON serialization

ConfigureJsonPolymorphismOptions now passes derivedType.Name as the
discriminator value for each JsonDerivedType, ensuring the $type property
is present in responses and the OpenAPI schema is valid.

* Move Delivery API OpenAPI contract tests to Umbraco.Api.Delivery folder

* Update Microsoft.AspNetCore.OpenApi to 10.0.3 and Swashbuckle.AspNetCore.SwaggerUI to 10.1.4

* Use JsonDerivedType attributes for Delivery API polymorphic serialization

Move discriminator configuration from ContentJsonTypeResolverBase to
JsonDerivedType attributes on the interfaces. This is the standard STJ
approach and keeps the resolver available for custom overrides only.

* Add OpenAPI test for custom derived type extensibility

Extract shared test infrastructure into OpenApiTestBase and add
OpenApiCustomDerivedTypeTest to verify the OpenAPI spec remains valid
when a consumer registers a custom derived type via
ContentJsonTypeResolverBase.GetDerivedTypes.

* Fix OpenAPI contract test failing on CI due to ContinuousIntegrationBuild path normalization

[CallerFilePath] embeds a compile-time source path that gets normalized to /_/... on Azure DevOps
agents when ContinuousIntegrationBuild=true. At runtime the expected contract file is not found at
that path, causing the test to attempt Directory.CreateDirectory("/_/...") which fails with
permission denied.

Fix by reading contract files from the output directory (CopyToOutputDirectory) instead of the
compile-time source path. The [CallerFilePath] approach is kept only for writing new contracts
during local development, wrapped in a try/catch so it fails gracefully on CI.

* Bump Swashbuckle.AspNetCore.SwaggerUI to 10.1.7

* Remove duplicate InternalsVisibleTo for Umbraco.Tests.UnitTests

* Extract ReplaceOpenApiSchemaService into shared Api.Common helper

Deduplicates the internal OpenApiSchemaService replacement logic
between Management API and Delivery API into a single internal
extension method in Umbraco.Cms.Api.Common. Uses assembly and type
name checks derived from a public type (OpenApiOptions) instead of
hardcoded strings for safer matching.

* Tighten visibility and improve DI extension structure

- Mark FixFileReturnTypesTransformer as internal (temporary workaround)
- Mark AddUmbracoApiOpenApiUI and AddUmbracoApi as internal
- Rename AddUmbracoApi to AddUmbracoOpenApiDocument on IUmbracoBuilder
- Move AddOpenApiDocumentToUi to OpenApiServiceCollectionExtensions
- Encapsulate ReplaceOpenApiSchemaService inside AddUmbracoOpenApiDocument
  as an optional jsonOptionsName parameter

* Regenerate OpenApi.json to fix duplicate document patch endpoint

* Move MimeTypesTransformer to shared base and respect [Consumes]

Moves the MIME type filtering from a Delivery API-only document
transformer to a shared operation transformer in Api.Common. When
[Consumes] is present, replaces content types with exactly what it
declares (fixing application/json-patch+json on the patch endpoint).
Otherwise strips non-application/json types. Regenerates OpenApi.json
and client SDK.

* Move Umbraco-specific transformers from shared base to API configs

RequireNonNullablePropertiesSchemaTransformer, FixFileReturnTypes
Transformer, and MimeTypesTransformer are now registered only in
the Management and Delivery API configs. The default API document
(used for consumer endpoints) no longer applies these opinionated
transformers.

* Clarify XML doc for UmbracoOpenApiOptions.Enabled

* Use alphabetically-first tag across all operations for stable path sorting

* Update MimeTypesTransformer tests for operation transformer interface

* Reference dotnet/aspnetcore#66340 in ReplaceOpenApiSchemaService docs

* Add unit tests to verify OpenApiSchemaServiceExtensions usage of internal types.

* Bumped Microsoft.AspNetCore.OpenApi from 10.0.4 to 10.0.6 to match Directory.Packages.props and removed unnecessary Swashbuckle reference.

* Fix indentation.

* Defensively handle a non-integer status code response key in ResponseHeaderTransformer.

* Use TryGetValue in RequireNonNullablePropertiesSchemaTransformer to avoid potential KeyNotFoundException.

* Additional tests and clarifying comments.

* Revert accidental local dev changes to Program.cs, Web.UI.csproj and StaticAssets.csproj.

* Tighten visibility of OpenAPI configuration and transformer classes to internal

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-30 19:12:44 +00:00
Nhu DinhandGitHub 8a50f80f28 E2E: QA Updated acceptance tests for Global elements to match the UI changes (#22511)
* Updated element creation step due to UI changes

* Updated element creation due to UI changes - cont

* Removed unused locator

* Updated locator for elementTreeItem

* Updated tests for library to match the UI changes

* Updated locator for elementVariantDropdown

* Updated tests for element permission and start nodes

* Removed @smoke tags

* Make tests run in the pipeline

* Added comment for failing tests

* Updated tests for element start nodes as the front-end does not support adding a element as start nodes

* Fix flaky tests

* Fixed comment
2026-04-30 13:08:06 +00:00
Andy ButlandandGitHub 6a754894d2 Code Tidy: Clean up further obsoleted code scheduled for removal in Umbraco 18 (IEmailSender, MemberConfigurationResponseModel, MediaPermissions) (#22642)
* Removed obsolete methods and default implementations on IEmailSender.

* Removed the obsolete and unused MemberConfigurationResponseModel.

* Remove the obsolete MediaPermissions and ensure test coverage is maintained.
2026-04-30 09:56:31 +09:00
Sven GeusensandGitHub 24b25f684a Enable single blocklist migration (#22627)
* Fix incorrect frontend propertyeditor alias

* Fix early return mistake

* Enable the plan

* Add memberTypes to the lookup
2026-04-29 15:54:20 +02:00
leekelleher 99c865e0bd Fix for broken UI test 2026-04-29 14:48:24 +01:00
Andy Butland b68624a961 Merge branch 'main' into v18/dev 2026-04-29 13:55:19 +02:00
leekelleher 2e84d11c53 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Cms.Api.Management/OpenApi.json
#	src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
#	src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts
2026-04-29 12:50:06 +01:00
Andy Butland 94d94e9f46 Merge branch 'main' into v18/dev 2026-04-28 10:34:36 +02:00
Sven GeusensandGitHub 8b504a2916 Change Element migrations to a premigrations (#22617)
* Change AddElements to a premigration

* Move AddAllowedInLibraryToContentType to premigration
2026-04-27 16:30:07 +02:00
32ec824dab Document Types: Show message for non-applicable Element Type settings (#22396)
* Conditionally render history & structure settings

* Show "not applicable" message instead of hiding the settings

* Refactored to reuse `#renderElementDoesNotSupport()`

* Modified "Allow in Library"

to display a message instead of hiding the field.

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-04-24 11:02:25 +00:00
Kenn JacobsenandGitHub 5e4791ea2f Fix the NullableLanguageId build errors after merge (#22589) 2026-04-24 09:42:47 +00:00
Andy Butland 53d46034d5 Merge branch 'main' into v18/dev 2026-04-23 17:06:22 +02:00
Andy ButlandandGitHub 0bf2d04885 Hosting: Make IHostingEnvironment.ApplicationMainUrl nullable (#22558)
* Make ApplicationMainUrl on IHostingEnvironment nullable.
Update usage in HttpsCheck healthcheck and add unit tests to verify refactor.

* Improves XML documentation for the property.
2026-04-23 06:21:14 +00:00
Andy Butland 2daf9dae80 Merge branch 'main' into v18/dev 2026-04-22 14:34:53 +02:00
Andy Butland 23ad35b7d7 Merge branch 'v18/dev' of https://github.com/umbraco/Umbraco-CMS into v18/dev 2026-04-22 12:09:09 +02:00
Andy Butland 99a94fbb93 Post-merge updates for elements. 2026-04-22 12:08:48 +02:00
Andy Butland 3b3a11f31b Merge branch 'main' into v18/dev 2026-04-22 11:25:20 +02:00
f981502781 Dependencies: Revert NUnit 4 upgrade to unblock integration tests (#22562)
Revert "Dependencies: Upgrade NUnit and related test dependencies to latest major versions (#22155)"

This reverts commit 7014f9a125 on v18/dev
to resolve the Part3Of4 SQL Server integration test nightly hangs that
started around 2026-04-10.

Root cause was confirmed by hang-dump analysis: a sync-over-async call
in ContentCacheRefresher.HandleMemoryCache
(.GetAwaiter().GetResult() on an async cache method) that NUnit 3's
pumping synchronization context had been quietly completing on the test
thread. NUnit 4 dropped that behaviour, so the continuation now requires
a free thread-pool thread; under CI conditions it deadlocks.

Reverting #22155 on a branch was verified to make the nightly pass.
This is a temporary rollback to unblock v18; the proper fix is to make
ContentCacheRefresher.Refresh async end-to-end, tracked separately.

Additional adjustments beyond the pure revert to keep the branch
compiling:

- CoreConfigurationHttpTests.cs: added `using Umbraco.Cms.Core.Services;`
  for IUserService (referenced by post-#22155 code unaffected by the
  revert).
- ContentVersionCleanupServiceTest.cs: merged imports so both
  AutoFixture.NUnit3 (from revert) and Microsoft.Extensions.Options
  (from unrelated later commit) stay.
- UdiTests.cs and ContentPermissionResourceTests.cs: removed unused
  `using NUnit.Framework.Legacy;` (namespace introduced in NUnit 4).
- BackOfficeAuthorizationInitializationMiddlewareTests.cs: replaced
  `[CancelAfter(5000)]` with its NUnit 3 equivalent `[Timeout(5000)]`.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 11:04:27 +02:00
Andreas ZerbstandGitHub 40a505dbc3 Integration Tests: Split Windows ManagementApi shard to avoid LocalDb memory pressure (#22559)
* Split Windows ManagementApi shard to avoid LocalDb memory pressure

* Fixed filter
2026-04-22 08:54:07 +02:00
Andy Butland 9adac463e9 Merge branch 'main' into v18/dev 2026-04-21 15:56:36 +02:00
Andy ButlandandGitHub 858710cfec Output Caching: Evict cached documents when a related element is published (#22496)
Evict documents from delivery API and website output cache when related element is published.
2026-04-21 14:10:55 +02:00
Andy Butland 7de26aee7e Merge branch 'main' into v18/dev 2026-04-21 13:15:22 +02:00
Andy Butland 558a6bd724 Merge branch 'main' into v18/dev 2026-04-21 11:23:10 +02:00
Laura Neto b6a048e4f6 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs
#	src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
#	tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackRelationsTests.cs
2026-04-21 11:13:15 +02:00
498c1ce2b8 Hybrid Cache: Element cache (#22369)
* Implement ElementCacheService with HybridCache backing and database cache support

Fully implements ElementCacheService as the elements equivalent of DocumentCacheService,
backed by Microsoft HybridCache (L1 in-memory + L2 distributed) with database cache table
persistence via cmsContentNu.

Key changes:
- ElementCacheService: full implementation with HybridCache, draft/published separation,
  converted element L0 cache, cache tagging, preview service support, and seeding infrastructure
- IDatabaseCacheRepository: added element CRUD methods (Get/Refresh/Rebuild) with SQL queries
  using ElementDto/ElementVersionDto
- IContentCacheService: extracted common base interface shared by Document, Media and Element
  cache services (8 shared methods including Seed, Rebuild, memory cache operations)
- CacheRefreshingNotificationHandler: added element notification handling, content type changes
  now route to element or document service based on IsElement, refactored to single-pass
  classification with shared RefreshCacheForContentTypeChanges method
- ElementRefreshNotification: new notification wired to ElementRepository.OnUowRefreshedEntity
- Renamed document-specific methods for clarity (GetContentSource -> GetDocumentSource,
  RefreshContent -> RefreshDocument, CreateContentNodeKit -> CreateDocumentNodeKit,
  RebuildContentDbCache -> RebuildDocumentDbCache)
- Renamed shared DTOs (CacheRebuildDocumentDto -> CacheRebuildPublishableContentDto) since
  they're used by both documents and elements
- Extracted shared RebuildPublishableDbCache method to eliminate duplication between document
  and element rebuild logic

* Add element navigation service, publish status tracking, and breadth-first seeding

Adds the infrastructure needed for element cache seeding:

- ElementNavigationService: provides tree traversal for elements, following the
  same pattern as DocumentNavigationService/MediaNavigationService
- Split PublishStatusService into an abstract base class with DocumentPublishStatusService
  and ElementPublishStatusService subclasses, each with their own interfaces
  (IDocumentPublishStatusQueryService, IElementPublishStatusQueryService, etc.)
- ElementBreadthFirstKeyProvider: seeds the element cache on startup by traversing
  the element tree breadth-first, filtering out unpublished elements
- Element publish status is initialized on startup and kept in sync via
  ElementCacheRefresher
- Old IPublishStatusQueryService/IPublishStatusManagementService interfaces kept
  as obsolete for backward compatibility
- Non-breaking constructor changes for ContentCacheRefresher, DocumentUrlService,
  ApiContentRouteBuilder via obsolete constructor overloads

* Fix element CacheNodeFactory to set IsDraft from preview parameter

CacheNodeFactory.ToContentCacheNode(IElement, bool preview) was hardcoding
IsDraft = false instead of using the preview parameter. This caused
RefreshElementAsync to never write draft cmsContentNu rows, because
DatabaseCacheRepository.RefreshElementAsync skipped the draft write when
IsDraft was false.

* Use ElementTree lock instead of ContentTree in ElementCacheService

RefreshMemoryCacheAsync was using Constants.Locks.ContentTree instead of
Constants.Locks.ElementTree for the read lock.

* Add ElementCacheServiceTests and fix PublishStatusServiceTests for abstract base

- ElementCacheServiceTests: 9 integration tests covering draft/published retrieval,
  rebuild, delete, and RefreshElementAsync behavior
- Updated PublishStatusServiceTests to use DocumentPublishStatusService instead of
  the now-abstract PublishStatusService

* Add IPublishedElementCache facade for public element cache access

Introduces the public-facing element cache interface and implementation,
following the same pattern as IPublishedContentCache/IPublishedMediaCache.

- IPublishedElementCache: async-only interface (no legacy sync methods)
- ElementCache: facade delegating to IElementCacheService
- Added Elements property to ICacheManager, IUmbracoContext, and their
  implementations

* Add ElementHybridCacheTests and ElementHybridCacheElementTypeTests

Integration tests exercising the full element cache pipeline via
IPublishedElementCache:

ElementHybridCacheTests (7 tests):
- Draft/published retrieval by key
- Unpublished element not accessible without preview
- Draft of published element accessible
- Updated draft element reflects changes
- Deleted element removed from cache
- Element name accessible

ElementHybridCacheElementTypeTests (3 tests):
- Structural type change removes property from cached element
- Non-structural type change preserves property values
- Element removed from cache when element type is deleted

* Fix element navigation to include containers and support breadth-first seeding

The element tree contains both elements and containers (folders) with different
object types. The navigation service now queries both object types to build
the full tree hierarchy.

- Added multi-objectType overloads to INavigationRepository and
  ContentNavigationRepository using LEFT JOIN to support nodes without
  content rows (containers)
- Single-objectType methods now delegate to the multi-objectType implementation
- ElementNavigationService queries both Element and ElementContainer object types
- ElementBreadthFirstKeyProvider traverses containers without seeding them,
  only counting published elements toward the seed limit
- Added ElementBreadthFirstKeyProviderTests (9 tests) including container
  traversal scenarios

* Add ElementContentTypeSeedKeyProvider for content-type-based element seeding

Seeds elements whose content types match the configured CacheSettings.ContentTypeKeys,
mirroring the existing ContentTypeSeedKeyProvider for documents. Both providers read
from the same configuration list — document type keys seed documents, element type
keys seed elements.

* Fix ContentNavigationServiceTest mocks for multi-objectType repository overload

The single-type GetContentNodesByObjectType(Guid) now delegates to the
multi-type overload. Updated test mocks to match the IEnumerable<Guid>
signature, verifying exactly one key containing Constants.ObjectTypes.Document.

* Skip Cannot_Get_Published_Again_After_Trashing test

Trashing does not clear the published cache — this is a pre-existing issue
that also affects documents. When a cached item is trashed, the HybridCache
entry remains because RefreshMemoryCacheAsync does not remove entries when
the database returns null for trashed items.

* Replace unsafe casts with StaticServiceProvider in obsolete constructors

The obsolete constructors in ApiContentRouteBuilder and DocumentUrlService
were using direct casts from IPublishStatusQueryService to
IDocumentPublishStatusQueryService, which would fail at runtime for
external consumers compiled against pre-v18 binaries. Use
StaticServiceProvider.Instance.GetRequiredService instead, consistent
with the pattern in ContentCacheRefresher.

* Remove duplicate XML doc summary in GetElementCultureDataForNodes

* Add obsolete constructors for backward compatibility

Preserve the old constructor signatures for CacheManager,
NavigationInitializationNotificationHandler, and
PublishStatusInitializationNotificationHandler so that external
consumers compiled against pre-element-cache versions don't break.
New dependencies are resolved via StaticServiceProvider.

* Pass cancellationToken to ExistsAsync in ElementCacheService.SeedAsync

* Fix DocumentUrlServiceTests to use IDocumentPublishStatusQueryService

* Trigger Build

* Address PR review feedback

- Rename HandlePublishedAsync to HandlePublishStatusAsync in
  ContentCacheRefresher for consistency with ElementCacheRefresher
- Make ElementCacheRefresher.HandlePublishStatusAsync async to align
  with ContentCacheRefresher's pattern
- Replace inline comments with #region blocks in IDatabaseCacheRepository
- Fix double enumeration in DocumentCacheService.SeedAsync and
  ElementCacheService.SeedAsync by materializing to List before logging

* Invalidate element cache entries when trashed

Apply the same fix from #22451 (documents/media) to elements:
- ElementCacheService.RefreshElementAsync: early-return for trashed
  elements, deleting from the database cache and removing from memory.
- ElementCacheService.RefreshMemoryCacheAsync: add symmetric else
  branches so memory cache entries are removed when the database cache
  has no corresponding draft or published node (self-healing).
- Re-enable Cannot_Get_Published_Again_After_Trashing integration test.

* Move element trash cache tests to ElementHybridCacheTests

Move Cannot_Get_Trashed_As_Published and
Cannot_Get_Published_Again_After_Trashing from
ElementPublishingServiceTests to ElementHybridCacheTests where they
belong — these test cache invalidation, not publishing behavior.

Add Cannot_Get_Published_Elements_After_Folder_Trashed to verify that
trashing an element folder clears its child elements from the published
cache.

* Add element hybrid cache variant tests

Add ElementHybridCacheVariantsTests covering culture variant behavior
for the element cache: variant property values per culture, invariant
property consistency across cultures, single culture updates, single
culture publishing, and draft access to both cultures.

Add isElement parameter to
CreateContentTypeWithTwoPropertiesOneVariantAndOneInvariant to support
creating variant element types without a separate builder method.

* Rename IPublishedElementCache.GetByIdAsync to GetByKeyAsync

Align with the codebase convention where Id refers to integer
identifiers and Key refers to GUID identifiers.

* Align IDocumentPublishStatusQueryService method names with element equivalent

Add IsPublished and IsPublishedInAnyCulture to
IDocumentPublishStatusQueryService to match
IElementPublishStatusQueryService naming.

Keep IsDocumentPublished and IsDocumentPublishedInAnyCulture as obsolete
default implementations delegating to the new methods, since
IPublishStatusQueryService (which exposes these names) ships on main.

Update all internal callers to use the new names.

* Keep INNER JOIN for document/media navigation queries

Only use LEFT JOIN when the query includes container types (e.g.
element containers) which don't have umbracoContent rows. Documents
and media always have content rows, so INNER JOIN preserves query
optimizer hints for those queries.

* Consolidate breadth-first seed key provider logic into base class

Make GetSeedKeys virtual on BreadthFirstKeyProvider and introduce
ShouldSeed and ShouldTraverseChildren hooks so subclasses only need
to override filtering logic instead of duplicating the entire
traversal.

- Document: overrides ShouldSeed to filter unpublished nodes
- Element: overrides ShouldSeed + ShouldTraverseChildren (always
  traverse, since containers may have published children)
- Media: uses base defaults (seed and traverse everything)

Removes the 'new' hiding pattern and the V16 TODO.

* Revert "Rename IPublishedElementCache.GetByIdAsync to GetByKeyAsync"

This reverts commit 139d66776f.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-20 15:08:10 +00:00
Andy Butland 2128aba603 Merge branch 'main' into v18/dev 2026-04-20 11:53:16 +02:00
leekelleher 3de50dc707 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/media-type.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/member-type.data.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document-type.db.ts
#	src/Umbraco.Web.UI.Client/mocks/msw-handlers/document-type/structure.handlers.ts
#	src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts
#	src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-recycle-bin.ts
2026-04-17 00:53:18 +01:00
fe8c25576e Collection Action: Refactor to use extension-with-api-slot (#21974)
* Use API-enabled slot for collection actions.

* Refactor collection actions; add APIs & button folder.

* Fixed relative import.

* Add collection create action API and UI updates.

* Mark UmbExtensionApiInitializer as type import

* Update imports.

* Add additionalOptions to collection create

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-04-16 10:18:11 +02:00
Andy Butland da486ca841 Merge branch 'main' into v18/dev 2026-04-16 09:39:15 +02:00
Laura NetoandGitHub 8e1c6c7a39 Document Types: Prevent disabling isElement when elements of that type exist (#22454)
* Document Types: Prevent disabling isElement when elements of that type exist

Mirrors the existing document-to-element guard: switching an element type
to a document type is now blocked when elements of that type exist. Adds
ElementToDocumentHasNoContentAsync to IElementSwitchValidator and a new
ContentTypeOperationStatus.InvalidElementFlagElementHasContent mapped to
a BadRequest in the document type controller.

* Address PR review feedback for isElement guard

Extract shared HasNoContentNodesAsync helper in ElementSwitchValidator
to deduplicate DocumentToElement and ElementToDocument checks. Make
WithAllowedInLibrary conditional on isElement in test setup.

* Add end-to-end integration tests for element switch validation

Add three tests to ContentTypeEditingServiceTests that verify
UpdateAsync returns the correct operation status when element
flag changes are blocked: document-to-element with existing
content, element-to-document with existing elements, and
element-to-document when used in block structures.

* Remove default interface implementation for ElementToDocumentHasNoContentAsync

Per review feedback: custom implementations of IElementSwitchValidator are unlikely, and a default implementation hides the fact that changes to the real implementation would need to be mirrored here. Accept the small breaking change for a clearer upgrade path.
2026-04-15 17:33:10 +02:00
Niels Lyngsø e40694ff9d Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/core/tree/data/unique-tree-store.ts
#	src/Umbraco.Web.UI.Client/src/packages/data-type/tree/data-type-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/dictionary-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/tree/document-blueprint-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/document-type.tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/data/document-recycle-bin-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/media-recycle-bin-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/member-type-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/static-file/tree/static-file-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/partial-view-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/script-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/stylesheet-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/templating/templates/tree/template-tree.store.ts
2026-04-15 17:07:49 +02:00
Laura NetoandGitHub 4638406fc9 Tests: Fix unit test build (#22453)
Fix ElementPickerValueConverterTests build by passing IPropertyRenderingContextAccessor

The PublishedProperty constructor was updated to take an
IPropertyRenderingContextAccessor as its 4th argument, but
ElementPickerValueConverterTests was not updated, breaking the
Umbraco.Tests.UnitTests build.
2026-04-13 18:54:50 +02:00
Laura Neto 704ee94101 Merge branch 'main' into v18/dev 2026-04-13 17:40:29 +02:00
6c96ad1f93 Elements: Cleanup element TODOs in infrastructure (#22443)
* Remove obsolete TODO (already fixed to the extend possible)

* Remove irrelevant TODO

* Refactor publishable entity building from DTOs

* Fix TODO for presentation factory

* Move shared view models from Document to Content

* Clarify TODO after testing refactoring feasibility

* Update src/Umbraco.Cms.Api.Management/ViewModels/Content/ScheduleRequestModel.cs

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

* Update src/Umbraco.Cms.Api.Management/Factories/IElementPresentationFactory.cs

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-13 08:09:13 +02:00
Kenn JacobsenandGitHub 8cebbd23d8 Elements: Cleanup element TODOs in core (#22399)
* Cleanup element TODOs in core (first take)

* Cleanup more element TODOs in PublishableContentServiceBase and ElementEditingService

* Implement Delivery API for ElementPickerValueConverter (removes TODOs and add a few new ones)

* Move the generic implementation of PublishedElementWrapped to its own class file

* Review comments for IPublishableContentRepository
2026-04-13 08:08:39 +02:00
4a4437b500 Rendering: Use explicit dependency instead of access-via-casting (#22442)
* Use explicit dependency instead of access-via-casting

* Update src/Umbraco.PublishedCache.HybridCache/PublishedElement.cs

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-13 07:21:08 +02:00
Andy Butland bd83df28bc Merge branch 'main' into v18/dev 2026-04-11 11:37:15 +02:00
010ceab910 Elements: Align ElementPermissionService for performance improvements (#22405)
* Align ElementPermissionService with ContentPermissionService performance improvements

* Don't fetch entities we don't need.

* Update src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs

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

* Add unit tests for ElementPermissionService

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-10 12:14:25 +00:00
Andy Butland c6f1cfdd4a Fixed test helpers build. 2026-04-09 21:12:25 +02:00
Andy Butland e09cf2aaf8 Fix build of integration tests and failing unit test. 2026-04-09 19:52:12 +02:00
Andy Butland 85680bd36d Merge branch 'v18/dev' of https://github.com/umbraco/Umbraco-CMS into v18/dev 2026-04-09 16:01:22 +02:00
Andy Butland 36f3bf6b9c Merge branch 'main' into v18/dev 2026-04-09 16:00:10 +02:00
8df1c3f152 Deprecations: Client-side removal of v18 deprecated code (#21984)
* chore(tree): remove deprecated tree store infrastructure

Remove the entire tree store pattern that was deprecated in favor of
direct tree repository queries. This deletes 29 tree store files,
removes the ManifestTreeStore extension type, updates all 15+ tree
repository constructors to remove store context token parameters,
cleans up manifests/constants/index exports, and migrates all
skip/take pagination to the paging property pattern.

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

* chore(workspace): remove deprecated methods and properties

Remove deprecated methods/properties across workspace contexts, menu
structures, tree items, and collections:
- Tree item context: getManifest(), loadMore()
- Content workspace: loadSegments()
- Entity detail workspace: parentUnique/parentEntityType observables,
  getParent/setParent/getParentUnique/getParentEntityType methods,
  _scaffoldProcessData (replaced by _processIncomingData)
- Menu structure contexts: #parent state, provideContext('UmbMenuStructureWorkspaceContext')
- Document/media/blueprint/member workspaces: contentTypeHasCollection,
  getCollectionAlias(), getContentTypeId() (replaced by getContentTypeUnique())
- Collection context: setManifest(), getManifest() from interface and implementation
- Bulk delete action: deprecated _items getter/setter

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

* chore(core): remove deprecated type aliases and exports

Remove deprecated type aliases scheduled for v18 removal:
- PackageManifestResponse (use UmbPackageManifestResponse)
- UmbSectionDefaultElement (use UmbDefaultSectionElement)
- ConditionsCollectionView (use UmbConditionsCollectionView)
- MediaValueType (use UmbMediaValueType)
- UrlParametersRecord (use UmbUrlParametersRecord)
- ActiveVariant (use UmbActiveVariant)
- UmbPropertyValueChangeEvent class and deprecated property-value-change
  event listeners

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

* chore(ui): remove deprecated config and UI exports

- Textarea: remove deprecated minHeight/maxHeight config reads
- Image cropper modal: remove deprecated default export
- UFM filters: remove 3 deprecated camelCase filter manifests
  (StripHtmlCamelCase, TitleCaseCamelCase, WordLimitCamelCase)

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

* chore(repository): make totalAfter/totalBefore mandatory in UmbTargetPagedModel

Make totalAfter and totalBefore required properties (were optional),
fulfilling the TODO to make these mandatory in v18. All downstream
tree data sources already provide these values.

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

* style: fix lint formatting

Auto-fixed formatting from lint run (line wrapping, trailing newlines).

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

* fix(collection): default filter parameter in element collection repositories

The UmbCollectionRepository interface defines filter as optional.
Without a default, calling requestCollection() without arguments
would throw when accessing filter.skip/filter.take.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* fix(backoffice): resolve ESLint errors, fix pagination metadata, and remove missed deprecations

- Remove unused UmbObjectState import (ESLint error from merge)
- Remove unused offsetPaging variable in tree-item-children.manager.ts
- Fix totalBefore/totalAfter in all tree data sources to account for skip
  offset (was always reporting totalBefore: 0 regardless of skip value)
- Remove deprecated entityType property from UmbElementValueModel
  (marked for v18 removal)
- Remove deprecated _items getter/setter from UmbTrashEntityBulkAction
  (marked for v18 removal)

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

* fix(backoffice): remove entityType references from tests and source after type removal

Remove entityType property from test fixtures and media-dropzone.manager.ts
following removal of the deprecated entityType from UmbElementValueModel.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-04-09 13:53:48 +00:00
ee44dbd677 Global Elements: Create options with allowed types and entityCreateOptionAction extensions (#22265)
* entity-action manifests shuffle

* feat(elements): show allowed element types in create action modal

Replace the generic document type picker with a custom create options
modal that fetches allowed element types from the library API and
displays them alongside a folder creation option, following the
established Media create pattern.

* feat(elements): add collection create action with allowed types and folder option

Add custom collection action element that fetches allowed element types
and discovers entityCreateOptionAction extensions (e.g. folder creation),
rendering them as a button or dropdown in the collection toolbar.

* refactor(elements): use dynamic entityCreateOptionAction extensions in create modals

Replace hardcoded folder option in the element create options modal with
UmbExtensionsApiInitializer to dynamically discover entityCreateOptionAction
extensions, enabling 3rd party extensibility.

* style(elements): clean up redundant state, magic strings, and empty styles

Use UMB_ELEMENT_ROOT_ENTITY_TYPE constant instead of magic string,
remove unused _headline state and empty css template, inline
single-use getter.

* fix(elements): address PR review feedback and export missing constants

- Extend UmbNamedEntityModel instead of duplicating name field
- Add getHref() support and error handling matching core patterns
- Add max-height on scroll container, icon fallbacks, element-specific
  localization key
- Export UMB_ELEMENT_CREATE_OPTIONS_MODAL and
  UMB_ELEMENT_TYPE_STRUCTURE_REPOSITORY_ALIAS through index chain
- Add feature parity checklist to clean-code docs

* style(elements): add noElementTypes localization entry and lint tweaks

* fix(elements): handle href navigation and error handling in create options modal

Navigate via history.pushState when href is present on create option
actions. Only close modal on successful execute, keeping it open on
failure so users can retry.

* Add temporary .skip tag to element smoke tests due to UI changes - to be fixed in another PR

---------

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>
Co-authored-by: Nhu Dinh <hnd@umbraco.dk>
2026-04-09 12:40:06 +01:00
MoleandGitHub 05112b559f Management API: Fix ambiguous constructor in PasswordConfigurationPresentationFactory (#22391)
* Fix ambiguous constructor

* Add clarifying comment
2026-04-09 10:09:33 +00:00
0fa669db94 Code Clean-up (18): Remove obsoleted code flagged for removal (Part 3) (#22335)
* remove obsolete code from services

* remove obsolete code from IEmailSenderClient

* remove obsolete code from Notifications

* unchange MemberServiceTest

* Remove obsolete code from CopyingNotification

* remove ContentFinderByUrl and ContentFinderByUrlAndTemplate

* Remove DefaultUrlProvider, remove obsolete code from ContentPermissions, update MemberRoleStoreTests

* unchange PropertyCacheLevelTests

* unchange ConvertersTests

* Update src/Umbraco.Core/Notifications/ContentCopiedNotification.cs

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

* Update src/Umbraco.Core/Notifications/ContentCopyingNotification.cs

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

* Update src/Umbraco.Core/Notifications/CopiedNotification.cs

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

* Update src/Umbraco.Core/Notifications/CopyingNotification.cs

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

* Update src/Umbraco.Core/Notifications/ElementCopiedNotification.cs

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

* Update src/Umbraco.Core/Notifications/ElementCopyingNotification.cs

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

* Update src/Umbraco.Core/Services/ContentService.cs

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

* Update src/Umbraco.Infrastructure/Mail/BasicSmtpEmailSenderClient.cs

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

* update tests, rename, remove file tests...

* Minor formatting tidy-up.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-09 06:57:00 +00:00
e5d44cd9c3 Code Clean-up (18): Resolve V18 TODO comments (#22357)
* todo cleanup

* adding activatorUtilitiesConstructor atribute

* fix failed test by adding ActivatorUtilitiesConstructor

* Apply suggestion from @AndyButland

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

* Apply suggestion from @AndyButland

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

* Apply suggestion from @AndyButland

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

* Apply suggestion from @AndyButland

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

* Apply suggestion from @AndyButland

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

* update umbracoPlan and remove ConfigureSecurityStampOptions

* Removed uneeded using.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-09 06:34:42 +00:00
5d682f69a8 UUI: Updates to UI Library version 2.0.0-alpha.1 (#21994)
* build(deps): bumps @umbraco-ui to 2.0.0-alpha.1 with new themes

* fix: updates paths to new themes

* feat: uses new uui themes for static cshtml files

* feat: updates to use UUISelectOption and UUIFormControlWithBasicsMixin

* build: copy all themes to "themes" folder

* build(uui): updates themes path so it works relatively with fonts

* build: updates minimum node.js version to build from 22 to 24 to support UUI

* fix: corrects paths to theme css

* docs: update CLAUDE.md files to reflect UUI 2.x for CMS v18

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

* docs(storybook): adds theme switcher

* docs(storybook): updates paths

* docs(web): document UUI theme CSS pipeline across build files

Add comments linking the files involved in UUI theme CSS handling:
- manifests.ts: where theme CSS paths are declared, with note on UUI origin
- external/uui/vite.config.ts: where themes are copied for production builds
- vite.config.ts: where themes are copied for dev server and PR previews
- copy-to-cms.js: clarifies UUI themes are already in dist-cms at this point

Each file points to the others, making the dependency on UUI theme
filenames visible without adding abstraction.

https://claude.ai/code/session_015ntS4GXa4s9BQHsjvigDh2

* Update package.json

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

* fix: adjusts types

* update lockfile

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-04-08 10:36:23 +00:00
Nhu DinhandGitHub df3724c830 E2E: QA Remove @smoke tags from element tests (temporary) (#22363)
Temporary remove .smoke tags for the element-related tests
2026-04-08 09:29:06 +07:00
e093ca5f49 Global Elements: Workspace UI updates: split view, variant selector, save modal, and pending changes (#21897)
* feat(elements): add contentTypeIcon observable and _handleSave override to workspace context

Adds contentTypeIcon observable, icon field to UmbElementDetailModel, and maps icon from server response. Adds _handleSave override to remap validation error colors to warning colors during save, matching Document workspace behavior.

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

* feat(elements): add loading state, variant selector, and cleanup to split view

Adds loading state observation and binding, variant selector slot with new element-specific variant selector component, and element sortVariants utility. Removes dead #breadcrumbs CSS rule and reorders splitViewIndex to match Document workspace conventions.

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

* feat(elements): wire up publishing workspace context in variant selector

Consumes UMB_ELEMENT_PUBLISHING_WORKSPACE_CONTEXT in the element variant selector, mirroring the Document pattern. Fixes PUBLISHED_PENDING_CHANGES localization to use the correct key.

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

* feat(elements): add save modal for element workspace variant picker

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

* Adds "Update" permission condition on Folder Rename entity-action

* feat(elements): add pending changes manager for element workspace

Mirror the Document workspace's UmbDocumentPublishedPendingChangesManager
to provide client-side comparison of persisted vs published element data.
The variant selector now uses this manager to determine pending changes
state instead of relying solely on the API state. The actual API call to
fetch published element data is left as a TODO until the backend endpoint
exists.

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

* Update src/Umbraco.Web.UI.Client/src/packages/elements/modals/save-modal/element-save-modal.element.ts

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

* Update src/Umbraco.Web.UI.Client/src/packages/elements/utils.ts

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

* refactor(menu): delegate breadcrumb href to menu structure context

Move the href resolution logic from the breadcrumb element into the
menu structure workspace context via a new `getItemHref` method on the
interface and base class. This eliminates the need for duplicate
breadcrumb elements that only differ in href behavior, and mirrors the
existing pattern used by the variant breadcrumb.

* feat(elements): add menu structure context and breadcrumb for element folders

Add UmbElementFolderMenuStructureContext that overrides getItemHref to
make folder ancestors and the section root clickable in the breadcrumb.
Register the menu structure context and breadcrumb footer app in the
element folder workspace manifests.

* fix(elements): provide synthetic variant data for folder tree items

Folders don't have variants from the API, so provide a synthetic
published variant using the folder name. This prevents errors when
the tree item mapper expects variant data.

* Updates "umb-element-table-collection-view"

to add the column elements for "name" and (published) "state".

* Refactor exports in constants.ts for clarity

* fix(workspace): prevent breadcrumb TypeError for contexts without getItemHref

Menu structure contexts that don't extend the tree base class (e.g.
UmbLanguageNavigationStructureWorkspaceContext) lack getItemHref, causing
a runtime TypeError in the breadcrumb element. Use optional chaining to
gracefully handle missing implementations.

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

* docs(menu): add JSDoc to UmbMenuStructureWorkspaceContext interface

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-04-07 11:17:41 +02:00
b972d1db5a Global Elements: Element Tree Item "Draft" state (#22228)
* Adds "umb-element-tree-item" custom component

Updates context to use the item data resolver..

* Adds manifests for Element entity-signs

for "Has Pending Changes" and "Has Scheduled Publish"

* Update src/Umbraco.Web.UI.Client/src/packages/elements/tree/element-tree-item.element.ts

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

* Attempt to fix the Item Data Resolver `setData` type-casting

* Align element tree item model with item model for type safety

Add required `flags` field to `UmbElementTreeItemModel` (via
`UmbEntityWithFlags`) and `UmbElementTreeItemVariantModel`, matching
the document tree pattern. This ensures the data resolver's `#setFlags()`
receives actual data instead of silently accessing undefined properties.

The `as unknown as` cast in the context remains due to nominal type
differences (entityType union, variant state enum) but is now structurally
safe at runtime.

* Maps `flags` in `UmbElementTreeItemVariantModel`

* Updated locator for element tree item due to UI changes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Nhu Dinh <hnd@umbraco.dk>
2026-04-03 07:45:12 +00:00
Andy Butland 015d17db3f Merge branch 'main' into v18/dev 2026-04-02 08:19:35 +02:00
Andy Butland 617f2441fc Merge branch 'main' into v18/dev 2026-04-01 15:39:23 +02:00
Niels Lyngsø c18f28d22d Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs
#	src/Umbraco.Core/Services/OperationStatus/UserOperationStatus.cs
2026-04-01 13:58:53 +02:00
Andy Butland f550eeec28 Fixed client-side build. 2026-04-01 12:36:20 +02:00
Andy Butland 22907e01ef Updated OpenApi.json. 2026-04-01 09:54:54 +02:00
59a7d99e3c Elements: Add PublishedCultures and UnpublishedCultures to ElementCacheRefresher (#22302)
* Add PublishedCultures and UnpublishedCultures to ElementCacheRefresher.JsonPayload

Adds culture-specific publishing details to the element cache refresher payload,
matching the existing ContentCacheRefresher.JsonPayload structure. Also replicates
the performance optimization from #21415 by only clearing partial view cache when
there are actual publish/unpublish culture changes, and fixes the Remove change
type check to use HasType instead of equality (flags enum).

* Reuse content cache logic for partial view cache clearing

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-04-01 09:27:33 +02:00
Niels Lyngsø f4b3a0f4ac update management api types 2026-04-01 09:19:47 +02:00
Niels Lyngsø 68d9e02417 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Cms.Api.Management/OpenApi.json
#	src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
2026-04-01 09:13:55 +02:00
6e85871595 Global Elements: Recycle Bin UI (#21872)
* Uncommented placeholders for restore endpoints

* Delete (inside Recycle Bin): wired up correct endpoints

* Added condition for "Empty Recycle Bin" collection-action

to only display in the Recycle Bin root.

* feat(recycle-bin): add destination entity overrides to restoreFromRecycleBin kind

Add optional destinationItemRepositoryAlias, destinationItemDataResolver,
and destinationRootEntityType properties to support cross-entity-type
restore (e.g. element restoring into element-folder). Existing document
and media manifests are unaffected as all new properties fall back to
the original values. Also adds element folder restore manifest.

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

* refactor(recycle-bin): extract #resolveDestinationItemName to reduce complexity

Extract resolver logic from setDestination into a dedicated method to
bring cyclomatic complexity under the threshold of 9.

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

* Removed Restore Element Folder From Recycle Bin Entity Action

(This is for a separate PR)

* feat(elements): enable element and folder restore from recycle bin

Uncomment element restore manifest with destination overrides, add
folder picker modal, and add null guard for restore item lookup.

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

* Fixes bug with selecting the Root for the restore target

* Corrected manifest aliases to use appropriate entity-type name for `ElementFolder`

* Added `UmbElementFolderItemDataResolver` to resolve folder names in recycle bin restore modal

* E2E: QA Added acceptance tests for restoring elements and deleting elements from recycle bin (#22069)

* Updated test helper for move a folder to recycle bin

* Added tests for restore element and delete element from recycle bin

* Added ocmment for the failing tests

* Make recycle bin tests run in the pipeline

* Fixed comment

* Removed duplication code

* Reverted npm command

* Adds `itemDataResolver` to the Element Trash entity-action

* Makes trashed Element Folder name to be read-only

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-04-01 09:01:05 +02:00
Andy ButlandandGitHub 6853f2c910 EF Core: Align casing of EF Core code constructs (closes #22247) (#22313)
* Align casing of EFCore code constructs.

* Handle code review feedback.
2026-04-01 06:56:48 +02:00
Andy Butland 9d94b26cc2 Merge branch 'main' into v18/dev 2026-03-31 12:12:57 +02:00
58cbc9790f Global Elements: Adds "Start Node" and "Ignore User Start Nodes" to Element Picker configuration (#22255)
* Element Picker property-editor: adds "Start Node" configuration

* [WIP] Adds server config for Element start node

* [WIP] Attempts to wire up the `dataTypeId`

for the Element Picker start node

* Removed `StartNodeId` from the server config

* Implemented `requestTreeStartNode`

on Element Picker data-source

* Fix duplicate config entries in input-element property setters

The `folderOnly` and `startNode` setters used `.push()` without
deduplication, causing config entries to accumulate on Lit re-renders.
Filter existing entries before pushing to prevent duplicates.

* Update OpenAPI spec and regenerate TypeScript bindings

Add dataTypeId query parameter to element tree endpoints.

* Refactor input-element to compute dataSourceConfig on demand

Replace mutable #dataSourceConfig array with plain Lit properties for
folderOnly and startNode, computing the config inline in render. This
eliminates the duplicate-entry bug and simplifies the component.

Also fix "dont" typo in ignoreUserStartNodes description.

---------

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>
2026-03-31 08:11:06 +01:00
8311bae2ab User Permissions: Resolve and persist element start node IDs when updating a user (#22297)
* Resolve and persist element start node IDs when updating a user

The UpdateAsync method in UserService only resolved Document and Media
start node keys to IDs, completely ignoring ElementStartNodeKeys from
the update model. This caused element start node configuration to be
silently lost on user save.

* Add ElementStartNodeNotFound status and fix XML doc for MapUserUpdate

Introduces a dedicated ElementStartNodeNotFound operation status to
distinguish missing element start nodes from missing element items in
other operations, consistent with ContentStartNodeNotFound and
MediaStartNodeNotFound. Also adds the missing XML doc param for
startElementIds on MapUserUpdate.

* Add blank line to re-trigger the build.

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-03-31 07:26:58 +02:00
2bfb83d6f7 Global Elements: Add "Allowed in library" toggle to Document Type structure view (#21875)
* Added localization keys

* Fixed mock data

* Adds UI for "Allowed in library" configuration

* Capitalize nouns regarding allow in library

* Focuses `allowedInLibrary` on Document/Element Types

* refactor(web): extract route setup from UmbDocumentTypeWorkspaceContext constructor

Move route configuration into a private #setupRoutes() method to reduce
constructor cyclomatic complexity below the threshold of 9.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-30 17:46:13 +01:00
Laura NetoandGitHub f1a7c6bbc1 Localization: Remove unused recycleBin keys from XML language files (#22299)
Remove unused recycleBin keys from XML language files

The recycleBin area contained keys (contentTrashed, mediaTrashed,
elementTrashed, elementContainerTrashed, itemCannotBeRestored,
itemCannotBeRestoredHelpText, wasRestored) that are no longer
referenced by any backend code since the audit logging was removed
from RelateOnTrashNotificationHandler in #21481.
2026-03-30 17:04:34 +02:00
4d993a6dd1 Elements: Add flag support for pending changes and scheduled publish (#21877)
* Add flag support for pending changes and scheduled publish

Add entity sign manifests, tree item rendering, and flag provider
support so element tree items display pending changes (pencil) and
scheduled publish (clock) icons, mirroring the existing document
behavior.

Refactor flag providers and presentation factories to reduce
duplication, and move shared IHasFlags implementation into
PublishableVariantResponseModelBase.

* Fix HasScheduleFlagProvider test mocks to match refactored per-item lookups

* Extract PublishableVariantItemResponseModelBase to deduplicate variant item models

* Extract shared base class from Document/Element presentation factories

Introduce PublishableContentPresentationFactoryBase to eliminate code
duplication between DocumentPresentationFactory and ElementPresentationFactory.
Add async alternatives (CreateVariantsItemResponseModelsAsync,
CreateItemResponseModelAsync, PopulateFlagsAsync) and migrate callers in
async contexts to use them. Sync callers in tree/recycle bin controllers
use .GetAwaiter().GetResult() to avoid breaking changes in base classes.

Add IPublishableContentEntitySlim overload to DocumentVariantStateHelper
to unify the identical IDocumentEntitySlim/IElementEntitySlim overloads.

Make RelationTypePresentationFactory properly async with Task.WhenAll.

* Fix flags fallback to use empty array instead of empty string

* Acceptance Tests: Fix element tree item locator to match both elements and folders

The element tree renders umb-element-tree-item for elements but
umb-default-tree-item for folders. Update the E2E test helper locator
to use :is() to match both custom element types.

* Split HasScheduleFlagProvider into document and element providers

Address PR review feedback:
- Split HasScheduleFlagProvider into HasDocumentScheduleFlagProvider and
  HasElementScheduleFlagProvider with a shared HasScheduleFlagProviderBase
- Fix N+1 query: use batch GetContentSchedulesByKeys instead of per-item
  GetContentScheduleByContentId
- Add GetContentSchedulesByKeys to IPublishableContentService and implement
  in PublishableContentServiceBase, removing the duplicate from IContentService
  and ContentService
- Inject TimeProvider into base class, replacing DateTime.Now with
  _timeProvider.GetUtcNow()
- Split tests to match new provider structure and verify batch retrieval

* Make tree and recycle bin mapping methods async

Remove .GetAwaiter().GetResult() calls introduced by the element flag
support changes. Rename MapTreeItemViewModel to MapTreeItemViewModelAsync
and MapRecycleBinViewModel to MapRecycleBinViewModelAsync across all
tree and recycle bin controllers, properly awaiting async factory calls.

* Extract Task.WhenAll select expressions into named variables

* Add missing XML docs to async methods on IDocumentPresentationFactory

* Fix DateTime vs DateTimeOffset comparison in schedule flag provider

Compare schedule.Date against _timeProvider.GetUtcNow().UtcDateTime
instead of the DateTimeOffset directly, avoiding implicit conversion
issues with DateTimeKind.Unspecified.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-03-30 12:54:37 +02:00
Andy Butland 950470aade Make long-running concurrent save test more robust. 2026-03-28 17:01:11 +01:00
7f1255b5e7 Content Types: Granular content type change types (#22223)
* Add more granularity to ContentTypeChangeTypes and handle for structucal changes (pending non-structucal changes).

* Integration tests to validate the granular, structucal change types

* Implement "other" changes

* Make "other" changes less granular.

* Update tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.ChangeTypes.cs

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

* Clean up

* Add test proving the sub-flags do not collide

* Support change detection for both structural and non-structural changes in one operation

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-03-27 09:55:35 +01:00
6e27ab2e0a Elements: Add missing notifications to element container and element editing services (#22012)
Add missing notifications to element container and element editing services

Add ElementDeletingNotification and ElementTreeChangeNotification to
ElementContainerService for EmptyRecycleBin, Move, MoveToRecycleBin,
and Delete operations, aligning with ContentService notification patterns.

Add ElementTreeChangeNotification to ElementEditingService for Move
and Copy operations.

Refactor DeleteDescendantsLocked to return deleted elements and
DeleteItem to return the deleted entity for use in tree change
notifications.

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-03-27 08:55:59 +01:00
Andy Butland da614e7d2c Fixed integration tests failing on SQL Server and NUnit 4. 2026-03-27 08:26:18 +01:00
Andy Butland 858c450223 Merge branch 'main' into v18/dev 2026-03-26 16:51:27 +01:00
e8f5b98cb0 Code Clean-up (18): Remove obsoleted code flagged for removal (Part 2) (#22137)
* Remove obsolete code

* Update tests in BlockEditorBackwardsCompatibilityTests

* update languageId, remove obsolete construcor from ApiLink

* remove the tests

* Fixed build of unit tests.

* Reverted removal of UmbracoApiController for now (we should do this in a single PR).

* Code style fix.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-03-26 15:08:10 +00:00
Nhu DinhandGitHub 339da5e19e E2E: V18 Fixed the failing smoke tests (#22248)
* Reverted fix

* Updated tests regarding duplication due to UI changes
2026-03-25 09:35:40 +00:00
Andy Butland 9ef7c5b955 Further fix to failing acceptance test. 2026-03-25 08:37:35 +01:00
Andy Butland ad8db9c567 Fixed failing integration and acceptance tests after merge. 2026-03-25 06:51:24 +01:00
Andy Butland 0210dd00a0 Fix after merge. 2026-03-24 17:37:17 +01:00
Andy Butland 41f62ae03b Merge branch 'main' into v18/dev 2026-03-24 16:58:46 +01:00
Sven Geusens b19f8a2eb1 Add v18/dev to nightly build trigger 2026-03-24 11:11:51 +01:00
Kenn JacobsenandGitHub 1a86c9f45c Change the default webhook payload type to "minimal" (#22217)
* Change the default webhook payload type to "minimal"

* Include expected defaults in webhook telemetry + use core constants instead of local strings
2026-03-23 09:19:17 +01:00
Nhu DinhandGitHub 5968bae002 Build: Cherry pick #22164 for V18 (#22165)
Serialize E2E stages and stagger branch schedules to reduce agent usage
2026-03-19 21:20:06 +07:00
7014f9a125 Dependencies: Upgrade NUnit and related test dependencies to latest major versions (#22155)
* Update Nunit and AutoFixture.Nunit to new versions

* Adding NonParallelizable

* Add blame-hang timeout to integration tests to detect hanging tests

* remove NonParallelizable, update NUnit3TestAdapter, add Ingore to CoreConfigurationHttpTests

* Resolve CoreConfigurationHttpTests hang with NUnit 4.

  - Use Task.Run in CreateHost to escape NUnit 4's SynchronizationContext
    which deadlocks sync-over-async calls from async test methods.
  - Use await using for factory disposal to avoid same deadlock on shutdown
  - Remove WithWebHostBuilder which wraps the factory in a
    DelegatedWebApplicationFactory that bypasses the CreateHost override.
  - Add ContentRoot property to UmbracoWebApplicationFactory so content
    root can be set without WithWebHostBuilder.
  - Set ModelsBuilder mode to Nothing to prevent BootFailedException.
  - Add AddTestServices for infrastructure test doubles (MainDom, etc.).

* Revert changes to pipelines.

* Remove remaining CollectionAssert using legacy syntax.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-03-19 11:49:08 +00:00
Niels LyngsøandGitHub 7536d6b95f Library: remove library sidebar app (#22139)
remove library sidebar app
2026-03-17 15:09:06 +00:00
Kenn JacobsenandGitHub 6036b13e94 Elements: Clean up container relations before deleting them (#22154)
Clean up element container relations before deleting them
2026-03-17 10:31:38 +01:00
d2e7fc1863 Dependencies: Update selected dependencies to latest major versions (#22060)
* update outdated dependencies to their latest major versions

* change version of JsonPatch.Net back to 3.*.*

* Upgrade Umbraco.Code package

* Update tests

* Resolve NUnit 4 migration issues causing test hangs

* Fix for dotnet test on the pipeline.

* Debug: Fix attempt for integration tests on the pipeline.

* Revert pipeline changes and go back to 5.2.0.

* Debug: Omit suspect tests.

* Debug: Disable tests with timeout.

* Debug: Try 4.6.0.

* Debug: Added reference to Microsoft.CodeAnalysis.CSharp.Workspaces.

* Roll back NUnit upgrade.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-03-17 06:37:21 +01:00
27b7f220a3 Elements: Fixes HasChildren for Element Folder entities (#22142)
* Fixes `HasChildren` for Element Folder entities

* Remove HasChilden mapping

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-03-16 17:01:08 +00:00
Andreas ZerbstandGitHub 9e360e8dc0 QA: Fix element tree integration tests and SQL Server container service error (#22130)
* fix SQL Server OFFSET/FETCH error

* added ActionElementBrowse.ActionLetter permission
2026-03-16 13:11:23 +07:00
a2acb7a53f Code Clean-up (18): Remove obsoleted code flagged for removal and address TODO comments (#21980)
* remove obsolete constructor

* adjust RootDictionaryTreeController constructor to use non-obsolete constructor and remove obsolete base

* todo action v18

* remove ActivatorUtilitiesConstructor atribute that there's only one constructor

* remove obsolete class and method

* remove obsolete code in v18

* remove obsolete code from repositories

* remove obsolete for blocks

* remove obsolete code from services

* remove Icomponent

* remove incorrect IRequestSegmmentService

* remove obsolete properties

* undo change of blocklayoutitembase because of test failed

* bring back somes code due to pr 21999

* bring back some codes and update ContentRouteBuildertests

* remove obsolete code from domains, notification controller and some services

* remove obsolete constructor from ElementMapDefinition

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-03-13 14:40:13 +01:00
Nhu DinhandGitHub d9db6b02ce E2E: QA Added .skip tags to failing acceptance tests due to known issues (#22122)
* Added .skip tags for the failing tests due to an actual issue

* Change the way to verify the validation message

* Added .skip tags for failing tests due to the actual issues
2026-03-13 16:30:00 +07:00
Nhu DinhandGitHub ae80d921c0 E2E: QA Updated acceptance tests in v18 due to the auth changes (#22110)
Updated tests due to the auth changes
2026-03-13 03:18:13 +00:00
Andy Butland 14a2ca89ea Adds XML documentation to elements management API controllers, models and mappers. 2026-03-12 18:20:13 +01:00
Andy Butland b2517e3302 Fix post merge issues. 2026-03-12 18:05:48 +01:00
Andy Butland f83949c508 Merge branch 'main' into v18/dev 2026-03-12 17:59:17 +01:00
Andy Butland af439c6a66 Fixes and additional documentation after merge. 2026-03-12 16:16:11 +01:00
Andy Butland 71d700ea28 Merge branch 'main' into v18/dev 2026-03-12 16:15:40 +01:00
Niels Lyngsø 90b2062d85 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs
#	src/Umbraco.Web.UI.Client/package.json
#	src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/ContentUiHelper.ts
#	tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml
#	version.json
2026-03-12 14:21:54 +01:00
Jacob Overgaard e6a91e5f6c Merge remote-tracking branch 'origin/main' into v18/dev 2026-03-11 15:30:02 +01:00
Andreas Zerbst 421d616682 Merge branch 'main' into v18/dev
# Conflicts:
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/ApiHelpers.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/DataTypeUiHelper.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/UiBaseLocators.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/UserApiHelper.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/ContentStartNodes.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/MediaStartNodes.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts
2026-03-10 17:05:11 +01:00
Niels LyngsøandGitHub fb5030010c Form Control: only validate if value was changed during focus (#21815)
* poc of minimizing unrelevant validation messages

* remove submit method from interface

* remove call to re-validate, as that is already trigger via `updated`--callback
2026-03-09 08:44:46 +00:00
Laura NetoandGitHub 421ad35034 Elements: Split content type validation for create and update (#21906)
* Split content type validation for create and update to allow saving elements no longer permitted in library

* Add integration test for element update after AllowedInLibrary toggle

Verify that ElementEditingService.UpdateAsync succeeds when the content
type's AllowedInLibrary flag is set to false after the element was
created, covering the split validation introduced for create vs update.

* Move content type validation into TryGetAndValidateContentType override

Eliminate redundant content type lookups in CreateAsync and UpdateAsync
by moving the IsElement/AllowedInLibrary check into the
TryGetAndValidateContentType override, which distinguishes create from
update by checking if the model is a ContentCreationModelBase.

* Use Assert.Multiple for element property assertions in update test

* Extract IsAllowedLibraryElement static method for readability
2026-03-06 17:34:00 +00:00
Andy Butland a17aef5af8 Fix sortable property update issue introduced in merge from main. 2026-03-06 17:55:07 +01:00
Andy Butland 6544c5cbcc Merge branch 'main' into v18/dev 2026-03-06 11:05:17 +01:00
Andy Butland a0518a0636 Merge branch 'main' into v18/dev 2026-03-06 08:09:37 +01:00
Andy Butland 5e669bb8c7 Merge branch 'main' into v18/dev 2026-03-06 08:08:28 +01:00
Nhu DinhandGitHub 29ef442e99 E2E: QA Added acceptance tests for element picker in content and element (#21745)
* Added tests for content with element picker

* Added tests for element with element picker

* Bumped version

* Renamed tests

* Make tests run in the pipeline

* Bumped version

* Fixed failing tests

* Moved goToBackOffice step to beforeEach

* Moved goToBackOffice to beforeEach

* Fixed comment

* Fixed afterEach() step

* Fixed import

* Fixed

* Reverted npm command
2026-03-06 10:51:52 +07:00
2ae5582a9e Recycle Bin: Adds destination overrides to restoreFromRecycleBin entity-action kind (#21867)
* feat(recycle-bin): add destination entity overrides to restoreFromRecycleBin kind

Add optional destinationItemRepositoryAlias, destinationItemDataResolver,
and destinationRootEntityType properties to support cross-entity-type
restore (e.g. element restoring into element-folder). Existing document
and media manifests are unaffected as all new properties fall back to
the original values. Also adds element folder restore manifest.

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

* refactor(recycle-bin): extract #resolveDestinationItemName to reduce complexity

Extract resolver logic from setDestination into a dedicated method to
bring cyclomatic complexity under the threshold of 9.

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

* Removed Restore Element Folder From Recycle Bin Entity Action

(This is for a separate PR)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:45:59 +01:00
0ec334e252 fix(media): allow focal point to be set to null in image cropper (#21340)
* fix(media): allow focal point to be set to null in image cropper

- Updated UmbImageCropperPropertyEditorValue type to allow null for focalPoint
- Changed component state to use null as default instead of { left: 0.5, top: 0.5 }
- Replaced logical OR (||) with nullish coalescing (??) to preserve null values
- Updated reset function to set focalPoint to null
- Added null handling in all rendering and calculation logic
- Components now default to center (0.5, 0.5) for display when focalPoint is null

Fixes #21273

* refactor(media): extract logic from initializeCrop to reduce function size

- Extracted mask dimension calculation into #calculateMaskDimensions
- Extracted mask style application into #applyMaskStyles
- Extracted image scale calculation into #calculateImageScales
- Extracted image position calculation into separate methods:
  - #calculateImagePositionWithCoordinates (for existing crops)
  - #calculateImagePositionWithFocalPoint (for focal point positioning)
- Extracted image style application into #applyImageStyles
- Extracted zoom level update into #updateZoomLevel

Reduces #initializeCrop from 72 lines to 33 lines, meeting CI/CD threshold of 70 lines.

Related to #21273

* refactor(media): replace primitive parameters with interfaces to fix code quality warnings

- Created ViewportDimensions interface to group viewport width/height
- Created MaskDimensions interface to group mask dimensions and position
- Created ImageDimensions interface to group image dimensions and position
- Refactored all functions to use interface objects instead of multiple primitives
- Reduced #calculateImageDimensionsAndPosition from 5 args to 2
- Reduced #calculateImagePositionWithCoordinates from 5 args to 2
- Reduced #calculateImagePositionWithFocalPoint from 4 args to 1

Fixes primitive obsession (85.7% -> reduced) and excessive function arguments warnings.

Related to #21273

* fix(media): update modal value interface to allow null focal point

- Updated UmbImageCropperEditorModalValue interface to allow null for focalPoint
- Added explicit null handling when assigning focalPoint in onChange handler

Fixes TypeScript build error where null focalPoint was not assignable to non-nullable type.

Related to #21273

* Refactor image cropper focal-point handling

* Set defaultFocalPoint to null in test file.

* Default focalPoint to null and adjust checks.

---------

Co-authored-by: Francluob <francluob.dev@gmail.com>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
Co-authored-by: Emma L Garland <1649855+emmagarland@users.noreply.github.com>
Co-authored-by: engjlr <enl@umbraco.dk>
2026-03-05 10:57:50 +00:00
Niels Lyngsø db9cbcb457 Merge branch 'main' into v18/dev 2026-03-05 11:07:26 +01:00
Niels Lyngsø d04f8d9029 fit unit test types 2026-03-05 11:04:41 +01:00
Andy Butland c4d09893cd Merge branch 'main' into v18/dev 2026-03-05 09:22:54 +01:00
Jacob Overgaard 040735b1c1 build: optimises azure static builds in order not to consume too many environments 2026-03-05 08:32:25 +01:00
Andy Butland d92502b212 Merge branch 'main' into v18/dev 2026-03-04 12:15:01 +01:00
Andy Butland 6b00925a6a Merge branch 'main' into v18/dev 2026-03-04 11:55:57 +01:00
Andy Butland b9142ad728 Merge branch 'main' into v18/dev 2026-03-04 11:54:49 +01:00
Nhu DinhandGitHub b71c2e53b7 E2E: QA Added acceptance tests for reference tracking info tab of elements (#21949)
* Added tests for element reference tracking in info tab

* Removed tags

* Make all ElementReferenceTracking tests run in the pipeline

* Moved goToBackOffice step to beforeEach

* Updated import file

* Make tests run in the pipeline before merging

* Fixed npm command

* Revert npm command
2026-03-04 10:11:46 +00:00
598a2186d7 Elements: Treat local elements as global elements (#21795)
* Make local and global elements behave the same (use the same implementation)

* Await async calls, don't fire-and-forget

* Fix the remaining unit tests

* Flush static fields on friendly published extensions before starting tests

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-03-04 10:05:52 +01:00
fcdfb9e588 E2E: QA: Merge moved testhelpers/builders from 17 to 18 (#21970)
* Moved helpers/builder from v18

* Updated existing helpers/builder

* Updated ui helper for updating property editor in document type

* Fixed failing tests

* Revert changes to package-lock

* Cherry pick latest updates from main

---------

Co-authored-by: Nhu Dinh <hnd@umbraco.dk>
2026-03-04 12:02:24 +07:00
Jacob Overgaard b00840e147 build(login): syncs lockfile 2026-03-03 16:20:43 +01:00
Jacob Overgaard f2473f7f74 build(login): syncs lockfile 2026-03-03 16:19:08 +01:00
Jacob Overgaard 673d97c19b build(login): syncs package files 2026-03-03 16:17:06 +01:00
Andy Butland 32acc62cde Merge branch 'main' into v18/dev 2026-03-03 15:44:45 +01:00
Laura Neto c568db2704 Re-generate Umbraco.Tests.AcceptanceTest/package-lock.json 2026-03-03 12:45:34 +01:00
Andy Butland fa6c5d0537 Merge branch 'main' into v18/dev 2026-03-03 11:44:10 +01:00
Laura NetoandGitHub 62d9a002f7 Elements: Add missing documentation endpoint attributes (#21979)
Add missing EndpointSummary and EndpointDescription attributes to element recycle bin restore controllers
2026-03-03 08:47:01 +01:00
Andy Butland 8b59e8eb3b Merge branch 'main' into v18/dev 2026-03-03 08:06:31 +01:00
Niels Lyngsø eec6a27cca Merge branch 'main' into v18/dev
# Conflicts:
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/ContentStartNodes.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/MediaStartNodes.spec.ts
2026-03-02 14:00:47 +01:00
Nhu DinhandGitHub 3b68fef220 E2E: QA Added acceptance tests for global elements (#21608) 2026-02-26 17:28:21 +00:00
Andy Butland 92ec78086d Merge branch 'main' into v18/dev 2026-02-26 07:06:34 +01:00
cf9c2908b4 Elements: Add permission-based filtering to element tree endpoints (#21729)
* Add permission-based filtering to element tree endpoints

The element tree endpoints now filter results based on the current
user's browse permissions via a new IElementPermissionFilterService,
mirroring the existing document tree behavior.

Also extracts shared filtering logic from DocumentPermissionFilterService
into a PermissionFilterServiceBase to avoid duplication.

* Add unit tests for ElementPermissionFilterService

* Replace document-specific inheritdoc with neutral XML docs in PermissionFilterServiceBase

* Fix GetPermissionsAsync to use the provided objectTypes parameter instead of hardcoded Document type

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2026-02-25 15:13:42 +00:00
Laura NetoandGitHub 765a3b2968 Tests: Set AllowedInLibrary on element content type in permission tests (#21908)
Set AllowedInLibrary on element content type in permission tests

The GetElementPermissionsCurrentUserControllerTests were failing because the
test setup created an element content type without setting AllowedInLibrary
to true. The ElementEditingService.TryGetAndValidateContentType method now
requires both IsElement and AllowedInLibrary to be true for element creation.
2026-02-25 15:02:12 +01:00
Laura NetoandGitHub 89c7bb356b Elements: Replace block keys on element copy and save (#21814)
* Handle element saving and copying notifications in complex property editors

Extend ComplexPropertyEditorContentNotificationHandler to also handle
ElementSavingNotification and ElementCopyingNotification, ensuring that
block property key replacement (BlockList, BlockGrid, RichText) is
applied to elements the same way it is for content.

* Add integration tests for element copy with block editors

Test that block keys are regenerated and block structure is preserved
when copying elements with BlockList, BlockGrid, and RichText editors,
for both invariant and culture-variant content.
2026-02-25 14:06:55 +01:00
Andy Butland 023c08fcdb Merge branch 'main' into v18/dev 2026-02-25 11:30:01 +01:00
Andreas ZerbstandGitHub 29233a3455 QA: E2E: Added v18/dev so it runs on the nightly test pipeline (#21902)
Removed v15dev and added 18dev to nightly pipeline
2026-02-25 10:22:04 +00:00
Andy Butland 7b97bdd0ef Merge branch 'main' into v18/dev 2026-02-25 09:41:15 +01:00
Andy Butland 21597fa2f8 Merge branch 'main' into v18/dev 2026-02-24 06:57:14 +01:00
Andy Butland 336039f963 Merge branch 'main' into v18/dev 2026-02-24 06:40:18 +01:00
Laura NetoandGitHub c7125967c9 Elements: Add scheduled publishing support for elements (#21796)
* Add scheduled publishing support for elements

Move PerformScheduledPublish from IContentService to the shared
IPublishableContentService<T> interface so both documents and elements
support scheduled publishing.

Filter ClearSchedule and HasContentForRelease/Expiration queries in
PublishableContentRepositoryBase by NodeObjectTypeId to prevent document
and element schedules from interfering with each other.

Update ScheduledPublishingJob to process both document and element
schedules, and add integration tests verifying cross-entity isolation.

* Simplify ScheduledPublishingJob.ExecuteAsync

Extract duplicated scheduled publishing logic into a generic helper
method and include the entity type in the log message.
2026-02-23 20:12:00 +01:00
3e0a3ff1ea Content Version Cleanup: Include element versions in the background cleanup job (#21839)
* Add element version cleanup to the content version cleanup background job

The existing ContentVersionCleanupJob only cleaned up document versions.
Element versions were left to accumulate despite having the same cleanup
service infrastructure available. This extends the job to also clean up
element versions using the same configuration toggle and schedule.

* Use PascalCase for structured logging names.

* Fixed code warnings and duplicate line breaks.

* Cleaned up usings.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-02-23 18:58:15 +00:00
Laura NetoandGitHub fa95f956a0 Elements: Add audit log retrieval endpoint and UI support (#21777)
* Add missing AuditType.Copy audit log for element copy operations

Make the abstract Copy method in ContentEditingServiceBase async and
accept a Guid userKey instead of int userId, allowing the element
copy implementation to use the audit service directly. Add the
missing _auditService.AddAsync(AuditType.Copy, ...) call in
ElementEditingService.CopyAsync to match the document equivalent
in ContentService.Copy.

* Fix copy audit log to record against the original element

The copy audit entry was being logged against the new copy's ID
instead of the original element's ID, inconsistent with how
documents handle copy audit logging.

* Add audit log retrieval endpoint for elements and wire up frontend

Add GET /{id:guid}/audit-log endpoint following the document audit
log pattern. Wire up the existing frontend data source to call the
new API, add element-specific localization strings, and remove the
unsupported sort audit type.
2026-02-23 08:43:36 +01:00
Niels Lyngsø ba83cc1006 Merge branch 'main' into v18/dev 2026-02-20 13:48:48 +01:00
Laura Neto 17dd0757d3 Merge branch 'main' into v18/dev 2026-02-20 13:38:06 +01:00
Laura NetoandGitHub e02a3d452c Repository Caches: Fix GUID cache key prefix in PublishableContentRepositoryBase (#21836)
Fix GUID repository cache key prefix in PublishableContentRepositoryBase

The merge of the GUID cache key collision fix (9ea0520) applied
IContent-specific changes from DocumentRepository's nested class, but
in v18/dev this code lives in the generic base class. Two issues:

- EntityByGuidReadRepository.GetCacheKey used the "uRepo_" prefix
  while GuidReadRepositoryCachePolicy looks up entries with "uRepoGuid_",
  causing PopulateCacheByKey to insert under a key the policy never finds.
- PersistUpdatedItem cleared GetGuidKey<IContent> instead of
  GetGuidKey<TEntity>, so ElementRepository would clear the wrong key.
2026-02-20 11:35:52 +00:00
Niels Lyngsø 33a30c38dc Merge branch 'main' into v18/dev 2026-02-19 10:50:40 +01:00
Andy Butland 9a0309ac62 Merge branch 'main' into v18/dev 2026-02-19 10:03:10 +01:00
Niels Lyngsø 31ffc9ff02 Merge branch 'main' into v18/dev 2026-02-17 10:37:21 +01:00
8e911d728d Elements: Add AllowedInLibrary flag to content types with dedicated endpoint (#21723)
* Add AllowedInLibrary flag to content types

Add a new boolean property AllowedInLibrary across all layers to
indicate whether a content type is allowed in the library. This is
only meaningful for element types (IsElement = true).

Changes span the core domain model, Management API request/response
models, persistence DTOs/mappers/factories, and a database migration
to add the column to the cmsContentType table.

* Enforce AllowedInLibrary in ElementEditingService.CreateAsync

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

* feat(api): add AllowedInLibrary filter to document type search endpoint

Add allowedInLibrary query parameter to GET /document-type/search,
following the same pattern as the existing isElement filter. The old
SearchAsync overload without the parameter is preserved as a default
interface method and marked obsolete (scheduled for removal in v19).

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

* chore(api): regenerate OpenApi.json and backoffice client types

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

* feat(api): make search query parameter nullable for document type search

Allow the document type search endpoint to be called without a text
query, enabling filter-only usage (e.g. filtering by isElement and
allowedInLibrary without requiring a search term).

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

* feat(api): replace search allowedInLibrary filter with dedicated endpoint

Revert the search endpoint changes (IContentTypeSearchService, controller)
and instead add a dedicated GET /document-type/allowed-in-library endpoint
that follows the AllowedAtRoot pattern. This ensures IContentTypeFilter
support and a cleaner API separation.

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

* test(api): add integration tests for GetAllAllowedInLibraryAsync and AllowedInLibraryDocumentTypeController

Add service-level tests verifying correct filtering by IsElement + AllowedInLibrary, pagination, and IContentTypeFilter integration. Add controller-level authorization tests for the allowed-in-library endpoint.

* Map allowedInLibrary through content type data sources

Add allowedInLibrary to UmbContentTypeModel and map it consistently
across document, media, and member type data sources for scaffold, read,
create, and update operations.

* Add AllowedInLibrary support to test builders and set it in all element tests

ElementEditingService.CreateAsync checks contentType.AllowedInLibrary and
returns NotAllowed if false. All element test types were missing this flag,
causing test failures. Adds IWithAllowedInLibraryBuilder interface, extension
method, and sets AllowedInLibrary=true on all element type creation in tests.

* Also enforce IsElement check when creating elements in the library

* Set IsElement and AllowedInLibrary on ElementPublishingServiceTests content types

* Remove AllowedInLibrary from document type tree item response model

The AllowedInLibrary property is not relevant for tree items and is not
used by the frontend. This removes it from the tree item model, its
mapping in the tree controller, and regenerates the OpenAPI spec and
TypeScript client accordingly.

* Refactor element content type validation into base class override

Make TryGetAndValidateContentType protected virtual in
ContentEditingServiceBase and override it in ElementEditingService to
check IsElement and AllowedInLibrary. This guards both create and update
paths (previously only create was guarded) and eliminates duplicate
ContentTypeNotFound handling.

Enable the previously-ignored
Cannot_Create_Element_Based_On_NonElement_ContentType test and add a new
test for the AllowedInLibrary check.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 10:01:34 +01:00
Andy Butland 7a12f056e4 Merge branch 'main' into v18/dev 2026-02-17 07:33:01 +01:00
Kenn JacobsenandGitHub 391e8a0867 Fix the integration tests project file structure (#21766) 2026-02-16 11:27:11 +00:00
0ca1a861e9 Elements: Add webhooks support (#21697)
* Add webhooks for elements

* Review: Removed unused payload type

* Use new object as empty payload

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-02-16 10:36:00 +01:00
Laura Neto e3135685da Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Core/Services/UserService.cs
2026-02-16 10:28:54 +01:00
6f0fd8f6ec Elements: Add reference settings support (#21601)
* Elements: Add DisableDeleteWhenReferenced support and fix delete notifications

- Add DisableDeleteWhenReferenced check to ElementContainerService delete operations
- Fire ElementDeletedNotification and EntityContainerDeletedNotification per item during descendant deletion
- Fix potential infinite loop when items are skipped due to being referenced
- Simplify EmptyRecycleBinAsync to use DeleteDescendantsLocked directly
- Use path descending ordering for consistent deletion order (children before parents)
- Add test for descendant delete notifications

* Elements: Fix EmptyRecycleBin pagination with DisableDeleteWhenReferenced

When DisableDeleteWhenReferenced is enabled and some items are skipped,
the standard skip/take pagination breaks. This change:

- Adds SqlLessThan/SqlGreaterThan SQL expression extensions for string
  comparison in LINQ queries
- Uses path-based cursor pagination instead of skip/take
- Tracks protected paths to prevent deleting containers that have
  referenced descendants
- Adds ElementRecycleBin to UmbracoObjectTypes enum

* Tests: Add DisableUnpublishWhenReferenced tests for elements

Verify that DisableUnpublishWhenReferenced works correctly for elements
(inherited from ContentPublishingServiceBase):
- Cannot unpublish an element that is being referenced
- Can unpublish an element that is doing the referencing

* Elements: Remove redundant Trashed filter from DeleteDescendantsLocked

The Trashed filter was redundant because:
- EmptyRecycleBinAsync only operates on items under the recycle bin root
- DeleteFromRecycleBinAsync requires containers to be trashed, and all
  descendants are marked as trashed when moved to recycle bin

Removing the filter simplifies the query and handles edge cases better.

* Elements: Add proper ProblemDetails responses for publish/unpublish endpoints

Move ContentPublishingOperationStatusResult from DocumentControllerBase to
ContentControllerBase so it can be shared. Add ElementPublishingOperationStatusResult
to ElementControllerBase and update PublishElementController and
UnpublishElementController to return proper error responses instead of empty
BadRequest() when operations fail (e.g., when DisableUnpublishWhenReferenced is enabled).

* Refactor: Use abstract EntityName for content controller error messages

Replace hardcoded "document" terminology in shared ContentControllerBase
error messages with an abstract EntityName property, so each subclass
(document, element, media, member, etc.) provides context-appropriate
error messages.

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

* Fix: Check DisableUnpublishWhenReferenced when moving elements to recycle bin

ElementEditingService.MoveToRecycleBinAsync was missing the reference
check that ContentEditingService already performs for documents. This
allowed referenced elements to be moved to the recycle bin even when
DisableUnpublishWhenReferenced was enabled.

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

* Prevent moving container to recycle bin when descendants are referenced

Add server-side validation to ElementContainerService.MoveToRecycleBinAsync
that checks for referenced descendants when DisableUnpublishWhenReferenced
is enabled. Uses ITrackedReferencesService.GetPagedDescendantsInReferencesAsync
as an upfront check before any move processing begins.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 16:02:43 +01:00
Laura NetoandGitHub ebcb996431 Elements: Fix delete blocked by trash-tracking relation (#21725)
Fix element delete blocked by trash-tracking relation

ElementEditingService was missing the RelateParentOnDeleteAlias
override, so the "relate parent on delete" relation created when
trashing was not excluded from the reference check. This caused
"Cannot delete a referenced content item" when
DisableDeleteWhenReferenced was enabled, even for unreferenced
elements.
2026-02-13 07:30:28 +01:00
8cbf68223a Global Elements: UI refinements, element picker and constants tidy-up (#21737)
* improvement(elements): general UI updates, element picker rework, and constants tidy-up

* fix(elements): forward min/max messages through umb-input-element and minor cleanups

Add minMessage/maxMessage properties to UmbInputElementElement so validation
messages are properly forwarded to the inner umb-input-entity-data component.
Also fix JSDoc grammar, variable naming, and comment tidying.

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

* fix(elements): sync value/selection and register inner form control in umb-input-element

Add getter/setter overrides for value and selection that keep them in sync
(matching umb-input-content pattern), and register the inner umb-input-entity-data
via addFormControlElement() in firstUpdated() so validation propagates correctly.

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

* fix(elements): use correct element ID in referenced-by mock handler

Change sentinel ID from 'all-property-editors-document-id' to 'simple-element-id'
to match the actual element mock fixture IDs in element.data.ts.

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

* test(elements): add unit test for umb-input-element

Add instantiation and conditional a11y audit tests following the
umb-input-document.test.ts pattern.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:49:56 +00:00
39f19bf3ab Global Elements: Rollback UI (#21712)
* feat(elements): add element rollback repository, modal, and audit log

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

* Exported rollback constants

* Update src/Umbraco.Web.UI.Client/src/packages/elements/rollback/modal/rollback-modal.element.ts

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-02-12 17:08:08 +00:00
Niels Lyngsø e8c0dd897b Merge branch 'main' into v18/dev 2026-02-12 16:27:28 +01:00
66fbade194 Global Elements: Workspace Validation UI (#21711)
* feat(elements): add element workspace validation repository

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

* Global Elements: Address Copilot review feedback on validation PR

Fix JSDoc comments on validation repository/data-source to accurately
describe validation behavior instead of persistence. Use barrel import
for validation repository and remove leftover commented-out code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Global Elements: Remove redundant guard clauses in validation data source

Remove TypeScript-redundant checks in validateCreate to reduce
cyclomatic complexity below the CodeScene threshold of 9.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-02-12 15:12:23 +01:00
ae2761f204 Global Elements: Reference Tracking UI (#21710)
* feat(elements): add element reference tracking repository

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

* Fixed linting errors

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:02:20 +01:00
Andy Butland a49981b6c5 Merge branch 'main' into v18/dev 2026-02-12 06:50:15 +01:00
Lee KelleherandGitHub d21d1453f5 Global Elements: Exports all constants for @umbraco-cms/backoffice/element (#21727) 2026-02-11 17:52:40 +01:00
Niels Lyngsø 11e19466f8 Merge branch 'main' into v18/dev 2026-02-11 12:52:14 +01:00
Laura Neto d51de53804 chore(api): regenerate OpenApi.json and backoffice client SDK
The OpenAPI definition and backoffice TypeScript client were out of
sync with recent Management API changes already on v18/dev. Regenerated
to bring them up to date.
2026-02-10 15:25:21 +01:00
Laura Neto 8b5448adcd Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Core/Constants-Security.cs
#	src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs
#	src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs
#	src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs
#	src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs
#	src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
2026-02-10 14:02:51 +01:00
cf70d7ff13 Global Elements: Refactor content and element repositories into a common base (#21637)
* Begin implementation of repo base

* Move internal mapping - part 1

* Move internal mapping - part 2

* Move versioning, persistence, GUID sub repo and utilities to base

* Fix wrong assumption in cache tests

* Move content repo + recycle bin to base

* Move schedule to base

* Move common delete clauses to base

* Move DTO mapping to base

* Fix a few of the pending TODOs for elements

* Abstract OnUowRefreshedEntity away to concrete implementations

* Handle template editing in a less hardcoded way

* Restore DTO visibility for elements

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

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* Update src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublishableContentRepositoryBase.cs

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* Update src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublishableContentRepositoryBase.cs

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* Update cache key (review comment)

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-02-10 09:54:38 +01:00
f0dc972bf2 Elements: Add restore from recycle bin functionality (#21556)
* Elements: Add restore from recycle bin functionality

- Add RestoreAsync to IElementEditingService and ElementEditingService
- Add RestoreAsync to IElementContainerService and ElementContainerService
- Add RestoreElementRecycleBinController and RestoreElementFolderRecycleBinController API endpoints
- Add TryGetContainedObjectType to EntityContainer for graceful handling of non-container types
- Update EntityContainerRepository to return null instead of throwing for non-container entities
- Add comprehensive integration tests for element and container restore operations

* Refactor: Remove entity return from Move/Restore/MoveToRecycleBin methods

Simplify the return types of IElementEditingService and IElementContainerService
move operations to return only the operation status instead of the entity.

These operations don't meaningfully change entity data (just location/state),
and no consumers were using the returned entities. Callers can use GetAsync
if they need the updated entity afterward.

* Fix: Capture original path before move for restore relation cleanup

The MoveEventInfo.OriginalPath was incorrectly set to the element's path
after the move, causing DeleteOriginalParentRelationsOnRestore to fail
because the path no longer contained the recycle bin path prefix.

* Tiny little formatting

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-02-09 13:39:25 +00:00
Andy Butland 81276fc048 Adds endpoint summaries and descriptions to new controllers introduced since 17. 2026-02-09 13:04:54 +01:00
Andy Butland 58f300a20d Merge branch 'main' into v18/dev 2026-02-09 12:36:23 +01:00
Niels Lyngsø f530772b80 Merge branch 'main' into v18/dev 2026-02-05 12:49:26 +01:00
Niels Lyngsø 8cea6cb6b5 Merge branch 'main' into v18/dev 2026-02-05 09:06:55 +01:00
Laura NetoandGitHub 7cf4c7857f Elements: Add reference tracking and recycle bin query support (#21481)
* Elements: Add reference tracking and recycle bin query support

- Add Element reference tracking API endpoints (referenced-by, are-referenced, referenced-descendants)
- Add Element recycle bin original-parent and referenced-by endpoints
- Add ElementReferenceResponseModel and ElementContainerReferenceResponseModel
- Add IElementRecycleBinQueryService for querying original parents of trashed elements
- Add Element relation type constants for parent tracking on delete
- Add translation strings for Element recycle bin operations
- Refactor RelateOnTrashNotificationHandler to reduce code duplication using generic helper methods
- Add Element and ElementContainer support to RelateOnTrashNotificationHandler

* Elements: Register Element notification handlers for relation tracking

Add Element and EntityContainer notification handlers for:
- RelateOnTrashNotificationHandler (move to/from recycle bin)
- ContentRelationsUpdate (track element content relations)

* Elements: Remove ReferencedDescendantsElementController

Elements are leaf nodes in the folder structure and cannot have
descendants, making this endpoint unnecessary.

* Elements: Fix ElementPickerPropertyEditor reference extraction

The element picker stores element IDs as Guids, not as UDI strings.
Updated GetReferences to deserialize as Guid array and create UDIs
from the Guid values.

* Elements: Fix TrackedReferencesRepository to include Element published state

Add LEFT JOIN to ElementDto and use COALESCE to get the published state
from either DocumentDto or ElementDto, fixing the issue where Element
references returned published = null.

* Elements: Add ReferencedDescendantsElementFolderController

Add endpoint to get referenced descendants of an element folder.
Unlike elements (which are leaf nodes), folders can have descendants
that may be referenced elsewhere.

* Elements: Add integration tests for Element reference tracking

Add TrackedReferencesServiceElementTests covering:
- GetPagedRelationsForItemAsync for Elements
- GetPagedRelationsForRecycleBinAsync for Elements
- GetPagedKeysWithDependentReferencesAsync for Elements
- GetPagedDescendantsInReferencesAsync for Element containers

* Elements: Add management API controller permission tests for Element reference endpoints

- Add ReferencedByElementControllerTests for element referenced-by endpoint permissions
- Add AreReferencedElementControllerTests for element are-referenced endpoint permissions
- Add ReferencedDescendantsElementFolderControllerTests for folder descendants endpoint permissions
- Fix GetManagementApiUrl to respect [FromQuery(Name="...")] attribute for proper URL generation

* Elements: Split OriginalParentElementRecycleBinController into two controllers

Split the controller to follow the controller-per-operation pattern,
consistent with DeleteElementRecycleBinController and
DeleteElementFolderRecycleBinController.

- OriginalParentElementRecycleBinController: for elements
- OriginalParentElementFolderRecycleBinController: for folders

* Elements: Fix user fallback for audit logging in RelateOnTrashNotificationHandler

Update the handler to properly resolve user key for audit logging, using a switch expression
to handle different entity types. Also sets CreatorId on EntityContainer creation.

* Tests: Fix duplicate query parameter in ItemElementItemControllerTests

Remove the ClientRequest() override that was appending a duplicate id query
parameter to the URL. The base MethodSelector already includes the element key
which gets converted to the query parameter by GetManagementApiUrl, causing
the URL to become ?id=<guid>?id=<guid> and model binding to fail.

* Tests: Fix permission controller tests to use correct entity types

These tests were passing on the base branch only because the URL was being
constructed incorrectly (missing query parameters). After be399134f8 fixed
the GetManagementApiUrl helper to properly include FromQuery parameters,
the tests now correctly build URLs and revealed that they were using user
keys instead of the expected document/media/element node keys.

Updated tests to create the appropriate entity type (document, media, or
element) and pass its key to the permission endpoints.

* Tests: Update RelationTypeRepositoryTest for new element relation types

Update expected counts and fix hardcoded ID lookup after new element
reference tracking relation types were added to the system:
- umbElement (RelatedElement)
- relateParentElementContainerOnElementDelete
- relateParentElementContainerOnContainerDelete

Changes:
- Store created test relation type in field to use actual ID instead of
  hardcoded ID 9 which shifted when built-in types were added
- Update GetAll expected count from 9 to 12 (9 built-in + 3 test data)
- Update Count query expected from 6 to 8 (aliases starting with "relate")

* Refactor: Rename methods in RelateOnTrashNotificationHandler for clarity

Rename methods and parameters to better describe their purpose:
- DeleteRelationsOnRestore → DeleteOriginalParentRelationsOnRestore
- CreateRelationsOnTrashAsync → CreateOriginalParentRelationOnTrashAsync
- relationTypeAlias → originalParentRelationTypeAlias
- relationTypeName → originalParentRelationTypeName

These names clarify that the methods handle "original parent" relations
used for restoring items from the recycle bin, not all relations.

* Tests: Fix ReferencedDescendantsElementFolderControllerTests expectations

- Use unique folder names to prevent conflicts between test runs
- Add assertion to verify folder creation succeeds
- Correct expected status codes for Editor and Writer to OK (not NotFound)

The NotFound responses were caused by folder creation failures due to
duplicate names, not actual permission restrictions.

* Tests: Add success assertions to Element controller test setup methods

Add Assert.IsTrue checks after service calls in test setup to ensure
test prerequisites are correctly established before running actual tests.
This prevents silent failures in setup from causing misleading test results.

Assertions added for:
- ElementContainerService.CreateAsync (9 tests)
- ElementEditingService.CreateAsync (19 tests)
- ElementEditingService.MoveToRecycleBinAsync (6 tests)
- ElementContainerService.MoveToRecycleBinAsync (3 tests)

* Elements: Fix MapReference to return un-enriched response when entity not found

Return the mapped response model instead of null when the matching entity
cannot be found for enrichment. This preserves basic reference information
even when variant data cannot be loaded, preventing valid references from
being silently dropped.

Also clean up ElementContainerReferenceResponseModel formatting.

* Fix: Guard GetSlimEntities against empty keys to prevent loading all entities

* Fix: Return ParentIsTrashed status when original parent is in recycle bin

* Breaking: Remove duplicate sync notification handler interfaces

Remove INotificationHandler<ContentMovedToRecycleBinNotification> and
INotificationHandler<MediaMovedToRecycleBinNotification> interfaces along
with their obsolete sync Handle methods. Only the async handlers should
be implemented.

* Tests: Simplify TrackedReferencesServiceElementTests

- Simplify assertions in Get_Descendants_In_References test
- Create Element3 after folder creation to avoid unnecessary update

* Revert: Remove changes to be moved to separate PRs

Revert EntityTypeContainerService.CreateAsync CreatorId change and
permission controller test changes - these should be addressed in
separate PRs.

* Revert: Remove GetMediaPermissionsCurrentUserControllerTests changes

This change should be addressed in a separate PR for v17.

* Refactor: Move recycle bin audit logging to services

Move audit logging for recycle bin operations from RelateOnTrashNotificationHandler
to the individual services (ContentService, MediaService, ElementEditingService,
ElementContainerService). This simplifies the notification handler and keeps audit
logging closer to the operations being performed.

- Simplify audit messages to "Moved to recycle bin from parent {parentId}"
- Add AuditMoveToRecycleBin helper methods to Content and Media services
- Add AuditMoveAsync helper methods to Element services
- Remove unused audit dependencies from RelateOnTrashNotificationHandler
- Add obsolete constructor bridge for backwards compatibility

* Refactor: Extract GetParentIdFromPath extension method

Add GetParentIdFromPath string extension to consolidate duplicate logic
for extracting parent ID from entity path strings. This replaces 5
instances of the same path parsing pattern across services and handlers.

- Add GetParentIdFromPath to StringExtensions.Parsing.cs
- Inline audit calls in ContentService, MediaService,
  ElementEditingService, and ElementContainerService
- Update RelateOnTrashNotificationHandler to use the new extension
- Add unit tests for the new extension method

* Refactor: Make CreateOriginalParentRelationOnTrash synchronous

Remove unnecessary async from CreateOriginalParentRelationOnTrash since
the method contains no async operations. Update handlers to return
Task.CompletedTask directly.
2026-02-04 14:54:50 +00:00
5fe5a0febf Elements: Implement validation for Element editing endpoints (#21562)
* Elements: Implement validation for Element editing endpoints

Move ValidateCulturesAndPropertiesAsync and GetCulturesToValidate from
ContentEditingService to ContentEditingServiceBase, enabling reuse in
ElementEditingService.

- Implement ValidateCreateAsync and ValidateUpdateAsync in ElementEditingService
- Update Element API controllers to return validation results properly
- Update all inheriting services (Media, Member, Blueprint) with new params

* Elements: Add validation tests for ElementEditingService

- Add tests for ValidateUpdateAsync and ValidateCreateAsync
- Cover invariant, culture variant, and permission-based validation scenarios

* Fix bad merge

* Removed unused fields

* Removed old editor UI

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-02-04 13:35:54 +00:00
Andy Butland d4ee843a01 Merge branch 'main' into v18/dev 2026-02-04 12:06:42 +01:00
Niels Lyngsø c48ef57a84 Merge branch 'main' into v18/dev 2026-02-04 09:20:08 +01:00
Andy Butland 0c3d1d7058 Merge branch 'main' into v18/dev 2026-02-03 17:14:16 +01:00
Andy Butland eda9857200 Merge branch 'main' into v18/dev 2026-02-03 09:37:26 +01:00
46b96f3811 Global Elements - take one (#21431)
* CRUD + folders + API

* Fix infinite recursion

* Distributed cache handling for Elements

* Publishing for Elements (incl. refactor)

* Fix bad file name

* Added "foldersOnly" option to the siblings endpoint

* Update src/Umbraco.Core/Models/UmbracoObjectTypes.cs

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

* API for publishing elements

* Published element cache (WIP)

* Fix delete at repo level

* Fixing up a little tests

* Element picker property editor

* Added tests to prove published element status

* Move scheduled content keys to base abstraction

* Add request caching for published element creation (similar to published document creation)

* Apply conditional appcache access to elements as well

* Fix test build errors

* Fix merge from main

* Fix merge

* Add cache invalidation on update (like content and media)

* Move element (incl. tests)

* Element copying

* Add items endpoint incl. variation info at item level

* Make the Element tree items look like Document tree items (with variations)

* Rename all things ElementType to DocumentType

* Move ElementRepository to the right place

* Fix auditing after merge (changes from #19357)

* Fix dates after merge (changes from #19822)

* Fix NPoco querying after merge (changes from #20184)

* Fix various build errors after merge

* Move containers

* Add migration to create element tables

* Re-implement #21105 at base class level

* Fix merge

* Add element tree recycle bin + move element to/from recycle bin

* Controllers for move to recycle bin + recycle bin root

* Support element containers in recycle bin (no controllers)

* Handle error cases for element moves and add more tests

* Do not allow creation of IPublishedElement for trashed elements

* Amend recycle bin controller output and add children controller

* Regenerate OpenApi.json with Element APIs

* Housekeeping: Organize element container service tests

* Fix bad housekeeping

* Add missing siblings controller for recycle bin

* Add "delete from recycle bin" and "empty recycle bin" operations (including API)

* Updated OpenApi.json to reflect new endpoints

* Added `CreateDate` to `ElementTreeItemResponseModel`

Marked `ElementRecycleBinItemResponseModel.DocumentTypeReferenceResponseModel` as nullable.

* Re-generated OpenAPI.json

* Add configuration endpoint for Elements

* Explicitly unpublish published elements when restoring from recycle bin.

* Elements: Remove invalid templateId from ElementVersionDto index definitions (#21384)

* Persistence: Remove invalid templateId from ElementVersionDto index definitions

The ElementVersionDto had index definitions that referenced templateId in
their IncludeColumns, but the ElementVersion table only has id and published
columns. This caused SQL Server clean installs to fail with error 1911:
"Column name 'templateId' does not exist in the target table or view."

This was likely a copy-paste error from DocumentVersionDto which does have
a templateId column.

* Ignore Cannot_Create_Element_Based_On_NonElement_ContentType for the time being

---------

Co-authored-by: kjac <kja@umbraco.dk>

* Fix the ordering of items in the tree

* It's 2026 now...

* Fix missing project structure

* Amend empty recycle bin

* Elements: Fix element recycle bin node insertion on SQL Server (#21390)

Enable IDENTITY_INSERT before inserting the element recycle bin node with an explicit ID, then disable it afterward. This fixes the migration failing on SQL Server with "Cannot insert explicit value for identity column" error.

* Fix count trashed children

* Moved newly added entity service tests to an isolated, per-test DB class so they do not interfere with the existing per-fixture DB tests

* Elements: Element start node permissions (#21375)

* Add Element start node support for Users and UserGroups

- Add StartElementId to UserGroup and element start nodes to User
- Add UserStartNodeFolderTreeControllerBase for tree filtering with folder support
- Update ElementTreeControllerBase to use start node filtering
- Add ElementTreeItemResponseModel.NoAccess property for "no access" items
- Add UserExtensions methods for element start node calculation
- Update User/UserGroup API models and factories
- Add database migration for startElementId column
- Add SectionAccessForElementTree authorization policy

Note: Granular element permissions deferred for future implementation

* Add element root access for default user groups on fresh install

Set StartElementId = -1 for Administrators, Writers, Editors, and
Translators user groups in DatabaseDataCreator, giving them element
root access on fresh installations (matching their content/media access).

* Add multi-type support to UserStartNodeEntitiesService

Added overloads to RootUserAccessEntities, ChildUserAccessEntities, and
SiblingUserAccessEntities that accept multiple UmbracoObjectTypes. This
enables querying for Elements and ElementContainers in a single call
rather than requiring separate queries for each type.

Also added GetAll and GetPagedChildren overloads to IEntityService and
IEntityRepository to support querying multiple object types efficiently
with a single database query.

* Add integration tests for Element start nodes with mixed hierarchy

Added UserStartNodeEntitiesServiceElementTests with a mixed hierarchy
structure containing both containers and elements at each level:
- Level 1: Containers (C1-C5) and Elements (E1-E3)
- Level 2: Child containers (C1-C1 through C1-C10) and Elements (C1-E1, C1-E2)
- Level 3: Leaf elements (C1-C1-E1 through C1-C1-E5)

This tests scenarios where containers and elements are siblings, ensuring
the access filtering works correctly for mixed-type queries.

Also refactored Content and Media tests to use a shared base class
(UserStartNodeEntitiesServiceTestsBase) to reduce code duplication.

* Add Library section for Elements

- Rename Constants.Applications.Elements to Library
- Add SectionAccessLibrary authorization policy
- Add library mapping to SectionMapper
- Grant Library section access to Administrators, Writers, and Editors on fresh install
- Update TreeAccessElements to use Library section

* Add Element tree controller authorization tests

Add integration tests for RootElementTreeController and
ChildrenElementTreeController to verify section-based
authorization works correctly for the Element tree endpoints.

* Fix ReadOnlyUserGroup not passing startElementId to constructor

The obsolete 13-parameter constructor was passing `null` instead of
the actual `startElementId` value to the next constructor, causing
user groups to appear to have no element start node access.

Also update UserFactory.ToReadOnlyGroup to pass the Description
parameter to the ReadOnlyUserGroup constructor.

* Add Element controller authorization tests

Add authorization tests for Element CRUD, Folder, RecycleBin, and Item
controllers to verify user group access permissions.

Tests cover Admin, Editor, Writer, SensitiveData, Translator, and
Unauthorized user groups for each controller endpoint.

* Re-generated OpenApi.json

* Fix Element start node handling to use ElementContainer object type

- Update UserStartNodeFolderTreeControllerBase to query both folder and
  item object types when filtering by user start nodes
- Fix UserGroupPresentationFactory to use ElementContainer instead of
  Element when resolving element start node IDs/keys

* Revert ByKeyElementController to use synchronous Task.FromResult

The method doesn't have any async operations, so async/await adds
unnecessary overhead.

* Fix UserPresentationFactory to use ElementContainer for element start nodes

Element start nodes reference ElementContainer (folders), not Element items.

* Add recycle bin start node access test for Element controllers

- Add WithStartElementId to UserGroupBuilder
- Add ElementRecycleBinControllerTestBase with shared test verifying
  users with non-root element start nodes cannot access recycle bin
- Update all Element recycle bin tests to use the new base class

* Fix UserGroupPresentationFactory and Element test section alias

- Use ElementContainer instead of Element for start node lookups in
  IReadOnlyUserGroup overload
- Use Constants.Applications.Library for Element test section alias

* Add obsolete User constructor overload for backward compatibility

- Add obsolete constructor without startElementIds parameter that delegates
  to the new constructor with an empty array
- Improve XML documentation for all User constructors

* Elements: Move NoAccess property to FolderTreeItemResponseModel base class

This allows both elements and folders to indicate access status in the tree.

* Elements: Add API versioning attributes to SiblingsElementTreeController

* Elements: Add integration tests for element tree start node permissions

Add tests to verify that users with element start node restrictions can only see
and access elements within their permitted hierarchy.

* Group test files

* Remove type check from GetAllPaths overload

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>

* Elements: Add rollback (#21393)

* Services, repos and tests

* Endpoints for Elements versioning

* Add extra test to prove handling of pinned versions

* Renaming from PR review

* Update tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ElementVersionCleanupServiceTest.cs

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* More code clean-up after review

* Use correct deleting/deleted versions notifications

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* Elements: Regenerate OpenApi.json

* Elements: Add default and granular permissions for Element controllers (#21385)

* Add Element start node support for Users and UserGroups

- Add StartElementId to UserGroup and element start nodes to User
- Add UserStartNodeFolderTreeControllerBase for tree filtering with folder support
- Update ElementTreeControllerBase to use start node filtering
- Add ElementTreeItemResponseModel.NoAccess property for "no access" items
- Add UserExtensions methods for element start node calculation
- Update User/UserGroup API models and factories
- Add database migration for startElementId column
- Add SectionAccessForElementTree authorization policy

Note: Granular element permissions deferred for future implementation

* Add element root access for default user groups on fresh install

Set StartElementId = -1 for Administrators, Writers, Editors, and
Translators user groups in DatabaseDataCreator, giving them element
root access on fresh installations (matching their content/media access).

* Add multi-type support to UserStartNodeEntitiesService

Added overloads to RootUserAccessEntities, ChildUserAccessEntities, and
SiblingUserAccessEntities that accept multiple UmbracoObjectTypes. This
enables querying for Elements and ElementContainers in a single call
rather than requiring separate queries for each type.

Also added GetAll and GetPagedChildren overloads to IEntityService and
IEntityRepository to support querying multiple object types efficiently
with a single database query.

* Add integration tests for Element start nodes with mixed hierarchy

Added UserStartNodeEntitiesServiceElementTests with a mixed hierarchy
structure containing both containers and elements at each level:
- Level 1: Containers (C1-C5) and Elements (E1-E3)
- Level 2: Child containers (C1-C1 through C1-C10) and Elements (C1-E1, C1-E2)
- Level 3: Leaf elements (C1-C1-E1 through C1-C1-E5)

This tests scenarios where containers and elements are siblings, ensuring
the access filtering works correctly for mixed-type queries.

Also refactored Content and Media tests to use a shared base class
(UserStartNodeEntitiesServiceTestsBase) to reduce code duplication.

* Add Library section for Elements

- Rename Constants.Applications.Elements to Library
- Add SectionAccessLibrary authorization policy
- Add library mapping to SectionMapper
- Grant Library section access to Administrators, Writers, and Editors on fresh install
- Update TreeAccessElements to use Library section

* Add Element tree controller authorization tests

Add integration tests for RootElementTreeController and
ChildrenElementTreeController to verify section-based
authorization works correctly for the Element tree endpoints.

* Fix ReadOnlyUserGroup not passing startElementId to constructor

The obsolete 13-parameter constructor was passing `null` instead of
the actual `startElementId` value to the next constructor, causing
user groups to appear to have no element start node access.

Also update UserFactory.ToReadOnlyGroup to pass the Description
parameter to the ReadOnlyUserGroup constructor.

* Add Element controller authorization tests

Add authorization tests for Element CRUD, Folder, RecycleBin, and Item
controllers to verify user group access permissions.

Tests cover Admin, Editor, Writer, SensitiveData, Translator, and
Unauthorized user groups for each controller endpoint.

* Re-generated OpenApi.json

* Fix Element start node handling to use ElementContainer object type

- Update UserStartNodeFolderTreeControllerBase to query both folder and
  item object types when filtering by user start nodes
- Fix UserGroupPresentationFactory to use ElementContainer instead of
  Element when resolving element start node IDs/keys

* Revert ByKeyElementController to use synchronous Task.FromResult

The method doesn't have any async operations, so async/await adds
unnecessary overhead.

* Fix UserPresentationFactory to use ElementContainer for element start nodes

Element start nodes reference ElementContainer (folders), not Element items.

* Add recycle bin start node access test for Element controllers

- Add WithStartElementId to UserGroupBuilder
- Add ElementRecycleBinControllerTestBase with shared test verifying
  users with non-root element start nodes cannot access recycle bin
- Update all Element recycle bin tests to use the new base class

* Fix UserGroupPresentationFactory and Element test section alias

- Use ElementContainer instead of Element for start node lookups in
  IReadOnlyUserGroup overload
- Use Constants.Applications.Library for Element test section alias

* Add obsolete User constructor overload for backward compatibility

- Add obsolete constructor without startElementIds parameter that delegates
  to the new constructor with an empty array
- Improve XML documentation for all User constructors

* Elements: Add granular permissions for Element controllers

Add Element-specific permission actions:
- ActionElementBrowse, ActionElementNew, ActionElementUpdate, ActionElementDelete
- ActionElementPublish, ActionElementUnpublish, ActionElementMove, ActionElementCopy

Add permission infrastructure:
- ElementPermissionResource for authorization checks
- ElementPermissionHandler and ElementPermissionRequirement
- ElementPermissionService and IElementPermissionService
- ElementPermissionAuthorizer and IElementPermissionAuthorizer
- ElementGranularPermission model
- ElementPermissionMapper for user group permissions

Update Element controllers with authorization:
- Add HandleRequest pattern via CreateElementControllerBase and UpdateElementControllerBase
- Pass cultures for Publish/Unpublish authorization
- Apply authorization checks to Element CRUD and publishing operations

* Elements: Add default element permissions to user groups

Add element action permissions for Admin, Editor, Writer, and Translator
user groups in DatabaseDataCreator, mirroring the document permission pattern.

* Elements: Add current user element permissions endpoint and fix folder authorization

- Add GetElementPermissionsCurrentUserController endpoint to get current user's element permissions
- Fix ElementPermissionService to authorize both Element and ElementContainer (folders)
- Add GetElementPermissionsAsync to IUserService/UserService
- Add ElementNodeNotFound to UserOperationStatus
- Add IEntityService.GetAll overloads for multiple object types

* Elements: Move NoAccess property to FolderTreeItemResponseModel base class

This allows both elements and folders to indicate access status in the tree.

* Elements: Add default implementation to IUserService.GetElementPermissionsAsync

Adds a default throwing implementation to avoid breaking existing IUserService implementations when this method is added.

* Elements: Add API versioning attributes to SiblingsElementTreeController

* Elements: Add integration tests for element tree start node permissions

Add tests to verify that users with element start node restrictions can only see
and access elements within their permitted hierarchy.

* Add granular permissions to element rollback

* Update src/Umbraco.Core/Actions/ActionElementCopy.cs

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>

* Elements: Use lowercase action aliases for consistency

Update all Element action aliases to lowercase to comply with the
IAction.Alias requirement for case-sensitive filesystems. Also rename
ActionElementNew alias from "elementNew" to "elementcreate" to match
the document action's "create" alias pattern.

* Elements: Refactor UserService permission methods to reduce duplication

Consolidate GetMediaPermissionsAsync, GetDocumentPermissionsAsync, and
GetElementPermissionsAsync into a single shared implementation via
a new private GetContentPermissionsAsync helper method.

---------

Co-authored-by: kjac <kja@umbraco.dk>

* Add element folder "item" endpoint

* Include "isTrashed" in folder response models

* Update TODOs

* Rollback a few unnecessarily breaking signature changes

* Use schema constants from #21327

* Elements: Add admin group element permissions during upgrade (#21452)

Grant the admin user group access to the element root node and all
element permissions when upgrading from a previous version. This
ensures parity with fresh installations where the admin group receives
these permissions by default.

* Elements: Fix Writer expected status codes in Element controller permission tests

Update WriterUserGroupAssertionModel to expect Forbidden for operations
that Writers don't have permission for, matching Document controller
behavior and the actual permissions assigned to the Writer group.

Changed from OK/Created to Forbidden:
- CopyElementControllerTests
- DeleteElementControllerTests
- MoveElementControllerTests
- MoveToRecycleBinElementControllerTests
- PublishElementControllerTests
- UnpublishElementControllerTests
- Folder/DeleteElementFolderControllerTests
- Folder/MoveElementFolderControllerTests
- Folder/MoveToRecycleBinElementFolderControllerTests
- RecycleBin/DeleteElementRecycleBinControllerTests
- RecycleBin/DeleteElementFolderRecycleBinControllerTests
- RecycleBin/EmptyElementRecycleBinControllerTests

* Elements: Fix duplicate column name in DocumentVersionDto index definition

The ForColumns parameter incorrectly specified PublishedColumnName twice
instead of IdColumnName and PublishedColumnName, causing SQL Server to
reject index creation with "duplicate column names" error on new installs.

* Add missing element mapper and allow deleting element types with active elements (#21483)

* Add missing element mapper and allow deleting element types with active elements

* Update src/Umbraco.Core/Services/ContentTypeService.cs

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* Update src/Umbraco.Core/Services/ContentTypeService.cs

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* Update src/Umbraco.Core/Cache/Refreshers/Implement/ElementCacheRefresher.cs

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

* Review comment: ReadOnlyUserGroup constructor

* Update comments in ElementEditingService

* Add Library section access to content, media, and member tree policies

* Elements: Add Elements access to data type, document type, and relation authorization policies (#21501)

Add Elements access to data type, document type, and relation authorization policies

* Amend merge from v18/dev

* Global Elements: Backoffice UI implementation (#21410)

* chore: generate new openapi types

* Added package/module for "Library"

* Added default dashboard for Library section

* [WIP] Adds "Elements" package module

Basics of the tree/menu.

* Adds entity-actions for Create and Reload

* Adds entity-action for Move To

* Adds collection workspace view

for root and folders

* Adds entity-action for Duplicate To

* "Reload Children" should only be for root & folders

* Reworked Library sidebar app

Replaced with Elements sidebar app
Removed the Library menu

* chore: generate new openapi types

* Added Item repository

* Added Reference repository

* Added Element Recycle Bin

Tree, menu, entity-actions, workspace (collection view)

* Adds "umb-element-tree-item" to identify the `isTrashed` state

* Re-added Library sidebar app

Removed Library dashboard (we'll figure it out later)

* Recycle Bin type tweaks

* [WIP] Element "Create" modal

* Reverted Element "Create" modal, to use create-options + picker

* chore: generate new openapi types

* Added Element Detail Repository

* [WIP] Element Workspace + Context

* Elements: Add workspace views for edit and info

Add edit and info workspace views to the Element workspace:
- Edit view using shared 'contentEditor' kind pattern
- Info view displaying state tag, dates, element type, and ID
- Menu structure context for tree navigation
- Split-view component for variant editing

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

* Elements: Add save action and trash state handling

- Add Save workspace action using UmbSubmitWorkspaceAction
- Add isTrashed property to UmbElementDetailModel
- Implement trash state change handling with read-only guard
- Add recycle bin event listeners for trash/restore actions

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

* Adds Workspace actions for Save, Publish, Scheduled Publish

* Adds Element Configuration repository

* Adds mock handle + data for Elements

* Adds Publish and Unpublish entity actions for Elements

Implements context menu actions for publishing and unpublishing elements
directly from the tree. Uses existing modals and publishing repository.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* package-lock.json update

* Adds bulk entity actions for Publish, Unpublish, Move and Trash

* Localization keys + code tweaks

* Adds reusable `emptyRecycleBin` `collectionAction` kind

* Adds `emptyRecycleBin` for Element Recycle Bin collection

* Element Recycle Bin refactoring

Working towards folder support

* Relations: exported entity-action types

* Restructured "Element Folder" code

* Restructured "Trash" entity-bulk-action code

* Adds `trashFolder` `entityAction` kind

* Adds "Trash" entity-action for Element Folders

* Tidy-up / restructuring

* [WIP] Element Picker property-editor UI

making use of an Elements property-data-source,
with Entity Picker.

* Renamed `UmbElementPropertyDatasetContext` to `UmbElementWorkspacePropertyDatasetContext`

to de-duplicate a class name clash with the underlying base class.

* Added "entity-data-picker" importmap

Exposing the "umb-input-entity-data" component

* Reworking the "Element Picker" property-editor UI

to reuse the Entity Picker internal input component

* Implemented "Element Item Data Resolver" helper

* chore: generate new openapi types

* Fixed up the mocks and types

with new Element start nodes and `noAccess` fields.

* Added UI for "Elements Start Nodes"

* Added "entity-data-picker" export to the Vite config

* Fixed Element Folder picker for "start nodes"

* Adds UI for Element's User Permissions

* Adds Element User Permission condition

Implemented the user permissions for entity actions, etc.

* Adds UI for Element's Granular Permissions

* Adds element-folder item repository

* Element Recycle Bin: implemented `isTrashed`

* Fixed mock folder data manager

* Adds move entity-action for element-folder

Implements the Move action for element folders using the
ElementService.putElementFolderByIdMove API endpoint.

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

* Fix typos and element tag name mismatches in elements package

- Fix typo 'now' -> 'no' in user-permissions/types.ts
- Fix HTMLElementTagNameMap tag name to match @customElement decorator
- Fix typo 'TDOD' -> 'TODO' in element-detail.server.data-source.ts
- Fix missing 'u' prefix in element-picker tag name declaration

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

* Ignore local Claude settings in UI Client

* Updated workspace assign access,

to disable root access when start nodes are selected.

* Elements: Display trashed state in Element workspace info panel (#21542)

The state tag in the Element workspace info view was missing a case
for the TRASHED state, causing trashed Elements to incorrectly display
"Not created" instead of "Trashed".

* Elements: Fix folder link in recycle bin list view (#21543)

The trashed element name column always used the element workspace path
pattern, causing folders clicked in the recycle bin list view to show
"Not found". Now checks isFolder and uses the correct workspace path
pattern for folders vs elements.

* Elements: Add missing delete permission conditions to recycle bin actions (#21547)

The Empty Recycle Bin collection action and the folder delete entity
action were missing user permission conditions, making them visible
to users without delete permission.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Co-authored-by: Lee Kelleher <leekelleher@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 07:46:31 +01:00
Laura Neto 7d73588c99 Merge branch 'main' into v18/dev 2026-01-28 13:27:22 +01:00
kjac 40c91262e6 Merge branch 'main' into v18/dev 2026-01-27 10:32:50 +01:00
Niels Lyngsø 31f23204f3 Merge branch 'main' into v18/dev 2026-01-27 09:19:30 +01:00
e53b8bcc52 Variants Sorting: Sort by language name (fix #21408) (#21435)
* Sort at last by language name

* ensure document language picker is sorted as variant selector

* Update src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/shared/document-variant-language-picker.element.ts

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

* Update src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts

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

* Update src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts

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

* refactor to avoid inline methods

* transform into a function

* revert config file commit

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-23 09:18:52 +01:00
Niels Lyngsø 25d3013949 Merge branch 'main' into v18/dev 2026-01-16 17:18:05 +01:00
Andy Butland 8ac46e99b7 Applied naming suggestions made in review of #21374. 2026-01-15 07:51:34 +01:00
Andy Butland 59cc153b84 Merge branch 'main' into v18/dev 2026-01-15 07:46:41 +01:00
29ecae7010 Entities: Prevent changing Key property on existing entities (#21374)
* Prevent setting of entity Key to a new value for already persisted entities.

* Handled file based entities that have a key dependent on their path, so need to be able to have the key changed on move.

* Fixed package data update of content type to resolve failing integration test.

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2026-01-14 06:45:11 +00:00
Laura Neto 409f5072af Merge branch 'main' into v18/dev 2026-01-13 10:09:56 +01:00
Andy ButlandandGitHub 58a0b160b2 Umbraco Helper: Align GetDictionaryValue nullability with behaviour (#21372)
Align GetDictionaryValue nullability with behaviour (returns empty string when no dictionary item is found for the provided key).
2026-01-13 06:42:17 +01:00
Andy Butland c8ba79b2d1 Merge branch 'main' into v18/dev 2026-01-12 11:54:52 +01:00
ef1b48c992 Obsolete Code: Remove obsolete methods and constants relating to allowed application and start node claims (#20124)
* Delete GetStartContentNodes

* Delete GetStartMediaNodes

* Delete GetAllowedApplications

* Delete ClaimTypes

* Update protected recycle bin functionality to no longer used removed claim details.  Added unit tests to verify behaviour.

* Fixed failing unit tests now the number of claims included is reduced.
Addressed comments from code review.

* Minor code tidy.

* Expose claim necessary for retrieving the user key.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-09 12:25:33 +00:00
Andy Butland e9819d6e57 Merge branch 'main' into v18/dev 2026-01-09 12:23:44 +01:00
Laura Neto 480a208655 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2026-01-06 14:52:30 +01:00
Laura Neto 179f2e709d Merge branch 'main' into v18/dev 2025-12-16 15:44:08 +01:00
Laura Neto 90c09b1bf1 Merge branch 'main' into v18/dev
# Conflicts:
#	tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml
2025-12-11 10:17:18 +01:00
Laura Neto e7719c2458 Cleanup compatibility suppressions 2025-12-04 11:15:19 +01:00
Laura Neto fbce5882c6 Set up new v18 branch 2025-12-04 11:03:26 +01:00
2591 changed files with 130829 additions and 85785 deletions
+4 -12
View File
@@ -70,18 +70,6 @@ trim_trailing_whitespace = true
[*.less]
trim_trailing_whitespace = false
##########################################
# File Header (Uncomment to support file headers)
# https://docs.microsoft.com/visualstudio/ide/reference/add-file-header
##########################################
# [*.{cs,csx,cake,vb,vbx}]
file_header_template = Copyright (c) Umbraco.\nSee LICENSE for more details.
# SA1636: File header copyright text should match
# Justification: .editorconfig supports file headers. If this is changed to a value other than "none", a stylecop.json file will need to added to the project.
# dotnet_diagnostic.SA1636.severity = none
##########################################
# .NET Language Conventions
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions
@@ -136,6 +124,10 @@ dotnet_code_quality_unused_parameters = all:warning
dotnet_style_operator_placement_when_wrapping = end_of_line
# https://github.com/dotnet/roslyn/pull/40070
dotnet_style_prefer_simplified_interpolation = true:warning
# File header preferences
file_header_template = Copyright (c) Umbraco.\nSee LICENSE for more details.
dotnet_diagnostic.SA1633.severity = none # Suppressed until we decide to enforce it
dotnet_diagnostic.SA1636.severity = none # Suppressed since we are using StyleCop
# C# Code Style Settings
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-code-style-settings
+1
View File
@@ -59,4 +59,5 @@
# Generated files - hidden by default in GitHub diffs
src/Umbraco.Web.UI.Client/src/packages/core/backend-api/** linguist-generated
src/Umbraco.Web.UI.Login/src/api/** linguist-generated
templates/UmbracoExtension/Client/src/api/** linguist-generated
src/Umbraco.Cms.Api.Management/OpenApi.json linguist-generated
+1 -1
View File
@@ -7,7 +7,7 @@ body:
id: "version"
attributes:
label: "Which Umbraco version are you using?"
description: "Please write the *exact* version, example: `10.1.0`. Use the help icon in the Umbraco backoffice to find the version you're using"
description: "Please write the *exact* version, example: `10.1.0`. Click the Umbraco logo in the top left corner of the backoffice to find the version you're using."
validations:
required: true
- type: textarea
+99
View File
@@ -0,0 +1,99 @@
name: "SonarQube Cloud - Analysis"
# This workflow runs the full SonarCloud analysis with the SONAR_TOKEN secret.
# It is skipped for fork PRs since secrets are not available in that context.
on:
push:
branches:
- main
- "v*/dev"
- "v*/main"
- "release/*"
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
permissions:
contents: read
env:
SONAR_PROJECT_KEY: umbraco_Umbraco-CMS
SONAR_ORGANIZATION: umbraco
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Build and analyze
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork != true
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup .NET from global.json
uses: actions/setup-dotnet@v5
- name: Setup Java 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "21"
- name: Cache SonarQube packages
uses: actions/cache@v5
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Install tools
run: |
dotnet tool install --global dotnet-sonarscanner
dotnet tool install --global dotnet-coverage
- name: Load sonar params
run: echo "SONARQUBE_SCANNER_PARAMS=$(jq -c . .github/workflows/sonarcloud/sonar-params.json)" >> $GITHUB_ENV
- name: Begin analysis
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
dotnet-sonarscanner begin \
/k:"$SONAR_PROJECT_KEY" \
/o:"$SONAR_ORGANIZATION" \
/d:sonar.token="$SONAR_TOKEN" \
/d:sonar.scanner.skipJreProvisioning=true
- name: Restore
run: dotnet restore umbraco.sln
- name: Build solution
run: GITHUB_ENV=/dev/null dotnet build umbraco.sln --no-restore -clp:ErrorsOnly # prevent sonar MSBuild integration from writing malformed values to $GITHUB_ENV
- name: Run unit tests with coverage
id: tests
continue-on-error: true
run: |
dotnet-coverage collect \
"dotnet test tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj --no-build" \
--output TestResults/coverage.xml \
--output-format xml
- name: Warn on test failure
if: steps.tests.outcome == 'failure'
run: |
if [ -f TestResults/coverage.xml ]; then
echo "::warning::Unit tests failed - SonarCloud analysis will proceed with the collected coverage data"
else
echo "::warning::Unit tests failed and no coverage data was collected"
fi
- name: End analysis
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN"
@@ -0,0 +1,7 @@
{
"sonar.cs.vscoveragexml.reportsPaths": "TestResults/coverage.xml",
"sonar.inclusions": "src/**,templates/**,tools/**,tests/**,.github/**,build/**",
"sonar.exclusions": "**/bin/**,**/obj/**,**/node_modules/**,**/lang/*.ts,**/mocks/**,**/wwwroot/**,**/dist-cms/**,**/*.generated.cs,src/Umbraco.Web.UI/umbraco/**,src/Umbraco.Cms.Persistence.EFCore.*/Migrations/**,src/Umbraco.Web.UI.Client/src/packages/core/backend-api/**,**/.nuget/**",
"sonar.test.inclusions": "tests/**,**/*.test.ts,**/*.spec.ts",
"sonar.typescript.tsconfigPaths": "src/Umbraco.Web.UI.Client/tsconfig.json,src/Umbraco.Web.UI.Client/tsconfig.node.json,src/Umbraco.Web.UI.Login/tsconfig.json"
}
+4
View File
@@ -120,3 +120,7 @@ trace.zip
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
/src/Umbraco.Cms/appsettings-schema.json
.playwright-mcp/
# SonarQube local analysis cache
.sonarqube/
-1
View File
@@ -48,7 +48,6 @@ dotnet_analyzer_diagnostic.category-StyleCop.CSharp.OrderingRules.severity = sug
dotnet_analyzer_diagnostic.category-StyleCop.CSharp.MaintainabilityRules.severity = suggestion
dotnet_analyzer_diagnostic.category-StyleCop.CSharp.LayoutRules.severity = suggestion
dotnet_diagnostic.SA1636.severity = none # SA1636: File header copyright text should match
dotnet_diagnostic.SA1101.severity = none # PrefixLocalCallsWithThis - stylecop appears to be ignoring dotnet_style_qualification_for_*
dotnet_diagnostic.SA1309.severity = none # FieldNamesMustNotBeginWithUnderscore
+18 -5
View File
@@ -46,7 +46,8 @@ Enterprise-grade CMS built on .NET 10.0. This repository contains 21 production
- **ASP.NET Core** - Web framework
- **Entity Framework Core** - Modern ORM
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
- **Swashbuckle** - OpenAPI/Swagger documentation
- **Microsoft.AspNetCore.OpenApi** - OpenAPI document generation
- **Swashbuckle.AspNetCore.SwaggerUI** - Swagger UI for API documentation
- **Lucene.NET** - Full-text search via Examine
- **ImageSharp** - Image processing
@@ -366,16 +367,27 @@ public interface IMyService
### Centralized Package Management
**All NuGet package versions** are centralized in `Directory.Packages.props`. Individual projects do NOT specify versions.
**NuGet package versions** are centralized in `Directory.Packages.props`. There are two `Directory.Packages.props` files in the source tree, with multi-level merging enabled so the test file inherits from the root:
| File | Scope |
|------|-------|
| `Directory.Packages.props` (root) | Production source code packages — referenced by all `src/**` projects |
| `tests/Directory.Packages.props` | Test-only packages (NUnit, Moq, Bogus, BenchmarkDotNet, etc.) — adds entries on top of the inherited root file |
When updating dependencies, decide which file the package belongs in:
- A package used only by test projects → `tests/Directory.Packages.props`
- A package used by any production project (or by both production and tests) → root `Directory.Packages.props`
```xml
<!-- Individual projects reference WITHOUT version -->
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<!-- Versions defined in Directory.Packages.props -->
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
```
**Opt-out**: `src/Umbraco.Web.UI/Umbraco.Web.UI.csproj` sets `<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>` and specifies versions inline (for `Microsoft.EntityFrameworkCore.Design`, `Microsoft.Build.Tasks.Core`, `Microsoft.ICU.ICU4C.Runtime`, etc.). Update those versions directly in that csproj when bumping. Two further `Directory.Packages.props` files exist under `templates/` for the project/extension templates and have their own version sets — keep `Microsoft.AspNetCore.OpenApi` aligned between the root file and `templates/UmbracoExtension/`.
### Build Configuration
- `Directory.Build.props` - Shared properties (target framework, company, copyright)
@@ -419,7 +431,8 @@ All APIs use **OpenIddict** (OAuth 2.0/OpenID Connect):
APIs use `Asp.Versioning.Mvc`:
- Management API: `/umbraco/management/api/v{version}/*`
- Delivery API: `/umbraco/delivery/api/v{version}/*`
- OpenAPI/Swagger docs per version
- OpenAPI docs: `/umbraco/openapi/management.json`, `/umbraco/openapi/delivery.json`
- Swagger UI: `/umbraco/openapi/`
### Updating `OpenApi.json` (Management API)
+11 -1
View File
@@ -41,7 +41,7 @@
<PropertyGroup>
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>17.0.0</PackageValidationBaselineVersion>
<PackageValidationBaselineVersion>18.0.0</PackageValidationBaselineVersion>
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
</PropertyGroup>
@@ -64,4 +64,14 @@
</_ProjectReferencesWithVersions>
</ItemGroup>
</Target>
<!-- Workaround for https://github.com/umbraco/Umbraco-CMS/issues/23018
Due to the amount of XML documentation in this solution, the OpenAPI XML documentation source generator produces
too many lines of code causing a StackOverflowException when running on IIS. For that reason we disable the analyzer.
See https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/openapi-comments?view=aspnetcore-10.0#disabling-xml-documentation-support -->
<Target Name="DisableCompileTimeOpenApiXmlGenerator" BeforeTargets="CoreCompile" Condition="'$(IsPackable)' != 'false' or '$(IsTestProject)' == 'true'">
<ItemGroup>
<Analyzer Remove="@(Analyzer)" Condition="'%(Filename)' == 'Microsoft.AspNetCore.OpenApi.SourceGenerators'" />
</ItemGroup>
</Target>
</Project>
+37 -34
View File
@@ -8,33 +8,37 @@
<ItemGroup>
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.9.50" />
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<GlobalPackageReference Include="Umbraco.Code" Version="2.4.0" />
<!-- TODO (V18): Bump Umbraco.Code to 3.0.0 stable before release of 18.0.0 -->
<GlobalPackageReference Include="Umbraco.Code" Version="3.0.0-beta" />
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.6" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.6" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.7" />
<!-- When updating this version, also update templates/UmbracoExtension/Umbraco.Extension.csproj -->
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.3.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.5.0" />
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
<PackageVersion Include="System.Linq.Async" Version="7.0.1" />
</ItemGroup>
<!-- Umbraco packages -->
<ItemGroup>
@@ -42,8 +46,8 @@
</ItemGroup>
<!-- Third-party packages -->
<ItemGroup>
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc" Version="10.0.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.0.0" />
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.8.0" />
<PackageVersion Include="Examine.Core" Version="3.8.0" />
@@ -51,7 +55,7 @@
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.16.0" />
<PackageVersion Include="Markdig" Version="0.45.0" />
<PackageVersion Include="Markdig" Version="1.1.3" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="3.1.7" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
@@ -59,25 +63,24 @@
<PackageVersion Include="ncrontab" Version="3.4.0" />
<PackageVersion Include="NPoco" Version="6.2.0" />
<PackageVersion Include="NPoco.SqlServer" Version="6.2.0" />
<PackageVersion Include="OpenIddict.Abstractions" Version="7.4.0" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.4.0" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.4.0" />
<PackageVersion Include="OpenIddict.Abstractions" Version="7.5.0" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.5.0" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.5.0" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="2.0.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
<!-- When updating this version, also update templates/UmbracoExtension/Umbraco.Extension.csproj -->
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" />
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
@@ -90,6 +93,6 @@
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<!-- Examine (via Microsoft.AspNetCore.DataProtection 8.0.4) references a vulnerable version of the following: -->
<!-- TODO: Remove this pinned dependency when Examine updates its Microsoft.AspNetCore.DataProtection reference. -->
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.6" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.7" />
</ItemGroup>
</Project>
+11 -11
View File
@@ -45,7 +45,7 @@ parameters:
- name: integrationNonReleaseTestFilter
displayName: TestFilter used for non-release type builds
type: string
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
default: "TestCategory!=LongRunning&TestCategory!=NonCritical"
- name: integrationReleaseTestFilter
displayName: TestFilter used for release type builds
type: string
@@ -53,7 +53,7 @@ parameters:
- name: nonWindowsIntegrationNonReleaseTestFilter
displayName: TestFilter used for non-release type builds on non Windows agents
type: string
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
default: "TestCategory!=LongRunning&TestCategory!=NonCritical"
- name: nonWindowsIntegrationReleaseTestFilter
displayName: TestFilter used for release type builds on non Windows agents
type: string
@@ -455,13 +455,13 @@ stages:
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
testRunTitle: Integration Tests SQLite - $(Agent.OS)
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
arguments: '--filter "$(testFilter) & ${{parameters.integrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
${{ else }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
arguments: '--filter "$(testFilter) & ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
# Integration Tests (SQL Server)
- job:
timeoutInMinutes: 180
@@ -569,13 +569,13 @@ stages:
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
arguments: '--filter "$(testFilter) & ${{parameters.integrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
${{ else }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
arguments: '--filter "$(testFilter) & ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
# Stop SQL Server
- pwsh: docker stop mssql
@@ -862,10 +862,10 @@ stages:
- job: WaitForApproval
displayName: Wait for manual approval
pool: server
timeoutInMinutes: 4320 # 3 days
steps:
- task: ManualValidation@0
displayName: Manual approval to push to NuGet
timeoutInMinutes: 4320 # 3 days
inputs:
notifyUsers: ''
instructions: 'Approve to push the NuGet release.'
+16 -10
View File
@@ -4,11 +4,11 @@ pr: none
trigger: none
schedules:
- cron: '0 3 * * *'
displayName: Daily 3AM build (v17/dev)
- cron: '0 0 * * *'
displayName: Daily 0AM build (main)
branches:
include:
- v17/dev
- main
parameters:
- name: skipIntegrationTests
@@ -199,31 +199,37 @@ stages:
SA_PASSWORD: UmbracoAcceptance123!
strategy:
matrix:
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
WindowsPart1Of4:
# Windows is split into 5 parts (ManagementApi split in two to avoid memory pressure on LocalDb); Linux into 4.
WindowsPart1Of5:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
WindowsPart2Of4:
WindowsPart2Of5:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
WindowsPart3Of4:
WindowsPart3Of5:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
WindowsPart4Of4:
WindowsPart4Of5:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the ManagementApi namespace.
testFilter: "(FullyQualifiedName~ManagementApi)"
# ManagementApi, heavier sub-namespaces. Trailing dots prevent "User." from matching "UserGroup." etc.
testFilter: "FullyQualifiedName~ManagementApi & (FullyQualifiedName~ManagementApi.Element. | FullyQualifiedName~ManagementApi.User. | FullyQualifiedName~ManagementApi.Document. | FullyQualifiedName~ManagementApi.DataType. | FullyQualifiedName~ManagementApi.DocumentType. | FullyQualifiedName~ManagementApi.MediaType. | FullyQualifiedName~ManagementApi.Template.)"
WindowsPart5Of5:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# ManagementApi, remainder (complement of Part4). vstest filters do not support group
testFilter: "FullyQualifiedName~ManagementApi & FullyQualifiedName!~ManagementApi.Element. & FullyQualifiedName!~ManagementApi.User. & FullyQualifiedName!~ManagementApi.Document. & FullyQualifiedName!~ManagementApi.DataType. & FullyQualifiedName!~ManagementApi.DocumentType. & FullyQualifiedName!~ManagementApi.MediaType. & FullyQualifiedName!~ManagementApi.Template."
LinuxPart1Of4:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
+42 -70
View File
@@ -13,7 +13,8 @@ Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery).
### Key Technologies
- **ASP.NET Core** - Web framework
- **Swashbuckle** - OpenAPI/Swagger documentation generation
- **Microsoft.AspNetCore.OpenApi** - OpenAPI document generation
- **Swashbuckle.AspNetCore.SwaggerUI** - Swagger UI for browsing API documentation
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
- **Asp.Versioning** - API versioning
- **System.Text.Json** - Polymorphic JSON serialization
@@ -27,14 +28,18 @@ Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery).
```
Umbraco.Cms.Api.Common/
├── OpenApi/ # Schema/Operation ID handlers for Swagger
│ ├── SchemaIdHandler.cs # Generates schema IDs (e.g., "PagedUserModel")
│ ├── OperationIdHandler.cs # Generates operation IDs
── SubTypesHandler.cs # Polymorphism support
├── OpenApi/ # OpenAPI transformers and schema generators
│ ├── UmbracoSchemaIdGenerator.cs # Generates schema IDs (e.g., "PagedUserModel")
│ ├── UmbracoOperationIdTransformer.cs # Generates operation IDs
── SortTagsAndPathsTransformer.cs # Sorts OpenAPI tags and paths
│ ├── TagActionsByGroupNameTransformer.cs # Tags operations by controller group
│ ├── FixFileReturnTypesTransformer.cs # Fixes file return type schemas
│ ├── RequireNonNullablePropertiesSchemaTransformer.cs # Schema nullability
│ └── OpenApiRouteTemplatePipelineFilter.cs # Adds OpenAPI endpoints
├── Serialization/ # JSON type resolution
│ └── UmbracoJsonTypeInfoResolver.cs
├── Configuration/ # Options configuration
│ ├── ConfigureUmbracoSwaggerGenOptions.cs
│ ├── ConfigureUmbracoOpenApiOptionsBase.cs
│ └── ConfigureOpenIddict.cs
├── DependencyInjection/ # Service registration
│ ├── UmbracoBuilderApiExtensions.cs
@@ -47,9 +52,8 @@ Umbraco.Cms.Api.Common/
### Design Patterns
1. **Strategy Pattern** - `ISchemaIdHandler`, `IOperationIdHandler` (extensible via inheritance)
2. **Builder Pattern** - `ProblemDetailsBuilder` for fluent error responses
3. **Options Pattern** - All configuration via `IConfigureOptions<T>`
1. **Builder Pattern** - `ProblemDetailsBuilder` for fluent error responses
2. **Options Pattern** - All configuration via `IConfigureOptions<T>`
---
@@ -61,25 +65,12 @@ See "Quick Reference" section at bottom for common commands.
## 3. Key Patterns
### Virtual Handlers for Extensibility
### Schema ID Generation (OpenApi/UmbracoSchemaIdGenerator.cs)
Handlers are intentionally virtual to allow consuming APIs to override:
Static utility class that generates OpenAPI schema IDs following Umbraco's naming conventions:
```csharp
// NOTE: Left unsealed on purpose, so it is extendable.
public class SchemaIdHandler : ISchemaIdHandler
{
public virtual bool CanHandle(Type type) { }
public virtual string Handle(Type type) { }
}
```
**Why**: Management and Delivery APIs can customize schema/operation ID generation.
### Schema ID Sanitization (OpenApi/SchemaIdHandler.cs:24-29, 32)
```csharp
// Add "Model" suffix to avoid TypeScript name clashes (lines 24-29)
// Add "Model" suffix to avoid TypeScript name clashes
if (name.EndsWith("Model") == false)
{
// because some models names clash with common classes in TypeScript (i.e. Document),
@@ -87,10 +78,12 @@ if (name.EndsWith("Model") == false)
name = $"{name}Model";
}
// Remove invalid characters to prevent OpenAPI generation errors (line 32)
// Remove invalid characters to prevent OpenAPI generation errors
return Regex.Replace(name, @"[^\w]", string.Empty);
```
**Generic Type Handling**: `PagedViewModel<RelationItemViewModel>` becomes `PagedRelationItemModel`
### Polymorphic Deserialization (Serialization/UmbracoJsonTypeInfoResolver.cs:29-35)
```csharp
@@ -116,9 +109,12 @@ if (type.IsInterface is false)
dotnet test tests/Umbraco.Tests.Integration/
# Verify OpenAPI generation
# 1. Run Management API
# 2. Navigate to /umbraco/swagger/
# 1. Run the application: dotnet run --project src/Umbraco.Web.UI
# 2. Navigate to /umbraco/openapi/ for Swagger UI
# 3. Check schema IDs and operation IDs
# OpenAPI JSON documents available at:
# - /umbraco/openapi/management.json (Management API)
# - /umbraco/openapi/delivery.json (Delivery API)
```
**Focus areas when testing**:
@@ -207,49 +203,24 @@ catch (NotSupportedException exception)
**Issue**: Type names like `Document` clash with TypeScript built-ins.
**Solution**: Add "Model" suffix (OpenApi/SchemaIdHandler.cs:24-29)
**Solution**: `UmbracoSchemaIdGenerator` adds "Model" suffix to all schema names.
### Generic Type Handling
**Issue**: `PagedViewModel<T>` needs flattened schema name.
**Solution** (OpenApi/SchemaIdHandler.cs:41-50):
```csharp
private string HandleGenerics(string name, Type type)
{
if (!type.IsGenericType)
return name;
// use attribute custom name or append the generic type names
// turns "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
}
```
**Solution**: `UmbracoSchemaIdGenerator.Generate()` flattens generic types:
- `PagedViewModel<RelationItemViewModel>` becomes `PagedRelationItemModel`
---
## 7. Extending This Library
### Adding a Custom OpenAPI Handler
### Adding Custom OpenAPI Transformers
1. **Implement interface**:
```csharp
public class MySchemaIdHandler : SchemaIdHandler
{
public override bool CanHandle(Type type)
=> type.Namespace?.StartsWith("MyProject") is true;
OpenAPI transformers are scoped per-document. To customize a document, implement `IOpenApiDocumentTransformer`, `IOpenApiOperationTransformer`, or `IOpenApiSchemaTransformer` and register with your OpenAPI options.
public override string Handle(Type type)
=> $"My{base.Handle(type)}";
}
```
2. **Register in consuming API**:
```csharp
builder.Services.AddSingleton<ISchemaIdHandler, MySchemaIdHandler>();
```
**Note**: Handlers registered later take precedence in the selector.
For schema ID generation, use the static `UmbracoSchemaIdGenerator.Generate(Type)` method.
### Customizing Problem Details
@@ -269,13 +240,9 @@ return BadRequest(problemDetails);
## 8. Project-Specific Notes
### Why Virtual Handlers?
### Per-Document Transformer Scoping
**Decision**: Make `SchemaIdHandler`, `OperationIdHandler`, etc. virtual.
**Why**: Management API and Delivery API have different schema ID requirements. Virtual methods allow override without rewriting the entire handler.
**Example**: Management API might prefix all schemas with "Management", Delivery API with "Delivery".
With Microsoft.AspNetCore.OpenApi, transformers are configured per OpenAPI document. This means custom transformers only apply to the documents they're registered with, not globally. Each API (Management, Delivery) configures its own transformers via `ConfigureUmbracoOpenApiOptionsBase` subclasses.
### Performance: Subtype Caching
@@ -304,9 +271,13 @@ return BadRequest(problemDetails);
- Version: See `Directory.Packages.props`
- Uses ASP.NET Core Data Protection for token encryption
**Swashbuckle**:
- OpenAPI 3.0 document generation
- Custom filters: `EnumSchemaFilter`, `MimeTypeDocumentFilter`, `RemoveSecuritySchemesDocumentFilter`
**Microsoft.AspNetCore.OpenApi**:
- OpenAPI 3.1.1 document generation
- Custom transformers: `SchemaIdTransformer`, `OperationIdTransformer`, `MimeTypeDocumentTransformer`, `ServerTransformer`
**Swashbuckle.AspNetCore.SwaggerUI**:
- Swagger UI for browsing and testing API endpoints
- Accessed at `/umbraco/openapi/`
**Asp.Versioning**:
- API versioning via `ApiVersion` attribute
@@ -318,7 +289,7 @@ return BadRequest(problemDetails);
### Usage Pattern
Consuming APIs call `builder.AddUmbracoApiOpenApiUI().AddUmbracoOpenIddict()`
Consuming APIs call `builder.AddUmbracoOpenApi().AddUmbracoOpenIddict()`
---
@@ -346,7 +317,8 @@ dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --v
| Class | Purpose | File |
|-------|---------|------|
| `ProblemDetailsBuilder` | Build RFC 7807 error responses | Builders/ProblemDetailsBuilder.cs |
| `SchemaIdHandler` | Generate OpenAPI schema IDs | OpenApi/SchemaIdHandler.cs |
| `UmbracoSchemaIdGenerator` | Generate OpenAPI schema IDs | OpenApi/UmbracoSchemaIdGenerator.cs |
| `UmbracoOperationIdTransformer` | Generate operation IDs | OpenApi/UmbracoOperationIdTransformer.cs |
| `UmbracoJsonTypeInfoResolver` | Polymorphic JSON serialization | Serialization/UmbracoJsonTypeInfoResolver.cs |
| `UmbracoBuilderAuthExtensions` | Configure OpenIddict | DependencyInjection/UmbracoBuilderAuthExtensions.cs |
| `HideBackOfficeTokensHandler` | Secure cookie-based token storage | DependencyInjection/HideBackOfficeTokensHandler.cs |
@@ -0,0 +1,47 @@
using System.Reflection;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Umbraco.Cms.Api.Common.OpenApi;
namespace Umbraco.Cms.Api.Common.Configuration;
/// <summary>
/// Configures the OpenAPI options for the Default API.
/// </summary>
internal class ConfigureDefaultApiOptions : ConfigureUmbracoOpenApiOptionsBase
{
/// <inheritdoc />
protected override string ApiName => DefaultApiConfiguration.ApiName;
/// <inheritdoc />
protected override string ApiTitle => "Default API";
/// <inheritdoc />
protected override string ApiVersion => "Latest";
/// <inheritdoc />
protected override string ApiDescription => "All endpoints not defined under specific APIs";
/// <inheritdoc />
protected override bool ShouldInclude(ApiDescription apiDescription)
{
// Exclude controllers with ExcludeFromDefaultOpenApiDocumentAttribute
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
&& controllerActionDescriptor.ControllerTypeInfo.GetCustomAttribute<ExcludeFromDefaultOpenApiDocumentAttribute>() is not null)
{
return false;
}
// Include if explicitly mapped to this document
if (base.ShouldInclude(apiDescription))
{
return true;
}
// Include endpoints not explicitly assigned to another document
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.ApiVersionMetadata;
return string.IsNullOrEmpty(apiVersionMetadata.Name);
}
}
@@ -0,0 +1,98 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.Configuration;
/// <summary>
/// Base class for configuring OpenAPI options for Umbraco APIs.
/// </summary>
internal abstract class ConfigureUmbracoOpenApiOptionsBase : IConfigureNamedOptions<OpenApiOptions>
{
/// <summary>
/// Gets the name/identifier of the API to configure.
/// </summary>
protected abstract string ApiName { get; }
/// <summary>
/// Gets the name/identifier of the API to configure.
/// </summary>
protected abstract string ApiTitle { get; }
/// <summary>
/// Gets the version of the API to configure.
/// </summary>
protected abstract string ApiVersion { get; }
/// <summary>
/// Gets the description of the API to configure.
/// </summary>
protected abstract string ApiDescription { get; }
/// <inheritdoc />
public void Configure(OpenApiOptions options) => Configure(Options.DefaultName, options);
/// <inheritdoc />
public void Configure(string? name, OpenApiOptions options)
{
if (name != ApiName)
{
return;
}
ConfigureOpenApi(options);
}
/// <summary>
/// Configure the OpenAPI options for the specified API.
/// </summary>
/// <param name="options">The <see cref="OpenApiOptions"/> instance to configure.</param>
protected virtual void ConfigureOpenApi(OpenApiOptions options)
{
options.AddDocumentTransformer((document, _, _) =>
{
document.Info = new OpenApiInfo
{
Title = ApiTitle,
Version = ApiVersion,
Description = ApiDescription,
};
document.Servers?.Clear();
return Task.CompletedTask;
});
options.ShouldInclude = ShouldInclude;
options.CreateSchemaReferenceId = UmbracoSchemaIdGenerator.CreateSchemaReferenceId;
options.AddOperationTransformer<UmbracoOperationIdTransformer>();
// Tag actions by group name and cleanup unused tags (caused by the tag changes)
options
.AddOperationTransformer<TagActionsByGroupNameTransformer>()
.AddDocumentTransformer<TagActionsByGroupNameTransformer>()
.AddDocumentTransformer<SortTagsAndPathsTransformer>();
}
/// <summary>
/// Determines whether the specified API description should be included in this OpenAPI document.
/// </summary>
/// <param name="apiDescription">The API description to evaluate.</param>
/// <returns><c>true</c> if the endpoint should be included; otherwise, <c>false</c>.</returns>
protected virtual bool ShouldInclude(ApiDescription apiDescription)
{
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
&& controllerActionDescriptor.HasMapToApiAttribute(ApiName))
{
return true;
}
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.ApiVersionMetadata;
return apiVersionMetadata.Name == ApiName;
}
}
@@ -1,94 +0,0 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Common.Configuration;
/// <summary>
/// Configures Swagger/OpenAPI generation options for Umbraco APIs.
/// </summary>
public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IOperationIdSelector _operationIdSelector;
private readonly ISchemaIdSelector _schemaIdSelector;
private readonly ISubTypesSelector _subTypesSelector;
private readonly IDocumentInclusionSelector _documentInclusionSelector;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureUmbracoSwaggerGenOptions"/> class.
/// </summary>
/// <param name="operationIdSelector">The operation ID selector.</param>
/// <param name="schemaIdSelector">The schema ID selector.</param>
/// <param name="subTypesSelector">The sub-types selector for polymorphism support.</param>
/// <param name="documentInclusionSelector">The document inclusion selector.</param>
public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector,
ISubTypesSelector subTypesSelector,
IDocumentInclusionSelector documentInclusionSelector)
{
_operationIdSelector = operationIdSelector;
_schemaIdSelector = schemaIdSelector;
_subTypesSelector = subTypesSelector;
_documentInclusionSelector = documentInclusionSelector;
}
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureUmbracoSwaggerGenOptions"/> class.
/// </summary>
/// <param name="operationIdSelector">The operation ID selector.</param>
/// <param name="schemaIdSelector">The schema ID selector.</param>
/// <param name="subTypesSelector">The sub-types selector for polymorphism support.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector,
ISubTypesSelector subTypesSelector)
: this(
operationIdSelector,
schemaIdSelector,
subTypesSelector,
StaticServiceProvider.Instance.GetRequiredService<IDocumentInclusionSelector>())
{
}
/// <inheritdoc/>
public void Configure(SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.SwaggerDoc(
DefaultApiConfiguration.ApiName,
new OpenApiInfo
{
Title = "Default API",
Version = "Latest",
Description = "All endpoints not defined under specific APIs",
});
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description));
swaggerGenOptions.DocInclusionPredicate(_documentInclusionSelector.Include);
swaggerGenOptions.TagActionsBy(api =>
api.GroupName is null
? []
: new[] { api.GroupName });
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
swaggerGenOptions.SelectSubTypesUsing(_subTypesSelector.SubTypes);
swaggerGenOptions.SupportNonNullableReferenceTypes();
}
/// <summary>
/// Generates a sort key for API actions.
/// </summary>
/// <param name="apiDesc">The API description.</param>
/// <returns>A string used to sort API operations in the documentation.</returns>
/// <remarks>
/// See https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting.
/// </remarks>
private static string ActionOrderBy(ApiDescription apiDesc)
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{(apiDesc.ActionDescriptor.RouteValues.TryGetValue("action", out var action) ? action : null)}_{apiDesc.HttpMethod}";
}
@@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
/// <summary>
/// Extension methods for replacing the internal Microsoft.AspNetCore.OpenApi schema service registration.
/// </summary>
internal static class OpenApiSchemaServiceExtensions
{
/// <summary>
/// The full name of the internal Microsoft type whose registration is replaced.
/// Used for a stringly-typed <see cref="ServiceDescriptor"/> lookup because the type is not publicly accessible.
/// </summary>
internal const string OpenApiSchemaServiceFullName = "Microsoft.AspNetCore.OpenApi.OpenApiSchemaService";
/// <summary>
/// Replaces the internal Microsoft <c>OpenApiSchemaService</c> registration for the specified document so that schema
/// generation uses the named <see cref="JsonOptions"/> rather than the default HTTP JSON options.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="documentName">The OpenAPI document key (matches the keyed singleton registered by <c>AddOpenApi(documentName)</c>).</param>
/// <param name="jsonOptionsName">The named <see cref="JsonOptions"/> to use during schema generation for this document.</param>
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
/// <remarks>
/// Workaround for <see href="https://github.com/dotnet/aspnetcore/issues/66340">dotnet/aspnetcore#66340</see>.
/// </remarks>
public static IServiceCollection ReplaceOpenApiSchemaService(
this IServiceCollection services,
string documentName,
string jsonOptionsName)
=> services.ReplaceOpenApiSchemaService(
documentName,
sp => sp.GetRequiredService<IOptionsMonitor<JsonOptions>>().Get(jsonOptionsName));
/// <summary>
/// Replaces the internal Microsoft <c>OpenApiSchemaService</c> registration for the specified document so that schema
/// generation uses the <see cref="JsonOptions"/> instance produced by the supplied factory. Use this overload when
/// the options need to be resolved from the service provider, computed at the last moment, or built in a way that
/// doesn't fit the named-options lookup.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="documentName">The OpenAPI document key.</param>
/// <param name="jsonOptionsFactory">Factory invoked when the schema service is first resolved. Receives the resolving <see cref="IServiceProvider"/> and returns the <see cref="JsonOptions"/> to use.</param>
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
/// <remarks>
/// Workaround for <see href="https://github.com/dotnet/aspnetcore/issues/66340">dotnet/aspnetcore#66340</see>.
/// </remarks>
public static IServiceCollection ReplaceOpenApiSchemaService(
this IServiceCollection services,
string documentName,
Func<IServiceProvider, JsonOptions> jsonOptionsFactory)
{
ServiceDescriptor descriptor = services.FirstOrDefault(sd =>
sd.ServiceType.FullName == OpenApiSchemaServiceFullName
&& Equals(sd.ServiceKey, documentName))
?? throw new InvalidOperationException(
$"Could not find a registration for {OpenApiSchemaServiceFullName} keyed with '{documentName}'. "
+ $"Ensure AddOpenApi(\"{documentName}\") has been called before {nameof(ReplaceOpenApiSchemaService)}, "
+ "or check whether the internal Microsoft.AspNetCore.OpenApi registration shape has changed.");
services.Remove(descriptor);
services.AddKeyedSingleton(
descriptor.ServiceType,
documentName,
(sp, key) => ActivatorUtilities.CreateInstance(
sp,
descriptor.ServiceType,
key,
Options.Create(jsonOptionsFactory(sp))));
return services;
}
}
@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerUI;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
/// <summary>
/// Extension methods for <see cref="IServiceCollection"/> to configure OpenAPI services.
/// </summary>
public static class OpenApiServiceCollectionExtensions
{
/// <summary>
/// Adds an OpenAPI document to the OpenAPI UI document selector dropdown.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> instance.</param>
/// <param name="documentName">The name/identifier of the OpenAPI document.</param>
/// <param name="documentTitle">The title to display in the UI dropdown. Defaults to <paramref name="documentName"/> if not specified.</param>
/// <returns>The <see cref="IServiceCollection"/> instance.</returns>
public static IServiceCollection AddOpenApiDocumentToUi(
this IServiceCollection services,
string documentName,
string? documentTitle = null)
=> services.AddOpenApiDocumentToUi(documentName, () => documentTitle);
/// <summary>
/// Adds an OpenAPI document to the OpenAPI UI document selector dropdown, resolving the title lazily so
/// callers (such as builder-pattern helpers) can defer it until SwaggerUI options are resolved.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> instance.</param>
/// <param name="documentName">The name/identifier of the OpenAPI document.</param>
/// <param name="documentTitleFactory">Factory invoked when SwaggerUI options are resolved. Returning <c>null</c> falls back to <paramref name="documentName"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> instance.</returns>
internal static IServiceCollection AddOpenApiDocumentToUi(
this IServiceCollection services,
string documentName,
Func<string?> documentTitleFactory)
{
services.AddOptions<SwaggerUIOptions>()
.Configure<IOptions<UmbracoOpenApiOptions>>((swaggerUiOptions, openApiOptions) =>
{
var openApiRoute = openApiOptions.Value.RouteTemplate.Replace("{documentName}", documentName).EnsureStartsWith("/");
swaggerUiOptions.SwaggerEndpoint(openApiRoute, documentTitleFactory() ?? documentName);
swaggerUiOptions.ConfigObject.Urls = swaggerUiOptions.ConfigObject.Urls.OrderBy(x => x.Name);
});
return services;
}
}
@@ -1,9 +1,14 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
@@ -16,26 +21,52 @@ public static class UmbracoBuilderApiExtensions
/// Adds Umbraco API OpenAPI/Swagger UI services to the builder.
/// </summary>
/// <param name="builder">The Umbraco builder.</param>
/// <returns>The Umbraco builder for method chaining.</returns>
public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builder)
internal static void AddUmbracoOpenApi(this IUmbracoBuilder builder)
{
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OperationIdSelector)))
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(UmbracoJsonTypeInfoResolver)))
{
return builder;
return;
}
builder.Services.AddSwaggerGen();
builder.Services.ConfigureOptions<ConfigureUmbracoSwaggerGenOptions>();
builder.Services.AddOptions<UmbracoOpenApiOptions>()
.Configure<IHostingEnvironment, IWebHostEnvironment>((options, hostingEnv, webHostEnv) =>
{
options.Enabled = webHostEnv.IsProduction() is false;
var backOfficePath = hostingEnv.GetBackOfficePath().TrimStart(Constants.CharArrays.ForwardSlash);
options.RouteTemplate = $"{backOfficePath}/openapi/{{documentName}}.json";
options.UiRoutePrefix = $"{backOfficePath}/openapi";
});
builder.AddUmbracoOpenApiDocument<ConfigureDefaultApiOptions>(DefaultApiConfiguration.ApiName, "Default API");
builder.Services.AddSingleton<IUmbracoJsonTypeInfoResolver, UmbracoJsonTypeInfoResolver>();
builder.Services.AddSingleton<IOperationIdSelector, OperationIdSelector>();
builder.Services.AddSingleton<IOperationIdHandler, OperationIdHandler>();
builder.Services.AddSingleton<ISchemaIdSelector, SchemaIdSelector>();
builder.Services.AddSingleton<ISchemaIdHandler, SchemaIdHandler>();
builder.Services.AddSingleton<ISubTypesSelector, SubTypesSelector>();
builder.Services.AddSingleton<ISubTypesHandler, SubTypesHandler>();
builder.Services.AddSingleton<IDocumentInclusionSelector, DocumentInclusionSelector>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new OpenApiRouteTemplatePipelineFilter("UmbracoApiCommon")));
}
return builder;
/// <summary>
/// Adds and configures an Umbraco OpenAPI document with shared transformers.
/// </summary>
/// <param name="builder">The Umbraco builder.</param>
/// <param name="apiName">The name/identifier of the API.</param>
/// <param name="apiTitle">The title of the API.</param>
/// <param name="jsonOptionsName">
/// Optional named <c>JsonOptions</c> to use for schema generation instead of the default HTTP JSON options.
/// When specified, replaces the internal <c>OpenApiSchemaService</c> registration for this document.
/// </param>
/// <typeparam name="TConfigureOptions">The type used to configure the OpenAPI options.</typeparam>
internal static void AddUmbracoOpenApiDocument<TConfigureOptions>(
this IUmbracoBuilder builder,
string apiName,
string apiTitle,
string? jsonOptionsName = null)
where TConfigureOptions : ConfigureUmbracoOpenApiOptionsBase
{
apiName = apiName.ToLowerInvariant();
builder.Services.AddOpenApi(apiName);
builder.Services.ConfigureOptions<TConfigureOptions>();
builder.Services.AddOpenApiDocumentToUi(apiName, apiTitle);
if (jsonOptionsName is not null)
{
builder.Services.ReplaceOpenApiSchemaService(apiName, jsonOptionsName);
}
}
}
@@ -0,0 +1,177 @@
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.Attributes;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Fluent builder for configuring a custom OpenAPI document.
/// </summary>
public sealed class BackOfficeOpenApiDocumentBuilder
{
private readonly List<Action<OpenApiOptions>> _configurations = [];
private string? _title;
private string? _uiTitle;
private bool _includedInUi = true;
private Func<IServiceProvider, JsonOptions>? _httpJsonOptionsFactory;
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeOpenApiDocumentBuilder"/> class.
/// </summary>
/// <param name="documentName">The name of the OpenAPI document being configured.</param>
internal BackOfficeOpenApiDocumentBuilder(string documentName)
=> DocumentName = documentName;
/// <summary>
/// Gets the name of the OpenAPI document being configured.
/// </summary>
public string DocumentName { get; }
/// <summary>
/// Sets the document's <c>Info.Title</c>. Also used as the UI dropdown label unless overridden via
/// <see cref="WithUiTitle"/>.
/// </summary>
/// <param name="title">The title to display.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder WithTitle(string title)
{
_title = title;
return this;
}
/// <summary>
/// Overrides the UI dropdown label for this document.
/// </summary>
/// <param name="uiTitle">The label to display.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder WithUiTitle(string uiTitle)
{
_uiTitle = uiTitle;
return this;
}
/// <summary>
/// Excludes this document from the UI dropdown.
/// </summary>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder ExcludeFromUi()
{
_includedInUi = false;
return this;
}
/// <summary>
/// Adds an <see cref="OpenApiOptions"/> configuration callback. Multiple calls compose.
/// </summary>
/// <param name="configure">Callback to configure the options.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder ConfigureOpenApiOptions(Action<OpenApiOptions> configure)
{
_configurations.Add(configure);
return this;
}
/// <summary>
/// Sets the named <see cref="JsonOptions">Microsoft.AspNetCore.Http.Json.JsonOptions</see> used when
/// generating this document's schema. Use this to match the serialization conventions of the API
/// endpoints the document describes.
/// </summary>
/// <param name="jsonOptionsName">The name of the registered HTTP <see cref="JsonOptions"/> to apply.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder WithJsonOptions(string jsonOptionsName)
=> WithJsonOptions(sp => sp.GetRequiredService<IOptionsMonitor<JsonOptions>>().Get(jsonOptionsName));
/// <summary>
/// Sets the <see cref="JsonOptions">Microsoft.AspNetCore.Http.Json.JsonOptions</see> used when
/// generating this document's schema. Use this to match the serialization conventions of the API
/// endpoints the document describes.
/// </summary>
/// <param name="jsonOptions">The HTTP JSON options to apply.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder WithJsonOptions(JsonOptions jsonOptions)
=> WithJsonOptions(_ => jsonOptions);
/// <summary>
/// Sets a factory that produces the <see cref="JsonOptions">Microsoft.AspNetCore.Http.Json.JsonOptions</see>
/// used when generating this document's schema. Use this to match the serialization conventions of the
/// API endpoints the document describes.
/// </summary>
/// <param name="jsonOptionsFactory">Factory invoked when the schema service is first resolved.</param>
/// <returns>The same builder for chaining.</returns>
public BackOfficeOpenApiDocumentBuilder WithJsonOptions(Func<IServiceProvider, JsonOptions> jsonOptionsFactory)
{
_httpJsonOptionsFactory = jsonOptionsFactory;
return this;
}
/// <summary>
/// Applies the accumulated configuration to the supplied <see cref="IUmbracoBuilder"/>'s service
/// collection. Called by <c>AddBackOfficeOpenApiDocument</c> once the user-supplied callback returns.
/// </summary>
/// <param name="builder">The Umbraco builder to register services against.</param>
internal void Build(IUmbracoBuilder builder)
{
// AddOpenApi lowercases the document name when registering its keyed services (https://github.com/dotnet/aspnetcore/blob/v10.0.9/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs#L64),
// so we must normalise here to keep AddOpenApiDocumentToUi and ReplaceOpenApiSchemaService in sync.
string lowercasedDocumentName = DocumentName.ToLowerInvariant();
builder.Services.AddOpenApi(
lowercasedDocumentName,
options =>
{
// ShouldInclude matches [MapToApi] case-insensitively to align with how documents are registered.
options.ShouldInclude = apiDescription =>
apiDescription.ActionDescriptor.EndpointMetadata
?.OfType<MapToApiAttribute>()
.Any(a => a.ApiName.Equals(DocumentName, StringComparison.OrdinalIgnoreCase))
?? false;
options.CreateSchemaReferenceId = UmbracoSchemaIdGenerator.CreateSchemaReferenceId;
if (_title is not null)
{
options.AddDocumentTransformer((document, _, _) =>
{
document.Info.Title = _title;
return Task.CompletedTask;
});
}
// Generate operation IDs using Umbraco's naming conventions.
options.AddOperationTransformer<UmbracoOperationIdTransformer>();
// Trim redundant JSON-equivalent MIME types (e.g. text/json, application/*+json, text/plain)
// that ASP.NET Core adds alongside application/json.
options.AddOperationTransformer<MimeTypesTransformer>();
// Mark non-nullable properties as required so generated SDKs reflect the C# nullability.
options.AddSchemaTransformer<RequireNonNullablePropertiesSchemaTransformer>();
// Tag actions by group name and cleanup unused tags (caused by the tag changes).
options
.AddOperationTransformer<TagActionsByGroupNameTransformer>()
.AddDocumentTransformer<TagActionsByGroupNameTransformer>()
.AddDocumentTransformer<SortTagsAndPathsTransformer>();
foreach (Action<OpenApiOptions> configure in _configurations)
{
configure(options);
}
});
if (_includedInUi)
{
builder.Services.AddOpenApiDocumentToUi(lowercasedDocumentName, _uiTitle ?? _title ?? DocumentName);
}
if (_httpJsonOptionsFactory is not null)
{
builder.Services.ReplaceOpenApiSchemaService(lowercasedDocumentName, _httpJsonOptionsFactory);
}
}
}
@@ -1,30 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Determines whether an API description should be included in a specific documentation set based on the document name
/// and API metadata.
/// </summary>
public class DocumentInclusionSelector : IDocumentInclusionSelector
{
/// <inheritdoc/>
public bool Include(string documentName, ApiDescription apiDescription)
{
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
&& controllerActionDescriptor.HasMapToApiAttribute(documentName))
{
return true;
}
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.GetApiVersionMetadata();
return apiVersionMetadata.Name == documentName
|| (string.IsNullOrEmpty(apiVersionMetadata.Name) && documentName == DefaultApiConfiguration.ApiName);
}
}
@@ -1,35 +0,0 @@
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json.Nodes;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// A schema filter that converts enum schemas to string type with enum member names.
/// </summary>
/// <remarks>
/// This filter ensures enums are represented as strings in the OpenAPI schema,
/// using <see cref="EnumMemberAttribute"/> values when available.
/// </remarks>
public class EnumSchemaFilter : ISchemaFilter
{
/// <inheritdoc/>
public void Apply(IOpenApiSchema model, SchemaFilterContext context)
{
if (model is not OpenApiSchema schema || context.Type.IsEnum is false)
{
return;
}
schema.Type = JsonSchemaType.String;
schema.Format = null;
schema.Enum = new List<JsonNode>();
foreach (var name in Enum.GetNames(context.Type))
{
var actualName = context.Type.GetField(name)?.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? name;
schema.Enum.Add(actualName);
}
}
}
@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Excludes the controller from the default OpenAPI document.
/// Use this when you have a custom OpenAPI document for your API.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class ExcludeFromDefaultOpenApiDocumentAttribute : Attribute
{
}
@@ -0,0 +1,47 @@
using System.IO.Pipelines;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Transformer to fix file return types in OpenAPI schema.
/// </summary>
/// <remarks>Can be removed once https://github.com/dotnet/aspnetcore/pull/63504 and
/// https://github.com/dotnet/aspnetcore/pull/64562 are released.</remarks>
internal class FixFileReturnTypesTransformer : IOpenApiSchemaTransformer
{
private static readonly Type[] _binaryStringTypes =
[
typeof(IFormFile),
typeof(FileResult),
typeof(Stream),
typeof(PipeReader),
];
/// <inheritdoc />
public Task TransformAsync(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
if (_binaryStringTypes.Any(possibleBaseType => possibleBaseType.IsAssignableFrom(context.JsonTypeInfo.Type)) is false)
{
return Task.CompletedTask;
}
// Clear all properties
schema.Properties?.Clear();
schema.Required?.Clear();
// Make it an inline schema
schema.Metadata?.Remove("x-schema-id");
// Set type to string with binary format
schema.Type = JsonSchemaType.String;
schema.Format = "binary";
return Task.CompletedTask;
}
}
@@ -1,19 +0,0 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a method that determines whether a given API description should be included in a specific documentation
/// document.
/// </summary>
public interface IDocumentInclusionSelector
{
/// <summary>
/// Determines whether the specified API description should be included in the generated documentation for the given
/// document name.
/// </summary>
/// <param name="documentName">The name of the documentation document being generated.</param>
/// <param name="apiDescription">The API description to evaluate for inclusion.</param>
/// <returns>true if the API description should be included in the documentation; otherwise, false.</returns>
bool Include(string documentName, ApiDescription apiDescription);
}
@@ -1,23 +0,0 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a handler for generating OpenAPI operation IDs.
/// </summary>
public interface IOperationIdHandler
{
/// <summary>
/// Determines whether this handler can generate an operation ID for the specified API description.
/// </summary>
/// <param name="apiDescription">The API description to check.</param>
/// <returns><c>true</c> if this handler can handle the API description; otherwise, <c>false</c>.</returns>
bool CanHandle(ApiDescription apiDescription);
/// <summary>
/// Generates an operation ID for the specified API description.
/// </summary>
/// <param name="apiDescription">The API description to generate an operation ID for.</param>
/// <returns>The generated operation ID.</returns>
string Handle(ApiDescription apiDescription);
}
@@ -1,17 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a selector for choosing operation IDs from registered handlers.
/// </summary>
public interface IOperationIdSelector
{
/// <summary>
/// Selects an operation ID for the specified API description.
/// </summary>
/// <param name="apiDescription">The API description to generate an operation ID for.</param>
/// <returns>The operation ID, or <c>null</c> if none could be determined.</returns>
string? OperationId(ApiDescription apiDescription);
}
@@ -1,21 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a handler for generating OpenAPI schema IDs.
/// </summary>
public interface ISchemaIdHandler
{
/// <summary>
/// Determines whether this handler can generate a schema ID for the specified type.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns><c>true</c> if this handler can handle the type; otherwise, <c>false</c>.</returns>
bool CanHandle(Type type);
/// <summary>
/// Generates a schema ID for the specified type.
/// </summary>
/// <param name="type">The type to generate a schema ID for.</param>
/// <returns>The generated schema ID.</returns>
string Handle(Type type);
}
@@ -1,14 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a selector for choosing schema IDs from registered handlers.
/// </summary>
public interface ISchemaIdSelector
{
/// <summary>
/// Selects a schema ID for the specified type.
/// </summary>
/// <param name="type">The type to generate a schema ID for.</param>
/// <returns>The schema ID.</returns>
string SchemaId(Type type);
}
@@ -1,22 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a handler for discovering sub-types for polymorphic OpenAPI schemas.
/// </summary>
public interface ISubTypesHandler
{
/// <summary>
/// Determines whether this handler can discover sub-types for the specified type and document.
/// </summary>
/// <param name="type">The type to check.</param>
/// <param name="documentName">The OpenAPI document name.</param>
/// <returns><c>true</c> if this handler can handle the type; otherwise, <c>false</c>.</returns>
bool CanHandle(Type type, string documentName);
/// <summary>
/// Discovers sub-types for the specified type.
/// </summary>
/// <param name="type">The type to discover sub-types for.</param>
/// <returns>An enumerable of discovered sub-types.</returns>
IEnumerable<Type> Handle(Type type);
}
@@ -1,14 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a selector for choosing sub-types from registered handlers.
/// </summary>
public interface ISubTypesSelector
{
/// <summary>
/// Selects sub-types for the specified type for polymorphic OpenAPI schema generation.
/// </summary>
/// <param name="type">The type to find sub-types for.</param>
/// <returns>An enumerable of sub-types.</returns>
IEnumerable<Type> SubTypes(Type type);
}
@@ -1,60 +0,0 @@
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// This filter explicitly removes all other mime types than application/json from a named OpenAPI document when application/json is accepted.
/// </summary>
public class MimeTypeDocumentFilter : IDocumentFilter
{
private readonly string _documentName;
/// <summary>
/// Initializes a new instance of the <see cref="MimeTypeDocumentFilter"/> class.
/// </summary>
/// <param name="documentName">The name of the OpenAPI document to filter.</param>
public MimeTypeDocumentFilter(string documentName) => _documentName = documentName;
/// <inheritdoc/>
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != _documentName)
{
return;
}
OpenApiOperation[] operations = swaggerDoc.Paths
.SelectMany(path => path.Value.Operations?.Values ?? Enumerable.Empty<OpenApiOperation>())
.ToArray();
static void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType>? content)
{
if (content is null || content.ContainsKey("application/json") is false)
{
return;
}
content.RemoveAll(r => r.Key != "application/json");
}
OpenApiRequestBody[] requestBodies = operations
.Select(operation => operation.RequestBody)
.OfType<OpenApiRequestBody>()
.ToArray();
foreach (OpenApiRequestBody requestBody in requestBodies)
{
RemoveUnwantedMimeTypes(requestBody.Content);
}
OpenApiResponse[] responses = operations
.SelectMany(operation => operation.Responses?.Values ?? Enumerable.Empty<IOpenApiResponse>())
.OfType<OpenApiResponse>()
.ToArray();
foreach (OpenApiResponse response in responses)
{
RemoveUnwantedMimeTypes(response.Content);
}
}
}
@@ -0,0 +1,88 @@
using System.Net.Mime;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Trims redundant JSON-equivalent media types from OpenAPI operations.
/// </summary>
/// <remarks>
/// <para>
/// ASP.NET Core's content negotiation populates operations with several media types that all serialize to JSON
/// (<c>text/json</c>, <c>application/*+json</c>, and <c>text/plain</c> alongside <c>application/json</c>).
/// When <c>application/json</c> is present on a response or request body, this transformer strips those
/// equivalents so OpenAPI consumers and generated SDKs aren't burdened with variants that produce identical
/// payloads. Non-JSON media types (e.g. <c>application/xml</c>, <c>application/octet-stream</c>) are preserved.
/// </para>
/// <para>
/// Request bodies additionally honour <c>[Consumes]</c>: when the attribute is present, the request content is
/// replaced entirely with the declared content types, taking precedence over the
/// JSON-equivalent stripping above.
/// </para>
/// </remarks>
internal class MimeTypesTransformer : IOpenApiOperationTransformer
{
private static readonly string[] _jsonEquivalentMimeTypes =
[
MediaTypeNames.Text.Plain,
"application/*+json",
"text/json"
];
/// <inheritdoc/>
public Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
// For request bodies, keep only the content types declared in [Consumes], or fall back to application/json.
if (operation.RequestBody?.Content is { } requestContent)
{
var explicitContentTypes = context.Description.ActionDescriptor.EndpointMetadata
.OfType<ConsumesAttribute>()
.SelectMany(p => p.ContentTypes)
.Distinct()
.ToArray();
if (explicitContentTypes.Length != 0)
{
// Replace content types entirely with what [Consumes] declares,
// preserving the schema from the existing entry.
OpenApiMediaType? existingMediaType = requestContent.Values.FirstOrDefault();
requestContent.Clear();
foreach (var contentType in explicitContentTypes)
{
requestContent[contentType] = existingMediaType ?? new OpenApiMediaType();
}
}
else
{
RemoveJsonEquivalentMimeTypes(requestContent);
}
}
// For responses, drop JSON-equivalent media types when application/json is present.
foreach (IOpenApiResponse response in (operation.Responses ?? []).Values)
{
if (response is OpenApiResponse openApiResponse)
{
RemoveJsonEquivalentMimeTypes(openApiResponse.Content);
}
}
return Task.CompletedTask;
}
private static void RemoveJsonEquivalentMimeTypes(IDictionary<string, OpenApiMediaType>? content)
{
if (content?.ContainsKey(MediaTypeNames.Application.Json) != true)
{
return;
}
content.RemoveAll(r => _jsonEquivalentMimeTypes.Contains(r.Key, StringComparer.OrdinalIgnoreCase));
}
}
@@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerUI;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
namespace Umbraco.Cms.Api.Common.OpenApi;
internal class OpenApiRouteTemplatePipelineFilter : UmbracoPipelineFilter
{
public OpenApiRouteTemplatePipelineFilter(string name)
: base(name)
{
PostPipeline = PostPipelineAction;
PreMapEndpoints = OnPreMapEndpointsAction;
}
private static void PostPipelineAction(IApplicationBuilder applicationBuilder)
{
UmbracoOpenApiOptions options = applicationBuilder.ApplicationServices
.GetRequiredService<IOptions<UmbracoOpenApiOptions>>().Value;
if (options.Enabled is false || options.DefaultUiEnabled is false)
{
return;
}
applicationBuilder.UseSwaggerUI(swaggerUiOptions => ConfigureSwaggerUi(swaggerUiOptions, options));
}
private static void OnPreMapEndpointsAction(IEndpointRouteBuilder endpoints)
{
UmbracoOpenApiOptions options = endpoints.ServiceProvider
.GetRequiredService<IOptions<UmbracoOpenApiOptions>>().Value;
if (options.Enabled is false)
{
return;
}
endpoints.MapOpenApi(options.RouteTemplate);
}
private static void ConfigureSwaggerUi(SwaggerUIOptions swaggerUiOptions, UmbracoOpenApiOptions options)
{
swaggerUiOptions.RoutePrefix = options.UiRoutePrefix;
// Add custom configuration from https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
swaggerUiOptions.ConfigObject.PersistAuthorization = true; // persists authorization data so it would not be lost on browser close/refresh
swaggerUiOptions.ConfigObject.Filter = string.Empty; // Enable the filter with an empty string as default filter.
swaggerUiOptions.OAuthClientId(Constants.OAuthClientIds.OpenApiUi);
swaggerUiOptions.OAuthUsePkce();
}
}
@@ -1,35 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Selects an operation ID for an API description using registered handlers.
/// </summary>
public class OperationIdSelector : IOperationIdSelector
{
private readonly IEnumerable<IOperationIdHandler> _operationIdHandlers;
/// <summary>
/// Initializes a new instance of the <see cref="OperationIdSelector"/> class.
/// </summary>
[Obsolete("Use non-obsolete constructor. Scheduled for removal in Umbraco 18.")]
public OperationIdSelector()
: this(Enumerable.Empty<IOperationIdHandler>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OperationIdSelector"/> class.
/// </summary>
/// <param name="operationIdHandlers">The registered operation ID handlers.</param>
public OperationIdSelector(IEnumerable<IOperationIdHandler> operationIdHandlers)
=> _operationIdHandlers = operationIdHandlers;
/// <inheritdoc/>
public virtual string? OperationId(ApiDescription apiDescription)
{
IOperationIdHandler? handler = _operationIdHandlers.FirstOrDefault(h => h.CanHandle(apiDescription));
return handler?.Handle(apiDescription);
}
}
@@ -1,30 +0,0 @@
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// This filter explicitly removes all security schemes from a named OpenAPI document.
/// </summary>
public class RemoveSecuritySchemesDocumentFilter : IDocumentFilter
{
private readonly string _documentName;
/// <summary>
/// Initializes a new instance of the <see cref="RemoveSecuritySchemesDocumentFilter"/> class.
/// </summary>
/// <param name="documentName">The name of the OpenAPI document to filter.</param>
public RemoveSecuritySchemesDocumentFilter(string documentName)
=> _documentName = documentName;
/// <inheritdoc/>
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != _documentName)
{
return;
}
swaggerDoc.Components?.SecuritySchemes?.Clear();
}
}
@@ -0,0 +1,48 @@
using System.Reflection;
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Ensures that all non-nullable properties are marked as required in the OpenAPI schema.
/// </summary>
/// <remarks>By default, only properties marked with the required keyword will actually show as required.
/// Non-nullable reference types were not taken into account.</remarks>
internal class RequireNonNullablePropertiesSchemaTransformer : IOpenApiSchemaTransformer
{
/// <inheritdoc />
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
IEnumerable<string> additionalRequiredProps = schema.Properties?
.Where(p => schema.Required?.Contains(p.Key) != true) // If it's already required, skip
.Where(x => IsRequiredProperty(schema, context.JsonTypeInfo, x.Key))
.Select(x => x.Key)
?? [];
schema.Required ??= new HashSet<string>();
foreach (var propKey in additionalRequiredProps)
{
schema.Required.Add(propKey);
}
return Task.CompletedTask;
}
private static bool IsRequiredProperty(OpenApiSchema schema, JsonTypeInfo jsonTypeInfo, string propertyName)
{
if (jsonTypeInfo.Properties.FirstOrDefault(p => p.Name == propertyName) is { } property)
{
return property.IsGetNullable is false;
}
// If we can't find the property in the type (e.g. discriminator '$type'), use the schema type information.
if (schema.Properties?.TryGetValue(propertyName, out IOpenApiSchema? schemaProperty) is true
&& schemaProperty?.Type is { } propertyType)
{
return propertyType.HasFlag(JsonSchemaType.Null) is false;
}
return false;
}
}
@@ -1,59 +0,0 @@
using System.Text.RegularExpressions;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Default handler for generating OpenAPI schema IDs for Umbraco types.
/// </summary>
/// <remarks>
/// Left unsealed on purpose, so it is extendable by consuming APIs.
/// Adds "Model" suffix to avoid TypeScript name clashes and removes invalid characters.
/// </remarks>
public class SchemaIdHandler : ISchemaIdHandler
{
/// <inheritdoc/>
public virtual bool CanHandle(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
/// <inheritdoc/>
public virtual string Handle(Type type)
=> UmbracoSchemaId(type);
/// <summary>
/// Generates a sanitized and consistent schema identifier for a given type following Umbraco's schema id naming conventions.
/// </summary>
protected string UmbracoSchemaId(Type type)
{
var name = SanitizedTypeName(type);
name = HandleGenerics(name, type);
if (name.EndsWith("Model") == false)
{
// because some models names clash with common classes in TypeScript (i.e. Document),
// we need to add a "Model" postfix to all models
name = $"{name}Model";
}
// make absolutely sure we don't pass any invalid named by removing all non-word chars
return Regex.Replace(name, @"[^\w]", string.Empty);
}
private string SanitizedTypeName(Type t) => t.Name
// first grab the "non-generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
.Split('`').First()
// then remove the "ViewModel" postfix from type names
.TrimEnd("ViewModel");
private string HandleGenerics(string name, Type type)
{
if (!type.IsGenericType)
{
return name;
}
// use attribute custom name or append the generic type names, ultimately turning i.e. "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
}
}
@@ -1,23 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Selects a schema ID for a type using registered handlers.
/// </summary>
public class SchemaIdSelector : ISchemaIdSelector
{
private readonly IEnumerable<ISchemaIdHandler> _schemaIdHandlers;
/// <summary>
/// Initializes a new instance of the <see cref="SchemaIdSelector"/> class.
/// </summary>
/// <param name="schemaIdHandlers">The registered schema ID handlers.</param>
public SchemaIdSelector(IEnumerable<ISchemaIdHandler> schemaIdHandlers)
=> _schemaIdHandlers = schemaIdHandlers;
/// <inheritdoc/>
public virtual string SchemaId(Type type)
{
ISchemaIdHandler? handler = _schemaIdHandlers.FirstOrDefault(h => h.CanHandle(type));
return handler?.Handle(type) ?? type.Name;
}
}
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Transforms the OpenAPI document to sort tags and paths alphabetically.
/// </summary>
internal class SortTagsAndPathsTransformer : IOpenApiDocumentTransformer
{
/// <summary>
/// Transforms the specified OpenAPI document to sort its tags and paths alphabetically.
/// </summary>
/// <param name="document">The <see cref="OpenApiDocument"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiDocumentTransformerContext"/> associated with the <paramref name="document"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
document.Tags = new SortedSet<OpenApiTag>(
document.Tags ?? Enumerable.Empty<OpenApiTag>(),
Comparer<OpenApiTag>.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)));
var sortedPaths = new OpenApiPaths();
foreach (KeyValuePair<string, IOpenApiPathItem> keyValuePair in document.Paths
.OrderBy(x => x.Value.Operations?.Values
.SelectMany(op => op.Tags ?? Enumerable.Empty<OpenApiTagReference>())
.OrderBy(t => t.Name)
.FirstOrDefault()?
.Name)
.ThenBy(x => x.Key))
{
sortedPaths.Add(keyValuePair.Key, keyValuePair.Value);
}
document.Paths = sortedPaths;
return Task.CompletedTask;
}
}
@@ -1,34 +0,0 @@
using Umbraco.Cms.Api.Common.Serialization;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Default handler for discovering sub-types for polymorphic OpenAPI schemas.
/// </summary>
public class SubTypesHandler : ISubTypesHandler
{
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
/// <summary>
/// Initializes a new instance of the <see cref="SubTypesHandler"/> class.
/// </summary>
/// <param name="umbracoJsonTypeInfoResolver">The JSON type info resolver for finding sub-types.</param>
public SubTypesHandler(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
=> _umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
/// <summary>
/// Determines whether this handler can process the specified type based on namespace.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns><c>true</c> if the type is in an Umbraco.Cms namespace; otherwise, <c>false</c>.</returns>
protected virtual bool CanHandle(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
/// <inheritdoc/>
public virtual bool CanHandle(Type type, string documentName)
=> CanHandle(type);
/// <inheritdoc/>
public virtual IEnumerable<Type> Handle(Type type)
=> _umbracoJsonTypeInfoResolver.FindSubTypes(type);
}
@@ -1,67 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Selects sub-types for polymorphic OpenAPI schemas using registered handlers.
/// </summary>
public class SubTypesSelector : ISubTypesSelector
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IEnumerable<ISubTypesHandler> _subTypeHandlers;
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
/// <summary>
/// Initializes a new instance of the <see cref="SubTypesSelector"/> class.
/// </summary>
/// <param name="hostingEnvironment">The hosting environment.</param>
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
/// <param name="subTypeHandlers">The registered sub-type handlers.</param>
/// <param name="umbracoJsonTypeInfoResolver">The JSON type info resolver for finding sub-types.</param>
public SubTypesSelector(
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor,
IEnumerable<ISubTypesHandler> subTypeHandlers,
IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
{
_hostingEnvironment = hostingEnvironment;
_httpContextAccessor = httpContextAccessor;
_subTypeHandlers = subTypeHandlers;
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
}
/// <inheritdoc/>
public IEnumerable<Type> SubTypes(Type type)
{
var backOfficePath = _hostingEnvironment.GetBackOfficePath();
var swaggerPath = $"{backOfficePath}/swagger";
if (_httpContextAccessor.HttpContext?.Request.Path.StartsWithSegments(swaggerPath) ?? false)
{
// Split the path into segments
var segments = _httpContextAccessor.HttpContext.Request.Path.Value![swaggerPath.Length..]
.TrimStart(Constants.CharArrays.ForwardSlash)
.Split(Constants.CharArrays.ForwardSlash);
// Extract the document name from the path
var documentName = segments[0];
// Find the first handler that can handle the type / document name combination
ISubTypesHandler? handler = _subTypeHandlers.FirstOrDefault(h => h.CanHandle(type, documentName));
if (handler != null)
{
return handler.Handle(type);
}
}
// Default implementation to maintain backwards compatibility
return _umbracoJsonTypeInfoResolver.FindSubTypes(type);
}
}
@@ -1,98 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerUI;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Pipeline filter that configures Swagger/OpenAPI endpoints for Umbraco APIs.
/// </summary>
public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
{
/// <summary>
/// Initializes a new instance of the <see cref="SwaggerRouteTemplatePipelineFilter"/> class.
/// </summary>
/// <param name="name">The name of the pipeline filter.</param>
public SwaggerRouteTemplatePipelineFilter(string name)
: base(name)
=> PostPipeline = PostPipelineAction;
private void PostPipelineAction(IApplicationBuilder applicationBuilder)
{
if (SwaggerIsEnabled(applicationBuilder) is false)
{
return;
}
IOptions<SwaggerGenOptions> swaggerGenOptions = applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SwaggerGenOptions>>();
applicationBuilder.UseSwagger(swaggerOptions =>
{
swaggerOptions.RouteTemplate = SwaggerRouteTemplate(applicationBuilder);
});
applicationBuilder.UseSwaggerUI(swaggerUiOptions => SwaggerUiConfiguration(swaggerUiOptions, swaggerGenOptions.Value, applicationBuilder));
}
/// <summary>
/// Determines whether Swagger is enabled for the application.
/// </summary>
/// <param name="applicationBuilder">The application builder.</param>
/// <returns><c>true</c> if Swagger is enabled; otherwise, <c>false</c>.</returns>
protected virtual bool SwaggerIsEnabled(IApplicationBuilder applicationBuilder)
=> applicationBuilder.ApplicationServices.GetRequiredService<IWebHostEnvironment>().IsProduction() is false;
/// <summary>
/// Gets the route template for Swagger JSON endpoints.
/// </summary>
/// <param name="applicationBuilder">The application builder.</param>
/// <returns>The Swagger route template.</returns>
protected virtual string SwaggerRouteTemplate(IApplicationBuilder applicationBuilder)
=> $"{GetBackOfficePath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json";
/// <summary>
/// Gets the route prefix for the Swagger UI.
/// </summary>
/// <param name="applicationBuilder">The application builder.</param>
/// <returns>The Swagger UI route prefix.</returns>
protected virtual string SwaggerUiRoutePrefix(IApplicationBuilder applicationBuilder)
=> $"{GetBackOfficePath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger";
/// <summary>
/// Configures the Swagger UI options.
/// </summary>
/// <param name="swaggerUiOptions">The Swagger UI options to configure.</param>
/// <param name="swaggerGenOptions">The Swagger generation options.</param>
/// <param name="applicationBuilder">The application builder.</param>
protected virtual void SwaggerUiConfiguration(
SwaggerUIOptions swaggerUiOptions,
SwaggerGenOptions swaggerGenOptions,
IApplicationBuilder applicationBuilder)
{
swaggerUiOptions.RoutePrefix = SwaggerUiRoutePrefix(applicationBuilder);
foreach ((var name, OpenApiInfo? apiInfo) in swaggerGenOptions.SwaggerGeneratorOptions.SwaggerDocs.OrderBy(x => x.Value.Title))
{
swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}");
}
// Add custom configuration from https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
swaggerUiOptions.ConfigObject.PersistAuthorization = true; // persists authorization data so it would not be lost on browser close/refresh
swaggerUiOptions.ConfigObject.Filter = string.Empty; // Enable the filter with an empty string as default filter.
swaggerUiOptions.OAuthClientId(Constants.OAuthClientIds.Swagger);
swaggerUiOptions.OAuthUsePkce();
}
private string GetBackOfficePath(IApplicationBuilder applicationBuilder)
=> applicationBuilder.ApplicationServices.GetRequiredService<IHostingEnvironment>().GetBackOfficePath();
}
@@ -0,0 +1,67 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Transformer that tags operations based on their group name.
/// </summary>
internal class TagActionsByGroupNameTransformer : IOpenApiOperationTransformer, IOpenApiDocumentTransformer
{
/// <summary>
/// Transforms the specified OpenAPI operation in order to tag it by its group name.
/// </summary>
/// <param name="operation">The <see cref="OpenApiOperation"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiOperationTransformerContext"/> associated with the <paramref name="operation"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
if (context.Document is null || context.Description.GroupName is not { } groupName)
{
return Task.CompletedTask;
}
operation.Tags = new HashSet<OpenApiTagReference> { new(groupName) };
if (context.Document.Tags?.Any(t => t.Name == groupName) == true)
{
return Task.CompletedTask;
}
context.Document.Tags ??= new HashSet<OpenApiTag>();
context.Document.Tags.Add(new OpenApiTag { Name = groupName });
return Task.CompletedTask;
}
/// <summary>
/// Transforms the specified OpenAPI document in order to clean up unused tags.
/// </summary>
/// <param name="document">The <see cref="OpenApiDocument"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiDocumentTransformerContext"/> associated with the <paramref name="document"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
var usedTags = new HashSet<string?>(document.Paths
.SelectMany(p => (p.Value.Operations ?? []).Values)
.SelectMany(o => o.Tags ?? new HashSet<OpenApiTagReference>())
.Select(t => t.Name));
var tagsToRemove = (document.Tags ?? Enumerable.Empty<OpenApiTag>())
.Where(tag => usedTags.Contains(tag.Name) is false)
.ToList();
foreach (OpenApiTag tag in tagsToRemove)
{
document.Tags?.Remove(tag);
}
return Task.CompletedTask;
}
}
@@ -0,0 +1,81 @@
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder"/> to register custom OpenAPI documents.
/// </summary>
public static class UmbracoBuilderOpenApiExtensions
{
/// <summary>
/// Registers a custom OpenAPI document with Umbraco's defaults applied.
/// </summary>
/// <param name="builder">The Umbraco builder.</param>
/// <param name="documentName">The document name. Matches the <c>[MapToApi]</c> value on controllers to include.</param>
/// <param name="configure">Optional callback to customize the document.</param>
/// <returns>The same <see cref="IUmbracoBuilder"/> for chaining.</returns>
/// <remarks>
/// <para>
/// The following defaults are applied to the document and can be customized or overridden via the
/// <paramref name="configure"/> callback:
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// Endpoints are filtered by <c>[MapToApi(documentName)]</c>; only matching endpoints appear in the document.
/// </description>
/// </item>
/// <item>
/// <description>
/// Schema reference IDs are generated by <see cref="UmbracoSchemaIdGenerator.CreateSchemaReferenceId"/>, applying
/// Umbraco naming conventions to types under the <c>Umbraco.Cms</c> namespace and falling back to the framework
/// default for everything else. Register your own <c>CreateSchemaReferenceId</c> delegate via
/// <see cref="BackOfficeOpenApiDocumentBuilder.ConfigureOpenApiOptions"/> to override.
/// </description>
/// </item>
/// <item>
/// <description>
/// Operation IDs are generated by <see cref="UmbracoOperationIdTransformer"/>. Register your own
/// <see cref="Microsoft.AspNetCore.OpenApi.IOpenApiOperationTransformer"/> via
/// <see cref="BackOfficeOpenApiDocumentBuilder.ConfigureOpenApiOptions"/> to override.
/// </description>
/// </item>
/// <item>
/// <description>
/// Operations are tagged by their controller's API group name, and the resulting tags and paths are sorted
/// for stable, diffable document output.
/// </description>
/// </item>
/// <item>
/// <description>
/// Redundant JSON-equivalent media types (such as <c>text/json</c>, <c>application/*+json</c>, and
/// <c>text/plain</c>) are stripped from request and response content when <c>application/json</c> is present,
/// so the document doesn't list spurious media types that ASP.NET Core adds by default.
/// </description>
/// </item>
/// <item>
/// <description>
/// Non-nullable properties are marked as <c>required</c> in the schema so generated client SDKs reflect
/// C# nullability. Override via <see cref="BackOfficeOpenApiDocumentBuilder.ConfigureOpenApiOptions"/>
/// if your types don't follow this convention.
/// </description>
/// </item>
/// <item>
/// <description>
/// The document is registered in the OpenAPI UI document selector dropdown. Call
/// <see cref="BackOfficeOpenApiDocumentBuilder.ExcludeFromUi"/> to opt out.
/// </description>
/// </item>
/// </list>
/// </remarks>
public static IUmbracoBuilder AddBackOfficeOpenApiDocument(
this IUmbracoBuilder builder,
string documentName,
Action<BackOfficeOpenApiDocumentBuilder>? configure = null)
{
var documentBuilder = new BackOfficeOpenApiDocumentBuilder(documentName);
configure?.Invoke(documentBuilder);
documentBuilder.Build(builder);
return builder;
}
}
@@ -0,0 +1,48 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Options for configuring OpenAPI documents and UI.
/// </summary>
/// <remarks>
/// These options are populated by <c>AddUmbracoOpenApi</c> during DI configuration, which resolves the back-office path
/// from <see cref="Core.Hosting.IHostingEnvironment"/> and sets the default values for
/// <see cref="RouteTemplate"/> and <see cref="UiRoutePrefix"/>. Consumers that read this options type before
/// <c>AddUmbracoOpenApi</c> has run will observe the uninitialised defaults (empty strings for the route properties).
/// </remarks>
public class UmbracoOpenApiOptions
{
/// <summary>
/// Gets or sets whether OpenAPI documents are enabled.
/// Configured to <c>true</c> in non-production environments by default; <c>false</c> until configured.
/// This avoids exposing API structure on public-facing websites.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Gets or sets whether the default OpenAPI UI is enabled.
/// Only applies when <see cref="Enabled"/> is true.
/// Set to false to disable the default UI while keeping OpenAPI documents available,
/// allowing you to use an alternative UI.
/// Default: true.
/// </summary>
public bool DefaultUiEnabled { get; set; } = true;
/// <summary>
/// Gets or sets the route template for OpenAPI JSON documents.
/// Use <c>{documentName}</c> as a placeholder for the document name.
/// </summary>
/// <remarks>
/// Populated by <c>AddUmbracoOpenApi</c> to <c>"{backOfficePath}/openapi/{documentName}.json"</c>. The initial
/// <see cref="string.Empty"/> default is a sentinel for "not yet configured" — it is not a usable route template.
/// </remarks>
public string RouteTemplate { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the route prefix for OpenAPI UI.
/// </summary>
/// <remarks>
/// Populated by <c>AddUmbracoOpenApi</c> to <c>"{backOfficePath}/openapi"</c>. The initial <see cref="string.Empty"/>
/// default is a sentinel for "not yet configured" — it is not a usable route prefix.
/// </remarks>
public string UiRoutePrefix { get; set; } = string.Empty;
}
@@ -1,63 +1,54 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Default handler for generating OpenAPI operation IDs for Umbraco API controllers.
/// Transforms OpenAPI operation IDs using Umbraco's naming conventions.
/// </summary>
/// <remarks>
/// Left unsealed on purpose, so it is extendable by consuming APIs.
/// This transformer can be registered manually for custom OpenAPI configurations.
/// </remarks>
public class OperationIdHandler : IOperationIdHandler
public class UmbracoOperationIdTransformer : IOpenApiOperationTransformer
{
private readonly ApiVersioningOptions _apiVersioningOptions;
/// <summary>
/// Initializes a new instance of the <see cref="OperationIdHandler"/> class.
/// Transforms the specified OpenAPI operation, setting its operation ID using a custom selector.
/// </summary>
/// <param name="apiVersioningOptions">The API versioning options.</param>
public OperationIdHandler(IOptions<ApiVersioningOptions> apiVersioningOptions)
=> _apiVersioningOptions = apiVersioningOptions.Value;
/// <inheritdoc/>
public bool CanHandle(ApiDescription apiDescription)
/// <param name="operation">The <see cref="OpenApiOperation"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiOperationTransformerContext"/> associated with the <paramref name="operation"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
var operationId = GenerateOperationId(context);
if (operationId is not null)
{
return false;
operation.OperationId = operationId;
}
return CanHandle(apiDescription, controllerActionDescriptor);
return Task.CompletedTask;
}
/// <summary>
/// Determines whether this handler can process the API description based on the controller namespace.
/// </summary>
/// <param name="apiDescription">The API description.</param>
/// <param name="controllerActionDescriptor">The controller action descriptor.</param>
/// <returns><c>true</c> if the controller is in an Umbraco.Cms.Api namespace; otherwise, <c>false</c>.</returns>
protected virtual bool CanHandle(ApiDescription apiDescription, ControllerActionDescriptor controllerActionDescriptor)
=> controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is true;
/// <inheritdoc/>
public virtual string Handle(ApiDescription apiDescription)
=> UmbracoOperationId(apiDescription);
/// <summary>
/// Generates a unique operation identifier for a given API following Umbraco's operation id naming conventions.
/// </summary>
protected string UmbracoOperationId(ApiDescription apiDescription)
private static string? GenerateOperationId(OpenApiOperationTransformerContext context)
{
ApiDescription apiDescription = context.Description;
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
{
throw new ArgumentException($"This handler operates only on {nameof(ControllerActionDescriptor)}.");
// Minimal APIs and other non-MVC endpoints don't carry a ControllerActionDescriptor; leave their
// operation ID untouched so the framework's default applies.
return null;
}
ApiVersion defaultVersion = _apiVersioningOptions.DefaultApiVersion;
ApiVersion defaultVersion = context.ApplicationServices.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.DefaultApiVersion;
var httpMethod = apiDescription.HttpMethod?.ToLower().ToFirstUpper() ?? "Get";
// if the route info "Name" is supplied we'll use this explicitly as the operation ID
@@ -0,0 +1,76 @@
using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.OpenApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Static utility for generating OpenAPI schema IDs following Umbraco's naming conventions.
/// </summary>
public static class UmbracoSchemaIdGenerator
{
/// <summary>
/// Generates a schema ID for the specified type following Umbraco's naming conventions.
/// </summary>
/// <param name="type">The type to generate a schema ID for.</param>
/// <returns>The generated schema ID.</returns>
public static string Generate(Type type)
{
var name = SanitizedTypeName(type);
name = HandleGenerics(name, type);
if (name.EndsWith("Model") == false)
{
// because some models names clash with common classes in TypeScript (i.e. Document),
// we need to add a "Model" postfix to all models
name = $"{name}Model";
}
// make absolutely sure we don't pass any invalid named by removing all non-word chars
return Regex.Replace(name, @"[^\w]", string.Empty);
}
/// <summary>
/// Creates a schema reference ID for the given JSON type info, applying Umbraco's naming conventions to
/// types in the <c>Umbraco.Cms</c> namespace and falling back to the framework default for other types.
/// </summary>
/// <param name="jsonTypeInfo">The JSON type info to create a schema reference ID for.</param>
/// <returns>The schema reference ID, or <c>null</c> if the type should be inlined.</returns>
internal static string? CreateSchemaReferenceId(JsonTypeInfo jsonTypeInfo)
{
// Ensure that only types that would normally be included in the schema generation are given a schema reference ID.
// Otherwise, we should return null to inline them.
var defaultSchemaReferenceId = OpenApiOptions.CreateDefaultSchemaReferenceId(jsonTypeInfo);
if (defaultSchemaReferenceId is null)
{
return null;
}
Type targetType = Nullable.GetUnderlyingType(jsonTypeInfo.Type) ?? jsonTypeInfo.Type;
if (targetType.Namespace?.StartsWith("Umbraco.Cms") is not true)
{
return defaultSchemaReferenceId;
}
return Generate(targetType);
}
private static string SanitizedTypeName(Type t) => t.Name
// first grab the "non-generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
.Split('`').First()
// then remove the "ViewModel" postfix from type names
.TrimEnd("ViewModel");
private static string HandleGenerics(string name, Type type)
{
if (!type.IsGenericType)
{
return name;
}
// use attribute custom name or append the generic type names, ultimately turning i.e. "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
}
}
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using OpenIddict.Server;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Extensions;
@@ -32,12 +33,12 @@ public class ExposeBackOfficeAuthenticationOpenIddictServerEventsHandler : IOpen
// These are the type identifiers for the claims required by the principal
// for the custom authentication scheme.
// We make available the ID, user name and allowed applications (sections) claims.
// We make available the ID and user name claims, plus the claim necessary for parsing the user key.
_claimTypes =
[
backOfficeIdentityOptions.Value.ClaimsIdentity.UserIdClaimType,
backOfficeIdentityOptions.Value.ClaimsIdentity.UserNameClaimType,
Core.Constants.Security.AllowedApplicationsClaimType,
Constants.Security.OpenIdDictSubClaimType
];
}
@@ -8,6 +8,12 @@
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Cms.Api.Management</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Cms.Api.Delivery</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
@@ -15,11 +21,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc "/>
<PackageReference Include="Asp.Versioning.Mvc" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="OpenIddict.Abstractions" />
<PackageReference Include="OpenIddict.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
</ItemGroup>
<ItemGroup>
+2 -3
View File
@@ -39,7 +39,7 @@ Umbraco.Cms.Api.Delivery/
├── Services/ # Business logic and query building
├── Caching/ # Output cache policies
├── Rendering/ # Output expansion strategies
├── Configuration/ # Swagger configuration
├── Configuration/ # OpenAPI configuration
└── Filters/ # Action filters (access, validation)
```
@@ -200,10 +200,9 @@ context.EnableOutputCaching = requestPreviewService.IsPreview() is false
### Technical Debt (TODOs in codebase)
1. **V1 Removal Pending** (4 locations):
1. **V1 Removal Pending** (2 locations):
- `DependencyInjection/UmbracoBuilderExtensions.cs:98` - FIXME: remove matcher policy
- `Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs:11` - FIXME: remove class
- `Filters/SwaggerDocumentationFilterBase.cs:79,83` - FIXME: remove V1 swagger docs
2. **Obsolete Reference Warnings** (csproj:9-13):
- `ASP0019` - IHeaderDictionary.Append usage
@@ -0,0 +1,67 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Web.Common.Caching;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Handles <see cref="ElementCacheRefresherNotification"/> to evict Delivery API output cache entries
/// for content that references the changed element via picker properties (umbElement relations).
/// </summary>
internal sealed class DeliveryApiElementOutputCacheEvictionHandler
: RelationOutputCacheEvictionHandlerBase, INotificationAsyncHandler<ElementCacheRefresherNotification>
{
private readonly ILogger<DeliveryApiElementOutputCacheEvictionHandler> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiElementOutputCacheEvictionHandler"/> class.
/// </summary>
/// <param name="outputCacheStore">The output cache store for evicting cached responses.</param>
/// <param name="relationService">The relation service for querying entity references.</param>
/// <param name="idKeyMap">The ID/key mapping service for converting between integer IDs and GUIDs.</param>
/// <param name="logger">The logger.</param>
public DeliveryApiElementOutputCacheEvictionHandler(
IOutputCacheStore outputCacheStore,
IRelationService relationService,
IIdKeyMap idKeyMap,
ILogger<DeliveryApiElementOutputCacheEvictionHandler> logger)
: base(outputCacheStore, relationService, idKeyMap)
=> _logger = logger;
/// <inheritdoc />
public async Task HandleAsync(ElementCacheRefresherNotification notification, CancellationToken cancellationToken)
{
if (notification.MessageType != MessageType.RefreshByPayload
|| notification.MessageObject is not ElementCacheRefresher.JsonPayload[] payloads)
{
return;
}
foreach (ElementCacheRefresher.JsonPayload payload in payloads)
{
if (payload.ChangeTypes.HasFlag(TreeChangeTypes.RefreshAll))
{
// Evict all Delivery API responses — content responses may include referenced elements,
// so evicting only element-related entries would leave stale element references in content responses.
_logger.LogDebug("Element refresh all — evicting all Delivery API output cache entries.");
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllTag, cancellationToken);
return;
}
}
// Evict content that references the changed elements via picker properties.
await EvictRelatedContentAsync(
payloads.Select(p => p.Id),
Constants.Conventions.RelationTypes.RelatedElementAlias,
Constants.DeliveryApi.OutputCache.ContentTagPrefix,
_logger,
cancellationToken);
}
}
@@ -0,0 +1,62 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Api.Delivery.Configuration;
/// <summary>
/// Configures the OpenAPI options for the Umbraco Delivery API.
/// </summary>
internal class ConfigureUmbracoDeliveryApiOpenApiOptions : ConfigureUmbracoOpenApiOptionsBase
{
private readonly DeliveryApiSettings _deliveryApiSettings;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureUmbracoDeliveryApiOpenApiOptions"/> class.
/// </summary>
/// <param name="deliveryApiSettings">The Delivery API settings.</param>
public ConfigureUmbracoDeliveryApiOpenApiOptions(IOptions<DeliveryApiSettings> deliveryApiSettings)
{
_deliveryApiSettings = deliveryApiSettings.Value;
}
/// <inheritdoc />
protected override string ApiName => DeliveryApiConfiguration.ApiName;
/// <inheritdoc />
protected override string ApiTitle => DeliveryApiConfiguration.ApiTitle;
/// <inheritdoc />
protected override string ApiVersion => "Latest";
/// <inheritdoc />
protected override string ApiDescription =>
$"You can find out more about the {DeliveryApiConfiguration.ApiTitle} in [the documentation]({DeliveryApiConfiguration.ApiDocumentationContentArticleLink}).";
/// <inheritdoc />
protected override void ConfigureOpenApi(OpenApiOptions options)
{
base.ConfigureOpenApi(options);
// Add API key security scheme and configure it for all operations
options
.AddDocumentTransformer<ApiKeyTransformer>()
.AddOperationTransformer<ApiKeyTransformer>();
options.AddSchemaTransformer<RequireNonNullablePropertiesSchemaTransformer>();
options.AddSchemaTransformer<FixFileReturnTypesTransformer>();
options.AddOperationTransformer<MimeTypesTransformer>();
options.AddOperationTransformer<ContentApiTransformer>();
options.AddOperationTransformer<MediaApiTransformer>();
if (_deliveryApiSettings.OpenApi.GenerateContentTypeSchemas)
{
options
.AddSchemaTransformer<ContentTypeSchemaTransformer>()
.AddDocumentTransformer<ContentTypeSchemaTransformer>();
}
}
}
@@ -1,31 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Delivery.Filters;
namespace Umbraco.Cms.Api.Delivery.Configuration;
public class ConfigureUmbracoDeliveryApiSwaggerGenOptions: IConfigureOptions<SwaggerGenOptions>
{
public void Configure(SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.SwaggerDoc(
DeliveryApiConfiguration.ApiName,
new OpenApiInfo
{
Title = DeliveryApiConfiguration.ApiTitle,
Version = "Latest",
Description = $"You can find out more about the {DeliveryApiConfiguration.ApiTitle} in [the documentation]({DeliveryApiConfiguration.ApiDocumentationContentArticleLink})."
});
swaggerGenOptions.DocumentFilter<MimeTypeDocumentFilter>(DeliveryApiConfiguration.ApiName);
swaggerGenOptions.DocumentFilter<RemoveSecuritySchemesDocumentFilter>(DeliveryApiConfiguration.ApiName);
swaggerGenOptions.OperationFilter<SwaggerContentDocumentationFilter>();
swaggerGenOptions.OperationFilter<SwaggerMediaDocumentationFilter>();
swaggerGenOptions.ParameterFilter<SwaggerContentDocumentationFilter>();
swaggerGenOptions.ParameterFilter<SwaggerMediaDocumentationFilter>();
}
}
@@ -0,0 +1,47 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.Configuration;
/// <summary>
/// Configures the Http JSON options for the Umbraco Delivery API.
/// </summary>
internal class ConfigureUmbracoDeliveryHttpJsonOptions : IConfigureNamedOptions<JsonOptions>
{
private readonly IOptionsMonitor<Microsoft.AspNetCore.Mvc.JsonOptions> _mvcJsonOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureUmbracoDeliveryHttpJsonOptions"/> class.
/// </summary>
/// <param name="mvcJsonOptions">The configured MVC json options.</param>
public ConfigureUmbracoDeliveryHttpJsonOptions(IOptionsMonitor<Microsoft.AspNetCore.Mvc.JsonOptions> mvcJsonOptions)
=> _mvcJsonOptions = mvcJsonOptions;
/// <inheritdoc />
public void Configure(JsonOptions options) => Configure(Options.DefaultName, options);
/// <inheritdoc />
public void Configure(string? name, JsonOptions options)
{
if (name != Constants.JsonOptionsNames.DeliveryApi)
{
return;
}
// Copy all converters from the Delivery API MVC JSON options
Microsoft.AspNetCore.Mvc.JsonOptions backofficeMvcJsonOptions = _mvcJsonOptions.Get(Constants.JsonOptionsNames.DeliveryApi);
foreach (JsonConverter jsonConverter in backofficeMvcJsonOptions.JsonSerializerOptions.Converters)
{
options.SerializerOptions.Converters.Add(jsonConverter);
}
options.SerializerOptions.PropertyNamingPolicy = backofficeMvcJsonOptions.JsonSerializerOptions.PropertyNamingPolicy;
options.SerializerOptions.TypeInfoResolver = backofficeMvcJsonOptions.JsonSerializerOptions.TypeInfoResolver;
options.SerializerOptions.MaxDepth = backofficeMvcJsonOptions.JsonSerializerOptions.MaxDepth;
// Open API specific settings
options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict;
}
}
@@ -1,69 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.Security;
using Umbraco.Cms.Api.Delivery.Controllers.Content;
using Umbraco.Cms.Api.Delivery.Filters;
namespace Umbraco.Cms.Api.Delivery.Configuration;
/// <summary>
/// This configures member authentication for the Delivery API in Swagger. Consult the docs for
/// member authentication within the Delivery API for instructions on how to use this.
/// </summary>
/// <remarks>
/// This class is not used by the core CMS due to the required installation dependencies (local login page among other things).
/// </remarks>
public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private const string AuthSchemeName = "UmbracoMember";
public void Configure(SwaggerGenOptions options)
{
// add security requirements for content API operations
options.DocumentFilter<DeliveryApiSecurityFilter>();
options.OperationFilter<DeliveryApiSecurityFilter>();
}
private sealed class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter, IDocumentFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (CanApply(context) is false)
{
return;
}
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document);
operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
}
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != DeliveryApiConfiguration.ApiName)
{
return;
}
swaggerDoc.AddComponent(
AuthSchemeName,
new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = AuthSchemeName,
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Member Authentication",
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative),
},
},
});
}
}
}
@@ -20,7 +20,7 @@ public abstract class DeliveryApiControllerBase : Controller, IUmbracoFeature
{
protected string DecodePath(string path)
{
// OpenAPI does not allow reserved chars as "in:path" parameters, so clients based on the Swagger JSON will URL
// OpenAPI does not allow reserved chars as "in:path" parameters, so clients based on the OpenAPI specification will URL
// encode the path. Normally, ASP.NET Core handles that encoding with an automatic decoding - apparently just not
// for forward slashes, for whatever reason... so we need to deal with those. Hopefully this will be addressed in
// an upcoming version of ASP.NET Core.
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Accessors;
@@ -53,7 +54,7 @@ public static class UmbracoBuilderExtensions
provider =>
{
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
ApiVersion? apiVersion = httpContext?.RequestedApiVersion;
if (apiVersion is null)
{
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
@@ -67,7 +68,6 @@ public static class UmbracoBuilderExtensions
ServiceLifetime.Scoped);
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
builder.Services.AddSingleton<IRequestSegmmentService, RequestSegmentService>();
builder.Services.AddSingleton<IRequestSegmentService, RequestSegmentService>();
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
@@ -86,19 +86,27 @@ public static class UmbracoBuilderExtensions
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
builder.AddUmbracoApiOpenApiUI();
builder.AddUmbracoOpenApi();
builder.AddUmbracoOpenApiDocument<ConfigureUmbracoDeliveryApiOpenApiOptions>(
DeliveryApiConfiguration.ApiName,
DeliveryApiConfiguration.ApiTitle,
Constants.JsonOptionsNames.DeliveryApi);
builder
.Services
.AddControllers()
.AddJsonOptions(Constants.JsonOptionsNames.DeliveryApi, options =>
{
// all Delivery API specific JSON options go here
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver();
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
.AddJsonOptions(
Constants.JsonOptionsNames.DeliveryApi,
options =>
{
// all Delivery API specific JSON options go here
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver();
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
// Configures the JSON options for the Open API schema generation (based on the Delivery API MVC JSON options)
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryHttpJsonOptions>();
builder.Services.AddAuthentication();
builder.AddUmbracoOpenIddict();
@@ -157,6 +165,7 @@ public static class UmbracoBuilderExtensions
builder.AddNotificationAsyncHandler<ContentCacheRefresherNotification, DeliveryApiDocumentOutputCacheEvictionHandler>();
builder.AddNotificationAsyncHandler<MediaCacheRefresherNotification, DeliveryApiMediaOutputCacheEvictionHandler>();
builder.AddNotificationAsyncHandler<MemberCacheRefresherNotification, DeliveryApiMemberOutputCacheEvictionHandler>();
builder.AddNotificationAsyncHandler<ElementCacheRefresherNotification, DeliveryApiElementOutputCacheEvictionHandler>();
// Register extension point default implementations.
builder.Services.AddSingleton<IDeliveryApiOutputCacheTagProvider, DeliveryApiContentTypeOutputCacheTagProvider>();
@@ -171,4 +180,5 @@ public static class UmbracoBuilderExtensions
return builder;
}
}
@@ -1,127 +0,0 @@
using System.Text.Json.Nodes;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.Controllers.Content;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFilterBase<ContentApiControllerBase>
{
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationContentArticleLink;
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
{
operation.Parameters ??= new List<IOpenApiParameter>();
AddExpand(operation, context);
AddFields(operation, context);
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.AcceptLanguage,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the language to return. Use this when querying language variant content items.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Default", new OpenApiExample { Value = string.Empty } },
{ "English culture", new OpenApiExample { Value = "en-us" } },
},
});
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.AcceptSegment,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the segment to return. Use this when querying segment variant content items.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Default", new OpenApiExample { Value = string.Empty } },
{ "Segment One", new OpenApiExample { Value = "segment-one" } },
},
});
AddApiKey(operation);
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.Preview,
In = ParameterLocation.Header,
Required = false,
Description = "Whether to request draft content.",
Schema = new OpenApiSchema { Type = JsonSchemaType.Boolean },
});
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.StartItem,
In = ParameterLocation.Header,
Required = false,
Description = "URL segment or GUID of a root content item.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
});
}
protected override void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context)
{
switch (parameter.Name)
{
case "fetch":
AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the content items to fetch");
break;
case "filter":
AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched content items");
break;
case "sort":
AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found content items");
break;
case "skip":
parameter.Description = PaginationDescription(true, "content");
break;
case "take":
parameter.Description = PaginationDescription(false, "content");
break;
default:
return;
}
}
private Dictionary<string, IOpenApiExample> FetchQueryParameterExamples() =>
new()
{
{ "Select all", new OpenApiExample { Value = string.Empty } },
{ "Select all ancestors of a node by id", new OpenApiExample { Value = "ancestors:id" } },
{ "Select all ancestors of a node by path", new OpenApiExample { Value = "ancestors:path" } },
{ "Select all children of a node by id", new OpenApiExample { Value = "children:id" } },
{ "Select all children of a node by path", new OpenApiExample { Value = "children:path" } },
{ "Select all descendants of a node by id", new OpenApiExample { Value = "descendants:id" } },
{ "Select all descendants of a node by path", new OpenApiExample { Value = "descendants:path" } },
};
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
new()
{
{ "Default filter", new OpenApiExample { Value = string.Empty } },
{ "Filter by content type (equals)", new OpenApiExample { Value = new JsonArray { "contentType:alias1" } } },
{ "Filter by name (contains)", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } },
{ "Filter by creation date (less than)", new OpenApiExample { Value = new JsonArray { "createDate<2024-01-01" } } },
{ "Filter by update date (greater than or equal)", new OpenApiExample { Value = new JsonArray { "updateDate>:2023-01-01" } } },
};
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
new()
{
{ "Default sort", new OpenApiExample { Value = string.Empty } },
{ "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } },
{ "Sort by level", new OpenApiExample { Value = new JsonArray { "level:asc", "level:desc" } } },
{ "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } },
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } },
{ "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } },
};
}
@@ -1,155 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal abstract class SwaggerDocumentationFilterBase<TBaseController>
: SwaggerFilterBase<TBaseController>, IOperationFilter, IParameterFilter
where TBaseController : Controller
{
protected abstract string DocumentationLink { get; }
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (CanApply(context))
{
ApplyOperation(operation, context);
}
}
public void Apply(IOpenApiParameter parameter, ParameterFilterContext context)
{
if (CanApply(context) && parameter is OpenApiParameter openApiParameter)
{
ApplyParameter(openApiParameter, context);
}
}
protected abstract void ApplyOperation(OpenApiOperation operation, OperationFilterContext context);
protected abstract void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context);
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, IOpenApiExample> examples, string description)
{
parameter.Description = QueryParameterDescription(description);
parameter.Examples = examples;
}
protected void AddExpand(OpenApiOperation operation, OperationFilterContext context)
{
if (IsApiV1(context))
{
AddExpandV1(operation);
}
else
{
AddExpand(operation);
}
}
protected void AddFields(OpenApiOperation operation, OperationFilterContext context)
{
if (IsApiV1(context))
{
// "fields" is not a thing in Delivery API V1
return;
}
AddFields(operation);
}
protected void AddApiKey(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.ApiKey,
In = ParameterLocation.Header,
Required = false,
Description = "API key specified through configuration to authorize access to the API.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
});
}
protected string PaginationDescription(bool skip, string itemType)
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
private string QueryParameterDescription(string description)
=> $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
private static bool IsApiV1(OperationFilterContext context)
=> context.ApiDescription.RelativePath?.Contains("api/v1") is true;
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
private void AddExpandV1(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = "expand",
In = ParameterLocation.Query,
Required = false,
Description =
QueryParameterDescription("Defines the properties that should be expanded in the response"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Expand none", new OpenApiExample { Value = string.Empty } },
{ "Expand all", new OpenApiExample { Value = "all" } },
{ "Expand specific property", new OpenApiExample { Value = "property:alias1" } },
{ "Expand specific properties", new OpenApiExample { Value = "property:alias1,alias2" } },
},
});
}
private void AddExpand(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = "expand",
In = ParameterLocation.Query,
Required = false,
Description =
QueryParameterDescription("Defines the properties that should be expanded in the response"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Expand none", new OpenApiExample { Value = string.Empty } },
{ "Expand all properties", new OpenApiExample { Value = "properties[$all]" } },
{ "Expand specific property", new OpenApiExample { Value = "properties[alias1]" } },
{ "Expand specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
{ "Expand nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
},
});
}
private void AddFields(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = "fields",
In = ParameterLocation.Query,
Required = false,
Description =
QueryParameterDescription(
"Explicitly defines which properties should be included in the response (by default all properties are included)"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Include all properties", new OpenApiExample { Value = "properties[$all]" } },
{ "Include only specific property", new OpenApiExample { Value = "properties[alias1]" } },
{ "Include only specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
{ "Include only specific nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
},
});
}
}
@@ -1,19 +0,0 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal abstract class SwaggerFilterBase<TBaseController>
where TBaseController : Controller
{
protected bool CanApply(OperationFilterContext context)
=> CanApply(context.MethodInfo);
protected bool CanApply(ParameterFilterContext context)
=> CanApply(context.ParameterInfo.Member);
private bool CanApply(MemberInfo member)
=> member.DeclaringType?.Implements<TBaseController>() is true;
}
@@ -51,7 +51,7 @@ public abstract class DeliveryApiVersionAwareJsonConverterBase<T> : JsonConverte
private int? GetApiVersion()
{
HttpContext? httpContext = _httpContextAccessor.HttpContext;
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
ApiVersion? apiVersion = httpContext?.RequestedApiVersion;
return apiVersion?.MajorVersion;
}
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Extensions;
/// <summary>
/// Provides extension methods for <see cref="OpenApiSchemaTransformerContext"/>.
/// </summary>
internal static class OpenApiSchemaTransformerContextExtensions
{
/// <summary>
/// Gets the OpenAPI document from the context, throwing if it is null.
/// </summary>
/// <param name="context">The schema transformer context.</param>
/// <returns>The OpenAPI document.</returns>
/// <exception cref="InvalidOperationException">Thrown when the document is null.</exception>
public static OpenApiDocument GetRequiredDocument(this OpenApiSchemaTransformerContext context)
=> context.Document ?? throw new InvalidOperationException("OpenAPI document context is required for schema registration.");
}
@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
namespace Umbraco.Cms.Api.Delivery.OpenApi;
/// <summary>
/// Extension methods for configuring OpenAPI support for the Delivery API.
/// </summary>
public static class OpenApiServiceCollectionExtensions
{
/// <summary>
/// Adds member authentication support to the Delivery API OpenAPI document.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
/// <returns>The configured <see cref="IServiceCollection"/> instance.</returns>
/// <remarks>
/// This enables the OAuth2 authorization code flow for member authentication in Swagger UI.
/// Consult the Delivery API member authentication documentation for setup instructions.
/// </remarks>
public static IServiceCollection AddDeliveryApiOpenApiMemberAuthentication(this IServiceCollection services)
{
services.PostConfigure<OpenApiOptions>(
DeliveryApiConfiguration.ApiName,
options =>
{
options.AddDocumentTransformer<MemberAuthenticationSecurityRequirementsTransformer>();
options.AddOperationTransformer<MemberAuthenticationSecurityRequirementsTransformer>();
});
return services;
}
}
@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
/// <summary>
/// Transforms the OpenAPI document to include API key security scheme.
/// </summary>
internal class ApiKeyTransformer : IOpenApiDocumentTransformer, IOpenApiOperationTransformer
{
private const string AuthSchemeName = "ApiKeyAuth";
/// <inheritdoc/>
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
var apiKeyScheme = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.ApiKey,
Name = Constants.DeliveryApi.HeaderNames.ApiKey,
In = ParameterLocation.Header,
Description = "API key specified through configuration to authorize access to the API.",
};
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
document.Components.SecuritySchemes[AuthSchemeName] = apiKeyScheme;
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, document);
document.Security ??= new List<OpenApiSecurityRequirement>();
document.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document);
operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
return Task.CompletedTask;
}
}
@@ -0,0 +1,146 @@
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.Controllers.Content;
using Umbraco.Cms.Core;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
/// <summary>
/// Transforms OpenAPI operations for the Content API, adding relevant parameters and documentation.
/// </summary>
internal sealed class ContentApiTransformer : DeliveryApiTransformerBase
{
/// <inheritdoc/>
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationContentArticleLink;
/// <inheritdoc/>
protected override bool ShouldApply(OpenApiOperationTransformerContext context) =>
context.Description.ActionDescriptor is ControllerActionDescriptor description
&& description.ControllerTypeInfo.Implements<ContentApiControllerBase>();
/// <inheritdoc/>
protected override Task ApplyAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.AcceptLanguage,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the language to return. Use this when querying language variant content items.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Default", new OpenApiExample { Value = string.Empty } },
{ "English culture", new OpenApiExample { Value = "en-us" } },
},
});
operation.Parameters.Add(
new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.AcceptSegment,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the segment to return. Use this when querying segment variant content items.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Default", new OpenApiExample { Value = string.Empty } },
{ "Segment One", new OpenApiExample { Value = "segment-one" } },
},
});
operation.Parameters.Add(
new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.Preview,
In = ParameterLocation.Header,
Required = false,
Description = "Whether to request draft content.",
Schema = new OpenApiSchema { Type = JsonSchemaType.Boolean },
});
operation.Parameters.Add(
new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.StartItem,
In = ParameterLocation.Header,
Required = false,
Description = "URL segment or GUID of a root content item.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
});
foreach (OpenApiParameter parameter in operation.Parameters?.OfType<OpenApiParameter>() ?? [])
{
ApplyParameter(parameter);
}
return Task.CompletedTask;
}
private void ApplyParameter(OpenApiParameter parameter)
{
switch (parameter.Name)
{
case "fetch":
AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the content items to fetch");
break;
case "filter":
AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched content items");
break;
case "sort":
AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found content items");
break;
case "skip":
parameter.Description = PaginationDescription(true, "content");
break;
case "take":
parameter.Description = PaginationDescription(false, "content");
break;
default:
return;
}
}
private Dictionary<string, IOpenApiExample> FetchQueryParameterExamples() =>
new()
{
{ "Select all", new OpenApiExample { Value = "" } },
{ "Select all ancestors of a node by id", new OpenApiExample { Value = "ancestors:id" } },
{ "Select all ancestors of a node by path", new OpenApiExample { Value = "ancestors:path" } },
{ "Select all children of a node by id", new OpenApiExample { Value = "children:id" } },
{ "Select all children of a node by path", new OpenApiExample { Value = "children:path" } },
{ "Select all descendants of a node by id", new OpenApiExample { Value = "descendants:id" } },
{ "Select all descendants of a node by path", new OpenApiExample { Value = "descendants:path" } },
};
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
new()
{
{ "Default filter", new OpenApiExample { Value = new JsonArray("") } },
{ "Filter by content type (equals)", new OpenApiExample { Value = new JsonArray("contentType:alias1") } },
{ "Filter by name (contains)", new OpenApiExample { Value = new JsonArray("name:nodeName") } },
{ "Filter by creation date (less than)", new OpenApiExample { Value = new JsonArray("createDate<2024-01-01") } },
{ "Filter by update date (greater than or equal)", new OpenApiExample { Value = new JsonArray("updateDate>:2023-01-01") } },
};
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
new()
{
{ "Default sort", new OpenApiExample { Value = new JsonArray("") } },
{ "Sort by create date", new OpenApiExample { Value = new JsonArray("createDate:asc", "createDate:desc") } },
{ "Sort by level", new OpenApiExample { Value = new JsonArray("level:asc", "level:desc") } },
{ "Sort by name", new OpenApiExample { Value = new JsonArray("name:asc", "name:desc") } },
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray("sortOrder:asc", "sortOrder:desc") } },
{ "Sort by update date", new OpenApiExample { Value = new JsonArray("updateDate:asc", "updateDate:desc") } },
};
}
@@ -0,0 +1,737 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Schema;
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Delivery.OpenApi.Extensions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
/// <summary>
/// Transforms the OpenAPI document to add schemas for the instance's document types.
/// </summary>
/// <remarks>
/// <para>
/// This transformer implements both <see cref="IOpenApiSchemaTransformer"/> and <see cref="IOpenApiDocumentTransformer"/>
/// to handle schema generation in two phases:
/// </para>
/// <para>
/// <b>Phase 1 - Schema Transformation:</b> When the schema transformer encounters types like
/// <see cref="IApiContentResponse"/> or <see cref="IApiMediaWithCrops"/>, it generates content-type-specific
/// schemas (e.g., "ArticleContentResponseModel") and registers them as components in the OpenAPI document.
/// </para>
/// <para>
/// <b>Circular Reference Handling:</b> Content type schemas can reference each other (e.g., a "Page"
/// might have a property of type "Article", which might reference "Page" again). To prevent infinite recursion
/// during schema generation, we use a placeholder pattern:
/// <list type="bullet">
/// <item>When generating a schema, we track its ID in <c>_handledSchemas</c></item>
/// <item>If we encounter the same schema ID again (circular reference), we return a temporary placeholder
/// schema with metadata marking it for later replacement</item>
/// <item>The placeholder contains a <c>x-recursive-ref</c> metadata key with the target schema ID</item>
/// </list>
/// </para>
/// <para>
/// <b>Phase 2 - Document Transformation:</b> After all schemas are generated, the document transformer
/// resolves inline schemas into proper <c>$ref</c> references. This handles two cases:
/// <list type="bullet">
/// <item>Circular reference placeholders (marked with <c>x-recursive-ref</c>) created during Phase 1</item>
/// <item>Componentized schemas (marked with <c>x-schema-id</c>) that the framework did not automatically
/// resolve to <c>$ref</c> — this can happen for schemas reached through properties or composition
/// rather than as direct API response types</item>
/// </list>
/// This is done by <see cref="ResolveSchemaReferences(OpenApiDocument, IOpenApiSchema)"/> which recursively walks
/// through all schemas and substitutes matching entries with <see cref="OpenApiSchemaReference"/> instances.
/// </para>
/// </remarks>
public sealed class ContentTypeSchemaTransformer : IOpenApiSchemaTransformer, IOpenApiDocumentTransformer
{
// Metadata keys
private const string RecursiveRefMetadataKey = "x-recursive-ref";
private const string SchemaIdMetadataKey = "x-schema-id";
// Schema ID suffixes
private const string ResponseModelSuffix = "ResponseModel";
private const string ModelSuffix = "Model";
private const string ContentSuffix = "Content";
private const string ElementSuffix = "Element";
private const string MediaSuffix = "Media";
private const string MediaWithCropsSuffix = "MediaWithCrops";
private const string PropertiesModelSuffix = "PropertiesModel";
private readonly IContentTypeSchemaService _contentTypeSchemaService;
private readonly IOptionsMonitor<DeliveryApiSettings> _deliveryApiSettings;
private readonly ILogger<ContentTypeSchemaTransformer> _logger;
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
/// <summary>
/// Tracks schema IDs that have been or are being generated to detect circular references.
/// When a schema ID is encountered a second time, a placeholder is returned instead of recursing infinitely.
/// </summary>
private readonly HashSet<string> _handledSchemas = [];
private readonly JsonSerializerOptions _serializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ContentTypeSchemaTransformer"/> class.
/// </summary>
/// <param name="contentTypeSchemaService">The content type info service.</param>
/// <param name="jsonOptionsMonitor">The JSON options monitor.</param>
/// <param name="deliveryApiSettings">The Delivery API settings, used to honour the allow/deny content type list.</param>
/// <param name="logger">The logger.</param>
public ContentTypeSchemaTransformer(
IContentTypeSchemaService contentTypeSchemaService,
IOptionsMonitor<JsonOptions> jsonOptionsMonitor,
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettings,
ILogger<ContentTypeSchemaTransformer> logger)
{
_contentTypeSchemaService = contentTypeSchemaService;
_deliveryApiSettings = deliveryApiSettings;
_logger = logger;
_serializerOptions = jsonOptionsMonitor
.Get(Constants.JsonOptionsNames.DeliveryApi)
.SerializerOptions;
_jsonTypeInfoResolver = _serializerOptions.TypeInfoResolver
?? throw new InvalidOperationException("The JSON serializer options must have a TypeInfoResolver configured.");
}
private IReadOnlyCollection<ContentTypeSchemaInfo> DocumentTypes
=> field ??= FilterAllowedDocumentTypes(_contentTypeSchemaService.GetDocumentTypes());
private IReadOnlyCollection<ContentTypeSchemaInfo> MediaTypes
=> field ??= _contentTypeSchemaService.GetMediaTypes();
/// <inheritdoc />
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
if (document.Components?.Schemas is not { Count: > 0 })
{
return Task.CompletedTask;
}
foreach ((var schemaId, IOpenApiSchema componentsSchema) in document.Components.Schemas)
{
ResolveSchemaReferences(document, componentsSchema);
FixAutoBuiltDiscriminatorMapping(document, schemaId, componentsSchema);
}
return Task.CompletedTask;
}
/// <summary>
/// Repairs broken discriminator mapping refs auto-built by the framework for polymorphic types.
/// </summary>
/// <remarks>
/// The framework prefixes each ref with the base schema id, but the derived schemas are
/// registered without that prefix. Stripping the prefix recovers the correct ref.
/// </remarks>
private static void FixAutoBuiltDiscriminatorMapping(OpenApiDocument document, string parentSchemaId, IOpenApiSchema schema)
{
if (schema is not OpenApiSchema concrete
|| concrete.Discriminator?.Mapping is not { } mapping
|| document.Components?.Schemas is not { } schemas)
{
return;
}
foreach ((var key, OpenApiSchemaReference currentRef) in mapping.ToList())
{
var targetId = currentRef.Reference.Id;
if (string.IsNullOrEmpty(targetId) || schemas.ContainsKey(targetId))
{
continue;
}
if (targetId.StartsWith(parentSchemaId, StringComparison.Ordinal) is false)
{
continue;
}
var stripped = targetId[parentSchemaId.Length..];
if (schemas.ContainsKey(stripped))
{
mapping[key] = new OpenApiSchemaReference(stripped, document);
}
}
}
/// <inheritdoc />
public async Task TransformAsync(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
switch (context.JsonTypeInfo.Type)
{
case var type when type == typeof(IApiContentResponse):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Content,
DocumentTypes.Where(c => !c.IsElement),
async (contentType, derivedTypeSchemas) =>
{
var schemaIdPrefix = $"{contentType.SchemaId}{ContentSuffix}";
return await CreateContentTypeResponseSchema(
schemaIdPrefix,
derivedTypeSchemas,
context);
},
cancellationToken);
await CreateSchema(GetJsonTypeInfo(typeof(IApiContent)), context, cancellationToken);
return;
case var type when type == typeof(IApiContent):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Content,
DocumentTypes.Where(c => !c.IsElement),
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{ContentSuffix}{ModelSuffix}";
return await CreateContentTypeSchema(
schemaId,
PublishedItemType.Content,
contentType,
derivedTypeSchemas,
context,
cancellationToken);
},
cancellationToken);
await CreateSchema(GetJsonTypeInfo(typeof(IApiElement)), context, cancellationToken);
return;
case var type when type == typeof(IApiElement):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Content,
DocumentTypes.Where(c => c.IsElement),
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{ElementSuffix}{ModelSuffix}";
return await CreateContentTypeSchema(
schemaId,
PublishedItemType.Content,
contentType,
derivedTypeSchemas,
context,
cancellationToken);
},
cancellationToken);
return;
case var type when type == typeof(IApiMediaWithCropsResponse):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Media,
MediaTypes,
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{MediaWithCropsSuffix}";
return await CreateContentTypeResponseSchema(
schemaId,
derivedTypeSchemas,
context);
},
cancellationToken);
await CreateSchema(GetJsonTypeInfo(typeof(IApiMediaWithCrops)), context, cancellationToken);
return;
case var type when type == typeof(IApiMediaWithCrops):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Media,
MediaTypes,
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{MediaWithCropsSuffix}{ModelSuffix}";
return await CreateContentTypeSchema(
schemaId,
PublishedItemType.Media,
contentType,
derivedTypeSchemas,
context,
cancellationToken);
},
cancellationToken);
return;
default:
// HACK: Some types with circular references (e.g. ApiBlockGridItem) get left
// inlined by the framework, breaking $ref resolution. Register them explicitly.
if (GetSchemaId(context.JsonTypeInfo) is not { } schemaId || !_handledSchemas.Add(schemaId))
{
return;
}
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, schema);
return;
}
}
private async Task ApplyPolymorphicContentType(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
PublishedItemType itemType,
IEnumerable<ContentTypeSchemaInfo> contentTypes,
Func<ContentTypeSchemaInfo, List<IOpenApiSchema>, Task<OpenApiSchema>> contentTypeSchemaFactory,
CancellationToken cancellationToken)
{
List<IOpenApiSchema> derivedTypeSchemas = await ResolveDerivedTypeSchemas(
schema,
context,
cancellationToken);
OpenApiDocument document = context.GetRequiredDocument();
var typePropertyName = GetTypePropertyName(itemType);
schema.Discriminator = new OpenApiDiscriminator
{
PropertyName = typePropertyName,
Mapping = new Dictionary<string, OpenApiSchemaReference>(),
};
schema.OneOf ??= new List<IOpenApiSchema>();
foreach (ContentTypeSchemaInfo contentType in contentTypes)
{
OpenApiSchema contentTypeSchema = await contentTypeSchemaFactory(contentType, derivedTypeSchemas);
var schemaId = (string)contentTypeSchema.Metadata![SchemaIdMetadataKey];
schema.Discriminator.Mapping[contentType.Alias] = new OpenApiSchemaReference(schemaId, document);
schema.OneOf.Add(contentTypeSchema);
}
// Remove all schema properties that are now handled by the derived types
schema.AnyOf = null;
schema.Properties = null;
schema.Required = new HashSet<string> { typePropertyName };
}
/// <summary>
/// Creates and adds a schema to the OpenAPI document if it does not already exist.
/// </summary>
/// <remarks>A placeholder schema is added first to avoid recursion issues when generating schemas that reference themselves.</remarks>
private async Task<IOpenApiSchema> CreateSchema(
JsonTypeInfo jsonTypeInfo,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
if (jsonTypeInfo.Type.IsArray || jsonTypeInfo.Kind == JsonTypeInfoKind.Enumerable)
{
Type elementType = jsonTypeInfo.ElementType ?? jsonTypeInfo.Type.GetElementType() ?? typeof(object);
JsonTypeInfo elementJsonTypeInfo = GetJsonTypeInfo(elementType);
IOpenApiSchema itemSchema = await CreateSchema(elementJsonTypeInfo, context, cancellationToken);
return new OpenApiSchema
{
Type = JsonSchemaType.Array,
Items = itemSchema,
};
}
var schemaId = GetSchemaId(jsonTypeInfo);
// Types that produce 'true' in JSON Schema (unconstrained: JsonNode, object, custom-converter types) should be inline {} rather than named components.
if (jsonTypeInfo.Kind == JsonTypeInfoKind.None && jsonTypeInfo.GetJsonSchemaAsNode().GetValueKind() == JsonValueKind.True)
{
return new OpenApiSchema();
}
// If this is one of the types we handle, and we already started generating it, return a placeholder
// to avoid circular reference issues.
// In the document transformer, these placeholders will be replaced with the actual schemas.
if (schemaId is not null && !_handledSchemas.Add(schemaId))
{
return GetPlaceholderSchema(schemaId);
}
OpenApiSchema schema;
try
{
schema = await context.GetOrCreateSchemaAsync(
jsonTypeInfo.Type,
cancellationToken: cancellationToken);
}
catch (Exception ex)
{
// Log the error but continue with a fallback schema to avoid failing the entire document generation.
// The fallback schema includes a description indicating the failure, making it visible to API consumers.
_logger.LogError(ex, "Failed to create OpenAPI schema for type {TypeName}", jsonTypeInfo.Type.FullName);
schema = new OpenApiSchema
{
Description = $"[Schema generation failed for type '{jsonTypeInfo.Type.FullName}'. See server logs for details.]",
};
}
if (schemaId is null)
{
return schema;
}
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, schema);
return new OpenApiSchemaReference(schemaId, document);
}
/// <summary>
/// Allows null at a property reference site without mutating any shared component schema.
/// Inline schemas have <c>null</c> OR-ed into their <c>type</c> flags; schema references and
/// recursive-ref placeholders are wrapped in a <c>oneOf</c> with an explicit null branch so the
/// shared component is left unchanged.
/// </summary>
private static IOpenApiSchema AsNullable(IOpenApiSchema schema)
{
if (schema is OpenApiSchema inline
&& inline.Metadata?.ContainsKey(RecursiveRefMetadataKey) is not true)
{
inline.Type |= JsonSchemaType.Null;
return inline;
}
return new OpenApiSchema
{
OneOf =
[
schema,
new OpenApiSchema { Type = JsonSchemaType.Null },
],
};
}
private static Task<OpenApiSchema> CreateContentTypeResponseSchema(
string schemaIdPrefix,
List<IOpenApiSchema> derivedTypeSchemas,
OpenApiSchemaTransformerContext context)
{
var schemaId = $"{schemaIdPrefix}{ResponseModelSuffix}";
OpenApiDocument document = context.GetRequiredDocument();
var schema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
AllOf = [..derivedTypeSchemas, new OpenApiSchemaReference($"{schemaIdPrefix}{ModelSuffix}", document)],
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId },
};
document.AddComponent(schemaId, schema);
return Task.FromResult(schema);
}
private async Task<OpenApiSchema> CreateContentTypeSchema(
string schemaId,
PublishedItemType itemType,
ContentTypeSchemaInfo contentType,
List<IOpenApiSchema> derivedTypeSchemas,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
var typePropertyName = GetTypePropertyName(itemType);
var schema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
Properties = new Dictionary<string, IOpenApiSchema>
{
[typePropertyName] = new OpenApiSchema { Const = contentType.Alias },
["properties"] = await CreatePropertiesSchema(contentType, itemType, context, cancellationToken),
},
Required = new HashSet<string> { typePropertyName },
AllOf = derivedTypeSchemas.Count > 0 ? derivedTypeSchemas : null,
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId, },
};
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, schema);
return schema;
}
private async Task<OpenApiSchemaReference> CreatePropertiesSchema(
ContentTypeSchemaInfo contentType,
PublishedItemType itemType,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
var schemaId = GetPropertiesModelSchemaId(contentType, itemType);
var propertiesSchema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
AllOf =
[
..contentType.CompositionSchemaIds.Select(compositionSchemaId
=> GetPlaceholderSchema(GetCompositionPropertiesModelSchemaId(compositionSchemaId, itemType)))
],
Properties = await CreateContentTypeProperties(contentType, context, cancellationToken),
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId },
};
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, propertiesSchema);
return new OpenApiSchemaReference(schemaId, document);
}
private static string GetPropertiesModelSchemaId(ContentTypeSchemaInfo contentType, PublishedItemType itemType) =>
$"{contentType.SchemaId}{GetItemTypeSuffix(itemType, contentType.IsElement)}{PropertiesModelSuffix}";
private string GetCompositionPropertiesModelSchemaId(string compositionSchemaId, PublishedItemType itemType)
{
// Look up the composition's own IsElement so its reference points at the right
// generated schema (element-type compositions live under the Element suffix).
IReadOnlyCollection<ContentTypeSchemaInfo> candidates = itemType == PublishedItemType.Media ? MediaTypes : DocumentTypes;
ContentTypeSchemaInfo? composition = candidates.FirstOrDefault(c => c.SchemaId == compositionSchemaId);
var suffix = GetItemTypeSuffix(itemType, composition?.IsElement ?? false);
return $"{compositionSchemaId}{suffix}{PropertiesModelSuffix}";
}
private static string GetItemTypeSuffix(PublishedItemType itemType, bool isElement) =>
itemType switch
{
PublishedItemType.Media => MediaSuffix,
PublishedItemType.Content => isElement ? ElementSuffix : ContentSuffix,
_ => throw new NotSupportedException($"Unsupported PublishedItemType: {itemType}"),
};
private async Task<Dictionary<string, IOpenApiSchema>> CreateContentTypeProperties(
ContentTypeSchemaInfo contentType,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
var properties = new Dictionary<string, IOpenApiSchema>();
foreach (ContentTypePropertySchemaInfo propertyInfo in contentType.Properties.Where(p => !p.Inherited))
{
IOpenApiSchema schema = await CreateSchema(
GetJsonTypeInfo(propertyInfo.DeliveryApiClrType),
context,
cancellationToken);
// Properties may be null (e.g. property added after content was last published).
// Nullability is applied at the reference site, never on a shared component schema.
properties[propertyInfo.Alias] = AsNullable(schema);
}
return properties;
}
private JsonTypeInfo GetJsonTypeInfo(Type type)
{
JsonTypeInfo? jsonTypeInfo = _jsonTypeInfoResolver.GetTypeInfo(type, _serializerOptions);
return jsonTypeInfo ?? throw new InvalidOperationException("Could not get JsonTypeInfo for type " + type.FullName);
}
private string GetTypePropertyName(PublishedItemType itemType)
{
var propertyName = itemType switch
{
PublishedItemType.Content => nameof(IApiElement.ContentType),
PublishedItemType.Media => nameof(IApiMedia.MediaType),
_ => throw new NotSupportedException($"Unsupported PublishedItemType: {itemType}"),
};
return _serializerOptions.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName;
}
private static string? GetSchemaId(JsonTypeInfo type)
=> UmbracoSchemaIdGenerator.CreateSchemaReferenceId(type);
/// <summary>
/// Creates a temporary placeholder schema to break circular reference chains during schema generation.
/// </summary>
/// <remarks>
/// The placeholder contains metadata with the target schema ID. During the document transformation phase,
/// <see cref="ResolveSchemaReferences(OpenApiDocument, IOpenApiSchema)"/> will replace these placeholders with actual schema references.
/// </remarks>
/// <param name="schemaId">The ID of the schema this placeholder represents.</param>
/// <returns>A placeholder schema with metadata indicating the target schema reference.</returns>
private static OpenApiSchema GetPlaceholderSchema(string schemaId)
=> new()
{
Metadata = new Dictionary<string, object>
{
[RecursiveRefMetadataKey] = schemaId,
},
};
/// <summary>
/// Recursively resolves inline schemas into proper <c>$ref</c> references.
/// </summary>
/// <remarks>
/// This method is called during the document transformation phase (after all schemas have been generated).
/// It walks through all schema properties, allOf, oneOf, and anyOf collections, resolving two types of
/// inline schemas:
/// <list type="bullet">
/// <item>Circular reference placeholders created by <see cref="GetPlaceholderSchema"/> (marked with <c>x-recursive-ref</c>)</item>
/// <item>Componentized schemas that should be references (marked with <c>x-schema-id</c>)</item>
/// </list>
/// Each match is replaced with an <see cref="OpenApiSchemaReference"/> pointing to the actual schema in the document's components.
/// </remarks>
/// <param name="document">The OpenAPI document containing the registered schema components.</param>
/// <param name="schema">The schema to process (will be modified in place).</param>
private static void ResolveSchemaReferences(OpenApiDocument document, IOpenApiSchema schema)
{
// Replace in allOf, oneOf, anyOf
ResolveSchemaReferences(document, schema.AllOf);
ResolveSchemaReferences(document, schema.OneOf);
ResolveSchemaReferences(document, schema.AnyOf);
// Process array items
if (schema is OpenApiSchema { Items: OpenApiSchema itemsSchema } parentSchema)
{
parentSchema.Items = GetActualSchemaOrReference(document, itemsSchema, out var itemsReplaced);
if (!itemsReplaced)
{
ResolveSchemaReferences(document, itemsSchema);
}
}
if (schema.Properties is not { Count: > 0 })
{
return;
}
// Process properties
foreach (var propertyKey in schema.Properties.Keys)
{
IOpenApiSchema propertySchema = schema.Properties[propertyKey];
if (propertySchema is not OpenApiSchema innerSchema)
{
continue;
}
schema.Properties[propertyKey] = GetActualSchemaOrReference(document, innerSchema, out var replaced);
if (replaced)
{
continue;
}
// Recursive call to handle the property schema
ResolveSchemaReferences(document, innerSchema);
}
}
private static void ResolveSchemaReferences(OpenApiDocument document, IList<IOpenApiSchema>? schemas)
{
if (schemas is null || schemas.Count == 0)
{
return;
}
for (var i = 0; i < schemas.Count; i++)
{
IOpenApiSchema allOfSchema = schemas[i];
schemas[i] = GetActualSchemaOrReference(document, allOfSchema, out var replaced);
if (!replaced)
{
ResolveSchemaReferences(document, schemas[i]);
}
}
}
[return: NotNullIfNotNull(nameof(schema))]
private static IOpenApiSchema? GetActualSchemaOrReference(
OpenApiDocument document,
IOpenApiSchema? schema,
out bool replaced)
{
if (schema is not OpenApiSchema openApiSchema)
{
replaced = false;
return schema;
}
// Check if this is a placeholder schema (circular reference)
if (openApiSchema.Metadata?.TryGetValue(RecursiveRefMetadataKey, out var recursiveRefIdObj) == true
&& recursiveRefIdObj is string recursiveRefId)
{
replaced = true;
return new OpenApiSchemaReference(recursiveRefId, document);
}
// Check if this is a componentized schema that should be a $ref
// Only resolve if the component actually exists — the framework also sets x-schema-id on
// schemas that may not end up as components.
if (openApiSchema.Metadata?.TryGetValue(SchemaIdMetadataKey, out var schemaIdObj) == true
&& schemaIdObj is string schemaId
&& !string.IsNullOrEmpty(schemaId)
&& document.Components?.Schemas?.ContainsKey(schemaId) == true)
{
replaced = true;
return new OpenApiSchemaReference(schemaId, document);
}
replaced = false;
return schema;
}
private IReadOnlyCollection<ContentTypeSchemaInfo> FilterAllowedDocumentTypes(IReadOnlyCollection<ContentTypeSchemaInfo> documentTypes)
{
DeliveryApiSettings settings = _deliveryApiSettings.CurrentValue;
return documentTypes
.Where(c => settings.IsAllowedContentType(c.Alias))
.ToList();
}
/// <summary>
/// Returns the schemas to use as the <c>allOf</c> bases for each typed content type
/// schema in a polymorphic union. Prefers concrete derived types declared on the
/// interface via <c>[JsonDerivedType]</c>; when none are advertised, falls back to a
/// schema built from the interface's own properties.
/// </summary>
/// <remarks>
/// The fallback exists for media interfaces, whose concrete classes are internal in
/// Umbraco.Infrastructure and therefore cannot be referenced via <c>[JsonDerivedType]</c>
/// from Umbraco.Core.
/// </remarks>
private async Task<List<IOpenApiSchema>> ResolveDerivedTypeSchemas(
OpenApiSchema interfaceSchema,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
List<IOpenApiSchema> derivedTypeSchemas = [];
foreach (JsonDerivedType derivedType in context.JsonTypeInfo.PolymorphismOptions?.DerivedTypes ?? [])
{
IOpenApiSchema derivedTypeSchema = await CreateSchema(
GetJsonTypeInfo(derivedType.DerivedType),
context,
cancellationToken);
derivedTypeSchemas.Add(derivedTypeSchema);
}
if (derivedTypeSchemas.Count == 0)
{
derivedTypeSchemas.Add(CreateBaseSchemaFromInterface(interfaceSchema, context));
}
return derivedTypeSchemas;
}
private static IOpenApiSchema CreateBaseSchemaFromInterface(
OpenApiSchema interfaceSchema,
OpenApiSchemaTransformerContext context)
{
// Append a "Base" marker so this schema stays distinct from the polymorphic union
// schema for the same interface (e.g. IApiMediaWithCropsResponseBaseModel vs.
// IApiMediaWithCropsResponseModel).
var baseSchemaId = $"{context.JsonTypeInfo.Type.Name}Base{ModelSuffix}";
OpenApiDocument document = context.GetRequiredDocument();
var baseSchema = new OpenApiSchema
{
Type = interfaceSchema.Type,
Properties = interfaceSchema.Properties,
Required = interfaceSchema.Required,
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = baseSchemaId },
};
document.AddComponent(baseSchemaId, baseSchema);
return new OpenApiSchemaReference(baseSchemaId, document);
}
}
@@ -0,0 +1,104 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
internal abstract class DeliveryApiTransformerBase : IOpenApiOperationTransformer
{
/// <summary>
/// Gets the link to the relevant documentation section.
/// </summary>
protected abstract string DocumentationLink { get; }
/// <inheritdoc/>
public async Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
if (!ShouldApply(context))
{
return;
}
AddExpand(operation);
AddFields(operation);
await ApplyAsync(operation, context, cancellationToken);
}
/// <summary>
/// Determines whether the transformer should be applied for the given context.
/// </summary>
/// <param name="context">The operation transformer context.</param>
/// <returns>>True if the transformer should be applied; otherwise, false.</returns>
protected abstract bool ShouldApply(OpenApiOperationTransformerContext context);
/// <summary>
/// Applies the specific transformations to the OpenAPI operation.
/// </summary>
/// <param name="operation">The <see cref="OpenApiOperation"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiOperationTransformerContext"/> associated with the <see paramref="operation"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
protected abstract Task ApplyAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken);
private void AddExpand(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = "expand",
In = ParameterLocation.Query,
Required = false,
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Expand none", new OpenApiExample { Value = "" } },
{ "Expand all properties", new OpenApiExample { Value = "properties[$all]" } },
{ "Expand specific property", new OpenApiExample { Value = "properties[alias1]" } },
{ "Expand specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
{ "Expand nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
},
});
}
private void AddFields(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = "fields",
In = ParameterLocation.Query,
Required = false,
Description =
QueryParameterDescription("Explicitly defines which properties should be included in the response (by default all properties are included)"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Include all properties", new OpenApiExample { Value = "properties[$all]" } },
{ "Include only specific property", new OpenApiExample { Value = "properties[alias1]" } },
{ "Include only specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
{ "Include only specific nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
},
});
}
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, IOpenApiExample> examples, string description)
{
parameter.Description = QueryParameterDescription(description);
parameter.Examples = examples;
}
protected string PaginationDescription(bool skip, string itemType)
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
private string QueryParameterDescription(string description)
=> $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
}
@@ -1,27 +1,41 @@
using System.Text.Json.Nodes;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.Controllers.Media;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Filters;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilterBase<MediaApiControllerBase>
/// <summary>
/// Transforms OpenAPI operations for the Media API, adding relevant parameters and documentation.
/// </summary>
internal sealed class MediaApiTransformer : DeliveryApiTransformerBase
{
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationMediaArticleLink;
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
/// <inheritdoc/>
protected override bool ShouldApply(OpenApiOperationTransformerContext context) =>
context.Description.ActionDescriptor is ControllerActionDescriptor description
&& description.ControllerTypeInfo.Implements<MediaApiControllerBase>();
/// <inheritdoc/>
protected override Task ApplyAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
operation.Parameters ??= new List<IOpenApiParameter>();
foreach (OpenApiParameter parameter in operation.Parameters?.OfType<OpenApiParameter>() ?? [])
{
ApplyParameter(parameter);
}
AddExpand(operation, context);
AddFields(operation, context);
AddApiKey(operation);
return Task.CompletedTask;
}
protected override void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context)
private void ApplyParameter(OpenApiParameter parameter)
{
switch (parameter.Name)
{
@@ -56,18 +70,18 @@ internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilt
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
new()
{
{ "Default filter", new OpenApiExample { Value = string.Empty } },
{ "Filter by media type", new OpenApiExample { Value = new JsonArray { "mediaType:alias1" } } },
{ "Filter by name", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } },
{ "Default filter", new OpenApiExample { Value = new JsonArray(string.Empty) } },
{ "Filter by media type", new OpenApiExample { Value = new JsonArray("mediaType:alias1") } },
{ "Filter by name", new OpenApiExample { Value = new JsonArray("name:nodeName") } },
};
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
new()
{
{ "Default sort", new OpenApiExample { Value = string.Empty } },
{ "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } },
{ "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } },
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } },
{ "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } },
{ "Default sort", new OpenApiExample { Value = new JsonArray(string.Empty) } },
{ "Sort by create date", new OpenApiExample { Value = new JsonArray("createDate:asc", "createDate:desc") } },
{ "Sort by name", new OpenApiExample { Value = new JsonArray("name:asc", "name:desc") } },
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray("sortOrder:asc", "sortOrder:desc") } },
{ "Sort by update date", new OpenApiExample { Value = new JsonArray("updateDate:asc", "updateDate:desc") } },
};
}
@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Umbraco.Cms.Api.Common.Security;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
/// <summary>
/// Transformer that adds member authentication security requirements to OpenAPI documents.
/// </summary>
internal class MemberAuthenticationSecurityRequirementsTransformer : IOpenApiOperationTransformer, IOpenApiDocumentTransformer
{
private const string AuthSchemeName = "UmbracoMember";
/// <inheritdoc />
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
var securityScheme = new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = AuthSchemeName,
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Member Authentication",
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative),
},
},
};
document.AddComponent(AuthSchemeName, securityScheme);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document);
operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
return Task.CompletedTask;
}
}
@@ -30,7 +30,7 @@ internal sealed class DeliveryApiItemsEndpointsMatcherPolicy : MatcherPolicy, IE
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
{
var hasIdQueryParameter = httpContext.Request.Query.ContainsKey("id");
ApiVersion? requestedApiVersion = httpContext.GetRequestedApiVersion();
ApiVersion? requestedApiVersion = httpContext.RequestedApiVersion;
for (var i = 0; i < candidates.Count; i++)
{
CandidateState candidate = candidates[i];
@@ -18,6 +18,7 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
private readonly IRedirectUrlService _redirectUrlService;
private readonly IApiPublishedContentCache _apiPublishedContentCache;
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
private readonly IDocumentUrlService _documentUrlService;
private readonly GlobalSettings _globalSettings;
public RequestRedirectService(
@@ -28,13 +29,15 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
IRedirectUrlService redirectUrlService,
IApiPublishedContentCache apiPublishedContentCache,
IApiContentRouteBuilder apiContentRouteBuilder,
IOptions<GlobalSettings> globalSettings)
IOptions<GlobalSettings> globalSettings,
IDocumentUrlService documentUrlService)
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor)
{
_requestCultureService = requestCultureService;
_redirectUrlService = redirectUrlService;
_apiPublishedContentCache = apiPublishedContentCache;
_apiContentRouteBuilder = apiContentRouteBuilder;
_documentUrlService = documentUrlService;
_globalSettings = globalSettings.Value;
}
@@ -43,16 +46,19 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
requestedPath = requestedPath.EnsureStartsWith("/");
IPublishedContent? startItem = GetStartItem();
var culture = _requestCultureService.GetRequestedCulture();
// must append the root content url segment if it is not hidden by config, because
// the URL tracking is based on the actual URL, including the root content url segment
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem is not null)
{
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
var startItemUrlSegment = _documentUrlService.GetUrlSegment(startItem.Key, culture ?? string.Empty, isDraft: false);
if (startItemUrlSegment is not null)
{
requestedPath = $"{startItemUrlSegment.EnsureStartsWith("/")}{requestedPath}";
}
}
var culture = _requestCultureService.GetRequestedCulture();
// important: redirect URLs are always tracked without trailing slashes
requestedPath = requestedPath.TrimEnd("/");
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
@@ -3,7 +3,7 @@ using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegmentService, IRequestSegmmentService
internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegmentService
{
public RequestSegmentService(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
@@ -3,6 +3,7 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
@@ -14,6 +15,7 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
private readonly IRequestPreviewService _requestPreviewService;
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
private readonly IPublishedContentCache _publishedContentCache;
private readonly IDocumentUrlService _documentUrlService;
// this provider lifetime is Scope, so we can cache this as a field
private IPublishedContent? _requestedStartContent;
@@ -23,14 +25,15 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
IVariationContextAccessor variationContextAccessor,
IRequestPreviewService requestPreviewService,
IDocumentNavigationQueryService documentNavigationQueryService,
IPublishedContentCache publishedContentCache)
IPublishedContentCache publishedContentCache,
IDocumentUrlService documentUrlService)
: base(httpContextAccessor)
{
_variationContextAccessor = variationContextAccessor;
_requestPreviewService = requestPreviewService;
_documentNavigationQueryService = documentNavigationQueryService;
_publishedContentCache = publishedContentCache;
_documentUrlService = documentUrlService;
}
/// <inheritdoc/>
@@ -47,14 +50,16 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
return null;
}
var isPreview = _requestPreviewService.IsPreview();
_documentNavigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys);
IEnumerable<IPublishedContent> rootContent = rootKeys
.Select(rootKey => _publishedContentCache.GetById(_requestPreviewService.IsPreview(), rootKey))
.Select(rootKey => _publishedContentCache.GetById(isPreview, rootKey))
.WhereNotNull();
var culture = _variationContextAccessor.VariationContext?.Culture ?? string.Empty;
_requestedStartContent = Guid.TryParse(headerValue, out Guid key)
? rootContent.FirstOrDefault(c => c.Key == key)
: rootContent.FirstOrDefault(c => c.UrlSegment(_variationContextAccessor).InvariantEquals(headerValue));
: rootContent.FirstOrDefault(c => _documentUrlService.GetUrlSegment(c.Key, culture, isPreview).InvariantEquals(headerValue));
return _requestedStartContent;
}
+9 -8
View File
@@ -20,7 +20,7 @@ RESTful API for Umbraco backoffice operations. Manages content, media, users, an
### Key Technologies
- **Web Framework**: ASP.NET Core MVC with `Asp.Versioning.Mvc` (v1.0 currently)
- **OpenAPI**: Swashbuckle.AspNetCore with custom schema/operation filters
- **OpenAPI**: Microsoft.AspNetCore.OpenApi with custom transformers, Swagger UI via Swashbuckle
- **Authentication**: OpenIddict via `Umbraco.Cms.Api.Common` (reference tokens, not JWT)
- **Authorization**: Policy-based with `IAuthorizationService`
- **Validation**: FluentValidation via base controllers
@@ -57,7 +57,7 @@ src/Umbraco.Cms.Api.Management/
├── Services/ # Business logic (thin layer over Core.Services)
├── Mapping/ # ViewModel → domain model mappers
├── Security/ # Auth providers, sign-in manager, external logins
├── OpenApi/ # Swashbuckle filters (schema, operation, security)
├── OpenApi/ # OpenAPI transformers (schema, operation, security)
├── Routing/ # Route configuration, SignalR hubs
├── DependencyInjection/ # Service registration (55+ files)
├── Middleware/ # Preview, server events
@@ -71,7 +71,8 @@ src/Umbraco.Cms.Api.Management/
- **Umbraco.Cms.Api.Common** - Shared API infrastructure (base controllers, OpenAPI config)
- **Umbraco.Infrastructure** - Service implementations, data access
- **Umbraco.PublishedCache.HybridCache** - Published content queries
- **Swashbuckle.AspNetCore** - OpenAPI generation
- **Microsoft.AspNetCore.OpenApi** - OpenAPI document generation
- **Swashbuckle.AspNetCore.SwaggerUI** - Swagger UI
### Design Patterns
1. **Controller-per-Operation** - Each endpoint is a separate controller class
@@ -122,8 +123,8 @@ dotnet build src/Umbraco.Cms.Api.Management /p:TreatWarningsAsErrors=true
### OpenAPI Documentation
The project embeds a pre-generated `OpenApi.json` (1.3MB). To regenerate:
```bash
# Run Umbraco.Web.UI, access /umbraco/swagger
# Export JSON from Swagger UI
# Run Umbraco.Web.UI, access /umbraco/openapi/
# Download JSON from /umbraco/openapi/management.json
```
### Package Management
@@ -201,7 +202,7 @@ dotnet test --filter "FullyQualifiedName~Management.Controllers.Document"
1. **Controller logic** - Request validation, authorization checks, status code mapping
2. **Factories** - ViewModel ↔ Domain model conversion accuracy
3. **Authorization** - Policy enforcement for each operation
4. **OpenAPI schema** - Ensure Swagger generation doesn't break
4. **OpenAPI schema** - Ensure OpenAPI document generation doesn't break
### InternalsVisibleTo
Tests have access to internal types (see .csproj:44-52):
@@ -465,7 +466,7 @@ TODO: [NL] This must return path segments for a query to work
1. All tests pass
2. Code formatted (`dotnet format`)
3. No new warnings (check suppressed warnings list in .csproj:23)
4. OpenAPI schema valid (run Swagger UI)
4. OpenAPI schema valid (check at `/umbraco/openapi/`)
5. Authorization tested (unit + integration tests)
### Common Pitfalls
@@ -612,7 +613,7 @@ Examples:
- `PUT /umbraco/management/api/v1/document/{id}` - Update document
- `DELETE /umbraco/management/api/v1/document/{id}` - Delete document
Full spec: See OpenApi.json or Swagger UI at `/umbraco/swagger`
Full spec: See OpenApi.json or Swagger UI at `/umbraco/openapi/`
### Getting Help
- **Root Documentation**: `/CLAUDE.md` (repository overview)
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Umbraco.Cms.Api.Management.Controllers.Document.GetPublicAccessDocumentController.GetPublicAccess(System.Threading.CancellationToken,System.Guid)</Target>
<Left>lib/net10.0/Umbraco.Cms.Api.Management.dll</Left>
<Right>lib/net10.0/Umbraco.Cms.Api.Management.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Umbraco.Cms.Api.Management.Controllers.UrlSegment.ResizeImagingController.Urls(System.Collections.Generic.HashSet{System.Guid},System.Int32,System.Int32,System.Nullable{Umbraco.Cms.Core.Models.ImageCropMode})</Target>
<Left>lib/net10.0/Umbraco.Cms.Api.Management.dll</Left>
<Right>lib/net10.0/Umbraco.Cms.Api.Management.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
@@ -1,79 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.Security;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Api.Management.DependencyInjection;
using Umbraco.Cms.Api.Management.OpenApi;
namespace Umbraco.Cms.Api.Management.Configuration;
/// <summary>
/// Provides configuration for Swagger generation options specific to the Umbraco Management API.
/// This class is used to customize the Swagger documentation for the API endpoints.
/// </summary>
public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureUmbracoManagementApiSwaggerGenOptions"/> class.
/// </summary>
/// <param name="umbracoJsonTypeInfoResolver">An instance of <see cref="IUmbracoJsonTypeInfoResolver"/> used to resolve JSON type information for Umbraco.</param>
public ConfigureUmbracoManagementApiSwaggerGenOptions(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
{
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
}
/// <summary>
/// Configures the <see cref="SwaggerGenOptions"/> for the Umbraco Management API.
/// Sets up the Swagger documentation, including API metadata, security definitions for OAuth2 authentication,
/// operation filters for response headers and security requirements, and schema filters for non-nullable properties.
/// Also configures polymorphism handling and discriminator properties for OpenAPI schemas.
/// </summary>
/// <param name="swaggerGenOptions">The <see cref="SwaggerGenOptions"/> instance to configure for the Management API.</param>
public void Configure(SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.SwaggerDoc(
ManagementApiConfiguration.ApiName,
new OpenApiInfo
{
Title = ManagementApiConfiguration.ApiTitle,
Version = "Latest",
Description = "This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility",
});
swaggerGenOptions.OperationFilter<ResponseHeaderOperationFilter>();
swaggerGenOptions.UseOneOfForPolymorphism();
// Ensure all types that implements the IOpenApiDiscriminator have a $type property in the OpenApi schema with the default value (The class name) that is expected by the server
swaggerGenOptions.SelectDiscriminatorNameUsing(type => _umbracoJsonTypeInfoResolver.GetTypeDiscriminatorValue(type) is not null ? "$type" : null);
swaggerGenOptions.SelectDiscriminatorValueUsing(_umbracoJsonTypeInfoResolver.GetTypeDiscriminatorValue);
swaggerGenOptions.AddSecurityDefinition(
ManagementApiConfiguration.ApiSecurityName,
new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = "Umbraco",
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Authentication",
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl =
new Uri(Paths.BackOfficeApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.BackOfficeApi.TokenEndpoint, UriKind.Relative),
},
},
});
// Sets Security requirement on backoffice apis
swaggerGenOptions.OperationFilter<BackOfficeSecurityRequirementsOperationFilter>();
swaggerGenOptions.OperationFilter<NotificationHeaderFilter>();
swaggerGenOptions.SchemaFilter<RequireNonNullablePropertiesSchemaFilter>();
}
}
@@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
using Umbraco.Cms.Core.Models.ContentPublishing;
using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
@@ -13,6 +15,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content;
/// </summary>
public abstract class ContentControllerBase : ManagementApiControllerBase
{
protected abstract string EntityName { get; }
protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status)
=> OperationStatusResult(status, problemDetailsBuilder => status switch
{
@@ -90,12 +94,12 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
.WithDetail("The supplied name is already in use for the same content type.")
.Build()),
ContentEditingOperationStatus.CannotDeleteWhenReferenced => BadRequest(problemDetailsBuilder
.WithTitle("Cannot delete a referenced content item")
.WithDetail("Cannot delete a referenced content item, while the setting ContentSettings.DisableDeleteWhenReferenced is enabled.")
.WithTitle($"Cannot delete a referenced {EntityName}")
.WithDetail($"Cannot delete a referenced {EntityName}, while the setting ContentSettings.DisableDeleteWhenReferenced is enabled.")
.Build()),
ContentEditingOperationStatus.CannotMoveToRecycleBinWhenReferenced => BadRequest(problemDetailsBuilder
.WithTitle("Cannot move a referenced content item to the recycle bin")
.WithDetail("Cannot move a referenced content item to the recycle bin, while the setting ContentSettings.DisableDeleteWhenReferenced is enabled.")
.WithTitle($"Cannot move a referenced {EntityName} to the recycle bin")
.WithDetail($"Cannot move a referenced {EntityName} to the recycle bin, while the setting ContentSettings.DisableUnpublishWhenReferenced is enabled.")
.Build()),
ContentEditingOperationStatus.Unknown => StatusCode(
StatusCodes.Status500InternalServerError,
@@ -118,6 +122,122 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
.Build()),
});
protected IActionResult ContentPublishingOperationStatusResult(
ContentPublishingOperationStatus status,
IEnumerable<string>? invalidPropertyAliases = null,
IEnumerable<ContentPublishingBranchItemResult>? failedBranchItems = null)
=> OperationStatusResult(
status,
problemDetailsBuilder => status switch
{
ContentPublishingOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder
.WithTitle($"The requested {EntityName} could not be found")
.Build()),
ContentPublishingOperationStatus.CancelledByEvent => BadRequest(problemDetailsBuilder
.WithTitle("Publish cancelled by event")
.WithDetail("The publish operation was cancelled by an event.")
.Build()),
ContentPublishingOperationStatus.ContentInvalid => BadRequest(problemDetailsBuilder
.WithTitle($"Invalid {EntityName}")
.WithDetail($"The specified {EntityName} had an invalid configuration.")
.WithExtension("invalidProperties", invalidPropertyAliases ?? Enumerable.Empty<string>())
.Build()),
ContentPublishingOperationStatus.NothingToPublish => BadRequest(problemDetailsBuilder
.WithTitle("Nothing to publish")
.WithDetail("None of the specified cultures needed publishing.")
.Build()),
ContentPublishingOperationStatus.MandatoryCultureMissing => BadRequest(problemDetailsBuilder
.WithTitle("Mandatory culture missing")
.WithDetail("Must include all mandatory cultures when publishing.")
.Build()),
ContentPublishingOperationStatus.HasExpired => BadRequest(problemDetailsBuilder
.WithTitle($"{EntityName.ToFirstUpperInvariant()} expired")
.WithDetail($"Could not publish the {EntityName} because it was expired.")
.Build()),
ContentPublishingOperationStatus.CultureHasExpired => BadRequest(problemDetailsBuilder
.WithTitle($"{EntityName.ToFirstUpperInvariant()} culture expired")
.WithDetail($"Could not publish the {EntityName} because some of the specified cultures were expired.")
.Build()),
ContentPublishingOperationStatus.AwaitingRelease => BadRequest(problemDetailsBuilder
.WithTitle($"{EntityName.ToFirstUpperInvariant()} awaiting release")
.WithDetail($"Could not publish the {EntityName} because it was awaiting release.")
.Build()),
ContentPublishingOperationStatus.CultureAwaitingRelease => BadRequest(problemDetailsBuilder
.WithTitle($"{EntityName.ToFirstUpperInvariant()} culture awaiting release")
.WithDetail(
$"Could not publish the {EntityName} because some of the specified cultures were awaiting release.")
.Build()),
ContentPublishingOperationStatus.InTrash => BadRequest(problemDetailsBuilder
.WithTitle($"{EntityName.ToFirstUpperInvariant()} in the recycle bin")
.WithDetail($"Could not publish the {EntityName} because it was in the recycle bin.")
.Build()),
ContentPublishingOperationStatus.PathNotPublished => BadRequest(problemDetailsBuilder
.WithTitle("Parent not published")
.WithDetail($"Could not publish the {EntityName} because its parent was not published.")
.Build()),
ContentPublishingOperationStatus.InvalidCulture => BadRequest(problemDetailsBuilder
.WithTitle("Invalid cultures specified")
.WithDetail("A specified culture is not valid for the operation.")
.Build()),
ContentPublishingOperationStatus.CultureMissing => BadRequest(problemDetailsBuilder
.WithTitle("Culture missing")
.WithDetail("A culture needs to be specified to execute the operation.")
.Build()),
ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant => BadRequest(problemDetailsBuilder
.WithTitle("Cannot publish invariant when variant")
.WithDetail($"Cannot publish invariant culture when the {EntityName} varies by culture.")
.Build()),
ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant => BadRequest(problemDetailsBuilder
.WithTitle("Cannot publish variant when not variant.")
.WithDetail($"Cannot publish a given culture when the {EntityName} is invariant.")
.Build()),
ContentPublishingOperationStatus.ConcurrencyViolation => BadRequest(problemDetailsBuilder
.WithTitle("Concurrency violation detected")
.WithDetail("An attempt was made to publish a version older than the latest version.")
.Build()),
ContentPublishingOperationStatus.UnsavedChanges => BadRequest(problemDetailsBuilder
.WithTitle("Unsaved changes")
.WithDetail(
$"Could not publish the {EntityName} because it had unsaved changes. Make sure to save all changes before attempting a publish.")
.Build()),
ContentPublishingOperationStatus.UnpublishTimeNeedsToBeAfterPublishTime => BadRequest(problemDetailsBuilder
.WithTitle("Unpublish time needs to be after the publish time")
.WithDetail(
"Cannot handle an unpublish time that is not after the specified publish time.")
.Build()),
ContentPublishingOperationStatus.PublishTimeNeedsToBeInFuture => BadRequest(problemDetailsBuilder
.WithTitle("Publish time needs to be higher than the current time")
.WithDetail(
"Cannot handle a publish time that is not after the current server time.")
.Build()),
ContentPublishingOperationStatus.UpublishTimeNeedsToBeInFuture => BadRequest(problemDetailsBuilder
.WithTitle("Unpublish time needs to be higher than the current time")
.WithDetail(
"Cannot handle an unpublish time that is not after the current server time.")
.Build()),
ContentPublishingOperationStatus.CannotUnpublishWhenReferenced => BadRequest(problemDetailsBuilder
.WithTitle($"Cannot unpublish {EntityName} when it's referenced somewhere else.")
.WithDetail(
$"Cannot unpublish a referenced {EntityName}, while the setting ContentSettings.DisableUnpublishWhenReferenced is enabled.")
.Build()),
ContentPublishingOperationStatus.FailedBranch => BadRequest(problemDetailsBuilder
.WithTitle("Failed branch operation")
.WithDetail("One or more items in the branch could not complete the operation.")
.WithExtension("failedBranchItems", failedBranchItems?.Select(item => new DocumentPublishBranchItemResult { Id = item.Key, OperationStatus = item.OperationStatus }) ?? [])
.Build()),
ContentPublishingOperationStatus.Failed => BadRequest(
problemDetailsBuilder
.WithTitle("Publish or unpublish failed")
.WithDetail(
"An unspecified error occurred while (un)publishing. Please check the logs for additional information.")
.Build()),
ContentPublishingOperationStatus.TaskResultNotFound => NotFound(problemDetailsBuilder
.WithTitle("The result of the submitted task could not be found")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status."),
});
protected IActionResult ContentEditingOperationStatusResult<TContentModelBase, TValueModel, TVariantModel>(
ContentEditingOperationStatus status,
TContentModelBase requestModel,
@@ -15,7 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType;
/// </summary>
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DataType)]
[ApiExplorerSettings(GroupName = "Data Type")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrMediaOrMembersOrContentTypes)]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrElementsOrMediaOrMembersOrContentTypes)]
public abstract class DataTypeControllerBase : ManagementApiControllerBase
{
protected IActionResult DataTypeOperationStatusResult(DataTypeOperationStatus status) =>
@@ -14,24 +14,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
[ApiVersion("1.0")]
public class AncestorsDataTypeTreeController : DataTypeTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsDataTypeTreeController"/> class, which provides API endpoints for retrieving ancestor data types in the tree structure.
/// </summary>
/// <param name="entityService">Service used for entity operations within the API.</param>
/// <param name="dataTypeService">Service used for data type management and retrieval.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public AncestorsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsDataTypeTreeController"/> class, which manages operations related to ancestor data type trees in the Umbraco CMS.
/// </summary>
/// <param name="entityService">Service used for entity-related operations.</param>
/// <param name="flagProviders">A collection of providers that supply flags for tree nodes.</param>
/// <param name="dataTypeService">Service used for data type management operations.</param>
[ActivatorUtilitiesConstructor]
public AncestorsDataTypeTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDataTypeService dataTypeService)
: base(entityService, flagProviders, dataTypeService)
{
@@ -15,24 +15,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
[ApiVersion("1.0")]
public class ChildrenDataTypeTreeController : DataTypeTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDataTypeTreeController"/> class.
/// </summary>
/// <param name="entityService">Service used for managing and retrieving entities within the system.</param>
/// <param name="dataTypeService">Service used for managing and retrieving data types.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public ChildrenDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDataTypeTreeController"/> class.
/// </summary>
/// <param name="entityService">Service used for managing and retrieving entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply additional flags or metadata for entities.</param>
/// <param name="dataTypeService">Service responsible for operations related to data types.</param>
[ActivatorUtilitiesConstructor]
public ChildrenDataTypeTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDataTypeService dataTypeService)
: base(entityService, flagProviders, dataTypeService)
{
@@ -25,26 +25,6 @@ public class DataTypeTreeControllerBase : FolderTreeControllerBase<DataTypeTreeI
{
private readonly IDataTypeService _dataTypeService;
/// <summary>
/// Initializes a new instance of the <see cref="DataTypeTreeControllerBase"/> class.
/// </summary>
/// <param name="entityService">Service for managing Umbraco entities.</param>
/// <param name="dataTypeService">Service for managing data types within Umbraco.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public DataTypeTreeControllerBase(IEntityService entityService, IDataTypeService dataTypeService)
: this(
entityService,
StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>(),
dataTypeService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DataTypeTreeControllerBase"/> class with the specified services.
/// </summary>
/// <param name="entityService">Service used for entity operations within the data type tree.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="dataTypeService">Service used for managing data types.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public DataTypeTreeControllerBase(IEntityService entityService, FlagProviderCollection flagProviders, IDataTypeService dataTypeService)
: this(
@@ -80,17 +60,17 @@ public class DataTypeTreeControllerBase : FolderTreeControllerBase<DataTypeTreeI
}
}
protected override DataTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentId, IEntitySlim[] entities)
protected override async Task<DataTypeTreeItemResponseModel[]> MapTreeItemViewModelsAsync(Guid? parentId, IEntitySlim[] entities)
{
Dictionary<int, IDataType> dataTypes = entities.Any()
? _dataTypeService
.GetAllAsync(entities.Select(entity => entity.Key).ToArray()).GetAwaiter().GetResult()
? (await _dataTypeService
.GetAllAsync(entities.Select(entity => entity.Key).ToArray()))
.ToDictionary(contentType => contentType.Id)
: new Dictionary<int, IDataType>();
return entities.Select(entity =>
IEnumerable<Task<DataTypeTreeItemResponseModel>> tasks = entities.Select(async entity =>
{
DataTypeTreeItemResponseModel responseModel = MapTreeItemViewModel(parentId, entity);
DataTypeTreeItemResponseModel responseModel = await MapTreeItemViewModelAsync(parentId, entity);
if (dataTypes.TryGetValue(entity.Id, out IDataType? dataType))
{
responseModel.EditorUiAlias = dataType.EditorUiAlias;
@@ -98,6 +78,8 @@ public class DataTypeTreeControllerBase : FolderTreeControllerBase<DataTypeTreeI
}
return responseModel;
}).ToArray();
});
return await Task.WhenAll(tasks);
}
}
@@ -15,24 +15,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
[ApiVersion("1.0")]
public class RootDataTypeTreeController : DataTypeTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="RootDataTypeTreeController"/> class.
/// </summary>
/// <param name="entityService">Service used for managing and retrieving entities within Umbraco.</param>
/// <param name="dataTypeService">Service used for managing data types in Umbraco.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public RootDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RootDataTypeTreeController"/> class, which manages the root of the data type tree in the Umbraco management API.
/// </summary>
/// <param name="entityService">Service used for entity operations within the tree.</param>
/// <param name="flagProviders">A collection of providers that supply flags for tree nodes.</param>
/// <param name="dataTypeService">Service used for data type management and retrieval.</param>
[ActivatorUtilitiesConstructor]
public RootDataTypeTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDataTypeService dataTypeService)
: base(entityService, flagProviders, dataTypeService)
{
@@ -13,24 +13,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
/// </summary>
public class SiblingsDataTypeTreeController : DataTypeTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsDataTypeTreeController"/> class.
/// </summary>
/// <param name="entityService">Service used for managing and retrieving entities within Umbraco.</param>
/// <param name="dataTypeService">Service used for managing data types in Umbraco.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public SiblingsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsDataTypeTreeController"/> class, which manages operations related to sibling data type trees in the Umbraco CMS.
/// </summary>
/// <param name="entityService">Service used for entity operations within the CMS.</param>
/// <param name="flagProviders">A collection of providers that supply flags for tree nodes.</param>
/// <param name="dataTypeService">Service used for managing data types.</param>
[ActivatorUtilitiesConstructor]
public SiblingsDataTypeTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDataTypeService dataTypeService)
: base(entityService, flagProviders, dataTypeService)
{
@@ -42,7 +42,7 @@ public class ExportDictionaryController : DictionaryControllerBase
/// </returns>
[HttpGet("{id:guid}/export")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK, MediaTypeNames.Application.Octet)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[EndpointSummary("Exports a dictionary.")]
[EndpointDescription("Exports the dictionary identified by the provided Id to a downloadable format.")]
@@ -14,24 +14,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
[ApiVersion("1.0")]
public class AncestorsDictionaryTreeController : DictionaryTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsDictionaryTreeController"/> class.
/// </summary>
/// <param name="entityService">Service used for managing and retrieving entities within Umbraco.</param>
/// <param name="dictionaryItemService">Service used for managing dictionary items in the Umbraco dictionary tree.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public AncestorsDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
: base(entityService, dictionaryItemService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsDictionaryTreeController"/> class, which handles operations related to retrieving ancestor dictionary tree items.
/// </summary>
/// <param name="entityService">The service used for entity operations.</param>
/// <param name="flagProviders">A collection of providers for entity flags.</param>
/// <param name="dictionaryItemService">The service used for dictionary item operations.</param>
[ActivatorUtilitiesConstructor]
public AncestorsDictionaryTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDictionaryItemService dictionaryItemService)
: base(entityService, flagProviders, dictionaryItemService)
{
@@ -16,24 +16,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
[ApiVersion("1.0")]
public class ChildrenDictionaryTreeController : DictionaryTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDictionaryTreeController"/> class.
/// </summary>
/// <param name="entityService">Service used for managing and retrieving entities within the system.</param>
/// <param name="dictionaryItemService">Service used for managing dictionary items.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public ChildrenDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
: base(entityService, dictionaryItemService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDictionaryTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers that supply additional flags or metadata for entities.</param>
/// <param name="dictionaryItemService">Service for managing dictionary items used for localization.</param>
[ActivatorUtilitiesConstructor]
public ChildrenDictionaryTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDictionaryItemService dictionaryItemService)
: base(entityService, flagProviders, dictionaryItemService)
{
@@ -1,13 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;
@@ -25,26 +23,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
// tree controller base. We'll keep it though, in the hope that we can mend EntityService.
public class DictionaryTreeControllerBase : NamedEntityTreeControllerBase<NamedEntityTreeItemResponseModel>
{
/// <summary>
/// Initializes a new instance of the <see cref="DictionaryTreeControllerBase"/> class.
/// </summary>
/// <param name="entityService">Service used for managing and retrieving entities within the Umbraco system.</param>
/// <param name="dictionaryItemService">Service used for managing and retrieving dictionary items for localization.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public DictionaryTreeControllerBase(IEntityService entityService, IDictionaryItemService dictionaryItemService)
: this(
entityService,
StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>(),
dictionaryItemService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DictionaryTreeControllerBase"/> class.
/// </summary>
/// <param name="entityService">Service for managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers for entity flags.</param>
/// <param name="dictionaryItemService">Service for managing dictionary items.</param>
public DictionaryTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
@@ -16,13 +17,12 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
[ApiVersion("1.0")]
public class RootDictionaryTreeController : DictionaryTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="RootDictionaryTreeController"/> class.
/// </summary>
/// <param name="entityService">Service used for entity operations within the dictionary tree.</param>
/// <param name="dictionaryItemService">Service used for managing dictionary items.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 20.")]
public RootDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
: base(entityService, dictionaryItemService)
: this(
entityService,
StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>(),
dictionaryItemService)
{
}
@@ -1,13 +1,11 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.ViewModels.Document.Collection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
@@ -26,16 +24,6 @@ public class ByKeyDocumentCollectionController : DocumentCollectionControllerBas
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IDocumentCollectionPresentationFactory _documentCollectionPresentationFactory;
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Document.Collection.ByKeyDocumentCollectionController"/> class,
/// which handles document collection operations by document key.
/// </summary>
/// <param name="contentListViewService">Service for retrieving and managing content list views.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
/// <param name="mapper">The Umbraco object mapper used for mapping between models.</param>
/// <param name="documentCollectionPresentationFactory">Factory for creating document collection presentation models.</param>
/// <param name="flagProviders">A collection of providers for document collection flags.</param>
[ActivatorUtilitiesConstructor]
public ByKeyDocumentCollectionController(
IContentListViewService contentListViewService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
@@ -49,28 +37,6 @@ public class ByKeyDocumentCollectionController : DocumentCollectionControllerBas
_documentCollectionPresentationFactory = documentCollectionPresentationFactory;
}
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Document.Collection.ByKeyDocumentCollectionController"/> class.
/// </summary>
/// <param name="contentListViewService">Service for managing content list views.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security operations.</param>
/// <param name="mapper">Maps Umbraco objects to API models.</param>
/// <param name="documentCollectionPresentationFactory">Factory for creating document collection presentation models.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
public ByKeyDocumentCollectionController(
IContentListViewService contentListViewService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUmbracoMapper mapper,
IDocumentCollectionPresentationFactory documentCollectionPresentationFactory)
: this(
contentListViewService,
backOfficeSecurityAccessor,
mapper,
documentCollectionPresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>())
{
}
/// <summary>
/// Retrieves a paged collection of documents identified by the provided unique identifier.
/// </summary>
@@ -21,6 +21,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document;
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
public abstract class DocumentControllerBase : ContentControllerBase
{
protected override string EntityName => "document";
protected IActionResult DocumentNotFound()
=> OperationStatusResult(ContentEditingOperationStatus.NotFound, problemDetailsBuilder
=> NotFound(problemDetailsBuilder
@@ -38,118 +40,7 @@ public abstract class DocumentControllerBase : ContentControllerBase
ContentPublishingOperationStatus status,
IEnumerable<string>? invalidPropertyAliases = null,
IEnumerable<ContentPublishingBranchItemResult>? failedBranchItems = null)
=> OperationStatusResult(status, problemDetailsBuilder => status switch
{
ContentPublishingOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder
.WithTitle("The requested document could not be found")
.Build()),
ContentPublishingOperationStatus.CancelledByEvent => BadRequest(problemDetailsBuilder
.WithTitle("Publish cancelled by event")
.WithDetail("The publish operation was cancelled by an event.")
.Build()),
ContentPublishingOperationStatus.ContentInvalid => BadRequest(problemDetailsBuilder
.WithTitle("Invalid document")
.WithDetail("The specified document had an invalid configuration.")
.WithExtension("invalidProperties", invalidPropertyAliases ?? Enumerable.Empty<string>())
.Build()),
ContentPublishingOperationStatus.NothingToPublish => BadRequest(problemDetailsBuilder
.WithTitle("Nothing to publish")
.WithDetail("None of the specified cultures needed publishing.")
.Build()),
ContentPublishingOperationStatus.MandatoryCultureMissing => BadRequest(problemDetailsBuilder
.WithTitle("Mandatory culture missing")
.WithDetail("Must include all mandatory cultures when publishing.")
.Build()),
ContentPublishingOperationStatus.HasExpired => BadRequest(problemDetailsBuilder
.WithTitle("Document expired")
.WithDetail("Could not publish the document because it was expired.")
.Build()),
ContentPublishingOperationStatus.CultureHasExpired => BadRequest(problemDetailsBuilder
.WithTitle("Document culture expired")
.WithDetail("Could not publish the document because some of the specified cultures were expired.")
.Build()),
ContentPublishingOperationStatus.AwaitingRelease => BadRequest(problemDetailsBuilder
.WithTitle("Document awaiting release")
.WithDetail("Could not publish the document because it was awaiting release.")
.Build()),
ContentPublishingOperationStatus.CultureAwaitingRelease => BadRequest(problemDetailsBuilder
.WithTitle("Document culture awaiting release")
.WithDetail(
"Could not publish the document because some of the specified cultures were awaiting release.")
.Build()),
ContentPublishingOperationStatus.InTrash => BadRequest(problemDetailsBuilder
.WithTitle("Document in the recycle bin")
.WithDetail("Could not publish the document because it was in the recycle bin.")
.Build()),
ContentPublishingOperationStatus.PathNotPublished => BadRequest(problemDetailsBuilder
.WithTitle("Parent not published")
.WithDetail("Could not publish the document because its parent was not published.")
.Build()),
ContentPublishingOperationStatus.InvalidCulture => BadRequest(problemDetailsBuilder
.WithTitle("Invalid cultures specified")
.WithDetail("A specified culture is not valid for the operation.")
.Build()),
ContentPublishingOperationStatus.CultureMissing => BadRequest(problemDetailsBuilder
.WithTitle("Culture missing")
.WithDetail("A culture needs to be specified to execute the operation.")
.Build()),
ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant => BadRequest(problemDetailsBuilder
.WithTitle("Cannot publish invariant when variant")
.WithDetail("Cannot publish invariant culture when the document varies by culture.")
.Build()),
ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant => BadRequest(problemDetailsBuilder
.WithTitle("Cannot publish variant when not variant.")
.WithDetail("Cannot publish a given culture when the document is invariant.")
.Build()),
ContentPublishingOperationStatus.ConcurrencyViolation => BadRequest(problemDetailsBuilder
.WithTitle("Concurrency violation detected")
.WithDetail("An attempt was made to publish a version older than the latest version.")
.Build()),
ContentPublishingOperationStatus.UnsavedChanges => BadRequest(problemDetailsBuilder
.WithTitle("Unsaved changes")
.WithDetail(
"Could not publish the document because it had unsaved changes. Make sure to save all changes before attempting a publish.")
.Build()),
ContentPublishingOperationStatus.UnpublishTimeNeedsToBeAfterPublishTime => BadRequest(problemDetailsBuilder
.WithTitle("Unpublish time needs to be after the publish time")
.WithDetail(
"Cannot handle an unpublish time that is not after the specified publish time.")
.Build()),
ContentPublishingOperationStatus.PublishTimeNeedsToBeInFuture => BadRequest(problemDetailsBuilder
.WithTitle("Publish time needs to be higher than the current time")
.WithDetail(
"Cannot handle a publish time that is not after the current server time.")
.Build()),
ContentPublishingOperationStatus.UpublishTimeNeedsToBeInFuture => BadRequest(problemDetailsBuilder
.WithTitle("Unpublish time needs to be higher than the current time")
.WithDetail(
"Cannot handle an unpublish time that is not after the current server time.")
.Build()),
ContentPublishingOperationStatus.CannotUnpublishWhenReferenced => BadRequest(problemDetailsBuilder
.WithTitle("Cannot unpublish document when it's referenced somewhere else.")
.WithDetail(
"Cannot unpublish a referenced document, while the setting ContentSettings.DisableUnpublishWhenReferenced is enabled.")
.Build()),
ContentPublishingOperationStatus.FailedBranch => BadRequest(problemDetailsBuilder
.WithTitle("Failed branch operation")
.WithDetail("One or more items in the branch could not complete the operation.")
.WithExtension("failedBranchItems", failedBranchItems?.Select(item => new DocumentPublishBranchItemResult
{
Id = item.Key,
OperationStatus = item.OperationStatus
}) ?? Enumerable.Empty<DocumentPublishBranchItemResult>())
.Build()),
ContentPublishingOperationStatus.Failed => BadRequest(problemDetailsBuilder
.WithTitle("Publish or unpublish failed")
.WithDetail(
"An unspecified error occurred while (un)publishing. Please check the logs for additional information.")
.Build()),
ContentPublishingOperationStatus.TaskResultNotFound => NotFound(problemDetailsBuilder
.WithTitle("The result of the submitted task could not be found")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status."),
});
=> ContentPublishingOperationStatusResult(status, invalidPropertyAliases, failedBranchItems);
protected IActionResult PublicAccessOperationStatusResult(PublicAccessOperationStatus status)
=> OperationStatusResult(status, problemDetailsBuilder => status switch
@@ -38,15 +38,6 @@ public class DomainsController : DocumentControllerBase
_umbracoMapper = umbracoMapper;
}
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public DomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper)
: this(
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
domainService,
umbracoMapper)
{
}
/// <summary>
/// Retrieves the list of domains and their associated culture settings assigned to the specified document.
/// </summary>
@@ -1,7 +1,6 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
using Umbraco.Cms.Core.Models;
@@ -26,7 +25,6 @@ public class ItemDocumentItemController : DocumentItemControllerBase
/// </summary>
/// <param name="entityService">The service used to manage and retrieve entities within the CMS.</param>
/// <param name="documentPresentationFactory">The factory responsible for creating document presentation models.</param>
[ActivatorUtilitiesConstructor]
public ItemDocumentItemController(
IEntityService entityService,
IDocumentPresentationFactory documentPresentationFactory)
@@ -59,7 +57,8 @@ public class ItemDocumentItemController : DocumentItemControllerBase
.GetAll(UmbracoObjectTypes.Document, ids.ToArray())
.OfType<IDocumentEntitySlim>();
IEnumerable<DocumentItemResponseModel> responseModels = documents.Select(_documentPresentationFactory.CreateItemResponseModel);
IEnumerable<Task<DocumentItemResponseModel>> tasks = documents.Select(_documentPresentationFactory.CreateItemResponseModelAsync);
DocumentItemResponseModel[] responseModels = await Task.WhenAll(tasks);
return Ok(responseModels);
}
}
@@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
@@ -28,7 +27,6 @@ public class SearchDocumentItemController : DocumentItemControllerBase
/// <param name="indexedEntitySearchService">Service for searching indexed entities.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="dataTypeService">Service for managing data types.</param>
[ActivatorUtilitiesConstructor]
public SearchDocumentItemController(
IIndexedEntitySearchService indexedEntitySearchService,
IDocumentPresentationFactory documentPresentationFactory,
@@ -39,56 +37,6 @@ public class SearchDocumentItemController : DocumentItemControllerBase
_dataTypeService = dataTypeService;
}
/// <summary>
/// Initializes a new instance of the <see cref="SearchDocumentItemController"/> class.
/// </summary>
/// <param name="indexedEntitySearchService">The service used to perform searches on indexed entities. This dependency is injected.</param>
/// <param name="documentPresentationFactory">The factory responsible for creating document presentation models. This dependency is injected.</param>
[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in Umbraco 18.")]
public SearchDocumentItemController(
IIndexedEntitySearchService indexedEntitySearchService,
IDocumentPresentationFactory documentPresentationFactory)
: this(
indexedEntitySearchService,
documentPresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IDataTypeService>())
{
}
/// <summary>
/// Searches for document items, including those in the recycle bin, using the specified query and filters.
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <param name="query">The search query string.</param>
/// <param name="trashed">If set, filters results to include only trashed items, only non-trashed items, or both (when null).</param>
/// <param name="culture">An optional culture code to filter search results.</param>
/// <param name="skip">The number of items to skip (for pagination).</param>
/// <param name="take">The maximum number of items to return (for pagination).</param>
/// <param name="parentId">An optional parent ID to filter results by parent.</param>
/// <param name="allowedDocumentTypes">An optional list of allowed document type IDs to filter results.</param>
/// <returns>A task representing the asynchronous operation, with an action result containing the search results.</returns>
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IActionResult> SearchWithTrashed(
CancellationToken cancellationToken,
string query,
bool? trashed = null,
string? culture = null,
int skip = 0,
int take = 100,
Guid? parentId = null,
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
=> await SearchWithTrashed(
cancellationToken,
query,
trashed,
culture,
skip,
take,
parentId,
allowedDocumentTypes,
null);
/// <summary>
/// Searches for document items, including those in the recycle bin, based on the specified query and filters.
/// </summary>
@@ -130,9 +78,12 @@ public class SearchDocumentItemController : DocumentItemControllerBase
take,
ignoreUserStartNodes);
IEnumerable<Task<DocumentItemResponseModel>> tasks = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModelAsync);
DocumentItemResponseModel[] items = await Task.WhenAll(tasks);
var result = new PagedModel<DocumentItemResponseModel>
{
Items = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModel),
Items = items,
Total = searchResult.Total,
};
@@ -1,3 +1,4 @@
using System.Net.Mime;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -44,7 +45,7 @@ public class PatchDocumentController : PatchDocumentControllerBase
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
[EndpointSummary("Make partial updates to a document. For more information, see the documentation at https://docs.umbraco.com/umbraco-cms/reference/management-api/patching/document-endpoint-guide or https://docs.umbraco.com/umbraco-cms/reference/management-api/patching/document-endpoint-spec")]
[Consumes("application/json-patch+json")]
[Consumes(MediaTypeNames.Application.JsonPatch)]
public async Task<IActionResult> Patch(
CancellationToken cancellationToken,
Guid id,
@@ -22,6 +22,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin;
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
public class DocumentRecycleBinControllerBase : RecycleBinControllerBase<DocumentRecycleBinItemResponseModel>
{
protected override string EntityName => "document";
private readonly IDocumentPresentationFactory _documentPresentationFactory;
/// <summary>
@@ -37,13 +39,13 @@ public class DocumentRecycleBinControllerBase : RecycleBinControllerBase<Documen
protected override Guid RecycleBinRootKey => Constants.System.RecycleBinContentKey;
protected override DocumentRecycleBinItemResponseModel MapRecycleBinViewModel(Guid? parentId, IEntitySlim entity)
protected override async Task<DocumentRecycleBinItemResponseModel> MapRecycleBinViewModelAsync(Guid? parentId, IEntitySlim entity)
{
DocumentRecycleBinItemResponseModel responseModel = base.MapRecycleBinViewModel(parentId, entity);
DocumentRecycleBinItemResponseModel responseModel = await base.MapRecycleBinViewModelAsync(parentId, entity);
if (entity is IDocumentEntitySlim documentEntitySlim)
{
responseModel.Variants = _documentPresentationFactory.CreateVariantsItemResponseModels(documentEntitySlim);
responseModel.Variants = await _documentPresentationFactory.CreateVariantsItemResponseModelsAsync(documentEntitySlim);
responseModel.DocumentType = _documentPresentationFactory.CreateDocumentTypeReferenceResponseModel(documentEntitySlim);
}
@@ -20,7 +20,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class AncestorsDocumentTreeController : DocumentTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
@@ -83,46 +82,6 @@ public class AncestorsDocumentTreeController : DocumentTreeControllerBase
}
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Document.Tree.AncestorsDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service used for entity operations within the Umbraco CMS.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for managing data types in the CMS.</param>
/// <param name="publicAccessService">Service for handling public access permissions on content.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public AncestorsDocumentTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory)
: base(
entityService,
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers for handling entity flags.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start node entities.</param>
/// <param name="dataTypeService">Service for managing data types in Umbraco.</param>
/// <param name="publicAccessService">Service for handling public access permissions.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public AncestorsDocumentTreeController(
IEntityService entityService,
@@ -21,10 +21,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class ChildrenDocumentTreeController : DocumentTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
@@ -87,43 +83,6 @@ public class ChildrenDocumentTreeController : DocumentTreeControllerBase
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="publicAccessService">Service for handling public access permissions and restrictions.</param>
/// <param name="appCaches">Provides application-level caching mechanisms.</param>
/// <param name="backofficeSecurityAccessor">Accessor for back office security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public ChildrenDocumentTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory)
: base(
entityService,
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class, responsible for managing child document tree operations in the Umbraco backoffice API.
/// </summary>
/// <param name="entityService">Service for accessing and managing entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user-specific start nodes in the content tree.</param>
/// <param name="dataTypeService">Service for managing data types in Umbraco.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="appCaches">Provides application-level caching functionality.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and authentication.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models for the API.</param>
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public ChildrenDocumentTreeController(
IEntityService entityService,
@@ -37,27 +37,6 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
private readonly AppCaches? _appCaches;
private readonly IBackOfficeSecurityAccessor? _backofficeSecurityAccessor;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
protected DocumentTreeControllerBase(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory)
: this(
entityService,
StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>(),
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory)
{
}
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
protected DocumentTreeControllerBase(
IEntityService entityService,
@@ -129,9 +108,9 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
protected override Ordering ItemOrdering => Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.SortOrderColumnName);
protected override DocumentTreeItemResponseModel MapTreeItemViewModel(Guid? parentId, IEntitySlim entity)
protected override async Task<DocumentTreeItemResponseModel> MapTreeItemViewModelAsync(Guid? parentId, IEntitySlim entity)
{
DocumentTreeItemResponseModel responseModel = base.MapTreeItemViewModel(parentId, entity);
DocumentTreeItemResponseModel responseModel = await base.MapTreeItemViewModelAsync(parentId, entity);
if (entity is IDocumentEntitySlim documentEntitySlim)
{
@@ -142,7 +121,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
responseModel.Id = entity.Key;
responseModel.CreateDate = entity.CreateDate;
responseModel.Variants = _documentPresentationFactory.CreateVariantsItemResponseModels(documentEntitySlim);
responseModel.Variants = await _documentPresentationFactory.CreateVariantsItemResponseModelsAsync(documentEntitySlim);
responseModel.DocumentType = _documentPresentationFactory.CreateDocumentTypeReferenceResponseModel(documentEntitySlim);
}
@@ -21,7 +21,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class RootDocumentTreeController : DocumentTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="RootDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
@@ -84,46 +83,6 @@ public class RootDocumentTreeController : DocumentTreeControllerBase
}
/// <summary>
/// Initializes a new instance of the <see cref="RootDocumentTreeController"/> class, which manages the root nodes of the document tree in the Umbraco backoffice.
/// </summary>
/// <param name="entityService">The service used for entity operations.</param>
/// <param name="userStartNodeEntitiesService">The service for resolving user start node entities.</param>
/// <param name="dataTypeService">The service for managing data types.</param>
/// <param name="publicAccessService">The service for handling public access permissions.</param>
/// <param name="appCaches">The application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public RootDocumentTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory)
: base(
entityService,
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Document.Tree.RootDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user-specific start nodes in the content tree.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="appCaches">Provides application-level caching mechanisms.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public RootDocumentTreeController(
IEntityService entityService,

Some files were not shown because too many files have changed in this diff Show More