diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateAndPublishDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateAndPublishDocumentController.cs
new file mode 100644
index 000000000000..8264a5ba69c4
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateAndPublishDocumentController.cs
@@ -0,0 +1,88 @@
+using Asp.Versioning;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.Factories;
+using Umbraco.Cms.Api.Management.ViewModels.Document;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Actions;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Security.Authorization;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.OperationStatus;
+using Umbraco.Cms.Web.Common.Authorization;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Api.Management.Controllers.Document;
+
+///
+/// API controller responsible for handling operations related to the creation of content documents in the Umbraco CMS.
+///
+[ApiVersion("1.0")]
+public class CreateAndPublishDocumentController : CreateDocumentControllerBase
+{
+ private readonly IAuthorizationService _authorizationService;
+ private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory;
+ private readonly IContentEditingService _contentEditingService;
+ private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Service used to authorize access to document creation and publishing operations.
+ /// Factory for creating document editing presentation models.
+ /// Service responsible for content editing functionality.
+ /// Accessor for back office security context.
+ public CreateAndPublishDocumentController(
+ IAuthorizationService authorizationService,
+ IDocumentEditingPresentationFactory documentEditingPresentationFactory,
+ IContentEditingService contentEditingService,
+ IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
+ : base(authorizationService)
+ {
+ _authorizationService = authorizationService;
+ _documentEditingPresentationFactory = documentEditingPresentationFactory;
+ _contentEditingService = contentEditingService;
+ _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
+ }
+
+ ///
+ /// Creates a new document using the specified request model, and subsequently publishes the document in the cultures provided.
+ ///
+ /// Token to monitor for cancellation requests.
+ /// The details of the document to create.
+ /// An representing the result of the operation.
+ [HttpPost("create-and-publish")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ [EndpointSummary("Creates and publishes a new document.")]
+ [EndpointDescription("Creates and publishes a new document with the configuration specified in the request model.")]
+ public async Task Create(
+ CancellationToken cancellationToken,
+ CreateAndPublishDocumentRequestModel requestModel)
+ => await HandleRequest(requestModel, async () =>
+ {
+ // The base HandleRequest verifies the user can create under the parent.
+ // Creating-and-publishing additionally requires publish permission, so we check that here.
+ AuthorizationResult publishAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
+ User,
+ ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, requestModel.Parent?.Id, requestModel.CulturesToPublish),
+ AuthorizationPolicies.ContentPermissionByResource);
+
+ if (publishAuthorizationResult.Succeeded is false)
+ {
+ return Forbidden();
+ }
+
+ ContentCreateModel model = _documentEditingPresentationFactory.MapCreateModel(requestModel);
+ Attempt result =
+ await _contentEditingService.CreateAndPublishAsync(model, requestModel.CulturesToPublish, CurrentUserKey(_backOfficeSecurityAccessor));
+
+ return result.Success
+ ? CreatedAtId(controller => nameof(controller.ByKey), result.Result.Content!.Key)
+ : ContentEditingOperationStatusResult(result.Status);
+ });
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateAndPublishDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateAndPublishDocumentController.cs
new file mode 100644
index 000000000000..16410791ec99
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateAndPublishDocumentController.cs
@@ -0,0 +1,90 @@
+using Asp.Versioning;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.Factories;
+using Umbraco.Cms.Api.Management.ViewModels.Document;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Actions;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Security.Authorization;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.OperationStatus;
+using Umbraco.Cms.Web.Common.Authorization;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Api.Management.Controllers.Document;
+
+///
+/// Controller responsible for handling update-and-publish operations on documents in the management API.
+///
+[ApiVersion("1.0")]
+public class UpdateAndPublishDocumentController : UpdateDocumentControllerBase
+{
+ private readonly IAuthorizationService _authorizationService;
+ private readonly IContentEditingService _contentEditingService;
+ private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory;
+ private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Service for verifying user permissions to update and publish.
+ /// Service for managing content updates.
+ /// Factory for creating document editing presentation models.
+ /// Accessor for the back office user security context.
+ public UpdateAndPublishDocumentController(
+ IAuthorizationService authorizationService,
+ IContentEditingService contentEditingService,
+ IDocumentEditingPresentationFactory documentEditingPresentationFactory,
+ IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
+ : base(authorizationService)
+ {
+ _authorizationService = authorizationService;
+ _contentEditingService = contentEditingService;
+ _documentEditingPresentationFactory = documentEditingPresentationFactory;
+ _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
+ }
+
+ ///
+ /// Updates the specified document with new details provided in the request model, and subsequently publishes the document in the cultures provided.
+ ///
+ /// A token to monitor for cancellation requests.
+ /// The unique identifier of the document to update.
+ /// The model containing the updated document details.
+ /// An representing the outcome of the update operation.
+ [HttpPut("{id:guid}/update-and-publish")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ [EndpointSummary("Updates and publishes a document.")]
+ [EndpointDescription("Updates and publishes a document identified by the provided Id with the details from the request model.")]
+ public async Task Update(
+ CancellationToken cancellationToken,
+ Guid id,
+ UpdateAndPublishDocumentRequestModel requestModel)
+ => await HandleRequest(id, requestModel, async () =>
+ {
+ // The base HandleRequest verifies the user can update the document.
+ // Updating-and-publishing additionally requires publish permission, so we check that here.
+ AuthorizationResult publishAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
+ User,
+ ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id, requestModel.CulturesToPublish),
+ AuthorizationPolicies.ContentPermissionByResource);
+
+ if (publishAuthorizationResult.Succeeded is false)
+ {
+ return Forbidden();
+ }
+
+ ContentUpdateModel model = _documentEditingPresentationFactory.MapUpdateModel(requestModel);
+ Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor);
+ Attempt result = await _contentEditingService.UpdateAndPublishAsync(id, model, requestModel.CulturesToPublish, currentUserKey);
+
+ return result.Success
+ ? Ok()
+ : ContentEditingOperationStatusResult(result.Status);
+ });
+}
diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json
index 99f20ab4cc7b..b319a2407dad 100644
--- a/src/Umbraco.Cms.Api.Management/OpenApi.json
+++ b/src/Umbraco.Cms.Api.Management/OpenApi.json
@@ -11176,6 +11176,150 @@
]
}
},
+ "/umbraco/management/api/v1/document/{id}/update-and-publish": {
+ "put": {
+ "tags": [
+ "Document"
+ ],
+ "summary": "Updates and publishes a document.",
+ "description": "Updates and publishes a document identified by the provided Id with the details from the request model.",
+ "operationId": "PutDocumentByIdUpdateAndPublish",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/UpdateAndPublishDocumentRequestModel"
+ }
+ ]
+ }
+ },
+ "text/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/UpdateAndPublishDocumentRequestModel"
+ }
+ ]
+ }
+ },
+ "application/*+json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/UpdateAndPublishDocumentRequestModel"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "The resource is protected and requires an authentication token"
+ },
+ "403": {
+ "description": "The authenticated user does not have access to this resource",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "Backoffice-User": [ ]
+ }
+ ]
+ }
+ },
"/umbraco/management/api/v1.1/document/{id}/validate": {
"put": {
"tags": [
@@ -11426,6 +11570,154 @@
]
}
},
+ "/umbraco/management/api/v1/document/create-and-publish": {
+ "post": {
+ "tags": [
+ "Document"
+ ],
+ "summary": "Creates and publishes a new document.",
+ "description": "Creates and publishes a new document with the configuration specified in the request model.",
+ "operationId": "PostDocumentCreateAndPublish",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/CreateAndPublishDocumentRequestModel"
+ }
+ ]
+ }
+ },
+ "text/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/CreateAndPublishDocumentRequestModel"
+ }
+ ]
+ }
+ },
+ "application/*+json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/CreateAndPublishDocumentRequestModel"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Created",
+ "headers": {
+ "Umb-Generated-Resource": {
+ "description": "Identifier of the newly created resource",
+ "schema": {
+ "type": "string",
+ "description": "Identifier of the newly created resource"
+ }
+ },
+ "Location": {
+ "description": "Location of the newly created resource",
+ "schema": {
+ "type": "string",
+ "description": "Location of the newly created resource",
+ "format": "uri"
+ }
+ },
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "The resource is protected and requires an authentication token"
+ },
+ "403": {
+ "description": "The authenticated user does not have access to this resource",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "Backoffice-User": [ ]
+ }
+ ]
+ }
+ },
"/umbraco/management/api/v1/document/root/sort-children": {
"put": {
"tags": [
@@ -19415,7 +19707,7 @@
"Media"
],
"summary": "Sorts the children of a media item by a field.",
- "description": "Sorts the children of the specified parent media item by a system field in the given direction. Media items do not vary by culture, so any supplied culture is ignored.",
+ "description": "Sorts the children of the specified parent media item by a system field in the given direction.",
"operationId": "PutMediaByIdSortChildren",
"parameters": [
{
@@ -19809,7 +20101,7 @@
"Media"
],
"summary": "Sorts the root-level media items by a field.",
- "description": "Sorts the root-level media items by a system field in the given direction. Media items do not vary by culture, so any supplied culture is ignored.",
+ "description": "Sorts the root-level media items by a system field in the given direction.",
"operationId": "PutMediaRootSortChildren",
"requestBody": {
"content": {
@@ -40446,6 +40738,73 @@
},
"additionalProperties": false
},
+ "CreateAndPublishDocumentRequestModel": {
+ "required": [
+ "culturesToPublish",
+ "documentType",
+ "template",
+ "values",
+ "variants"
+ ],
+ "type": "object",
+ "properties": {
+ "values": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/DocumentValueModel"
+ }
+ ]
+ }
+ },
+ "variants": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/DocumentVariantRequestModel"
+ }
+ ]
+ }
+ },
+ "id": {
+ "type": "string",
+ "format": "uuid",
+ "nullable": true
+ },
+ "parent": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ReferenceByIdModel"
+ }
+ ],
+ "nullable": true
+ },
+ "documentType": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ReferenceByIdModel"
+ }
+ ]
+ },
+ "template": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ReferenceByIdModel"
+ }
+ ],
+ "nullable": true
+ },
+ "culturesToPublish": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
"CreateDataTypeRequestModel": {
"required": [
"editorAlias",
@@ -51723,6 +52082,51 @@
},
"additionalProperties": false
},
+ "UpdateAndPublishDocumentRequestModel": {
+ "required": [
+ "culturesToPublish",
+ "values",
+ "variants"
+ ],
+ "type": "object",
+ "properties": {
+ "values": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/DocumentValueModel"
+ }
+ ]
+ }
+ },
+ "variants": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/DocumentVariantRequestModel"
+ }
+ ]
+ }
+ },
+ "template": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ReferenceByIdModel"
+ }
+ ],
+ "nullable": true
+ },
+ "culturesToPublish": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
"UpdateCurrentUserRequestModel": {
"required": [
"languageIsoCode"
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/CreateAndPublishDocumentRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/CreateAndPublishDocumentRequestModel.cs
new file mode 100644
index 000000000000..244d623d0f5d
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/CreateAndPublishDocumentRequestModel.cs
@@ -0,0 +1,12 @@
+namespace Umbraco.Cms.Api.Management.ViewModels.Document;
+
+///
+/// Represents the API request model used for creating and publishing a new content document in Umbraco.
+///
+public class CreateAndPublishDocumentRequestModel : CreateDocumentRequestModel
+{
+ ///
+ /// The cultures to publish after creating the document.
+ ///
+ public string[] CulturesToPublish { get; set; } = [];
+}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/UpdateAndPublishDocumentRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/UpdateAndPublishDocumentRequestModel.cs
new file mode 100644
index 000000000000..7459e22dfaa5
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/UpdateAndPublishDocumentRequestModel.cs
@@ -0,0 +1,12 @@
+namespace Umbraco.Cms.Api.Management.ViewModels.Document;
+
+///
+/// Represents a request model used for updating and publishing a document via the API.
+///
+public class UpdateAndPublishDocumentRequestModel : UpdateDocumentRequestModel
+{
+ ///
+ /// The cultures to publish after updating the document.
+ ///
+ public string[] CulturesToPublish { get; set; } = [];
+}
diff --git a/src/Umbraco.Core/Services/ContentEditingService.cs b/src/Umbraco.Core/Services/ContentEditingService.cs
index e760d4fa0150..f7a13f27a8ef 100644
--- a/src/Umbraco.Core/Services/ContentEditingService.cs
+++ b/src/Umbraco.Core/Services/ContentEditingService.cs
@@ -143,6 +143,13 @@ public async Task
public async Task> CreateAsync(ContentCreateModel createModel, Guid userKey)
+ => await HandleCreateAsync(createModel, null, userKey);
+
+ ///
+ public async Task> CreateAndPublishAsync(ContentCreateModel createModel, string[] culturesToPublish, Guid userKey)
+ => await HandleCreateAsync(createModel, culturesToPublish, userKey);
+
+ private async Task> HandleCreateAsync(ContentCreateModel createModel, string[]? culturesToPublish, Guid userKey)
{
if (await ValidateCulturesAsync(createModel) is false)
{
@@ -167,7 +174,9 @@ public async Task> C
return Attempt.FailWithStatus(updateTemplateStatus, new ContentCreateResult { Content = content });
}
- ContentEditingOperationStatus saveStatus = await Save(content, userKey);
+ ContentEditingOperationStatus saveStatus = culturesToPublish is null
+ ? await Save(content, userKey)
+ : await SaveAndPublish(content, culturesToPublish, userKey);
return saveStatus == ContentEditingOperationStatus.Success
? Attempt.SucceedWithStatus(validationStatus, new ContentCreateResult { Content = content, ValidationResult = validationResult })
: Attempt.FailWithStatus(saveStatus, new ContentCreateResult { Content = content });
@@ -272,6 +281,13 @@ private async Task> GetAllowedCulturesForEditingUser(Guid userKe
///
public async Task> UpdateAsync(Guid key, ContentUpdateModel updateModel, Guid userKey)
+ => await HandleUpdateAsync(key, updateModel, null, userKey);
+
+ ///
+ public async Task> UpdateAndPublishAsync(Guid key, ContentUpdateModel updateModel, string[] culturesToPublish, Guid userKey)
+ => await HandleUpdateAsync(key, updateModel, culturesToPublish, userKey);
+
+ private async Task> HandleUpdateAsync(Guid key, ContentUpdateModel updateModel, string[]? culturesToPublish, Guid userKey)
{
IContent? content = ContentService.GetById(key);
if (content is null)
@@ -303,7 +319,9 @@ public async Task> U
return Attempt.FailWithStatus(updateTemplateStatus, new ContentUpdateResult { Content = content });
}
- ContentEditingOperationStatus saveStatus = await Save(content, userKey);
+ ContentEditingOperationStatus saveStatus = culturesToPublish is null
+ ? await Save(content, userKey)
+ : await SaveAndPublish(content, culturesToPublish, userKey);
return saveStatus == ContentEditingOperationStatus.Success
? Attempt.SucceedWithStatus(validationStatus, new ContentUpdateResult { Content = content, ValidationResult = validationResult })
: Attempt.FailWithStatus(saveStatus, new ContentUpdateResult { Content = content });
@@ -431,7 +449,31 @@ private async Task Save(IContent content, Guid us
OperationResultType.FailedCancelledByEvent => ContentEditingOperationStatus.CancelledByNotification,
// for any other state we'll return "unknown" so we know that we need to amend this
- _ => ContentEditingOperationStatus.Unknown
+ _ => ContentEditingOperationStatus.Unknown,
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Content save operation failed");
+ return ContentEditingOperationStatus.Unknown;
+ }
+ }
+
+ private async Task SaveAndPublish(IContent content, string[] culturesToPublish, Guid userKey)
+ {
+ try
+ {
+ var currentUserId = await GetUserIdAsync(userKey);
+ PublishResult publishResult = ContentService.SaveAndPublish(content, culturesToPublish, userId: currentUserId);
+ if (publishResult.Success)
+ {
+ return ContentEditingOperationStatus.Success;
+ }
+
+ return publishResult.Result switch
+ {
+ PublishResultType.FailedPublishCancelledByEvent => ContentEditingOperationStatus.CancelledByNotification,
+ _ => ContentEditingOperationStatus.Unknown,
};
}
catch (Exception ex)
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index 4e0308ce9d63..624e3f4f6f76 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -1233,11 +1233,7 @@ public OperationResult Save(IContent content, int? userId = null, ContentSchedul
$"Cannot save (un)publishing content with name: {content.Name} - and state: {content.PublishedState}, use the dedicated SavePublished method.");
}
- if (content.Name != null && content.Name.Length > 255)
- {
- throw new InvalidOperationException(
- $"Content with the name {content.Name} cannot be more than 255 characters in length.");
- }
+ EnsureNameLengthIsValid(content);
EventMessages eventMessages = EventMessagesFactory.Get();
@@ -1361,10 +1357,7 @@ public PublishResult Publish(IContent content, string[] cultures, int userId = C
throw new ArgumentNullException(nameof(cultures));
}
- if (cultures.Any(c => c.IsNullOrWhiteSpace()) || cultures.Distinct().Count() != cultures.Length)
- {
- throw new ArgumentException("Cultures cannot be null or whitespace", nameof(cultures));
- }
+ EnsureCulturesAreValid(cultures, nameof(cultures));
cultures = cultures.Select(x => x.EnsureCultureCode()!).ToArray();
@@ -1376,17 +1369,9 @@ public PublishResult Publish(IContent content, string[] cultures, int userId = C
return new PublishResult(PublishResultType.FailedPublishUnsavedChanges, evtMsgs, content);
}
- if (content.Name != null && content.Name.Length > 255)
- {
- throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
- }
+ EnsureNameLengthIsValid(content);
- PublishedState publishedState = content.PublishedState;
- if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
- {
- throw new InvalidOperationException(
- $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
- }
+ EnsurePublishedStateAllowsPublish(content);
// cannot accept invariant (null or empty) culture for variant content type
// cannot accept a specific culture for invariant content type (but '*' is ok)
@@ -1437,6 +1422,130 @@ public PublishResult Publish(IContent content, string[] cultures, int userId = C
}
}
+ ///
+ public PublishResult SaveAndPublish(IContent content, string[] culturesToPublish, int userId = Constants.Security.SuperUserId)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException(nameof(content));
+ }
+
+ if (culturesToPublish == null)
+ {
+ throw new ArgumentNullException(nameof(culturesToPublish));
+ }
+
+ // wildcards and nulls are not accepted here; cultures must be explicit
+ if (culturesToPublish.Any(x => x == null || x == "*"))
+ {
+ throw new InvalidOperationException(
+ "Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed");
+ }
+
+ EnsureCulturesAreValid(culturesToPublish, nameof(culturesToPublish));
+
+ culturesToPublish = culturesToPublish.Select(x => x.EnsureCultureCode()!).ToArray();
+
+ EnsureNameLengthIsValid(content);
+
+ EnsurePublishedStateAllowsPublish(content);
+
+ var varies = content.ContentType.VariesByCulture();
+ if (varies is false)
+ {
+ if (culturesToPublish.Length > 0)
+ {
+ throw new ArgumentException(
+ "Cultures cannot be specified when publishing invariant content types.",
+ nameof(culturesToPublish));
+ }
+
+ // doesn't vary; publish the invariant culture in a single scope alongside the save
+ return SaveAndPublish(content, userId: userId);
+ }
+
+ using ICoreScope scope = ScopeProvider.CreateCoreScope();
+ scope.WriteLock(Constants.Locks.ContentTree);
+
+ var allLangs = _languageRepository.GetMany().ToList();
+
+ EventMessages evtMsgs = EventMessagesFactory.Get();
+
+ var savingNotification = new ContentSavingNotification(content, evtMsgs);
+ if (scope.Notifications.PublishCancelable(savingNotification))
+ {
+ return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+ }
+
+ IEnumerable impacts =
+ culturesToPublish.Select(x => _cultureImpactFactory.ImpactExplicit(x, IsDefaultCulture(allLangs, x)));
+
+ // publish the culture(s)
+ // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
+ foreach (CultureImpact impact in impacts)
+ {
+ content.PublishCulture(impact, DateTime.UtcNow, _propertyEditorCollection);
+ }
+
+ PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+ scope.Complete();
+ return result;
+ }
+
+ private PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId)
+ {
+ EventMessages evtMsgs = EventMessagesFactory.Get();
+
+ EnsurePublishedStateAllowsPublish(content);
+
+ // cannot accept invariant (null or empty) culture for variant content type
+ // cannot accept a specific culture for invariant content type (but '*' is ok)
+ if (content.ContentType.VariesByCulture())
+ {
+ if (culture.IsNullOrWhiteSpace())
+ {
+ throw new NotSupportedException("Invariant culture is not supported by variant content types.");
+ }
+ }
+ else
+ {
+ if (!culture.IsNullOrWhiteSpace() && culture != "*")
+ {
+ throw new NotSupportedException(
+ $"Culture \"{culture}\" is not supported by invariant content types.");
+ }
+ }
+
+ EnsureNameLengthIsValid(content);
+
+ using ICoreScope scope = ScopeProvider.CreateCoreScope();
+ scope.WriteLock(Constants.Locks.ContentTree);
+
+ var allLangs = _languageRepository.GetMany().ToList();
+
+ // Change state to publishing
+ content.PublishedState = PublishedState.Publishing;
+ var savingNotification = new ContentSavingNotification(content, evtMsgs);
+ if (scope.Notifications.PublishCancelable(savingNotification))
+ {
+ return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+ }
+
+ // if culture is specific, first publish the invariant values, then publish the culture itself.
+ // if culture is '*', then publish them all (including variants)
+
+ // this will create the correct culture impact even if culture is * or null
+ var impact = _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content);
+
+ // publish the culture(s)
+ // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
+ content.PublishCulture(impact, DateTime.UtcNow, _propertyEditorCollection);
+
+ PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+ scope.Complete();
+ return result;
+ }
+
///
public PublishResult Unpublish(IContent content, string? culture = "*", int userId = Constants.Security.SuperUserId)
{
@@ -3285,6 +3394,34 @@ private OperationResult Sort(ICoreScope scope, IContent[] itemsA, int userId, Ev
private static bool HasUnsavedChanges(IContent content) => content.HasIdentity is false || content.IsDirty();
+ private const int MaxContentNameLength = 255;
+
+ private static void EnsureNameLengthIsValid(IContent content)
+ {
+ if (content.Name?.Length > MaxContentNameLength)
+ {
+ throw new InvalidOperationException($"Name cannot be more than {MaxContentNameLength} characters in length.");
+ }
+ }
+
+ private static void EnsureCulturesAreValid(string[] cultures, string paramName)
+ {
+ if (cultures.Any(c => c.IsNullOrWhiteSpace()) || cultures.Distinct().Count() != cultures.Length)
+ {
+ throw new ArgumentException("Cultures cannot be null or whitespace, and must be distinct.", paramName);
+ }
+ }
+
+ private static void EnsurePublishedStateAllowsPublish(IContent content)
+ {
+ PublishedState publishedState = content.PublishedState;
+ if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
+ {
+ throw new InvalidOperationException(
+ $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
+ }
+ }
+
///
/// Checks the data integrity of the content tree and optionally fixes issues.
///
diff --git a/src/Umbraco.Core/Services/IContentEditingService.cs b/src/Umbraco.Core/Services/IContentEditingService.cs
index 35c43ad18112..f0d297c654e6 100644
--- a/src/Umbraco.Core/Services/IContentEditingService.cs
+++ b/src/Umbraco.Core/Services/IContentEditingService.cs
@@ -41,6 +41,17 @@ public interface IContentEditingService
/// An attempt containing the creation result or an error status.
Task> CreateAsync(ContentCreateModel createModel, Guid userKey);
+ ///
+ /// Creates and publishes a new content item.
+ ///
+ /// The model containing the content data.
+ /// The cultures to publish.
+ /// The unique identifier of the user performing the action.
+ /// An attempt containing the creation result or an error status.
+ // TODO (V19): Remove default implementation.
+ Task> CreateAndPublishAsync(ContentCreateModel createModel, string[] culturesToPublish, Guid userKey)
+ => throw new NotImplementedException();
+
///
/// Updates an existing content item.
///
@@ -50,6 +61,18 @@ public interface IContentEditingService
/// An attempt containing the update result or an error status.
Task> UpdateAsync(Guid key, ContentUpdateModel updateModel, Guid userKey);
+ ///
+ /// Updates and publishes an existing content item.
+ ///
+ /// The unique identifier of the content item to update.
+ /// The model containing the updated content data.
+ /// The cultures to publish.
+ /// The unique identifier of the user performing the action.
+ /// An attempt containing the update result or an error status.
+ // TODO (V19): Remove default implementation.
+ Task> UpdateAndPublishAsync(Guid key, ContentUpdateModel updateModel, string[] culturesToPublish, Guid userKey)
+ => throw new NotImplementedException();
+
///
/// Moves a content item to the recycle bin.
///
diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs
index 91b134286a10..875b441c546b 100644
--- a/src/Umbraco.Core/Services/IContentService.cs
+++ b/src/Umbraco.Core/Services/IContentService.cs
@@ -575,6 +575,44 @@ OperationResult SortChildren(int parentId, IReadOnlyList orderedChildIds, i
/// The identifier of the user performing the action.
PublishResult Publish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId);
+ ///
+ /// Saves and publishes a document in a single scope.
+ ///
+ ///
+ ///
+ /// For invariant content types, must be empty; the document is
+ /// saved and the invariant culture is published.
+ ///
+ ///
+ /// For variant content types, only the cultures listed in are
+ /// published. Wildcards ("*"), nulls, whitespace and duplicate entries are not accepted. Passing
+ /// an empty array saves the document without publishing any culture.
+ ///
+ /// When a culture is being published, it includes all varying values along with all invariant values.
+ ///
+ /// The save and publish run in the same scope. If publishing fails for a business reason (for example,
+ /// invalid content or an expired schedule) the save still takes effect; both are skipped only when a
+ /// saving notification handler cancels the operation.
+ ///
+ ///
+ /// The document to publish.
+ /// The cultures to publish, or an empty array for invariant content.
+ /// The identifier of the user performing the action.
+ // TODO (V19): Remove the default implementation when the method is no longer new.
+ PublishResult SaveAndPublish(IContent content, string[] culturesToPublish, int userId = Constants.Security.SuperUserId)
+ {
+ OperationResult saveResult = Save(content, userId);
+ if (saveResult.Success)
+ {
+ return Publish(content, culturesToPublish, userId);
+ }
+
+ PublishResultType resultType = saveResult.Result == OperationResultType.FailedCancelledByEvent
+ ? PublishResultType.FailedPublishCancelledByEvent
+ : PublishResultType.FailedPublish;
+ return new PublishResult(resultType, saveResult.EventMessages, content);
+ }
+
///
/// Publishes a document branch.
///
diff --git a/src/Umbraco.Web.UI.Client/mocks/db/document-publishing.manager.ts b/src/Umbraco.Web.UI.Client/mocks/db/document-publishing.manager.ts
index 67d8f54ba498..cf61b5ba8abb 100644
--- a/src/Umbraco.Web.UI.Client/mocks/db/document-publishing.manager.ts
+++ b/src/Umbraco.Web.UI.Client/mocks/db/document-publishing.manager.ts
@@ -1,9 +1,11 @@
import type { UmbMockDocumentModel } from '../data/mock-data-set.types.js';
import type { UmbDocumentMockDB } from './document.db.js';
import type {
+ CreateAndPublishDocumentRequestModel,
PublishDocumentRequestModel,
PublishDocumentWithDescendantsRequestModel,
UnpublishDocumentRequestModel,
+ UpdateAndPublishDocumentRequestModel,
} from '@umbraco-cms/backoffice/external/backend-api';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type { DocumentVariantResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
@@ -55,6 +57,34 @@ export class UmbMockDocumentPublishingManager {
this.#documentDb.detail.update(id, document);
}
+ createAndPublish(data: CreateAndPublishDocumentRequestModel) {
+ const id = this.#documentDb.detail.create(data);
+ this.#publishCultures(id, data.culturesToPublish);
+ return id;
+ }
+
+ updateAndPublish(id: string, data: UpdateAndPublishDocumentRequestModel) {
+ this.#documentDb.detail.update(id, data);
+ this.#publishCultures(id, data.culturesToPublish);
+ }
+
+ #publishCultures(id: string, culturesToPublish: Array) {
+ const document: UmbMockDocumentModel = this.#documentDb.detail.read(id);
+
+ // Invariant content types publish with an empty cultures array; publish the invariant variant in that case.
+ const cultures: Array = culturesToPublish.length > 0 ? culturesToPublish : [null];
+
+ cultures.forEach((culture) => {
+ const variant = document.variants.find((x) => x.culture === culture);
+ if (variant) {
+ variant.state = 'Published' as UmbDocumentVariantState;
+ variant.updateDate = new Date().toISOString();
+ }
+ });
+
+ this.#documentDb.detail.update(id, document);
+ }
+
publishWithDescendants(id: string, data: PublishDocumentWithDescendantsRequestModel) {
const document: UmbMockDocumentModel = this.#documentDb.detail.read(id);
const documents = this.getDescendants(id, []);
@@ -64,10 +94,7 @@ export class UmbMockDocumentPublishingManager {
for (const culture of data.cultures) {
for (const d of documents) {
const variant = document.variants.find((x) => x.culture === culture);
- if (
- variant &&
- (data.includeUnpublishedDescendants || variant.state !== 'Published')
- ) {
+ if (variant && (data.includeUnpublishedDescendants || variant.state !== 'Published')) {
variant.state = 'Published' as UmbDocumentVariantState;
variant.updateDate = new Date().toISOString();
}
diff --git a/src/Umbraco.Web.UI.Client/mocks/msw-handlers/document/detail.handlers.ts b/src/Umbraco.Web.UI.Client/mocks/msw-handlers/document/detail.handlers.ts
index c9d891ea1d77..5f12b4f463f2 100644
--- a/src/Umbraco.Web.UI.Client/mocks/msw-handlers/document/detail.handlers.ts
+++ b/src/Umbraco.Web.UI.Client/mocks/msw-handlers/document/detail.handlers.ts
@@ -4,11 +4,13 @@ import { umbMockManager } from '../../mock-manager.js';
import { umbDocumentMockDb } from '../../db/document.db.js';
import { UMB_SLUG } from './slug.js';
import type {
+ CreateAndPublishDocumentRequestModel,
CreateDocumentRequestModel,
DefaultReferenceResponseModel,
GetDocumentByIdAvailableSegmentOptionsResponse,
GetDocumentByIdReferencedDescendantsResponse,
PagedIReferenceResponseModel,
+ UpdateAndPublishDocumentRequestModel,
UpdateDocumentRequestModel,
} from '@umbraco-cms/backoffice/external/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
@@ -38,6 +40,21 @@ export const detailHandlers = [
});
}),
+ http.post(umbracoPath(`${UMB_SLUG}/create-and-publish`), async ({ request }) => {
+ const requestBody = (await request.json()) as CreateAndPublishDocumentRequestModel;
+ if (!requestBody) return new HttpResponse(null, { status: 400, statusText: 'no body found' });
+
+ const id = umbDocumentMockDb.publishing.createAndPublish(requestBody);
+
+ return HttpResponse.json(null, {
+ status: 201,
+ headers: {
+ Location: request.url + '/' + id,
+ 'Umb-Generated-Resource': id,
+ },
+ });
+ }),
+
http.get(umbracoPath(`${UMB_SLUG}/configuration`), () => {
return HttpResponse.json(umbDocumentMockDb.getConfiguration());
}),
@@ -155,6 +172,19 @@ export const detailHandlers = [
}
}),
+ http.put(umbracoPath(`${UMB_SLUG}/:id/update-and-publish`), async ({ request, params }) => {
+ const id = params.id as string;
+ if (!id) return new HttpResponse(null, { status: 400 });
+ if (id === 'forbidden') {
+ // Simulate a forbidden response
+ return new HttpResponse(null, { status: 403 });
+ }
+ const requestBody = (await request.json()) as UpdateAndPublishDocumentRequestModel;
+ if (!requestBody) return new HttpResponse(null, { status: 400, statusText: 'no body found' });
+ umbDocumentMockDb.publishing.updateAndPublish(id, requestBody);
+ return new HttpResponse(null, { status: 200 });
+ }),
+
http.put(umbracoPath(`${UMB_SLUG}/:id`), async ({ request, params }) => {
const id = params.id as string;
if (!id) return new HttpResponse(null, { status: 400 });
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
index 93ae938e7ccf..8796ae02c3f7 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
@@ -1542,9 +1542,11 @@ export default {
cssSavedText: 'Stylesheet saved without any errors',
dataTypeSaved: 'Datatype saved',
dictionaryItemSaved: 'Dictionary item saved',
+ editContentPublishedFailed: 'Document could not be published or saved',
editContentPublishedFailedByValidation: 'Document could not be published, but we saved it for you',
editContentPublishedFailedByParent: 'Document could not be published, because a parent page is not published',
editContentPublishedHeader: 'Document published',
+ editContentPublishedReloadFailed: 'Document published, but the editor could not be refreshed',
editContentPublishedText: 'and is visible on the website',
editContentUnpublishedHeader: 'Document unpublished',
editContentUnpublishedText: 'and is no longer visible on the website',
@@ -2204,7 +2206,7 @@ export default {
updateDate: 'User last updated',
userCreated: 'has been created',
userCreatedSuccessHelp: 'The new user has successfully been created. To log in to Umbraco use the password below.',
- userCreatedApiSuccessHelp: 'Set client credentials for the account via the user\'s profile.',
+ userCreatedApiSuccessHelp: "Set client credentials for the account via the user's profile.",
userHasPassword: 'The user already has a password set',
userHasGroup: "The user is already in group '%0%'",
userLockoutNotEnabled: 'Lockout is not enabled for this user',
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts
index 333a5124837d..54a1651899ce 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts
@@ -13,6 +13,7 @@ import {
UmbWorkspaceIsNewRedirectController,
type ManifestWorkspace,
UmbWorkspaceIsNewRedirectControllerAlias,
+ umbWorkspaceWillNavigateAway,
} from '@umbraco-cms/backoffice/workspace';
import {
UmbBooleanState,
@@ -351,10 +352,7 @@ export class UmbBlockWorkspaceContext, UmbContentVariantPickerValue>;
}
+/**
+ * Interface for the third argument of performCreateOrUpdate, relevant if the persistence method should be different from default.
+ */
+export interface UmbContentWorkspaceCreateOrUpdatePersistMethods {
+ create?: (
+ saveData: DetailModelType,
+ variantIds: Array,
+ parent: UmbEntityModel,
+ ) => Promise;
+ update?: (saveData: DetailModelType, variantIds: Array) => Promise;
+}
+
/**
* The base class for a content detail workspace context.
* @exports
@@ -96,16 +110,16 @@ export interface UmbContentDetailWorkspaceContextArgs<
* @template CreateArgsType
*/
export abstract class UmbContentDetailWorkspaceContextBase<
- DetailModelType extends UmbContentDetailModel,
- DetailRepositoryType extends UmbDetailRepository = UmbDetailRepository,
- ContentTypeDetailModelType extends UmbContentTypeDetailModel = UmbContentTypeDetailModel,
- VariantModelType extends UmbEntityVariantModel = DetailModelType extends { variants: UmbEntityVariantModel[] }
- ? DetailModelType['variants'][0]
- : never,
- VariantOptionModelType extends UmbEntityVariantOptionModel = UmbEntityVariantOptionModel,
- CreateArgsType extends
- UmbEntityDetailWorkspaceContextCreateArgs = UmbEntityDetailWorkspaceContextCreateArgs,
- >
+ DetailModelType extends UmbContentDetailModel,
+ DetailRepositoryType extends UmbDetailRepository = UmbDetailRepository,
+ ContentTypeDetailModelType extends UmbContentTypeDetailModel = UmbContentTypeDetailModel,
+ VariantModelType extends UmbEntityVariantModel = DetailModelType extends { variants: UmbEntityVariantModel[] }
+ ? DetailModelType['variants'][0]
+ : never,
+ VariantOptionModelType extends UmbEntityVariantOptionModel = UmbEntityVariantOptionModel,
+ CreateArgsType extends UmbEntityDetailWorkspaceContextCreateArgs =
+ UmbEntityDetailWorkspaceContextCreateArgs,
+>
extends UmbEntityDetailWorkspaceContextBase
implements
UmbContentWorkspaceContext,
@@ -1060,119 +1074,113 @@ export abstract class UmbContentDetailWorkspaceContextBase<
* Perform the create or update of the content
* @param {Array} variantIds - The variant ids to save
* @param {DetailModelType} saveData - The data to save
+ * @param {UmbContentWorkspaceCreateOrUpdatePersistMethods} [persistenceMethod] - Optional custom persistence logic.
* @memberof UmbContentDetailWorkspaceContextBase
*/
- public async performCreateOrUpdate(variantIds: Array, saveData: DetailModelType) {
+ public async performCreateOrUpdate(
+ variantIds: Array,
+ saveData: DetailModelType,
+ persistenceMethod?: UmbContentWorkspaceCreateOrUpdatePersistMethods,
+ ) {
if (this.getIsNew()) {
- await this.#create(variantIds, saveData);
+ await this.#create(variantIds, saveData, persistenceMethod?.create);
} else {
- await this.#update(variantIds, saveData);
+ await this.#update(variantIds, saveData, persistenceMethod?.update);
}
}
- async #create(variantIds: Array, saveData: DetailModelType) {
- if (!this._detailRepository) throw new Error('Detail repository is not set');
-
+ async #create(
+ variantIds: Array,
+ saveData: DetailModelType,
+ overwriteCreate?: UmbContentWorkspaceCreateOrUpdatePersistMethods['create'],
+ ) {
const parent = this._internal_getCreateUnderParent();
if (!parent) throw new Error('Parent is not set');
- const { data, error } = await this._detailRepository.create(saveData, parent.unique);
- if (!data || error) {
- throw new Error('Error creating content');
- }
-
- const variantIdsIncludingInvariant = [...variantIds, UmbVariantId.CreateInvariant()];
+ const persisted = overwriteCreate
+ ? await overwriteCreate(saveData, variantIds, parent)
+ : await this.#defaultCreatePersistence(saveData, parent.unique);
- // Only update the variants that was chosen to be saved:
- const persistedData = this._data.getCurrent();
- const newPersistedData = await new UmbMergeContentVariantDataController(this).process(
- persistedData,
- data,
- variantIds,
- variantIdsIncludingInvariant,
- );
- this._data.setPersisted(newPersistedData);
-
- // Only update the variants that was chosen to be saved:
- const currentData = this._data.getCurrent();
- const newCurrentData = await new UmbMergeContentVariantDataController(this).process(
- currentData,
- data,
- variantIds,
- variantIdsIncludingInvariant,
- );
- this._data.setCurrent(newCurrentData);
+ // Set persisted AND current before flipping isNew: the flip triggers the new->edit redirect,
+ // whose navigation guard compares the two states with an order-sensitive comparison.
+ await this.#applyPersistedData(persisted, variantIds);
this.setIsNew(false);
- const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
- if (!eventContext) {
- throw new Error('Event context is missing');
- }
-
- const reloadStructureEvent = new UmbRequestReloadStructureForEntityEvent({
- entityType: parent.entityType,
- unique: parent.unique,
- });
+ await this.#dispatchActionEvents(
+ new UmbRequestReloadStructureForEntityEvent({ entityType: parent.entityType, unique: parent.unique }),
+ new UmbRequestReloadChildrenOfEntityEvent({ entityType: parent.entityType, unique: parent.unique }),
+ );
+ }
- eventContext.dispatchEvent(reloadStructureEvent);
+ async #update(
+ variantIds: Array,
+ saveData: DetailModelType,
+ overwriteUpdate?: UmbContentWorkspaceCreateOrUpdatePersistMethods['update'],
+ ) {
+ const persisted = overwriteUpdate
+ ? await overwriteUpdate(saveData, variantIds)
+ : await this.#defaultUpdatePersistence(saveData);
- const reloadChildrenEvent = new UmbRequestReloadChildrenOfEntityEvent({
- entityType: parent.entityType,
- unique: parent.unique,
- });
+ await this.#applyPersistedData(persisted, variantIds);
- eventContext.dispatchEvent(reloadChildrenEvent);
+ const unique = this.getUnique();
+ if (!unique) {
+ return;
+ }
+ const entityType = this.getEntityType();
+ await this.#dispatchActionEvents(
+ new UmbRequestReloadStructureForEntityEvent({ unique, entityType }),
+ new UmbEntityUpdatedEvent({ unique, entityType, eventUnique: this._workspaceEventUnique }),
+ );
}
- async #update(variantIds: Array, saveData: DetailModelType) {
+ async #defaultCreatePersistence(saveData: DetailModelType, parentUnique: string | null): Promise {
if (!this._detailRepository) throw new Error('Detail repository is not set');
+ const { data, error } = await this._detailRepository.create(saveData, parentUnique);
+ if (!data || error) throw new Error('Error creating content');
+ return data;
+ }
+ async #defaultUpdatePersistence(saveData: DetailModelType): Promise {
+ if (!this._detailRepository) throw new Error('Detail repository is not set');
const { data, error } = await this._detailRepository.save(saveData);
- if (!data || error) {
- throw new Error('Error saving content');
+ if (!data || error) throw new Error('Error saving content');
+ return data;
+ }
+
+ /**
+ * Partial update the current data with the persisted data, only for the variants that were saved.
+ * @param {DetailModelType | undefined} persisted - The saved document, or undefined if not returned
+ * @param {Array} variantIds - The variants that were saved
+ */
+ async #applyPersistedData(persisted: DetailModelType | undefined, variantIds: Array) {
+ if (!persisted) {
+ throw new Error('Persisted data is missing, persistence methods should return latest draft from the server.');
}
const variantIdsIncludingInvariant = [...variantIds, UmbVariantId.CreateInvariant()];
- // Only update the variants that was chosen to be saved:
- // Use getPersisted() as the base so non-saved variants retain the actual
- // server state, not unsaved local edits from current data.
- const persistedData = this._data.getPersisted();
const newPersistedData = await new UmbMergeContentVariantDataController(this).process(
- persistedData,
- data,
+ this._data.getPersisted(),
+ persisted,
variantIds,
variantIdsIncludingInvariant,
);
this._data.setPersisted(newPersistedData);
- // Only update the variants that was chosen to be saved:
- const currentData = this._data.getCurrent();
const newCurrentData = await new UmbMergeContentVariantDataController(this).process(
- currentData,
- data,
+ this._data.getCurrent(),
+ persisted,
variantIds,
variantIdsIncludingInvariant,
);
this._data.setCurrent(newCurrentData);
+ }
- const unique = this.getUnique()!;
- const entityType = this.getEntityType();
-
+ async #dispatchActionEvents(...events: Array) {
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
- if (!eventContext) {
- throw new Error('Event context is missing');
- }
- const structureEvent = new UmbRequestReloadStructureForEntityEvent({ unique, entityType });
- eventContext.dispatchEvent(structureEvent);
-
- const updatedEvent = new UmbEntityUpdatedEvent({
- unique,
- entityType,
- eventUnique: this._workspaceEventUnique,
- });
-
- eventContext.dispatchEvent(updatedEvent);
+ if (!eventContext) throw new Error('Event context is missing');
+ events.forEach((event) => eventContext.dispatchEvent(event));
}
override resetState() {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
index e68b0957e9d6..c18f7224ae90 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
@@ -2,7 +2,7 @@
import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client';
import { client } from './client.gen';
-import type { DeleteDataTypeByIdData, DeleteDataTypeByIdErrors, DeleteDataTypeByIdResponses, DeleteDataTypeFolderByIdData, DeleteDataTypeFolderByIdErrors, DeleteDataTypeFolderByIdResponses, DeleteDictionaryByIdData, DeleteDictionaryByIdErrors, DeleteDictionaryByIdResponses, DeleteDocumentBlueprintByIdData, DeleteDocumentBlueprintByIdErrors, DeleteDocumentBlueprintByIdResponses, DeleteDocumentBlueprintFolderByIdData, DeleteDocumentBlueprintFolderByIdErrors, DeleteDocumentBlueprintFolderByIdResponses, DeleteDocumentByIdData, DeleteDocumentByIdErrors, DeleteDocumentByIdPublicAccessData, DeleteDocumentByIdPublicAccessErrors, DeleteDocumentByIdPublicAccessResponses, DeleteDocumentByIdResponses, DeleteDocumentTypeByIdData, DeleteDocumentTypeByIdErrors, DeleteDocumentTypeByIdResponses, DeleteDocumentTypeFolderByIdData, DeleteDocumentTypeFolderByIdErrors, DeleteDocumentTypeFolderByIdResponses, DeleteLanguageByIsoCodeData, DeleteLanguageByIsoCodeErrors, DeleteLanguageByIsoCodeResponses, DeleteLogViewerSavedSearchByNameData, DeleteLogViewerSavedSearchByNameErrors, DeleteLogViewerSavedSearchByNameResponses, DeleteMediaByIdData, DeleteMediaByIdErrors, DeleteMediaByIdResponses, DeleteMediaTypeByIdData, DeleteMediaTypeByIdErrors, DeleteMediaTypeByIdResponses, DeleteMediaTypeFolderByIdData, DeleteMediaTypeFolderByIdErrors, DeleteMediaTypeFolderByIdResponses, DeleteMemberByIdData, DeleteMemberByIdErrors, DeleteMemberByIdResponses, DeleteMemberGroupByIdData, DeleteMemberGroupByIdErrors, DeleteMemberGroupByIdResponses, DeleteMemberTypeByIdData, DeleteMemberTypeByIdErrors, DeleteMemberTypeByIdResponses, DeleteMemberTypeFolderByIdData, DeleteMemberTypeFolderByIdErrors, DeleteMemberTypeFolderByIdResponses, DeletePackageCreatedByIdData, DeletePackageCreatedByIdErrors, DeletePackageCreatedByIdResponses, DeletePartialViewByPathData, DeletePartialViewByPathErrors, DeletePartialViewByPathResponses, DeletePartialViewFolderByPathData, DeletePartialViewFolderByPathErrors, DeletePartialViewFolderByPathResponses, DeletePreviewData, DeletePreviewResponses, DeleteRecycleBinDocumentByIdData, DeleteRecycleBinDocumentByIdErrors, DeleteRecycleBinDocumentByIdResponses, DeleteRecycleBinDocumentData, DeleteRecycleBinDocumentErrors, DeleteRecycleBinDocumentResponses, DeleteRecycleBinMediaByIdData, DeleteRecycleBinMediaByIdErrors, DeleteRecycleBinMediaByIdResponses, DeleteRecycleBinMediaData, DeleteRecycleBinMediaErrors, DeleteRecycleBinMediaResponses, DeleteRedirectManagementByIdData, DeleteRedirectManagementByIdErrors, DeleteRedirectManagementByIdResponses, DeleteScriptByPathData, DeleteScriptByPathErrors, DeleteScriptByPathResponses, DeleteScriptFolderByPathData, DeleteScriptFolderByPathErrors, DeleteScriptFolderByPathResponses, DeleteStylesheetByPathData, DeleteStylesheetByPathErrors, DeleteStylesheetByPathResponses, DeleteStylesheetFolderByPathData, DeleteStylesheetFolderByPathErrors, DeleteStylesheetFolderByPathResponses, DeleteTemplateByIdData, DeleteTemplateByIdErrors, DeleteTemplateByIdResponses, DeleteTemporaryFileByIdData, DeleteTemporaryFileByIdErrors, DeleteTemporaryFileByIdResponses, DeleteUserAvatarByIdData, DeleteUserAvatarByIdErrors, DeleteUserAvatarByIdResponses, DeleteUserById2FaByProviderNameData, DeleteUserById2FaByProviderNameErrors, DeleteUserById2FaByProviderNameResponses, DeleteUserByIdClientCredentialsByClientIdData, DeleteUserByIdClientCredentialsByClientIdErrors, DeleteUserByIdClientCredentialsByClientIdResponses, DeleteUserByIdData, DeleteUserByIdErrors, DeleteUserByIdResponses, DeleteUserCurrent2FaByProviderNameData, DeleteUserCurrent2FaByProviderNameErrors, DeleteUserCurrent2FaByProviderNameResponses, DeleteUserCurrentAvatarData, DeleteUserCurrentAvatarErrors, DeleteUserCurrentAvatarResponses, DeleteUserData, DeleteUserDataByIdData, DeleteUserDataByIdErrors, DeleteUserDataByIdResponses, DeleteUserErrors, DeleteUserGroupByIdData, DeleteUserGroupByIdErrors, DeleteUserGroupByIdResponses, DeleteUserGroupByIdUsersData, DeleteUserGroupByIdUsersErrors, DeleteUserGroupByIdUsersResponses, DeleteUserGroupData, DeleteUserGroupErrors, DeleteUserGroupResponses, DeleteUserResponses, DeleteWebhookByIdData, DeleteWebhookByIdErrors, DeleteWebhookByIdResponses, GetCollectionDocumentByIdData, GetCollectionDocumentByIdErrors, GetCollectionDocumentByIdResponses, GetCollectionMediaData, GetCollectionMediaErrors, GetCollectionMediaResponses, GetCultureData, GetCultureErrors, GetCultureResponses, GetDataTypeBatchData, GetDataTypeBatchErrors, GetDataTypeBatchResponses, GetDataTypeByIdData, GetDataTypeByIdErrors, GetDataTypeByIdIsUsedData, GetDataTypeByIdIsUsedErrors, GetDataTypeByIdIsUsedResponses, GetDataTypeByIdReferencedByData, GetDataTypeByIdReferencedByErrors, GetDataTypeByIdReferencedByResponses, GetDataTypeByIdResponses, GetDataTypeByIdSchemaData, GetDataTypeByIdSchemaErrors, GetDataTypeByIdSchemaResponses, GetDataTypeConfigurationData, GetDataTypeConfigurationErrors, GetDataTypeConfigurationResponses, GetDataTypeFolderByIdData, GetDataTypeFolderByIdErrors, GetDataTypeFolderByIdResponses, GetDataTypeSchemasBatchData, GetDataTypeSchemasBatchErrors, GetDataTypeSchemasBatchResponses, GetDictionaryByIdData, GetDictionaryByIdErrors, GetDictionaryByIdExportData, GetDictionaryByIdExportErrors, GetDictionaryByIdExportResponses, GetDictionaryByIdResponses, GetDictionaryData, GetDictionaryErrors, GetDictionaryResponses, GetDocumentAreReferencedData, GetDocumentAreReferencedErrors, GetDocumentAreReferencedResponses, GetDocumentBlueprintByIdAuditLogData, GetDocumentBlueprintByIdAuditLogErrors, GetDocumentBlueprintByIdAuditLogResponses, GetDocumentBlueprintByIdData, GetDocumentBlueprintByIdErrors, GetDocumentBlueprintByIdResponses, GetDocumentBlueprintByIdScaffoldData, GetDocumentBlueprintByIdScaffoldErrors, GetDocumentBlueprintByIdScaffoldResponses, GetDocumentBlueprintFolderByIdData, GetDocumentBlueprintFolderByIdErrors, GetDocumentBlueprintFolderByIdResponses, GetDocumentByIdAuditLogData, GetDocumentByIdAuditLogErrors, GetDocumentByIdAuditLogResponses, GetDocumentByIdAvailableSegmentOptionsData, GetDocumentByIdAvailableSegmentOptionsErrors, GetDocumentByIdAvailableSegmentOptionsResponses, GetDocumentByIdData, GetDocumentByIdDomainsData, GetDocumentByIdDomainsErrors, GetDocumentByIdDomainsResponses, GetDocumentByIdErrors, GetDocumentByIdNotificationsData, GetDocumentByIdNotificationsErrors, GetDocumentByIdNotificationsResponses, GetDocumentByIdPreviewUrlData, GetDocumentByIdPreviewUrlErrors, GetDocumentByIdPreviewUrlResponses, GetDocumentByIdPublicAccessData, GetDocumentByIdPublicAccessErrors, GetDocumentByIdPublicAccessResponses, GetDocumentByIdPublishedData, GetDocumentByIdPublishedErrors, GetDocumentByIdPublishedResponses, GetDocumentByIdPublishWithDescendantsResultByTaskIdData, GetDocumentByIdPublishWithDescendantsResultByTaskIdErrors, GetDocumentByIdPublishWithDescendantsResultByTaskIdResponses, GetDocumentByIdReferencedByData, GetDocumentByIdReferencedByErrors, GetDocumentByIdReferencedByResponses, GetDocumentByIdReferencedDescendantsData, GetDocumentByIdReferencedDescendantsErrors, GetDocumentByIdReferencedDescendantsResponses, GetDocumentByIdResponses, GetDocumentConfigurationData, GetDocumentConfigurationErrors, GetDocumentConfigurationResponses, GetDocumentTypeAllowedAtRootData, GetDocumentTypeAllowedAtRootErrors, GetDocumentTypeAllowedAtRootResponses, GetDocumentTypeBatchData, GetDocumentTypeBatchErrors, GetDocumentTypeBatchResponses, GetDocumentTypeByIdAllowedChildrenData, GetDocumentTypeByIdAllowedChildrenErrors, GetDocumentTypeByIdAllowedChildrenResponses, GetDocumentTypeByIdAllowedParentsData, GetDocumentTypeByIdAllowedParentsErrors, GetDocumentTypeByIdAllowedParentsResponses, GetDocumentTypeByIdBlueprintData, GetDocumentTypeByIdBlueprintErrors, GetDocumentTypeByIdBlueprintResponses, GetDocumentTypeByIdCompositionReferencesData, GetDocumentTypeByIdCompositionReferencesErrors, GetDocumentTypeByIdCompositionReferencesResponses, GetDocumentTypeByIdData, GetDocumentTypeByIdErrors, GetDocumentTypeByIdExportData, GetDocumentTypeByIdExportErrors, GetDocumentTypeByIdExportResponses, GetDocumentTypeByIdResponses, GetDocumentTypeByIdSchemaData, GetDocumentTypeByIdSchemaErrors, GetDocumentTypeByIdSchemaResponses, GetDocumentTypeConfigurationData, GetDocumentTypeConfigurationErrors, GetDocumentTypeConfigurationResponses, GetDocumentTypeFolderByIdData, GetDocumentTypeFolderByIdErrors, GetDocumentTypeFolderByIdResponses, GetDocumentUrlsData, GetDocumentUrlsErrors, GetDocumentUrlsResponses, GetDocumentVersionByIdData, GetDocumentVersionByIdErrors, GetDocumentVersionByIdResponses, GetDocumentVersionData, GetDocumentVersionErrors, GetDocumentVersionResponses, GetDynamicRootStepsData, GetDynamicRootStepsErrors, GetDynamicRootStepsResponses, GetFilterDataTypeData, GetFilterDataTypeErrors, GetFilterDataTypeResponses, GetFilterMemberData, GetFilterMemberErrors, GetFilterMemberResponses, GetFilterUserData, GetFilterUserErrors, GetFilterUserGroupData, GetFilterUserGroupErrors, GetFilterUserGroupResponses, GetFilterUserResponses, GetHealthCheckGroupByNameData, GetHealthCheckGroupByNameErrors, GetHealthCheckGroupByNameResponses, GetHealthCheckGroupData, GetHealthCheckGroupErrors, GetHealthCheckGroupResponses, GetHelpData, GetHelpErrors, GetHelpResponses, GetImagingResizeUrlsData, GetImagingResizeUrlsErrors, GetImagingResizeUrlsResponses, GetImportAnalyzeData, GetImportAnalyzeErrors, GetImportAnalyzeResponses, GetIndexerByIndexNameData, GetIndexerByIndexNameErrors, GetIndexerByIndexNameResponses, GetIndexerData, GetIndexerErrors, GetIndexerResponses, GetInstallSettingsData, GetInstallSettingsErrors, GetInstallSettingsResponses, GetItemDataTypeAncestorsData, GetItemDataTypeAncestorsErrors, GetItemDataTypeAncestorsResponses, GetItemDataTypeData, GetItemDataTypeErrors, GetItemDataTypeResponses, GetItemDataTypeSearchData, GetItemDataTypeSearchErrors, GetItemDataTypeSearchResponses, GetItemDictionaryData, GetItemDictionaryErrors, GetItemDictionaryResponses, GetItemDocumentAncestorsData, GetItemDocumentAncestorsErrors, GetItemDocumentAncestorsResponses, GetItemDocumentBlueprintData, GetItemDocumentBlueprintErrors, GetItemDocumentBlueprintResponses, GetItemDocumentData, GetItemDocumentErrors, GetItemDocumentResponses, GetItemDocumentSearchData, GetItemDocumentSearchErrors, GetItemDocumentSearchResponses, GetItemDocumentTypeAncestorsData, GetItemDocumentTypeAncestorsErrors, GetItemDocumentTypeAncestorsResponses, GetItemDocumentTypeData, GetItemDocumentTypeErrors, GetItemDocumentTypeResponses, GetItemDocumentTypeSearchData, GetItemDocumentTypeSearchErrors, GetItemDocumentTypeSearchResponses, GetItemLanguageData, GetItemLanguageDefaultData, GetItemLanguageDefaultErrors, GetItemLanguageDefaultResponses, GetItemLanguageErrors, GetItemLanguageResponses, GetItemMediaAncestorsData, GetItemMediaAncestorsErrors, GetItemMediaAncestorsResponses, GetItemMediaData, GetItemMediaErrors, GetItemMediaResponses, GetItemMediaSearchData, GetItemMediaSearchErrors, GetItemMediaSearchResponses, GetItemMediaTypeAllowedData, GetItemMediaTypeAllowedErrors, GetItemMediaTypeAllowedResponses, GetItemMediaTypeAncestorsData, GetItemMediaTypeAncestorsErrors, GetItemMediaTypeAncestorsResponses, GetItemMediaTypeData, GetItemMediaTypeErrors, GetItemMediaTypeFoldersData, GetItemMediaTypeFoldersErrors, GetItemMediaTypeFoldersResponses, GetItemMediaTypeResponses, GetItemMediaTypeSearchData, GetItemMediaTypeSearchErrors, GetItemMediaTypeSearchResponses, GetItemMemberAncestorsData, GetItemMemberAncestorsErrors, GetItemMemberAncestorsResponses, GetItemMemberData, GetItemMemberErrors, GetItemMemberGroupData, GetItemMemberGroupErrors, GetItemMemberGroupResponses, GetItemMemberResponses, GetItemMemberSearchData, GetItemMemberSearchErrors, GetItemMemberSearchResponses, GetItemMemberTypeAncestorsData, GetItemMemberTypeAncestorsErrors, GetItemMemberTypeAncestorsResponses, GetItemMemberTypeData, GetItemMemberTypeErrors, GetItemMemberTypeResponses, GetItemMemberTypeSearchData, GetItemMemberTypeSearchErrors, GetItemMemberTypeSearchResponses, GetItemPartialViewData, GetItemPartialViewErrors, GetItemPartialViewResponses, GetItemRelationTypeData, GetItemRelationTypeErrors, GetItemRelationTypeResponses, GetItemScriptData, GetItemScriptErrors, GetItemScriptResponses, GetItemStaticFileData, GetItemStaticFileErrors, GetItemStaticFileResponses, GetItemStylesheetData, GetItemStylesheetErrors, GetItemStylesheetResponses, GetItemTemplateAncestorsData, GetItemTemplateAncestorsErrors, GetItemTemplateAncestorsResponses, GetItemTemplateData, GetItemTemplateErrors, GetItemTemplateResponses, GetItemTemplateSearchData, GetItemTemplateSearchErrors, GetItemTemplateSearchResponses, GetItemUserData, GetItemUserErrors, GetItemUserGroupData, GetItemUserGroupErrors, GetItemUserGroupResponses, GetItemUserResponses, GetItemWebhookData, GetItemWebhookErrors, GetItemWebhookResponses, GetLanguageByIsoCodeData, GetLanguageByIsoCodeErrors, GetLanguageByIsoCodeResponses, GetLanguageData, GetLanguageErrors, GetLanguageResponses, GetLogViewerLevelCountData, GetLogViewerLevelCountErrors, GetLogViewerLevelCountResponses, GetLogViewerLevelData, GetLogViewerLevelErrors, GetLogViewerLevelResponses, GetLogViewerLogData, GetLogViewerLogErrors, GetLogViewerLogResponses, GetLogViewerMessageTemplateData, GetLogViewerMessageTemplateErrors, GetLogViewerMessageTemplateResponses, GetLogViewerSavedSearchByNameData, GetLogViewerSavedSearchByNameErrors, GetLogViewerSavedSearchByNameResponses, GetLogViewerSavedSearchData, GetLogViewerSavedSearchErrors, GetLogViewerSavedSearchResponses, GetLogViewerValidateLogsSizeData, GetLogViewerValidateLogsSizeErrors, GetLogViewerValidateLogsSizeResponses, GetManifestManifestData, GetManifestManifestErrors, GetManifestManifestPrivateData, GetManifestManifestPrivateErrors, GetManifestManifestPrivateResponses, GetManifestManifestPublicData, GetManifestManifestPublicResponses, GetManifestManifestResponses, GetMediaAreReferencedData, GetMediaAreReferencedErrors, GetMediaAreReferencedResponses, GetMediaByIdAuditLogData, GetMediaByIdAuditLogErrors, GetMediaByIdAuditLogResponses, GetMediaByIdData, GetMediaByIdErrors, GetMediaByIdReferencedByData, GetMediaByIdReferencedByErrors, GetMediaByIdReferencedByResponses, GetMediaByIdReferencedDescendantsData, GetMediaByIdReferencedDescendantsErrors, GetMediaByIdReferencedDescendantsResponses, GetMediaByIdResponses, GetMediaConfigurationData, GetMediaConfigurationErrors, GetMediaConfigurationResponses, GetMediaTypeAllowedAtRootData, GetMediaTypeAllowedAtRootErrors, GetMediaTypeAllowedAtRootResponses, GetMediaTypeBatchData, GetMediaTypeBatchErrors, GetMediaTypeBatchResponses, GetMediaTypeByIdAllowedChildrenData, GetMediaTypeByIdAllowedChildrenErrors, GetMediaTypeByIdAllowedChildrenResponses, GetMediaTypeByIdAllowedParentsData, GetMediaTypeByIdAllowedParentsErrors, GetMediaTypeByIdAllowedParentsResponses, GetMediaTypeByIdCompositionReferencesData, GetMediaTypeByIdCompositionReferencesErrors, GetMediaTypeByIdCompositionReferencesResponses, GetMediaTypeByIdData, GetMediaTypeByIdErrors, GetMediaTypeByIdExportData, GetMediaTypeByIdExportErrors, GetMediaTypeByIdExportResponses, GetMediaTypeByIdResponses, GetMediaTypeByIdSchemaData, GetMediaTypeByIdSchemaErrors, GetMediaTypeByIdSchemaResponses, GetMediaTypeConfigurationData, GetMediaTypeConfigurationErrors, GetMediaTypeConfigurationResponses, GetMediaTypeFolderByIdData, GetMediaTypeFolderByIdErrors, GetMediaTypeFolderByIdResponses, GetMediaUrlsData, GetMediaUrlsErrors, GetMediaUrlsResponses, GetMemberAreReferencedData, GetMemberAreReferencedErrors, GetMemberAreReferencedResponses, GetMemberByIdData, GetMemberByIdErrors, GetMemberByIdReferencedByData, GetMemberByIdReferencedByErrors, GetMemberByIdReferencedByResponses, GetMemberByIdReferencedDescendantsData, GetMemberByIdReferencedDescendantsErrors, GetMemberByIdReferencedDescendantsResponses, GetMemberByIdResponses, GetMemberConfigurationData, GetMemberConfigurationErrors, GetMemberConfigurationResponses, GetMemberGroupByIdData, GetMemberGroupByIdErrors, GetMemberGroupByIdResponses, GetMemberGroupData, GetMemberGroupErrors, GetMemberGroupResponses, GetMemberTypeAllowedAtRootData, GetMemberTypeAllowedAtRootErrors, GetMemberTypeAllowedAtRootResponses, GetMemberTypeBatchData, GetMemberTypeBatchErrors, GetMemberTypeBatchResponses, GetMemberTypeByIdCompositionReferencesData, GetMemberTypeByIdCompositionReferencesErrors, GetMemberTypeByIdCompositionReferencesResponses, GetMemberTypeByIdData, GetMemberTypeByIdErrors, GetMemberTypeByIdExportData, GetMemberTypeByIdExportErrors, GetMemberTypeByIdExportResponses, GetMemberTypeByIdResponses, GetMemberTypeByIdSchemaData, GetMemberTypeByIdSchemaErrors, GetMemberTypeByIdSchemaResponses, GetMemberTypeConfigurationData, GetMemberTypeConfigurationErrors, GetMemberTypeConfigurationResponses, GetMemberTypeFolderByIdData, GetMemberTypeFolderByIdErrors, GetMemberTypeFolderByIdResponses, GetModelsBuilderDashboardData, GetModelsBuilderDashboardErrors, GetModelsBuilderDashboardResponses, GetModelsBuilderStatusData, GetModelsBuilderStatusErrors, GetModelsBuilderStatusResponses, GetNewsDashboardData, GetNewsDashboardErrors, GetNewsDashboardResponses, GetObjectTypesData, GetObjectTypesErrors, GetObjectTypesResponses, GetOembedQueryData, GetOembedQueryErrors, GetOembedQueryResponses, GetPackageConfigurationData, GetPackageConfigurationErrors, GetPackageConfigurationResponses, GetPackageCreatedByIdData, GetPackageCreatedByIdDownloadData, GetPackageCreatedByIdDownloadErrors, GetPackageCreatedByIdDownloadResponses, GetPackageCreatedByIdErrors, GetPackageCreatedByIdResponses, GetPackageCreatedData, GetPackageCreatedErrors, GetPackageCreatedResponses, GetPackageMigrationStatusData, GetPackageMigrationStatusErrors, GetPackageMigrationStatusResponses, GetPartialViewByPathData, GetPartialViewByPathErrors, GetPartialViewByPathResponses, GetPartialViewFolderByPathData, GetPartialViewFolderByPathErrors, GetPartialViewFolderByPathResponses, GetPartialViewSnippetByIdData, GetPartialViewSnippetByIdErrors, GetPartialViewSnippetByIdResponses, GetPartialViewSnippetData, GetPartialViewSnippetErrors, GetPartialViewSnippetResponses, GetProfilingStatusData, GetProfilingStatusErrors, GetProfilingStatusResponses, GetPropertyTypeIsUsedData, GetPropertyTypeIsUsedErrors, GetPropertyTypeIsUsedResponses, GetPublishedCacheRebuildStatusData, GetPublishedCacheRebuildStatusErrors, GetPublishedCacheRebuildStatusResponses, GetRecycleBinDocumentByIdOriginalParentData, GetRecycleBinDocumentByIdOriginalParentErrors, GetRecycleBinDocumentByIdOriginalParentResponses, GetRecycleBinDocumentChildrenData, GetRecycleBinDocumentChildrenErrors, GetRecycleBinDocumentChildrenResponses, GetRecycleBinDocumentReferencedByData, GetRecycleBinDocumentReferencedByErrors, GetRecycleBinDocumentReferencedByResponses, GetRecycleBinDocumentRootData, GetRecycleBinDocumentRootErrors, GetRecycleBinDocumentRootResponses, GetRecycleBinDocumentSiblingsData, GetRecycleBinDocumentSiblingsErrors, GetRecycleBinDocumentSiblingsResponses, GetRecycleBinMediaByIdOriginalParentData, GetRecycleBinMediaByIdOriginalParentErrors, GetRecycleBinMediaByIdOriginalParentResponses, GetRecycleBinMediaChildrenData, GetRecycleBinMediaChildrenErrors, GetRecycleBinMediaChildrenResponses, GetRecycleBinMediaReferencedByData, GetRecycleBinMediaReferencedByErrors, GetRecycleBinMediaReferencedByResponses, GetRecycleBinMediaRootData, GetRecycleBinMediaRootErrors, GetRecycleBinMediaRootResponses, GetRecycleBinMediaSiblingsData, GetRecycleBinMediaSiblingsErrors, GetRecycleBinMediaSiblingsResponses, GetRedirectManagementByIdData, GetRedirectManagementByIdErrors, GetRedirectManagementByIdResponses, GetRedirectManagementData, GetRedirectManagementErrors, GetRedirectManagementResponses, GetRedirectManagementStatusData, GetRedirectManagementStatusErrors, GetRedirectManagementStatusResponses, GetRelationByRelationTypeIdData, GetRelationByRelationTypeIdErrors, GetRelationByRelationTypeIdResponses, GetRelationTypeByIdData, GetRelationTypeByIdErrors, GetRelationTypeByIdResponses, GetRelationTypeData, GetRelationTypeErrors, GetRelationTypeResponses, GetScriptByPathData, GetScriptByPathErrors, GetScriptByPathResponses, GetScriptFolderByPathData, GetScriptFolderByPathErrors, GetScriptFolderByPathResponses, GetSearcherBySearcherNameQueryData, GetSearcherBySearcherNameQueryErrors, GetSearcherBySearcherNameQueryResponses, GetSearcherData, GetSearcherErrors, GetSearcherResponses, GetSecurityConfigurationData, GetSecurityConfigurationErrors, GetSecurityConfigurationResponses, GetSegmentData, GetSegmentErrors, GetSegmentResponses, GetServerConfigurationData, GetServerConfigurationResponses, GetServerInformationData, GetServerInformationErrors, GetServerInformationResponses, GetServerStatusData, GetServerStatusErrors, GetServerStatusResponses, GetServerTroubleshootingData, GetServerTroubleshootingErrors, GetServerTroubleshootingResponses, GetServerUpgradeCheckData, GetServerUpgradeCheckErrors, GetServerUpgradeCheckResponses, GetStylesheetByPathData, GetStylesheetByPathErrors, GetStylesheetByPathResponses, GetStylesheetFolderByPathData, GetStylesheetFolderByPathErrors, GetStylesheetFolderByPathResponses, GetTagData, GetTagErrors, GetTagResponses, GetTelemetryData, GetTelemetryErrors, GetTelemetryLevelData, GetTelemetryLevelErrors, GetTelemetryLevelResponses, GetTelemetryResponses, GetTemplateByIdData, GetTemplateByIdErrors, GetTemplateByIdResponses, GetTemplateConfigurationData, GetTemplateConfigurationErrors, GetTemplateConfigurationResponses, GetTemplateQuerySettingsData, GetTemplateQuerySettingsErrors, GetTemplateQuerySettingsResponses, GetTemporaryFileByIdData, GetTemporaryFileByIdErrors, GetTemporaryFileByIdResponses, GetTemporaryFileConfigurationData, GetTemporaryFileConfigurationErrors, GetTemporaryFileConfigurationResponses, GetTreeDataTypeAncestorsData, GetTreeDataTypeAncestorsErrors, GetTreeDataTypeAncestorsResponses, GetTreeDataTypeChildrenData, GetTreeDataTypeChildrenErrors, GetTreeDataTypeChildrenResponses, GetTreeDataTypeRootData, GetTreeDataTypeRootErrors, GetTreeDataTypeRootResponses, GetTreeDataTypeSearchData, GetTreeDataTypeSearchErrors, GetTreeDataTypeSearchResponses, GetTreeDataTypeSiblingsData, GetTreeDataTypeSiblingsErrors, GetTreeDataTypeSiblingsResponses, GetTreeDictionaryAncestorsData, GetTreeDictionaryAncestorsErrors, GetTreeDictionaryAncestorsResponses, GetTreeDictionaryChildrenData, GetTreeDictionaryChildrenErrors, GetTreeDictionaryChildrenResponses, GetTreeDictionaryRootData, GetTreeDictionaryRootErrors, GetTreeDictionaryRootResponses, GetTreeDocumentAncestorsData, GetTreeDocumentAncestorsErrors, GetTreeDocumentAncestorsResponses, GetTreeDocumentBlueprintAncestorsData, GetTreeDocumentBlueprintAncestorsErrors, GetTreeDocumentBlueprintAncestorsResponses, GetTreeDocumentBlueprintChildrenData, GetTreeDocumentBlueprintChildrenErrors, GetTreeDocumentBlueprintChildrenResponses, GetTreeDocumentBlueprintRootData, GetTreeDocumentBlueprintRootErrors, GetTreeDocumentBlueprintRootResponses, GetTreeDocumentBlueprintSiblingsData, GetTreeDocumentBlueprintSiblingsErrors, GetTreeDocumentBlueprintSiblingsResponses, GetTreeDocumentChildrenData, GetTreeDocumentChildrenErrors, GetTreeDocumentChildrenResponses, GetTreeDocumentRootData, GetTreeDocumentRootErrors, GetTreeDocumentRootResponses, GetTreeDocumentSiblingsData, GetTreeDocumentSiblingsErrors, GetTreeDocumentSiblingsResponses, GetTreeDocumentTypeAncestorsData, GetTreeDocumentTypeAncestorsErrors, GetTreeDocumentTypeAncestorsResponses, GetTreeDocumentTypeChildrenData, GetTreeDocumentTypeChildrenErrors, GetTreeDocumentTypeChildrenResponses, GetTreeDocumentTypeRootData, GetTreeDocumentTypeRootErrors, GetTreeDocumentTypeRootResponses, GetTreeDocumentTypeSearchData, GetTreeDocumentTypeSearchErrors, GetTreeDocumentTypeSearchResponses, GetTreeDocumentTypeSiblingsData, GetTreeDocumentTypeSiblingsErrors, GetTreeDocumentTypeSiblingsResponses, GetTreeMediaAncestorsData, GetTreeMediaAncestorsErrors, GetTreeMediaAncestorsResponses, GetTreeMediaChildrenData, GetTreeMediaChildrenErrors, GetTreeMediaChildrenResponses, GetTreeMediaRootData, GetTreeMediaRootErrors, GetTreeMediaRootResponses, GetTreeMediaSiblingsData, GetTreeMediaSiblingsErrors, GetTreeMediaSiblingsResponses, GetTreeMediaTypeAncestorsData, GetTreeMediaTypeAncestorsErrors, GetTreeMediaTypeAncestorsResponses, GetTreeMediaTypeChildrenData, GetTreeMediaTypeChildrenErrors, GetTreeMediaTypeChildrenResponses, GetTreeMediaTypeRootData, GetTreeMediaTypeRootErrors, GetTreeMediaTypeRootResponses, GetTreeMediaTypeSiblingsData, GetTreeMediaTypeSiblingsErrors, GetTreeMediaTypeSiblingsResponses, GetTreeMemberGroupRootData, GetTreeMemberGroupRootErrors, GetTreeMemberGroupRootResponses, GetTreeMemberTypeAncestorsData, GetTreeMemberTypeAncestorsErrors, GetTreeMemberTypeAncestorsResponses, GetTreeMemberTypeChildrenData, GetTreeMemberTypeChildrenErrors, GetTreeMemberTypeChildrenResponses, GetTreeMemberTypeRootData, GetTreeMemberTypeRootErrors, GetTreeMemberTypeRootResponses, GetTreeMemberTypeSiblingsData, GetTreeMemberTypeSiblingsErrors, GetTreeMemberTypeSiblingsResponses, GetTreePartialViewAncestorsData, GetTreePartialViewAncestorsErrors, GetTreePartialViewAncestorsResponses, GetTreePartialViewChildrenData, GetTreePartialViewChildrenErrors, GetTreePartialViewChildrenResponses, GetTreePartialViewRootData, GetTreePartialViewRootErrors, GetTreePartialViewRootResponses, GetTreePartialViewSiblingsData, GetTreePartialViewSiblingsErrors, GetTreePartialViewSiblingsResponses, GetTreeScriptAncestorsData, GetTreeScriptAncestorsErrors, GetTreeScriptAncestorsResponses, GetTreeScriptChildrenData, GetTreeScriptChildrenErrors, GetTreeScriptChildrenResponses, GetTreeScriptRootData, GetTreeScriptRootErrors, GetTreeScriptRootResponses, GetTreeScriptSiblingsData, GetTreeScriptSiblingsErrors, GetTreeScriptSiblingsResponses, GetTreeStaticFileAncestorsData, GetTreeStaticFileAncestorsErrors, GetTreeStaticFileAncestorsResponses, GetTreeStaticFileChildrenData, GetTreeStaticFileChildrenErrors, GetTreeStaticFileChildrenResponses, GetTreeStaticFileRootData, GetTreeStaticFileRootErrors, GetTreeStaticFileRootResponses, GetTreeStylesheetAncestorsData, GetTreeStylesheetAncestorsErrors, GetTreeStylesheetAncestorsResponses, GetTreeStylesheetChildrenData, GetTreeStylesheetChildrenErrors, GetTreeStylesheetChildrenResponses, GetTreeStylesheetRootData, GetTreeStylesheetRootErrors, GetTreeStylesheetRootResponses, GetTreeStylesheetSiblingsData, GetTreeStylesheetSiblingsErrors, GetTreeStylesheetSiblingsResponses, GetTreeTemplateAncestorsData, GetTreeTemplateAncestorsErrors, GetTreeTemplateAncestorsResponses, GetTreeTemplateChildrenData, GetTreeTemplateChildrenErrors, GetTreeTemplateChildrenResponses, GetTreeTemplateRootData, GetTreeTemplateRootErrors, GetTreeTemplateRootResponses, GetTreeTemplateSiblingsData, GetTreeTemplateSiblingsErrors, GetTreeTemplateSiblingsResponses, GetUpgradeSettingsData, GetUpgradeSettingsErrors, GetUpgradeSettingsResponses, GetUserById2FaData, GetUserById2FaErrors, GetUserById2FaResponses, GetUserByIdCalculateStartNodesData, GetUserByIdCalculateStartNodesErrors, GetUserByIdCalculateStartNodesResponses, GetUserByIdClientCredentialsData, GetUserByIdClientCredentialsErrors, GetUserByIdClientCredentialsResponses, GetUserByIdData, GetUserByIdErrors, GetUserByIdResponses, GetUserConfigurationData, GetUserConfigurationErrors, GetUserConfigurationResponses, GetUserCurrent2FaByProviderNameData, GetUserCurrent2FaByProviderNameErrors, GetUserCurrent2FaByProviderNameResponses, GetUserCurrent2FaData, GetUserCurrent2FaErrors, GetUserCurrent2FaResponses, GetUserCurrentConfigurationData, GetUserCurrentConfigurationErrors, GetUserCurrentConfigurationResponses, GetUserCurrentData, GetUserCurrentErrors, GetUserCurrentLoginProvidersData, GetUserCurrentLoginProvidersErrors, GetUserCurrentLoginProvidersResponses, GetUserCurrentPermissionsData, GetUserCurrentPermissionsDocumentData, GetUserCurrentPermissionsDocumentErrors, GetUserCurrentPermissionsDocumentResponses, GetUserCurrentPermissionsErrors, GetUserCurrentPermissionsMediaData, GetUserCurrentPermissionsMediaErrors, GetUserCurrentPermissionsMediaResponses, GetUserCurrentPermissionsResponses, GetUserCurrentResponses, GetUserData, GetUserDataByIdData, GetUserDataByIdErrors, GetUserDataByIdResponses, GetUserDataData, GetUserDataErrors, GetUserDataResponses, GetUserErrors, GetUserGroupByIdData, GetUserGroupByIdErrors, GetUserGroupByIdResponses, GetUserGroupData, GetUserGroupErrors, GetUserGroupResponses, GetUserResponses, GetWebhookByIdData, GetWebhookByIdErrors, GetWebhookByIdLogsData, GetWebhookByIdLogsErrors, GetWebhookByIdLogsResponses, GetWebhookByIdResponses, GetWebhookData, GetWebhookErrors, GetWebhookEventsData, GetWebhookEventsErrors, GetWebhookEventsResponses, GetWebhookLogsData, GetWebhookLogsErrors, GetWebhookLogsResponses, GetWebhookResponses, PatchDocumentByIdPatchData, PatchDocumentByIdPatchErrors, PatchDocumentByIdPatchResponses, PostDataTypeByIdCopyData, PostDataTypeByIdCopyErrors, PostDataTypeByIdCopyResponses, PostDataTypeData, PostDataTypeErrors, PostDataTypeFolderData, PostDataTypeFolderErrors, PostDataTypeFolderResponses, PostDataTypeResponses, PostDictionaryData, PostDictionaryErrors, PostDictionaryImportData, PostDictionaryImportErrors, PostDictionaryImportResponses, PostDictionaryResponses, PostDocumentBlueprintData, PostDocumentBlueprintErrors, PostDocumentBlueprintFolderData, PostDocumentBlueprintFolderErrors, PostDocumentBlueprintFolderResponses, PostDocumentBlueprintFromDocumentData, PostDocumentBlueprintFromDocumentErrors, PostDocumentBlueprintFromDocumentResponses, PostDocumentBlueprintResponses, PostDocumentByIdCopyData, PostDocumentByIdCopyErrors, PostDocumentByIdCopyResponses, PostDocumentByIdPublicAccessData, PostDocumentByIdPublicAccessErrors, PostDocumentByIdPublicAccessResponses, PostDocumentData, PostDocumentErrors, PostDocumentResponses, PostDocumentTypeAvailableCompositionsData, PostDocumentTypeAvailableCompositionsErrors, PostDocumentTypeAvailableCompositionsResponses, PostDocumentTypeByIdCopyData, PostDocumentTypeByIdCopyErrors, PostDocumentTypeByIdCopyResponses, PostDocumentTypeByIdTemplateData, PostDocumentTypeByIdTemplateErrors, PostDocumentTypeByIdTemplateResponses, PostDocumentTypeData, PostDocumentTypeErrors, PostDocumentTypeFolderData, PostDocumentTypeFolderErrors, PostDocumentTypeFolderResponses, PostDocumentTypeImportData, PostDocumentTypeImportErrors, PostDocumentTypeImportResponses, PostDocumentTypeResponses, PostDocumentValidateData, PostDocumentValidateErrors, PostDocumentValidateResponses, PostDocumentVersionByIdRollbackData, PostDocumentVersionByIdRollbackErrors, PostDocumentVersionByIdRollbackResponses, PostDynamicRootQueryData, PostDynamicRootQueryErrors, PostDynamicRootQueryResponses, PostHealthCheckExecuteActionData, PostHealthCheckExecuteActionErrors, PostHealthCheckExecuteActionResponses, PostHealthCheckGroupByNameCheckData, PostHealthCheckGroupByNameCheckErrors, PostHealthCheckGroupByNameCheckResponses, PostIndexerByIndexNameRebuildData, PostIndexerByIndexNameRebuildErrors, PostIndexerByIndexNameRebuildResponses, PostInstallSetupData, PostInstallSetupErrors, PostInstallSetupResponses, PostInstallValidateDatabaseData, PostInstallValidateDatabaseErrors, PostInstallValidateDatabaseResponses, PostLanguageData, PostLanguageErrors, PostLanguageResponses, PostLogViewerSavedSearchData, PostLogViewerSavedSearchErrors, PostLogViewerSavedSearchResponses, PostMediaData, PostMediaErrors, PostMediaResponses, PostMediaTypeAvailableCompositionsData, PostMediaTypeAvailableCompositionsErrors, PostMediaTypeAvailableCompositionsResponses, PostMediaTypeByIdCopyData, PostMediaTypeByIdCopyErrors, PostMediaTypeByIdCopyResponses, PostMediaTypeData, PostMediaTypeErrors, PostMediaTypeFolderData, PostMediaTypeFolderErrors, PostMediaTypeFolderResponses, PostMediaTypeImportData, PostMediaTypeImportErrors, PostMediaTypeImportResponses, PostMediaTypeResponses, PostMediaValidateData, PostMediaValidateErrors, PostMediaValidateResponses, PostMemberData, PostMemberErrors, PostMemberGroupData, PostMemberGroupErrors, PostMemberGroupResponses, PostMemberResponses, PostMemberTypeAvailableCompositionsData, PostMemberTypeAvailableCompositionsErrors, PostMemberTypeAvailableCompositionsResponses, PostMemberTypeByIdCopyData, PostMemberTypeByIdCopyErrors, PostMemberTypeByIdCopyResponses, PostMemberTypeData, PostMemberTypeErrors, PostMemberTypeFolderData, PostMemberTypeFolderErrors, PostMemberTypeFolderResponses, PostMemberTypeImportData, PostMemberTypeImportErrors, PostMemberTypeImportResponses, PostMemberTypeResponses, PostMemberValidateData, PostMemberValidateErrors, PostMemberValidateResponses, PostModelsBuilderBuildData, PostModelsBuilderBuildErrors, PostModelsBuilderBuildResponses, PostPackageByNameRunMigrationData, PostPackageByNameRunMigrationErrors, PostPackageByNameRunMigrationResponses, PostPackageCreatedData, PostPackageCreatedErrors, PostPackageCreatedResponses, PostPartialViewData, PostPartialViewErrors, PostPartialViewFolderData, PostPartialViewFolderErrors, PostPartialViewFolderResponses, PostPartialViewResponses, PostPreviewData, PostPreviewErrors, PostPreviewResponses, PostPublishedCacheRebuildData, PostPublishedCacheRebuildErrors, PostPublishedCacheRebuildResponses, PostPublishedCacheReloadData, PostPublishedCacheReloadErrors, PostPublishedCacheReloadResponses, PostRedirectManagementStatusData, PostRedirectManagementStatusErrors, PostRedirectManagementStatusResponses, PostScriptData, PostScriptErrors, PostScriptFolderData, PostScriptFolderErrors, PostScriptFolderResponses, PostScriptResponses, PostSecurityForgotPasswordData, PostSecurityForgotPasswordErrors, PostSecurityForgotPasswordResetData, PostSecurityForgotPasswordResetErrors, PostSecurityForgotPasswordResetResponses, PostSecurityForgotPasswordResponses, PostSecurityForgotPasswordVerifyData, PostSecurityForgotPasswordVerifyErrors, PostSecurityForgotPasswordVerifyResponses, PostStylesheetData, PostStylesheetErrors, PostStylesheetFolderData, PostStylesheetFolderErrors, PostStylesheetFolderResponses, PostStylesheetResponses, PostTelemetryLevelData, PostTelemetryLevelErrors, PostTelemetryLevelResponses, PostTemplateData, PostTemplateErrors, PostTemplateQueryExecuteData, PostTemplateQueryExecuteErrors, PostTemplateQueryExecuteResponses, PostTemplateResponses, PostTemporaryFileData, PostTemporaryFileErrors, PostTemporaryFileResponses, PostUpgradeAuthorizeData, PostUpgradeAuthorizeErrors, PostUpgradeAuthorizeResponses, PostUserAvatarByIdData, PostUserAvatarByIdErrors, PostUserAvatarByIdResponses, PostUserByIdChangePasswordData, PostUserByIdChangePasswordErrors, PostUserByIdChangePasswordResponses, PostUserByIdClientCredentialsData, PostUserByIdClientCredentialsErrors, PostUserByIdClientCredentialsResponses, PostUserByIdResetPasswordData, PostUserByIdResetPasswordErrors, PostUserByIdResetPasswordResponses, PostUserCurrent2FaByProviderNameData, PostUserCurrent2FaByProviderNameErrors, PostUserCurrent2FaByProviderNameResponses, PostUserCurrentAvatarData, PostUserCurrentAvatarErrors, PostUserCurrentAvatarResponses, PostUserCurrentChangePasswordData, PostUserCurrentChangePasswordErrors, PostUserCurrentChangePasswordResponses, PostUserData, PostUserDataData, PostUserDataErrors, PostUserDataResponses, PostUserDisableData, PostUserDisableErrors, PostUserDisableResponses, PostUserEnableData, PostUserEnableErrors, PostUserEnableResponses, PostUserErrors, PostUserGroupByIdUsersData, PostUserGroupByIdUsersErrors, PostUserGroupByIdUsersResponses, PostUserGroupData, PostUserGroupErrors, PostUserGroupResponses, PostUserInviteCreatePasswordData, PostUserInviteCreatePasswordErrors, PostUserInviteCreatePasswordResponses, PostUserInviteData, PostUserInviteErrors, PostUserInviteResendData, PostUserInviteResendErrors, PostUserInviteResendResponses, PostUserInviteResponses, PostUserInviteVerifyData, PostUserInviteVerifyErrors, PostUserInviteVerifyResponses, PostUserResponses, PostUserSetUserGroupsData, PostUserSetUserGroupsErrors, PostUserSetUserGroupsResponses, PostUserUnlockData, PostUserUnlockErrors, PostUserUnlockResponses, PostWebhookData, PostWebhookErrors, PostWebhookResponses, PutDataTypeByIdData, PutDataTypeByIdErrors, PutDataTypeByIdMoveData, PutDataTypeByIdMoveErrors, PutDataTypeByIdMoveResponses, PutDataTypeByIdResponses, PutDataTypeFolderByIdData, PutDataTypeFolderByIdErrors, PutDataTypeFolderByIdResponses, PutDictionaryByIdData, PutDictionaryByIdErrors, PutDictionaryByIdMoveData, PutDictionaryByIdMoveErrors, PutDictionaryByIdMoveResponses, PutDictionaryByIdResponses, PutDocumentBlueprintByIdData, PutDocumentBlueprintByIdErrors, PutDocumentBlueprintByIdMoveData, PutDocumentBlueprintByIdMoveErrors, PutDocumentBlueprintByIdMoveResponses, PutDocumentBlueprintByIdResponses, PutDocumentBlueprintFolderByIdData, PutDocumentBlueprintFolderByIdErrors, PutDocumentBlueprintFolderByIdResponses, PutDocumentByIdData, PutDocumentByIdDomainsData, PutDocumentByIdDomainsErrors, PutDocumentByIdDomainsResponses, PutDocumentByIdErrors, PutDocumentByIdMoveData, PutDocumentByIdMoveErrors, PutDocumentByIdMoveResponses, PutDocumentByIdMoveToRecycleBinData, PutDocumentByIdMoveToRecycleBinErrors, PutDocumentByIdMoveToRecycleBinResponses, PutDocumentByIdNotificationsData, PutDocumentByIdNotificationsErrors, PutDocumentByIdNotificationsResponses, PutDocumentByIdPublicAccessData, PutDocumentByIdPublicAccessErrors, PutDocumentByIdPublicAccessResponses, PutDocumentByIdPublishData, PutDocumentByIdPublishErrors, PutDocumentByIdPublishResponses, PutDocumentByIdPublishWithDescendantsData, PutDocumentByIdPublishWithDescendantsErrors, PutDocumentByIdPublishWithDescendantsResponses, PutDocumentByIdResponses, PutDocumentByIdUnpublishData, PutDocumentByIdUnpublishErrors, PutDocumentByIdUnpublishResponses, PutDocumentSortData, PutDocumentSortErrors, PutDocumentSortResponses, PutDocumentTypeByIdData, PutDocumentTypeByIdErrors, PutDocumentTypeByIdImportData, PutDocumentTypeByIdImportErrors, PutDocumentTypeByIdImportResponses, PutDocumentTypeByIdMoveData, PutDocumentTypeByIdMoveErrors, PutDocumentTypeByIdMoveResponses, PutDocumentTypeByIdResponses, PutDocumentTypeFolderByIdData, PutDocumentTypeFolderByIdErrors, PutDocumentTypeFolderByIdResponses, PutDocumentVersionByIdPreventCleanupData, PutDocumentVersionByIdPreventCleanupErrors, PutDocumentVersionByIdPreventCleanupResponses, PutLanguageByIsoCodeData, PutLanguageByIsoCodeErrors, PutLanguageByIsoCodeResponses, PutMediaByIdData, PutMediaByIdErrors, PutMediaByIdMoveData, PutMediaByIdMoveErrors, PutMediaByIdMoveResponses, PutMediaByIdMoveToRecycleBinData, PutMediaByIdMoveToRecycleBinErrors, PutMediaByIdMoveToRecycleBinResponses, PutMediaByIdResponses, PutMediaByIdValidateData, PutMediaByIdValidateErrors, PutMediaByIdValidateResponses, PutMediaSortData, PutMediaSortErrors, PutMediaSortResponses, PutMediaTypeByIdData, PutMediaTypeByIdErrors, PutMediaTypeByIdImportData, PutMediaTypeByIdImportErrors, PutMediaTypeByIdImportResponses, PutMediaTypeByIdMoveData, PutMediaTypeByIdMoveErrors, PutMediaTypeByIdMoveResponses, PutMediaTypeByIdResponses, PutMediaTypeFolderByIdData, PutMediaTypeFolderByIdErrors, PutMediaTypeFolderByIdResponses, PutMemberByIdData, PutMemberByIdErrors, PutMemberByIdResponses, PutMemberByIdValidateData, PutMemberByIdValidateErrors, PutMemberByIdValidateResponses, PutMemberGroupByIdData, PutMemberGroupByIdErrors, PutMemberGroupByIdResponses, PutMemberTypeByIdData, PutMemberTypeByIdErrors, PutMemberTypeByIdImportData, PutMemberTypeByIdImportErrors, PutMemberTypeByIdImportResponses, PutMemberTypeByIdMoveData, PutMemberTypeByIdMoveErrors, PutMemberTypeByIdMoveResponses, PutMemberTypeByIdResponses, PutMemberTypeFolderByIdData, PutMemberTypeFolderByIdErrors, PutMemberTypeFolderByIdResponses, PutPackageCreatedByIdData, PutPackageCreatedByIdErrors, PutPackageCreatedByIdResponses, PutPartialViewByPathData, PutPartialViewByPathErrors, PutPartialViewByPathRenameData, PutPartialViewByPathRenameErrors, PutPartialViewByPathRenameResponses, PutPartialViewByPathResponses, PutProfilingStatusData, PutProfilingStatusErrors, PutProfilingStatusResponses, PutRecycleBinDocumentByIdRestoreData, PutRecycleBinDocumentByIdRestoreErrors, PutRecycleBinDocumentByIdRestoreResponses, PutRecycleBinMediaByIdRestoreData, PutRecycleBinMediaByIdRestoreErrors, PutRecycleBinMediaByIdRestoreResponses, PutScriptByPathData, PutScriptByPathErrors, PutScriptByPathRenameData, PutScriptByPathRenameErrors, PutScriptByPathRenameResponses, PutScriptByPathResponses, PutStylesheetByPathData, PutStylesheetByPathErrors, PutStylesheetByPathRenameData, PutStylesheetByPathRenameErrors, PutStylesheetByPathRenameResponses, PutStylesheetByPathResponses, PutTemplateByIdData, PutTemplateByIdErrors, PutTemplateByIdResponses, PutUmbracoManagementApiV11DocumentByIdValidate11Data, PutUmbracoManagementApiV11DocumentByIdValidate11Errors, PutUmbracoManagementApiV11DocumentByIdValidate11Responses, PutUserByIdData, PutUserByIdErrors, PutUserByIdResponses, PutUserCurrentProfileData, PutUserCurrentProfileErrors, PutUserCurrentProfileResponses, PutUserDataData, PutUserDataErrors, PutUserDataResponses, PutUserGroupByIdData, PutUserGroupByIdErrors, PutUserGroupByIdResponses, PutWebhookByIdData, PutWebhookByIdErrors, PutWebhookByIdResponses } from './types.gen';
+import type { DeleteDataTypeByIdData, DeleteDataTypeByIdErrors, DeleteDataTypeByIdResponses, DeleteDataTypeFolderByIdData, DeleteDataTypeFolderByIdErrors, DeleteDataTypeFolderByIdResponses, DeleteDictionaryByIdData, DeleteDictionaryByIdErrors, DeleteDictionaryByIdResponses, DeleteDocumentBlueprintByIdData, DeleteDocumentBlueprintByIdErrors, DeleteDocumentBlueprintByIdResponses, DeleteDocumentBlueprintFolderByIdData, DeleteDocumentBlueprintFolderByIdErrors, DeleteDocumentBlueprintFolderByIdResponses, DeleteDocumentByIdData, DeleteDocumentByIdErrors, DeleteDocumentByIdPublicAccessData, DeleteDocumentByIdPublicAccessErrors, DeleteDocumentByIdPublicAccessResponses, DeleteDocumentByIdResponses, DeleteDocumentTypeByIdData, DeleteDocumentTypeByIdErrors, DeleteDocumentTypeByIdResponses, DeleteDocumentTypeFolderByIdData, DeleteDocumentTypeFolderByIdErrors, DeleteDocumentTypeFolderByIdResponses, DeleteLanguageByIsoCodeData, DeleteLanguageByIsoCodeErrors, DeleteLanguageByIsoCodeResponses, DeleteLogViewerSavedSearchByNameData, DeleteLogViewerSavedSearchByNameErrors, DeleteLogViewerSavedSearchByNameResponses, DeleteMediaByIdData, DeleteMediaByIdErrors, DeleteMediaByIdResponses, DeleteMediaTypeByIdData, DeleteMediaTypeByIdErrors, DeleteMediaTypeByIdResponses, DeleteMediaTypeFolderByIdData, DeleteMediaTypeFolderByIdErrors, DeleteMediaTypeFolderByIdResponses, DeleteMemberByIdData, DeleteMemberByIdErrors, DeleteMemberByIdResponses, DeleteMemberGroupByIdData, DeleteMemberGroupByIdErrors, DeleteMemberGroupByIdResponses, DeleteMemberTypeByIdData, DeleteMemberTypeByIdErrors, DeleteMemberTypeByIdResponses, DeleteMemberTypeFolderByIdData, DeleteMemberTypeFolderByIdErrors, DeleteMemberTypeFolderByIdResponses, DeletePackageCreatedByIdData, DeletePackageCreatedByIdErrors, DeletePackageCreatedByIdResponses, DeletePartialViewByPathData, DeletePartialViewByPathErrors, DeletePartialViewByPathResponses, DeletePartialViewFolderByPathData, DeletePartialViewFolderByPathErrors, DeletePartialViewFolderByPathResponses, DeletePreviewData, DeletePreviewResponses, DeleteRecycleBinDocumentByIdData, DeleteRecycleBinDocumentByIdErrors, DeleteRecycleBinDocumentByIdResponses, DeleteRecycleBinDocumentData, DeleteRecycleBinDocumentErrors, DeleteRecycleBinDocumentResponses, DeleteRecycleBinMediaByIdData, DeleteRecycleBinMediaByIdErrors, DeleteRecycleBinMediaByIdResponses, DeleteRecycleBinMediaData, DeleteRecycleBinMediaErrors, DeleteRecycleBinMediaResponses, DeleteRedirectManagementByIdData, DeleteRedirectManagementByIdErrors, DeleteRedirectManagementByIdResponses, DeleteScriptByPathData, DeleteScriptByPathErrors, DeleteScriptByPathResponses, DeleteScriptFolderByPathData, DeleteScriptFolderByPathErrors, DeleteScriptFolderByPathResponses, DeleteStylesheetByPathData, DeleteStylesheetByPathErrors, DeleteStylesheetByPathResponses, DeleteStylesheetFolderByPathData, DeleteStylesheetFolderByPathErrors, DeleteStylesheetFolderByPathResponses, DeleteTemplateByIdData, DeleteTemplateByIdErrors, DeleteTemplateByIdResponses, DeleteTemporaryFileByIdData, DeleteTemporaryFileByIdErrors, DeleteTemporaryFileByIdResponses, DeleteUserAvatarByIdData, DeleteUserAvatarByIdErrors, DeleteUserAvatarByIdResponses, DeleteUserById2FaByProviderNameData, DeleteUserById2FaByProviderNameErrors, DeleteUserById2FaByProviderNameResponses, DeleteUserByIdClientCredentialsByClientIdData, DeleteUserByIdClientCredentialsByClientIdErrors, DeleteUserByIdClientCredentialsByClientIdResponses, DeleteUserByIdData, DeleteUserByIdErrors, DeleteUserByIdResponses, DeleteUserCurrent2FaByProviderNameData, DeleteUserCurrent2FaByProviderNameErrors, DeleteUserCurrent2FaByProviderNameResponses, DeleteUserCurrentAvatarData, DeleteUserCurrentAvatarErrors, DeleteUserCurrentAvatarResponses, DeleteUserData, DeleteUserDataByIdData, DeleteUserDataByIdErrors, DeleteUserDataByIdResponses, DeleteUserErrors, DeleteUserGroupByIdData, DeleteUserGroupByIdErrors, DeleteUserGroupByIdResponses, DeleteUserGroupByIdUsersData, DeleteUserGroupByIdUsersErrors, DeleteUserGroupByIdUsersResponses, DeleteUserGroupData, DeleteUserGroupErrors, DeleteUserGroupResponses, DeleteUserResponses, DeleteWebhookByIdData, DeleteWebhookByIdErrors, DeleteWebhookByIdResponses, GetCollectionDocumentByIdData, GetCollectionDocumentByIdErrors, GetCollectionDocumentByIdResponses, GetCollectionMediaData, GetCollectionMediaErrors, GetCollectionMediaResponses, GetCultureData, GetCultureErrors, GetCultureResponses, GetDataTypeBatchData, GetDataTypeBatchErrors, GetDataTypeBatchResponses, GetDataTypeByIdData, GetDataTypeByIdErrors, GetDataTypeByIdIsUsedData, GetDataTypeByIdIsUsedErrors, GetDataTypeByIdIsUsedResponses, GetDataTypeByIdReferencedByData, GetDataTypeByIdReferencedByErrors, GetDataTypeByIdReferencedByResponses, GetDataTypeByIdResponses, GetDataTypeByIdSchemaData, GetDataTypeByIdSchemaErrors, GetDataTypeByIdSchemaResponses, GetDataTypeConfigurationData, GetDataTypeConfigurationErrors, GetDataTypeConfigurationResponses, GetDataTypeFolderByIdData, GetDataTypeFolderByIdErrors, GetDataTypeFolderByIdResponses, GetDataTypeSchemasBatchData, GetDataTypeSchemasBatchErrors, GetDataTypeSchemasBatchResponses, GetDictionaryByIdData, GetDictionaryByIdErrors, GetDictionaryByIdExportData, GetDictionaryByIdExportErrors, GetDictionaryByIdExportResponses, GetDictionaryByIdResponses, GetDictionaryData, GetDictionaryErrors, GetDictionaryResponses, GetDocumentAreReferencedData, GetDocumentAreReferencedErrors, GetDocumentAreReferencedResponses, GetDocumentBlueprintByIdAuditLogData, GetDocumentBlueprintByIdAuditLogErrors, GetDocumentBlueprintByIdAuditLogResponses, GetDocumentBlueprintByIdData, GetDocumentBlueprintByIdErrors, GetDocumentBlueprintByIdResponses, GetDocumentBlueprintByIdScaffoldData, GetDocumentBlueprintByIdScaffoldErrors, GetDocumentBlueprintByIdScaffoldResponses, GetDocumentBlueprintFolderByIdData, GetDocumentBlueprintFolderByIdErrors, GetDocumentBlueprintFolderByIdResponses, GetDocumentByIdAuditLogData, GetDocumentByIdAuditLogErrors, GetDocumentByIdAuditLogResponses, GetDocumentByIdAvailableSegmentOptionsData, GetDocumentByIdAvailableSegmentOptionsErrors, GetDocumentByIdAvailableSegmentOptionsResponses, GetDocumentByIdData, GetDocumentByIdDomainsData, GetDocumentByIdDomainsErrors, GetDocumentByIdDomainsResponses, GetDocumentByIdErrors, GetDocumentByIdNotificationsData, GetDocumentByIdNotificationsErrors, GetDocumentByIdNotificationsResponses, GetDocumentByIdPreviewUrlData, GetDocumentByIdPreviewUrlErrors, GetDocumentByIdPreviewUrlResponses, GetDocumentByIdPublicAccessData, GetDocumentByIdPublicAccessErrors, GetDocumentByIdPublicAccessResponses, GetDocumentByIdPublishedData, GetDocumentByIdPublishedErrors, GetDocumentByIdPublishedResponses, GetDocumentByIdPublishWithDescendantsResultByTaskIdData, GetDocumentByIdPublishWithDescendantsResultByTaskIdErrors, GetDocumentByIdPublishWithDescendantsResultByTaskIdResponses, GetDocumentByIdReferencedByData, GetDocumentByIdReferencedByErrors, GetDocumentByIdReferencedByResponses, GetDocumentByIdReferencedDescendantsData, GetDocumentByIdReferencedDescendantsErrors, GetDocumentByIdReferencedDescendantsResponses, GetDocumentByIdResponses, GetDocumentConfigurationData, GetDocumentConfigurationErrors, GetDocumentConfigurationResponses, GetDocumentTypeAllowedAtRootData, GetDocumentTypeAllowedAtRootErrors, GetDocumentTypeAllowedAtRootResponses, GetDocumentTypeBatchData, GetDocumentTypeBatchErrors, GetDocumentTypeBatchResponses, GetDocumentTypeByIdAllowedChildrenData, GetDocumentTypeByIdAllowedChildrenErrors, GetDocumentTypeByIdAllowedChildrenResponses, GetDocumentTypeByIdAllowedParentsData, GetDocumentTypeByIdAllowedParentsErrors, GetDocumentTypeByIdAllowedParentsResponses, GetDocumentTypeByIdBlueprintData, GetDocumentTypeByIdBlueprintErrors, GetDocumentTypeByIdBlueprintResponses, GetDocumentTypeByIdCompositionReferencesData, GetDocumentTypeByIdCompositionReferencesErrors, GetDocumentTypeByIdCompositionReferencesResponses, GetDocumentTypeByIdData, GetDocumentTypeByIdErrors, GetDocumentTypeByIdExportData, GetDocumentTypeByIdExportErrors, GetDocumentTypeByIdExportResponses, GetDocumentTypeByIdResponses, GetDocumentTypeByIdSchemaData, GetDocumentTypeByIdSchemaErrors, GetDocumentTypeByIdSchemaResponses, GetDocumentTypeConfigurationData, GetDocumentTypeConfigurationErrors, GetDocumentTypeConfigurationResponses, GetDocumentTypeFolderByIdData, GetDocumentTypeFolderByIdErrors, GetDocumentTypeFolderByIdResponses, GetDocumentUrlsData, GetDocumentUrlsErrors, GetDocumentUrlsResponses, GetDocumentVersionByIdData, GetDocumentVersionByIdErrors, GetDocumentVersionByIdResponses, GetDocumentVersionData, GetDocumentVersionErrors, GetDocumentVersionResponses, GetDynamicRootStepsData, GetDynamicRootStepsErrors, GetDynamicRootStepsResponses, GetFilterDataTypeData, GetFilterDataTypeErrors, GetFilterDataTypeResponses, GetFilterMemberData, GetFilterMemberErrors, GetFilterMemberResponses, GetFilterUserData, GetFilterUserErrors, GetFilterUserGroupData, GetFilterUserGroupErrors, GetFilterUserGroupResponses, GetFilterUserResponses, GetHealthCheckGroupByNameData, GetHealthCheckGroupByNameErrors, GetHealthCheckGroupByNameResponses, GetHealthCheckGroupData, GetHealthCheckGroupErrors, GetHealthCheckGroupResponses, GetHelpData, GetHelpErrors, GetHelpResponses, GetImagingResizeUrlsData, GetImagingResizeUrlsErrors, GetImagingResizeUrlsResponses, GetImportAnalyzeData, GetImportAnalyzeErrors, GetImportAnalyzeResponses, GetIndexerByIndexNameData, GetIndexerByIndexNameErrors, GetIndexerByIndexNameResponses, GetIndexerData, GetIndexerErrors, GetIndexerResponses, GetInstallSettingsData, GetInstallSettingsErrors, GetInstallSettingsResponses, GetItemDataTypeAncestorsData, GetItemDataTypeAncestorsErrors, GetItemDataTypeAncestorsResponses, GetItemDataTypeData, GetItemDataTypeErrors, GetItemDataTypeResponses, GetItemDataTypeSearchData, GetItemDataTypeSearchErrors, GetItemDataTypeSearchResponses, GetItemDictionaryData, GetItemDictionaryErrors, GetItemDictionaryResponses, GetItemDocumentAncestorsData, GetItemDocumentAncestorsErrors, GetItemDocumentAncestorsResponses, GetItemDocumentBlueprintData, GetItemDocumentBlueprintErrors, GetItemDocumentBlueprintResponses, GetItemDocumentData, GetItemDocumentErrors, GetItemDocumentResponses, GetItemDocumentSearchData, GetItemDocumentSearchErrors, GetItemDocumentSearchResponses, GetItemDocumentTypeAncestorsData, GetItemDocumentTypeAncestorsErrors, GetItemDocumentTypeAncestorsResponses, GetItemDocumentTypeData, GetItemDocumentTypeErrors, GetItemDocumentTypeResponses, GetItemDocumentTypeSearchData, GetItemDocumentTypeSearchErrors, GetItemDocumentTypeSearchResponses, GetItemLanguageData, GetItemLanguageDefaultData, GetItemLanguageDefaultErrors, GetItemLanguageDefaultResponses, GetItemLanguageErrors, GetItemLanguageResponses, GetItemMediaAncestorsData, GetItemMediaAncestorsErrors, GetItemMediaAncestorsResponses, GetItemMediaData, GetItemMediaErrors, GetItemMediaResponses, GetItemMediaSearchData, GetItemMediaSearchErrors, GetItemMediaSearchResponses, GetItemMediaTypeAllowedData, GetItemMediaTypeAllowedErrors, GetItemMediaTypeAllowedResponses, GetItemMediaTypeAncestorsData, GetItemMediaTypeAncestorsErrors, GetItemMediaTypeAncestorsResponses, GetItemMediaTypeData, GetItemMediaTypeErrors, GetItemMediaTypeFoldersData, GetItemMediaTypeFoldersErrors, GetItemMediaTypeFoldersResponses, GetItemMediaTypeResponses, GetItemMediaTypeSearchData, GetItemMediaTypeSearchErrors, GetItemMediaTypeSearchResponses, GetItemMemberAncestorsData, GetItemMemberAncestorsErrors, GetItemMemberAncestorsResponses, GetItemMemberData, GetItemMemberErrors, GetItemMemberGroupData, GetItemMemberGroupErrors, GetItemMemberGroupResponses, GetItemMemberResponses, GetItemMemberSearchData, GetItemMemberSearchErrors, GetItemMemberSearchResponses, GetItemMemberTypeAncestorsData, GetItemMemberTypeAncestorsErrors, GetItemMemberTypeAncestorsResponses, GetItemMemberTypeData, GetItemMemberTypeErrors, GetItemMemberTypeResponses, GetItemMemberTypeSearchData, GetItemMemberTypeSearchErrors, GetItemMemberTypeSearchResponses, GetItemPartialViewData, GetItemPartialViewErrors, GetItemPartialViewResponses, GetItemRelationTypeData, GetItemRelationTypeErrors, GetItemRelationTypeResponses, GetItemScriptData, GetItemScriptErrors, GetItemScriptResponses, GetItemStaticFileData, GetItemStaticFileErrors, GetItemStaticFileResponses, GetItemStylesheetData, GetItemStylesheetErrors, GetItemStylesheetResponses, GetItemTemplateAncestorsData, GetItemTemplateAncestorsErrors, GetItemTemplateAncestorsResponses, GetItemTemplateData, GetItemTemplateErrors, GetItemTemplateResponses, GetItemTemplateSearchData, GetItemTemplateSearchErrors, GetItemTemplateSearchResponses, GetItemUserData, GetItemUserErrors, GetItemUserGroupData, GetItemUserGroupErrors, GetItemUserGroupResponses, GetItemUserResponses, GetItemWebhookData, GetItemWebhookErrors, GetItemWebhookResponses, GetLanguageByIsoCodeData, GetLanguageByIsoCodeErrors, GetLanguageByIsoCodeResponses, GetLanguageData, GetLanguageErrors, GetLanguageResponses, GetLogViewerLevelCountData, GetLogViewerLevelCountErrors, GetLogViewerLevelCountResponses, GetLogViewerLevelData, GetLogViewerLevelErrors, GetLogViewerLevelResponses, GetLogViewerLogData, GetLogViewerLogErrors, GetLogViewerLogResponses, GetLogViewerMessageTemplateData, GetLogViewerMessageTemplateErrors, GetLogViewerMessageTemplateResponses, GetLogViewerSavedSearchByNameData, GetLogViewerSavedSearchByNameErrors, GetLogViewerSavedSearchByNameResponses, GetLogViewerSavedSearchData, GetLogViewerSavedSearchErrors, GetLogViewerSavedSearchResponses, GetLogViewerValidateLogsSizeData, GetLogViewerValidateLogsSizeErrors, GetLogViewerValidateLogsSizeResponses, GetManifestManifestData, GetManifestManifestErrors, GetManifestManifestPrivateData, GetManifestManifestPrivateErrors, GetManifestManifestPrivateResponses, GetManifestManifestPublicData, GetManifestManifestPublicResponses, GetManifestManifestResponses, GetMediaAreReferencedData, GetMediaAreReferencedErrors, GetMediaAreReferencedResponses, GetMediaByIdAuditLogData, GetMediaByIdAuditLogErrors, GetMediaByIdAuditLogResponses, GetMediaByIdData, GetMediaByIdErrors, GetMediaByIdReferencedByData, GetMediaByIdReferencedByErrors, GetMediaByIdReferencedByResponses, GetMediaByIdReferencedDescendantsData, GetMediaByIdReferencedDescendantsErrors, GetMediaByIdReferencedDescendantsResponses, GetMediaByIdResponses, GetMediaConfigurationData, GetMediaConfigurationErrors, GetMediaConfigurationResponses, GetMediaTypeAllowedAtRootData, GetMediaTypeAllowedAtRootErrors, GetMediaTypeAllowedAtRootResponses, GetMediaTypeBatchData, GetMediaTypeBatchErrors, GetMediaTypeBatchResponses, GetMediaTypeByIdAllowedChildrenData, GetMediaTypeByIdAllowedChildrenErrors, GetMediaTypeByIdAllowedChildrenResponses, GetMediaTypeByIdAllowedParentsData, GetMediaTypeByIdAllowedParentsErrors, GetMediaTypeByIdAllowedParentsResponses, GetMediaTypeByIdCompositionReferencesData, GetMediaTypeByIdCompositionReferencesErrors, GetMediaTypeByIdCompositionReferencesResponses, GetMediaTypeByIdData, GetMediaTypeByIdErrors, GetMediaTypeByIdExportData, GetMediaTypeByIdExportErrors, GetMediaTypeByIdExportResponses, GetMediaTypeByIdResponses, GetMediaTypeByIdSchemaData, GetMediaTypeByIdSchemaErrors, GetMediaTypeByIdSchemaResponses, GetMediaTypeConfigurationData, GetMediaTypeConfigurationErrors, GetMediaTypeConfigurationResponses, GetMediaTypeFolderByIdData, GetMediaTypeFolderByIdErrors, GetMediaTypeFolderByIdResponses, GetMediaUrlsData, GetMediaUrlsErrors, GetMediaUrlsResponses, GetMemberAreReferencedData, GetMemberAreReferencedErrors, GetMemberAreReferencedResponses, GetMemberByIdData, GetMemberByIdErrors, GetMemberByIdReferencedByData, GetMemberByIdReferencedByErrors, GetMemberByIdReferencedByResponses, GetMemberByIdReferencedDescendantsData, GetMemberByIdReferencedDescendantsErrors, GetMemberByIdReferencedDescendantsResponses, GetMemberByIdResponses, GetMemberConfigurationData, GetMemberConfigurationErrors, GetMemberConfigurationResponses, GetMemberGroupByIdData, GetMemberGroupByIdErrors, GetMemberGroupByIdResponses, GetMemberGroupData, GetMemberGroupErrors, GetMemberGroupResponses, GetMemberTypeAllowedAtRootData, GetMemberTypeAllowedAtRootErrors, GetMemberTypeAllowedAtRootResponses, GetMemberTypeBatchData, GetMemberTypeBatchErrors, GetMemberTypeBatchResponses, GetMemberTypeByIdCompositionReferencesData, GetMemberTypeByIdCompositionReferencesErrors, GetMemberTypeByIdCompositionReferencesResponses, GetMemberTypeByIdData, GetMemberTypeByIdErrors, GetMemberTypeByIdExportData, GetMemberTypeByIdExportErrors, GetMemberTypeByIdExportResponses, GetMemberTypeByIdResponses, GetMemberTypeByIdSchemaData, GetMemberTypeByIdSchemaErrors, GetMemberTypeByIdSchemaResponses, GetMemberTypeConfigurationData, GetMemberTypeConfigurationErrors, GetMemberTypeConfigurationResponses, GetMemberTypeFolderByIdData, GetMemberTypeFolderByIdErrors, GetMemberTypeFolderByIdResponses, GetModelsBuilderDashboardData, GetModelsBuilderDashboardErrors, GetModelsBuilderDashboardResponses, GetModelsBuilderStatusData, GetModelsBuilderStatusErrors, GetModelsBuilderStatusResponses, GetNewsDashboardData, GetNewsDashboardErrors, GetNewsDashboardResponses, GetObjectTypesData, GetObjectTypesErrors, GetObjectTypesResponses, GetOembedQueryData, GetOembedQueryErrors, GetOembedQueryResponses, GetPackageConfigurationData, GetPackageConfigurationErrors, GetPackageConfigurationResponses, GetPackageCreatedByIdData, GetPackageCreatedByIdDownloadData, GetPackageCreatedByIdDownloadErrors, GetPackageCreatedByIdDownloadResponses, GetPackageCreatedByIdErrors, GetPackageCreatedByIdResponses, GetPackageCreatedData, GetPackageCreatedErrors, GetPackageCreatedResponses, GetPackageMigrationStatusData, GetPackageMigrationStatusErrors, GetPackageMigrationStatusResponses, GetPartialViewByPathData, GetPartialViewByPathErrors, GetPartialViewByPathResponses, GetPartialViewFolderByPathData, GetPartialViewFolderByPathErrors, GetPartialViewFolderByPathResponses, GetPartialViewSnippetByIdData, GetPartialViewSnippetByIdErrors, GetPartialViewSnippetByIdResponses, GetPartialViewSnippetData, GetPartialViewSnippetErrors, GetPartialViewSnippetResponses, GetProfilingStatusData, GetProfilingStatusErrors, GetProfilingStatusResponses, GetPropertyTypeIsUsedData, GetPropertyTypeIsUsedErrors, GetPropertyTypeIsUsedResponses, GetPublishedCacheRebuildStatusData, GetPublishedCacheRebuildStatusErrors, GetPublishedCacheRebuildStatusResponses, GetRecycleBinDocumentByIdOriginalParentData, GetRecycleBinDocumentByIdOriginalParentErrors, GetRecycleBinDocumentByIdOriginalParentResponses, GetRecycleBinDocumentChildrenData, GetRecycleBinDocumentChildrenErrors, GetRecycleBinDocumentChildrenResponses, GetRecycleBinDocumentReferencedByData, GetRecycleBinDocumentReferencedByErrors, GetRecycleBinDocumentReferencedByResponses, GetRecycleBinDocumentRootData, GetRecycleBinDocumentRootErrors, GetRecycleBinDocumentRootResponses, GetRecycleBinDocumentSiblingsData, GetRecycleBinDocumentSiblingsErrors, GetRecycleBinDocumentSiblingsResponses, GetRecycleBinMediaByIdOriginalParentData, GetRecycleBinMediaByIdOriginalParentErrors, GetRecycleBinMediaByIdOriginalParentResponses, GetRecycleBinMediaChildrenData, GetRecycleBinMediaChildrenErrors, GetRecycleBinMediaChildrenResponses, GetRecycleBinMediaReferencedByData, GetRecycleBinMediaReferencedByErrors, GetRecycleBinMediaReferencedByResponses, GetRecycleBinMediaRootData, GetRecycleBinMediaRootErrors, GetRecycleBinMediaRootResponses, GetRecycleBinMediaSiblingsData, GetRecycleBinMediaSiblingsErrors, GetRecycleBinMediaSiblingsResponses, GetRedirectManagementByIdData, GetRedirectManagementByIdErrors, GetRedirectManagementByIdResponses, GetRedirectManagementData, GetRedirectManagementErrors, GetRedirectManagementResponses, GetRedirectManagementStatusData, GetRedirectManagementStatusErrors, GetRedirectManagementStatusResponses, GetRelationByRelationTypeIdData, GetRelationByRelationTypeIdErrors, GetRelationByRelationTypeIdResponses, GetRelationTypeByIdData, GetRelationTypeByIdErrors, GetRelationTypeByIdResponses, GetRelationTypeData, GetRelationTypeErrors, GetRelationTypeResponses, GetScriptByPathData, GetScriptByPathErrors, GetScriptByPathResponses, GetScriptFolderByPathData, GetScriptFolderByPathErrors, GetScriptFolderByPathResponses, GetSearcherBySearcherNameQueryData, GetSearcherBySearcherNameQueryErrors, GetSearcherBySearcherNameQueryResponses, GetSearcherData, GetSearcherErrors, GetSearcherResponses, GetSecurityConfigurationData, GetSecurityConfigurationErrors, GetSecurityConfigurationResponses, GetSegmentData, GetSegmentErrors, GetSegmentResponses, GetServerConfigurationData, GetServerConfigurationResponses, GetServerInformationData, GetServerInformationErrors, GetServerInformationResponses, GetServerStatusData, GetServerStatusErrors, GetServerStatusResponses, GetServerTroubleshootingData, GetServerTroubleshootingErrors, GetServerTroubleshootingResponses, GetServerUpgradeCheckData, GetServerUpgradeCheckErrors, GetServerUpgradeCheckResponses, GetStylesheetByPathData, GetStylesheetByPathErrors, GetStylesheetByPathResponses, GetStylesheetFolderByPathData, GetStylesheetFolderByPathErrors, GetStylesheetFolderByPathResponses, GetTagData, GetTagErrors, GetTagResponses, GetTelemetryData, GetTelemetryErrors, GetTelemetryLevelData, GetTelemetryLevelErrors, GetTelemetryLevelResponses, GetTelemetryResponses, GetTemplateByIdData, GetTemplateByIdErrors, GetTemplateByIdResponses, GetTemplateConfigurationData, GetTemplateConfigurationErrors, GetTemplateConfigurationResponses, GetTemplateQuerySettingsData, GetTemplateQuerySettingsErrors, GetTemplateQuerySettingsResponses, GetTemporaryFileByIdData, GetTemporaryFileByIdErrors, GetTemporaryFileByIdResponses, GetTemporaryFileConfigurationData, GetTemporaryFileConfigurationErrors, GetTemporaryFileConfigurationResponses, GetTreeDataTypeAncestorsData, GetTreeDataTypeAncestorsErrors, GetTreeDataTypeAncestorsResponses, GetTreeDataTypeChildrenData, GetTreeDataTypeChildrenErrors, GetTreeDataTypeChildrenResponses, GetTreeDataTypeRootData, GetTreeDataTypeRootErrors, GetTreeDataTypeRootResponses, GetTreeDataTypeSearchData, GetTreeDataTypeSearchErrors, GetTreeDataTypeSearchResponses, GetTreeDataTypeSiblingsData, GetTreeDataTypeSiblingsErrors, GetTreeDataTypeSiblingsResponses, GetTreeDictionaryAncestorsData, GetTreeDictionaryAncestorsErrors, GetTreeDictionaryAncestorsResponses, GetTreeDictionaryChildrenData, GetTreeDictionaryChildrenErrors, GetTreeDictionaryChildrenResponses, GetTreeDictionaryRootData, GetTreeDictionaryRootErrors, GetTreeDictionaryRootResponses, GetTreeDocumentAncestorsData, GetTreeDocumentAncestorsErrors, GetTreeDocumentAncestorsResponses, GetTreeDocumentBlueprintAncestorsData, GetTreeDocumentBlueprintAncestorsErrors, GetTreeDocumentBlueprintAncestorsResponses, GetTreeDocumentBlueprintChildrenData, GetTreeDocumentBlueprintChildrenErrors, GetTreeDocumentBlueprintChildrenResponses, GetTreeDocumentBlueprintRootData, GetTreeDocumentBlueprintRootErrors, GetTreeDocumentBlueprintRootResponses, GetTreeDocumentBlueprintSiblingsData, GetTreeDocumentBlueprintSiblingsErrors, GetTreeDocumentBlueprintSiblingsResponses, GetTreeDocumentChildrenData, GetTreeDocumentChildrenErrors, GetTreeDocumentChildrenResponses, GetTreeDocumentRootData, GetTreeDocumentRootErrors, GetTreeDocumentRootResponses, GetTreeDocumentSiblingsData, GetTreeDocumentSiblingsErrors, GetTreeDocumentSiblingsResponses, GetTreeDocumentTypeAncestorsData, GetTreeDocumentTypeAncestorsErrors, GetTreeDocumentTypeAncestorsResponses, GetTreeDocumentTypeChildrenData, GetTreeDocumentTypeChildrenErrors, GetTreeDocumentTypeChildrenResponses, GetTreeDocumentTypeRootData, GetTreeDocumentTypeRootErrors, GetTreeDocumentTypeRootResponses, GetTreeDocumentTypeSearchData, GetTreeDocumentTypeSearchErrors, GetTreeDocumentTypeSearchResponses, GetTreeDocumentTypeSiblingsData, GetTreeDocumentTypeSiblingsErrors, GetTreeDocumentTypeSiblingsResponses, GetTreeMediaAncestorsData, GetTreeMediaAncestorsErrors, GetTreeMediaAncestorsResponses, GetTreeMediaChildrenData, GetTreeMediaChildrenErrors, GetTreeMediaChildrenResponses, GetTreeMediaRootData, GetTreeMediaRootErrors, GetTreeMediaRootResponses, GetTreeMediaSiblingsData, GetTreeMediaSiblingsErrors, GetTreeMediaSiblingsResponses, GetTreeMediaTypeAncestorsData, GetTreeMediaTypeAncestorsErrors, GetTreeMediaTypeAncestorsResponses, GetTreeMediaTypeChildrenData, GetTreeMediaTypeChildrenErrors, GetTreeMediaTypeChildrenResponses, GetTreeMediaTypeRootData, GetTreeMediaTypeRootErrors, GetTreeMediaTypeRootResponses, GetTreeMediaTypeSiblingsData, GetTreeMediaTypeSiblingsErrors, GetTreeMediaTypeSiblingsResponses, GetTreeMemberGroupRootData, GetTreeMemberGroupRootErrors, GetTreeMemberGroupRootResponses, GetTreeMemberTypeAncestorsData, GetTreeMemberTypeAncestorsErrors, GetTreeMemberTypeAncestorsResponses, GetTreeMemberTypeChildrenData, GetTreeMemberTypeChildrenErrors, GetTreeMemberTypeChildrenResponses, GetTreeMemberTypeRootData, GetTreeMemberTypeRootErrors, GetTreeMemberTypeRootResponses, GetTreeMemberTypeSiblingsData, GetTreeMemberTypeSiblingsErrors, GetTreeMemberTypeSiblingsResponses, GetTreePartialViewAncestorsData, GetTreePartialViewAncestorsErrors, GetTreePartialViewAncestorsResponses, GetTreePartialViewChildrenData, GetTreePartialViewChildrenErrors, GetTreePartialViewChildrenResponses, GetTreePartialViewRootData, GetTreePartialViewRootErrors, GetTreePartialViewRootResponses, GetTreePartialViewSiblingsData, GetTreePartialViewSiblingsErrors, GetTreePartialViewSiblingsResponses, GetTreeScriptAncestorsData, GetTreeScriptAncestorsErrors, GetTreeScriptAncestorsResponses, GetTreeScriptChildrenData, GetTreeScriptChildrenErrors, GetTreeScriptChildrenResponses, GetTreeScriptRootData, GetTreeScriptRootErrors, GetTreeScriptRootResponses, GetTreeScriptSiblingsData, GetTreeScriptSiblingsErrors, GetTreeScriptSiblingsResponses, GetTreeStaticFileAncestorsData, GetTreeStaticFileAncestorsErrors, GetTreeStaticFileAncestorsResponses, GetTreeStaticFileChildrenData, GetTreeStaticFileChildrenErrors, GetTreeStaticFileChildrenResponses, GetTreeStaticFileRootData, GetTreeStaticFileRootErrors, GetTreeStaticFileRootResponses, GetTreeStylesheetAncestorsData, GetTreeStylesheetAncestorsErrors, GetTreeStylesheetAncestorsResponses, GetTreeStylesheetChildrenData, GetTreeStylesheetChildrenErrors, GetTreeStylesheetChildrenResponses, GetTreeStylesheetRootData, GetTreeStylesheetRootErrors, GetTreeStylesheetRootResponses, GetTreeStylesheetSiblingsData, GetTreeStylesheetSiblingsErrors, GetTreeStylesheetSiblingsResponses, GetTreeTemplateAncestorsData, GetTreeTemplateAncestorsErrors, GetTreeTemplateAncestorsResponses, GetTreeTemplateChildrenData, GetTreeTemplateChildrenErrors, GetTreeTemplateChildrenResponses, GetTreeTemplateRootData, GetTreeTemplateRootErrors, GetTreeTemplateRootResponses, GetTreeTemplateSiblingsData, GetTreeTemplateSiblingsErrors, GetTreeTemplateSiblingsResponses, GetUpgradeSettingsData, GetUpgradeSettingsErrors, GetUpgradeSettingsResponses, GetUserById2FaData, GetUserById2FaErrors, GetUserById2FaResponses, GetUserByIdCalculateStartNodesData, GetUserByIdCalculateStartNodesErrors, GetUserByIdCalculateStartNodesResponses, GetUserByIdClientCredentialsData, GetUserByIdClientCredentialsErrors, GetUserByIdClientCredentialsResponses, GetUserByIdData, GetUserByIdErrors, GetUserByIdResponses, GetUserConfigurationData, GetUserConfigurationErrors, GetUserConfigurationResponses, GetUserCurrent2FaByProviderNameData, GetUserCurrent2FaByProviderNameErrors, GetUserCurrent2FaByProviderNameResponses, GetUserCurrent2FaData, GetUserCurrent2FaErrors, GetUserCurrent2FaResponses, GetUserCurrentConfigurationData, GetUserCurrentConfigurationErrors, GetUserCurrentConfigurationResponses, GetUserCurrentData, GetUserCurrentErrors, GetUserCurrentLoginProvidersData, GetUserCurrentLoginProvidersErrors, GetUserCurrentLoginProvidersResponses, GetUserCurrentPermissionsData, GetUserCurrentPermissionsDocumentData, GetUserCurrentPermissionsDocumentErrors, GetUserCurrentPermissionsDocumentResponses, GetUserCurrentPermissionsErrors, GetUserCurrentPermissionsMediaData, GetUserCurrentPermissionsMediaErrors, GetUserCurrentPermissionsMediaResponses, GetUserCurrentPermissionsResponses, GetUserCurrentResponses, GetUserData, GetUserDataByIdData, GetUserDataByIdErrors, GetUserDataByIdResponses, GetUserDataData, GetUserDataErrors, GetUserDataResponses, GetUserErrors, GetUserGroupByIdData, GetUserGroupByIdErrors, GetUserGroupByIdResponses, GetUserGroupData, GetUserGroupErrors, GetUserGroupResponses, GetUserResponses, GetWebhookByIdData, GetWebhookByIdErrors, GetWebhookByIdLogsData, GetWebhookByIdLogsErrors, GetWebhookByIdLogsResponses, GetWebhookByIdResponses, GetWebhookData, GetWebhookErrors, GetWebhookEventsData, GetWebhookEventsErrors, GetWebhookEventsResponses, GetWebhookLogsData, GetWebhookLogsErrors, GetWebhookLogsResponses, GetWebhookResponses, PatchDocumentByIdPatchData, PatchDocumentByIdPatchErrors, PatchDocumentByIdPatchResponses, PostDataTypeByIdCopyData, PostDataTypeByIdCopyErrors, PostDataTypeByIdCopyResponses, PostDataTypeData, PostDataTypeErrors, PostDataTypeFolderData, PostDataTypeFolderErrors, PostDataTypeFolderResponses, PostDataTypeResponses, PostDictionaryData, PostDictionaryErrors, PostDictionaryImportData, PostDictionaryImportErrors, PostDictionaryImportResponses, PostDictionaryResponses, PostDocumentBlueprintData, PostDocumentBlueprintErrors, PostDocumentBlueprintFolderData, PostDocumentBlueprintFolderErrors, PostDocumentBlueprintFolderResponses, PostDocumentBlueprintFromDocumentData, PostDocumentBlueprintFromDocumentErrors, PostDocumentBlueprintFromDocumentResponses, PostDocumentBlueprintResponses, PostDocumentByIdCopyData, PostDocumentByIdCopyErrors, PostDocumentByIdCopyResponses, PostDocumentByIdPublicAccessData, PostDocumentByIdPublicAccessErrors, PostDocumentByIdPublicAccessResponses, PostDocumentCreateAndPublishData, PostDocumentCreateAndPublishErrors, PostDocumentCreateAndPublishResponses, PostDocumentData, PostDocumentErrors, PostDocumentResponses, PostDocumentTypeAvailableCompositionsData, PostDocumentTypeAvailableCompositionsErrors, PostDocumentTypeAvailableCompositionsResponses, PostDocumentTypeByIdCopyData, PostDocumentTypeByIdCopyErrors, PostDocumentTypeByIdCopyResponses, PostDocumentTypeByIdTemplateData, PostDocumentTypeByIdTemplateErrors, PostDocumentTypeByIdTemplateResponses, PostDocumentTypeData, PostDocumentTypeErrors, PostDocumentTypeFolderData, PostDocumentTypeFolderErrors, PostDocumentTypeFolderResponses, PostDocumentTypeImportData, PostDocumentTypeImportErrors, PostDocumentTypeImportResponses, PostDocumentTypeResponses, PostDocumentValidateData, PostDocumentValidateErrors, PostDocumentValidateResponses, PostDocumentVersionByIdRollbackData, PostDocumentVersionByIdRollbackErrors, PostDocumentVersionByIdRollbackResponses, PostDynamicRootQueryData, PostDynamicRootQueryErrors, PostDynamicRootQueryResponses, PostHealthCheckExecuteActionData, PostHealthCheckExecuteActionErrors, PostHealthCheckExecuteActionResponses, PostHealthCheckGroupByNameCheckData, PostHealthCheckGroupByNameCheckErrors, PostHealthCheckGroupByNameCheckResponses, PostIndexerByIndexNameRebuildData, PostIndexerByIndexNameRebuildErrors, PostIndexerByIndexNameRebuildResponses, PostInstallSetupData, PostInstallSetupErrors, PostInstallSetupResponses, PostInstallValidateDatabaseData, PostInstallValidateDatabaseErrors, PostInstallValidateDatabaseResponses, PostLanguageData, PostLanguageErrors, PostLanguageResponses, PostLogViewerSavedSearchData, PostLogViewerSavedSearchErrors, PostLogViewerSavedSearchResponses, PostMediaData, PostMediaErrors, PostMediaResponses, PostMediaTypeAvailableCompositionsData, PostMediaTypeAvailableCompositionsErrors, PostMediaTypeAvailableCompositionsResponses, PostMediaTypeByIdCopyData, PostMediaTypeByIdCopyErrors, PostMediaTypeByIdCopyResponses, PostMediaTypeData, PostMediaTypeErrors, PostMediaTypeFolderData, PostMediaTypeFolderErrors, PostMediaTypeFolderResponses, PostMediaTypeImportData, PostMediaTypeImportErrors, PostMediaTypeImportResponses, PostMediaTypeResponses, PostMediaValidateData, PostMediaValidateErrors, PostMediaValidateResponses, PostMemberData, PostMemberErrors, PostMemberGroupData, PostMemberGroupErrors, PostMemberGroupResponses, PostMemberResponses, PostMemberTypeAvailableCompositionsData, PostMemberTypeAvailableCompositionsErrors, PostMemberTypeAvailableCompositionsResponses, PostMemberTypeByIdCopyData, PostMemberTypeByIdCopyErrors, PostMemberTypeByIdCopyResponses, PostMemberTypeData, PostMemberTypeErrors, PostMemberTypeFolderData, PostMemberTypeFolderErrors, PostMemberTypeFolderResponses, PostMemberTypeImportData, PostMemberTypeImportErrors, PostMemberTypeImportResponses, PostMemberTypeResponses, PostMemberValidateData, PostMemberValidateErrors, PostMemberValidateResponses, PostModelsBuilderBuildData, PostModelsBuilderBuildErrors, PostModelsBuilderBuildResponses, PostPackageByNameRunMigrationData, PostPackageByNameRunMigrationErrors, PostPackageByNameRunMigrationResponses, PostPackageCreatedData, PostPackageCreatedErrors, PostPackageCreatedResponses, PostPartialViewData, PostPartialViewErrors, PostPartialViewFolderData, PostPartialViewFolderErrors, PostPartialViewFolderResponses, PostPartialViewResponses, PostPreviewData, PostPreviewErrors, PostPreviewResponses, PostPublishedCacheRebuildData, PostPublishedCacheRebuildErrors, PostPublishedCacheRebuildResponses, PostPublishedCacheReloadData, PostPublishedCacheReloadErrors, PostPublishedCacheReloadResponses, PostRedirectManagementStatusData, PostRedirectManagementStatusErrors, PostRedirectManagementStatusResponses, PostScriptData, PostScriptErrors, PostScriptFolderData, PostScriptFolderErrors, PostScriptFolderResponses, PostScriptResponses, PostSecurityForgotPasswordData, PostSecurityForgotPasswordErrors, PostSecurityForgotPasswordResetData, PostSecurityForgotPasswordResetErrors, PostSecurityForgotPasswordResetResponses, PostSecurityForgotPasswordResponses, PostSecurityForgotPasswordVerifyData, PostSecurityForgotPasswordVerifyErrors, PostSecurityForgotPasswordVerifyResponses, PostStylesheetData, PostStylesheetErrors, PostStylesheetFolderData, PostStylesheetFolderErrors, PostStylesheetFolderResponses, PostStylesheetResponses, PostTelemetryLevelData, PostTelemetryLevelErrors, PostTelemetryLevelResponses, PostTemplateData, PostTemplateErrors, PostTemplateQueryExecuteData, PostTemplateQueryExecuteErrors, PostTemplateQueryExecuteResponses, PostTemplateResponses, PostTemporaryFileData, PostTemporaryFileErrors, PostTemporaryFileResponses, PostUpgradeAuthorizeData, PostUpgradeAuthorizeErrors, PostUpgradeAuthorizeResponses, PostUserAvatarByIdData, PostUserAvatarByIdErrors, PostUserAvatarByIdResponses, PostUserByIdChangePasswordData, PostUserByIdChangePasswordErrors, PostUserByIdChangePasswordResponses, PostUserByIdClientCredentialsData, PostUserByIdClientCredentialsErrors, PostUserByIdClientCredentialsResponses, PostUserByIdResetPasswordData, PostUserByIdResetPasswordErrors, PostUserByIdResetPasswordResponses, PostUserCurrent2FaByProviderNameData, PostUserCurrent2FaByProviderNameErrors, PostUserCurrent2FaByProviderNameResponses, PostUserCurrentAvatarData, PostUserCurrentAvatarErrors, PostUserCurrentAvatarResponses, PostUserCurrentChangePasswordData, PostUserCurrentChangePasswordErrors, PostUserCurrentChangePasswordResponses, PostUserData, PostUserDataData, PostUserDataErrors, PostUserDataResponses, PostUserDisableData, PostUserDisableErrors, PostUserDisableResponses, PostUserEnableData, PostUserEnableErrors, PostUserEnableResponses, PostUserErrors, PostUserGroupByIdUsersData, PostUserGroupByIdUsersErrors, PostUserGroupByIdUsersResponses, PostUserGroupData, PostUserGroupErrors, PostUserGroupResponses, PostUserInviteCreatePasswordData, PostUserInviteCreatePasswordErrors, PostUserInviteCreatePasswordResponses, PostUserInviteData, PostUserInviteErrors, PostUserInviteResendData, PostUserInviteResendErrors, PostUserInviteResendResponses, PostUserInviteResponses, PostUserInviteVerifyData, PostUserInviteVerifyErrors, PostUserInviteVerifyResponses, PostUserResponses, PostUserSetUserGroupsData, PostUserSetUserGroupsErrors, PostUserSetUserGroupsResponses, PostUserUnlockData, PostUserUnlockErrors, PostUserUnlockResponses, PostWebhookData, PostWebhookErrors, PostWebhookResponses, PutDataTypeByIdData, PutDataTypeByIdErrors, PutDataTypeByIdMoveData, PutDataTypeByIdMoveErrors, PutDataTypeByIdMoveResponses, PutDataTypeByIdResponses, PutDataTypeFolderByIdData, PutDataTypeFolderByIdErrors, PutDataTypeFolderByIdResponses, PutDictionaryByIdData, PutDictionaryByIdErrors, PutDictionaryByIdMoveData, PutDictionaryByIdMoveErrors, PutDictionaryByIdMoveResponses, PutDictionaryByIdResponses, PutDocumentBlueprintByIdData, PutDocumentBlueprintByIdErrors, PutDocumentBlueprintByIdMoveData, PutDocumentBlueprintByIdMoveErrors, PutDocumentBlueprintByIdMoveResponses, PutDocumentBlueprintByIdResponses, PutDocumentBlueprintFolderByIdData, PutDocumentBlueprintFolderByIdErrors, PutDocumentBlueprintFolderByIdResponses, PutDocumentByIdData, PutDocumentByIdDomainsData, PutDocumentByIdDomainsErrors, PutDocumentByIdDomainsResponses, PutDocumentByIdErrors, PutDocumentByIdMoveData, PutDocumentByIdMoveErrors, PutDocumentByIdMoveResponses, PutDocumentByIdMoveToRecycleBinData, PutDocumentByIdMoveToRecycleBinErrors, PutDocumentByIdMoveToRecycleBinResponses, PutDocumentByIdNotificationsData, PutDocumentByIdNotificationsErrors, PutDocumentByIdNotificationsResponses, PutDocumentByIdPublicAccessData, PutDocumentByIdPublicAccessErrors, PutDocumentByIdPublicAccessResponses, PutDocumentByIdPublishData, PutDocumentByIdPublishErrors, PutDocumentByIdPublishResponses, PutDocumentByIdPublishWithDescendantsData, PutDocumentByIdPublishWithDescendantsErrors, PutDocumentByIdPublishWithDescendantsResponses, PutDocumentByIdResponses, PutDocumentByIdUnpublishData, PutDocumentByIdUnpublishErrors, PutDocumentByIdUnpublishResponses, PutDocumentByIdUpdateAndPublishData, PutDocumentByIdUpdateAndPublishErrors, PutDocumentByIdUpdateAndPublishResponses, PutDocumentSortData, PutDocumentSortErrors, PutDocumentSortResponses, PutDocumentTypeByIdData, PutDocumentTypeByIdErrors, PutDocumentTypeByIdImportData, PutDocumentTypeByIdImportErrors, PutDocumentTypeByIdImportResponses, PutDocumentTypeByIdMoveData, PutDocumentTypeByIdMoveErrors, PutDocumentTypeByIdMoveResponses, PutDocumentTypeByIdResponses, PutDocumentTypeFolderByIdData, PutDocumentTypeFolderByIdErrors, PutDocumentTypeFolderByIdResponses, PutDocumentVersionByIdPreventCleanupData, PutDocumentVersionByIdPreventCleanupErrors, PutDocumentVersionByIdPreventCleanupResponses, PutLanguageByIsoCodeData, PutLanguageByIsoCodeErrors, PutLanguageByIsoCodeResponses, PutMediaByIdData, PutMediaByIdErrors, PutMediaByIdMoveData, PutMediaByIdMoveErrors, PutMediaByIdMoveResponses, PutMediaByIdMoveToRecycleBinData, PutMediaByIdMoveToRecycleBinErrors, PutMediaByIdMoveToRecycleBinResponses, PutMediaByIdResponses, PutMediaByIdValidateData, PutMediaByIdValidateErrors, PutMediaByIdValidateResponses, PutMediaSortData, PutMediaSortErrors, PutMediaSortResponses, PutMediaTypeByIdData, PutMediaTypeByIdErrors, PutMediaTypeByIdImportData, PutMediaTypeByIdImportErrors, PutMediaTypeByIdImportResponses, PutMediaTypeByIdMoveData, PutMediaTypeByIdMoveErrors, PutMediaTypeByIdMoveResponses, PutMediaTypeByIdResponses, PutMediaTypeFolderByIdData, PutMediaTypeFolderByIdErrors, PutMediaTypeFolderByIdResponses, PutMemberByIdData, PutMemberByIdErrors, PutMemberByIdResponses, PutMemberByIdValidateData, PutMemberByIdValidateErrors, PutMemberByIdValidateResponses, PutMemberGroupByIdData, PutMemberGroupByIdErrors, PutMemberGroupByIdResponses, PutMemberTypeByIdData, PutMemberTypeByIdErrors, PutMemberTypeByIdImportData, PutMemberTypeByIdImportErrors, PutMemberTypeByIdImportResponses, PutMemberTypeByIdMoveData, PutMemberTypeByIdMoveErrors, PutMemberTypeByIdMoveResponses, PutMemberTypeByIdResponses, PutMemberTypeFolderByIdData, PutMemberTypeFolderByIdErrors, PutMemberTypeFolderByIdResponses, PutPackageCreatedByIdData, PutPackageCreatedByIdErrors, PutPackageCreatedByIdResponses, PutPartialViewByPathData, PutPartialViewByPathErrors, PutPartialViewByPathRenameData, PutPartialViewByPathRenameErrors, PutPartialViewByPathRenameResponses, PutPartialViewByPathResponses, PutProfilingStatusData, PutProfilingStatusErrors, PutProfilingStatusResponses, PutRecycleBinDocumentByIdRestoreData, PutRecycleBinDocumentByIdRestoreErrors, PutRecycleBinDocumentByIdRestoreResponses, PutRecycleBinMediaByIdRestoreData, PutRecycleBinMediaByIdRestoreErrors, PutRecycleBinMediaByIdRestoreResponses, PutScriptByPathData, PutScriptByPathErrors, PutScriptByPathRenameData, PutScriptByPathRenameErrors, PutScriptByPathRenameResponses, PutScriptByPathResponses, PutStylesheetByPathData, PutStylesheetByPathErrors, PutStylesheetByPathRenameData, PutStylesheetByPathRenameErrors, PutStylesheetByPathRenameResponses, PutStylesheetByPathResponses, PutTemplateByIdData, PutTemplateByIdErrors, PutTemplateByIdResponses, PutUmbracoManagementApiV11DocumentByIdValidate11Data, PutUmbracoManagementApiV11DocumentByIdValidate11Errors, PutUmbracoManagementApiV11DocumentByIdValidate11Responses, PutUserByIdData, PutUserByIdErrors, PutUserByIdResponses, PutUserCurrentProfileData, PutUserCurrentProfileErrors, PutUserCurrentProfileResponses, PutUserDataData, PutUserDataErrors, PutUserDataResponses, PutUserGroupByIdData, PutUserGroupByIdErrors, PutUserGroupByIdResponses, PutWebhookByIdData, PutWebhookByIdErrors, PutWebhookByIdResponses } from './types.gen';
export type Options = Options2 & {
/**
@@ -2292,6 +2292,28 @@ export class DocumentService {
});
}
+ /**
+ * Updates and publishes a document.
+ *
+ * Updates and publishes a document identified by the provided Id with the details from the request model.
+ */
+ public static putDocumentByIdUpdateAndPublish(options: Options) {
+ return (options.client ?? client).put({
+ security: [
+ {
+ scheme: 'bearer',
+ type: 'http'
+ }
+ ],
+ url: '/umbraco/management/api/v1/document/{id}/update-and-publish',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options.headers
+ }
+ });
+ }
+
/**
* Gets a collection of items that reference documents.
*
@@ -2328,6 +2350,28 @@ export class DocumentService {
});
}
+ /**
+ * Creates and publishes a new document.
+ *
+ * Creates and publishes a new document with the configuration specified in the request model.
+ */
+ public static postDocumentCreateAndPublish(options?: Options) {
+ return (options?.client ?? client).post({
+ security: [
+ {
+ scheme: 'bearer',
+ type: 'http'
+ }
+ ],
+ url: '/umbraco/management/api/v1/document/create-and-publish',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers
+ }
+ });
+ }
+
/**
* Sorts documents.
*
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts
index 9f2f8ed2b0c0..3de4fde7fc27 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts
@@ -161,6 +161,16 @@ export type CopyMemberTypeRequestModel = {
target?: ReferenceByIdModel | null;
};
+export type CreateAndPublishDocumentRequestModel = {
+ values: Array;
+ variants: Array;
+ id?: string | null;
+ parent?: ReferenceByIdModel | null;
+ documentType: ReferenceByIdModel;
+ template: ReferenceByIdModel | null;
+ culturesToPublish: Array;
+};
+
export type CreateDataTypeRequestModel = {
name: string;
editorAlias: string;
@@ -2734,6 +2744,13 @@ export type UnpublishDocumentRequestModel = {
cultures?: Array | null;
};
+export type UpdateAndPublishDocumentRequestModel = {
+ values: Array;
+ variants: Array;
+ template?: ReferenceByIdModel | null;
+ culturesToPublish: Array;
+};
+
export type UpdateCurrentUserRequestModel = {
languageIsoCode: string;
};
@@ -7320,6 +7337,43 @@ export type PutDocumentByIdUnpublishResponses = {
200: unknown;
};
+export type PutDocumentByIdUpdateAndPublishData = {
+ body?: UpdateAndPublishDocumentRequestModel;
+ path: {
+ id: string;
+ };
+ query?: never;
+ url: '/umbraco/management/api/v1/document/{id}/update-and-publish';
+};
+
+export type PutDocumentByIdUpdateAndPublishErrors = {
+ /**
+ * Bad Request
+ */
+ 400: ProblemDetails;
+ /**
+ * The resource is protected and requires an authentication token
+ */
+ 401: unknown;
+ /**
+ * The authenticated user does not have access to this resource
+ */
+ 403: unknown;
+ /**
+ * Not Found
+ */
+ 404: ProblemDetails;
+};
+
+export type PutDocumentByIdUpdateAndPublishError = PutDocumentByIdUpdateAndPublishErrors[keyof PutDocumentByIdUpdateAndPublishErrors];
+
+export type PutDocumentByIdUpdateAndPublishResponses = {
+ /**
+ * OK
+ */
+ 200: unknown;
+};
+
export type PutUmbracoManagementApiV11DocumentByIdValidate11Data = {
body?: ValidateUpdateDocumentRequestModel;
path: {
@@ -7415,6 +7469,41 @@ export type GetDocumentConfigurationResponses = {
export type GetDocumentConfigurationResponse = GetDocumentConfigurationResponses[keyof GetDocumentConfigurationResponses];
+export type PostDocumentCreateAndPublishData = {
+ body?: CreateAndPublishDocumentRequestModel;
+ path?: never;
+ query?: never;
+ url: '/umbraco/management/api/v1/document/create-and-publish';
+};
+
+export type PostDocumentCreateAndPublishErrors = {
+ /**
+ * Bad Request
+ */
+ 400: ProblemDetails;
+ /**
+ * The resource is protected and requires an authentication token
+ */
+ 401: unknown;
+ /**
+ * The authenticated user does not have access to this resource
+ */
+ 403: unknown;
+ /**
+ * Not Found
+ */
+ 404: ProblemDetails;
+};
+
+export type PostDocumentCreateAndPublishError = PostDocumentCreateAndPublishErrors[keyof PostDocumentCreateAndPublishErrors];
+
+export type PostDocumentCreateAndPublishResponses = {
+ /**
+ * Created
+ */
+ 201: unknown;
+};
+
export type PutDocumentSortData = {
body?: SortingRequestModel;
path?: never;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/url-pattern-to-string.function.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/url-pattern-to-string.function.test.ts
new file mode 100644
index 000000000000..cc4745ebe8e6
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/url-pattern-to-string.function.test.ts
@@ -0,0 +1,61 @@
+import { umbUrlPatternToString } from './url-pattern-to-string.function.js';
+import { expect } from '@open-wc/testing';
+
+describe('umbUrlPatternToString', () => {
+ describe('when params is null', () => {
+ it('should return the pattern unchanged', () => {
+ expect(umbUrlPatternToString('/section/:id/edit', null)).to.eq('/section/:id/edit');
+ });
+
+ it('should return a pattern without parameters unchanged', () => {
+ expect(umbUrlPatternToString('/section/dashboard', null)).to.eq('/section/dashboard');
+ });
+ });
+
+ describe('parameter replacement', () => {
+ it('should replace a single parameter with its value', () => {
+ expect(umbUrlPatternToString('/section/:id', { id: '123' })).to.eq('/section/123');
+ });
+
+ it('should replace multiple parameters with their values', () => {
+ expect(umbUrlPatternToString('/:section/:id/edit', { section: 'content', id: '123' })).to.eq(
+ '/content/123/edit',
+ );
+ });
+
+ it('should leave the pattern unchanged when it has no parameters', () => {
+ expect(umbUrlPatternToString('/section/dashboard', { id: '123' })).to.eq('/section/dashboard');
+ });
+
+ it('should replace a parameter that spans the rest of a path segment', () => {
+ expect(umbUrlPatternToString('/section/:entityType', { entityType: 'document-type' })).to.eq(
+ '/section/document-type',
+ );
+ });
+ });
+
+ describe('value coercion', () => {
+ it('should coerce a number value to a string', () => {
+ expect(umbUrlPatternToString('/page/:index', { index: 42 })).to.eq('/page/42');
+ });
+
+ it('should call toString on an object value', () => {
+ const value = { toString: () => 'custom' };
+ expect(umbUrlPatternToString('/section/:id', { id: value })).to.eq('/section/custom');
+ });
+
+ it('should render a null value as the string "null"', () => {
+ expect(umbUrlPatternToString('/section/:id', { id: null })).to.eq('/section/null');
+ });
+ });
+
+ describe('missing parameters', () => {
+ it('should keep the literal token when the value is undefined', () => {
+ expect(umbUrlPatternToString('/section/:id', {})).to.eq('/section/:id');
+ });
+
+ it('should replace known parameters and keep unknown ones literal', () => {
+ expect(umbUrlPatternToString('/:section/:id', { section: 'content' })).to.eq('/content/:id');
+ });
+ });
+});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/url-pattern-to-string.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/url-pattern-to-string.function.ts
index e7175e73082b..b36ab9149518 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/url-pattern-to-string.function.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/url-pattern-to-string.function.ts
@@ -8,9 +8,12 @@ export type UrlParametersRecord = UmbUrlParametersRecord;
const PARAM_IDENTIFIER = /:([^/]+)/g;
/**
- *
- * @param pattern
- * @param params
+ * Resolves the `:parameter` tokens in a URL pattern using the given parameter values.
+ * @param {string} pattern - A URL pattern where parameters are written as `:name` (e.g. `/section/:id/edit`).
+ * @param {UmbUrlParametersRecord | null} params - Key value object, where keys are parameter names and values are the values to replace them with.
+ * @returns {string} The pattern with its parameter tokens replaced.
+ * @example
+ * umbUrlPatternToString('/section/:id/edit', { id: 123 }); // '/section/123/edit'
*/
export function umbUrlPatternToString(pattern: string, params: UmbUrlParametersRecord | null): string {
return params
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-route-manager.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-route-manager.controller.ts
index 6aa5332d926e..7caf10f17f0d 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-route-manager.controller.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-route-manager.controller.ts
@@ -1,5 +1,5 @@
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
-import { UmbArrayState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
+import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import type { IComponentRoute, UmbRoute } from '@umbraco-cms/backoffice/router';
/**
@@ -12,7 +12,8 @@ export class UmbWorkspaceRouteManager extends UmbControllerBase {
#routes = new UmbArrayState([], (x) => x.path);
public readonly routes = this.#routes.asObservable();
- #activeLocalPath = new UmbStringState('');
+ #absolutePath?: string;
+ #localPath?: string;
/**
* Set the routes for the workspace.
@@ -34,7 +35,10 @@ export class UmbWorkspaceRouteManager extends UmbControllerBase {
const oldSetupCallback = route.setup;
route.setup = (_component: any, info: any) => {
- this.#activeLocalPath.setValue(info.match.fragments.consumed);
+ // TODO: could this be invalidated by the time it's used? [NL]
+ // That would be a parent router switches path without the view being changed...
+ this.#absolutePath = info.slot?.constructAbsolutePath();
+ this.#localPath = info.match.fragments.consumed;
if (oldSetupCallback) {
oldSetupCallback(_component, info);
@@ -58,10 +62,19 @@ export class UmbWorkspaceRouteManager extends UmbControllerBase {
/**
* Get the active local path.
- * @returns {*} {string}
+ * @returns {string} The active local path.
* @memberof UmbWorkspaceRouteManager
*/
getActiveLocalPath(): string {
- return this.#activeLocalPath.getValue();
+ return this.#localPath ?? '';
+ }
+
+ /**
+ * Get the absolute path.
+ * @returns {string | undefined} The absolute path.
+ * @memberof UmbWorkspaceRouteManager
+ */
+ getAbsolutePath(): string | undefined {
+ return this.#absolutePath;
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts
index 0499e6769d22..dd78848286f5 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts
@@ -1,4 +1,5 @@
import { UmbSubmittableWorkspaceContextBase } from '../submittable/index.js';
+import { umbWorkspaceWillNavigateAway } from '../utils/check-will-navigate-away.function.js';
import { UmbEntityWorkspaceDataManager } from '../entity/entity-workspace-data-manager.js';
import type { UmbSubmittableTreeEntityWorkspaceContext } from '../contexts/tokens/index.js';
import type { UmbEntityDetailWorkspaceContextArgs, UmbEntityDetailWorkspaceContextCreateArgs } from './types.js';
@@ -28,11 +29,11 @@ const LOADING_STATE_UNIQUE = 'umbLoadingEntityDetail';
const FORBIDDEN_STATE_UNIQUE = 'umbForbiddenEntityDetail';
export abstract class UmbEntityDetailWorkspaceContextBase<
- DetailModelType extends UmbEntityModel = UmbEntityModel,
- DetailRepositoryType extends UmbDetailRepository = UmbDetailRepository,
- CreateArgsType extends
- UmbEntityDetailWorkspaceContextCreateArgs = UmbEntityDetailWorkspaceContextCreateArgs,
- >
+ DetailModelType extends UmbEntityModel = UmbEntityModel,
+ DetailRepositoryType extends UmbDetailRepository = UmbDetailRepository,
+ CreateArgsType extends UmbEntityDetailWorkspaceContextCreateArgs =
+ UmbEntityDetailWorkspaceContextCreateArgs,
+>
extends UmbSubmittableWorkspaceContextBase
implements UmbSubmittableTreeEntityWorkspaceContext
{
@@ -291,6 +292,19 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
}
}
+ /**
+ * Requests the latest persisted version of the entity from the server WITHOUT applying it to the
+ * workspace state, and returns the processed data.
+ * @returns { Promise } The latest persisted data.
+ */
+ public async loadWithoutPersist(): Promise {
+ const unique = this.getUnique();
+ if (!unique) throw new Error('Unique is not set');
+ const { data, error } = await this._detailRepository!.requestByUnique(unique);
+ if (error || !data) throw new Error('Error loading entity', { cause: error });
+ return await this._processIncomingData(data);
+ }
+
/**
* Method to check if the workspace data is loaded.
* @returns { Promise | undefined } true if the workspace data is loaded.
@@ -391,10 +405,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
* @memberof UmbEntityWorkspaceContextBase
*/
protected _checkWillNavigateAway(newUrl: string | URL): boolean {
- if (newUrl instanceof URL) {
- newUrl = newUrl.href;
- }
- return !newUrl.includes(this.routes.getActiveLocalPath());
+ return umbWorkspaceWillNavigateAway(this.routes, this.getUnique(), newUrl);
}
protected async _create(currentData: DetailModelType, parent: UmbEntityModel) {
@@ -402,7 +413,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
const { error, data } = await this._detailRepository.create(currentData, parent.unique);
if (error || !data) {
- throw error?.message ?? 'Repository did not return data after create.';
+ throw new Error('Repository did not return data after create.', { cause: error });
}
this.#entityContext.setUnique(data.unique);
@@ -431,7 +442,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
protected async _update(currentData: DetailModelType) {
const { error, data } = await this._detailRepository!.save(currentData);
if (error || !data) {
- throw error?.message ?? 'Repository did not return data after create.';
+ throw new Error('Entity Detail Repository failed saving', { cause: error });
}
this._data.setPersisted(data);
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts
index 3ee9f937020e..783e90aa5ef7 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts
@@ -8,6 +8,7 @@ export * from './info-app/index.js';
export * from './modals/index.js';
export * from './paths.js';
export * from './submittable/index.js';
+export * from './utils/check-will-navigate-away.function.js';
export * from './utils/object-to-property-value-array.function.js';
export * from './workspace-property-dataset/index.js';
export * from './workspace.context-token.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/utils/check-will-navigate-away.function.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/utils/check-will-navigate-away.function.test.ts
new file mode 100644
index 000000000000..575ea7dc1a88
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/utils/check-will-navigate-away.function.test.ts
@@ -0,0 +1,73 @@
+import { umbWorkspaceWillNavigateAway } from './check-will-navigate-away.function.js';
+import type { UmbWorkspaceRouteManager } from '../controllers/workspace-route-manager.controller.js';
+import type { UmbRoute } from '@umbraco-cms/backoffice/router';
+import { expect } from '@open-wc/testing';
+
+const BASE = '/umbraco/section/content/workspace/document';
+
+function routeManager(config: {
+ absolutePath?: string;
+ activeLocalPath?: string;
+ routes?: Array>;
+}): UmbWorkspaceRouteManager {
+ return {
+ getAbsolutePath: () => config.absolutePath,
+ getActiveLocalPath: () => config.activeLocalPath ?? '',
+ getRoutes: () => (config.routes ?? []) as Array,
+ } as unknown as UmbWorkspaceRouteManager;
+}
+
+describe('umbWorkspaceWillNavigateAway', () => {
+ it('does not block before the workspace has been routed (no absolute path)', () => {
+ const routes = routeManager({ absolutePath: undefined, activeLocalPath: 'edit/123' });
+ expect(umbWorkspaceWillNavigateAway(routes, '123', `${BASE}/edit/123`)).to.be.false;
+ });
+
+ it('stays when navigating to a sub-view of the active route', () => {
+ const routes = routeManager({ absolutePath: BASE, activeLocalPath: 'edit/123' });
+ expect(umbWorkspaceWillNavigateAway(routes, '123', `${BASE}/edit/123/view/content`)).to.be.false;
+ });
+
+ it('stays when the target is exactly the active route', () => {
+ const routes = routeManager({ absolutePath: BASE, activeLocalPath: 'edit/123' });
+ expect(umbWorkspaceWillNavigateAway(routes, '123', `${BASE}/edit/123`)).to.be.false;
+ });
+
+ it('navigates away when an identical segment appears elsewhere in the URL (positional anchoring)', () => {
+ // The bug: a duplicate `edit/123` deeper in the path must not mask a real change to the owned segment.
+ const routes = routeManager({
+ absolutePath: BASE,
+ activeLocalPath: 'edit/123',
+ routes: [{ path: 'edit/:unique' }],
+ });
+ expect(umbWorkspaceWillNavigateAway(routes, '123', `${BASE}/edit/456/something/edit/123`)).to.be.true;
+ });
+
+ it('does not match a unique that is only a string prefix of the new segment', () => {
+ const routes = routeManager({ absolutePath: BASE, activeLocalPath: 'edit/123' });
+ expect(umbWorkspaceWillNavigateAway(routes, '123', `${BASE}/edit/1234`)).to.be.true;
+ });
+
+ it('stays through the create -> edit redirect for the freshly created entity', () => {
+ const routes = routeManager({
+ absolutePath: BASE,
+ activeLocalPath: 'create/parent/document-root/null/dt-1',
+ routes: [{ path: 'create/parent/:parentEntityType/:parentUnique/:documentTypeUnique' }, { path: 'edit/:unique' }],
+ });
+ expect(umbWorkspaceWillNavigateAway(routes, 'new-1', `${BASE}/edit/new-1`)).to.be.false;
+ });
+
+ it('navigates away when leaving the workspace mount entirely', () => {
+ const routes = routeManager({
+ absolutePath: BASE,
+ activeLocalPath: 'edit/123',
+ routes: [{ path: 'edit/:unique' }],
+ });
+ expect(umbWorkspaceWillNavigateAway(routes, '123', '/umbraco/section/media/workspace/media/edit/123')).to.be.true;
+ });
+
+ it('ignores query strings when matching', () => {
+ const routes = routeManager({ absolutePath: BASE, activeLocalPath: 'edit/123' });
+ expect(umbWorkspaceWillNavigateAway(routes, '123', `${BASE}/edit/123?foo=bar`)).to.be.false;
+ });
+});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/utils/check-will-navigate-away.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/utils/check-will-navigate-away.function.ts
new file mode 100644
index 000000000000..1a5f35e6ed11
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/utils/check-will-navigate-away.function.ts
@@ -0,0 +1,41 @@
+import type { UmbWorkspaceRouteManager } from '../controllers/workspace-route-manager.controller.js';
+import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity';
+import { umbUrlPatternToString } from '@umbraco-cms/backoffice/utils';
+
+/**
+ * Determines whether a pending navigation leaves the workspace that owns the given routes.
+ * @param {UmbWorkspaceRouteManager} routeManager The route manager of the workspace.
+ * @param {UmbEntityUnique | undefined} unique The unique of the entity currently being edited.
+ * @param {string | URL} newUrl The url the navigation is heading towards.
+ * @returns {boolean} true if the navigation leaves the workspace.
+ */
+export function umbWorkspaceWillNavigateAway(
+ routeManager: UmbWorkspaceRouteManager,
+ unique: UmbEntityUnique | undefined,
+ newUrl: string | URL,
+): boolean {
+ const basePath = routeManager.getAbsolutePath();
+ // Before the workspace has been routed we cannot position the match, so we never block.
+ if (basePath === undefined) return false;
+
+ const newPath = (newUrl instanceof URL ? newUrl : new URL(newUrl, window.location.origin)).pathname;
+
+ // Check against the active local path
+ const sameEntityPaths = [routeManager.getActiveLocalPath()];
+
+ // Check against all routes of the workspace that carry the same unique.
+ if (unique) {
+ for (const route of routeManager.getRoutes()) {
+ // Routes that don't carry the unique keep their unresolved `:param` tokens, so they
+ // can never match a concrete URL and are effectively skipped.
+ sameEntityPaths.push(umbUrlPatternToString(route.path, { unique }));
+ }
+ }
+
+ // Run the checks:
+ return !sameEntityPaths.some((localPath) => {
+ if (localPath === '') return false;
+ const absolutePath = `${basePath}/${localPath}`;
+ return newPath === absolutePath || newPath.startsWith(`${absolutePath}/`);
+ });
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.repository.ts
index 56582bc8ef8d..562a348c386f 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.repository.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.repository.ts
@@ -6,6 +6,38 @@ import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
export class UmbDocumentPublishingRepository extends UmbRepositoryBase {
#publishingDataSource = new UmbDocumentPublishingServerDataSource(this);
+ /**
+ * Creates and publishes a new Document in a single operation
+ * @param {UmbDocumentDetailModel} model - The Document to create
+ * @param {Array} variantIds - The variants to publish after creating
+ * @param {string | null} parentUnique - The unique of the parent to create under
+ * @returns {*}
+ * @memberof UmbDocumentPublishingRepository
+ */
+ async createAndPublish(
+ model: UmbDocumentDetailModel,
+ variantIds: Array,
+ parentUnique: string | null = null,
+ ) {
+ if (!model) throw new Error('Document is missing');
+ if (!model.unique) throw new Error('Document unique is missing');
+
+ return this.#publishingDataSource.createAndPublish(model, variantIds, parentUnique);
+ }
+
+ /**
+ * Updates and publishes an existing Document in a single operation
+ * @param {UmbDocumentDetailModel} model - The Document to update
+ * @param {Array} variantIds - The variants to publish after updating
+ * @returns {*}
+ * @memberof UmbDocumentPublishingRepository
+ */
+ async updateAndPublish(model: UmbDocumentDetailModel, variantIds: Array) {
+ if (!model.unique) throw new Error('Unique is missing');
+
+ return this.#publishingDataSource.updateAndPublish(model, variantIds);
+ }
+
/**
* Publish one or more variants of a Document
* @param {string} id
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.test.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.test.ts
new file mode 100644
index 000000000000..a903e6c5bee7
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.test.ts
@@ -0,0 +1,98 @@
+import { expect } from '@open-wc/testing';
+import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
+import { useMockSet } from '@umbraco-cms/internal/mock-manager';
+import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
+import { customElement } from '@umbraco-cms/backoffice/external/lit';
+import { UmbId } from '@umbraco-cms/backoffice/id';
+import { UmbDocumentServerDataSource } from '../../repository/detail/document-detail.server.data-source.js';
+import { UmbDocumentPublishingServerDataSource } from './document-publishing.server.data-source.js';
+
+const VARIANT_DOCUMENT_ID = 'variant-documents-variant-document-id';
+const INVARIANT_DOCUMENT_ID = 'variant-documents-invariant-document-id';
+
+@customElement('umb-test-document-publishing-data-source-host')
+class UmbTestHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
+
+describe('UmbDocumentPublishingServerDataSource (create/update-and-publish)', () => {
+ let hostElement: UmbTestHostElement;
+ // The detail data source is used only to read the document back and assert the published outcome,
+ // since the and-publish endpoints return no document body.
+ let detailDataSource: UmbDocumentServerDataSource;
+ let publishingDataSource: UmbDocumentPublishingServerDataSource;
+
+ beforeEach(async () => {
+ await useMockSet('documents');
+ hostElement = new UmbTestHostElement();
+ document.body.appendChild(hostElement);
+ detailDataSource = new UmbDocumentServerDataSource(hostElement);
+ publishingDataSource = new UmbDocumentPublishingServerDataSource(hostElement);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ describe('createAndPublish', () => {
+ it('creates a new document and publishes only the requested culture', async () => {
+ // Use an existing document as a valid model template, with a fresh unique so it is created anew.
+ const { data: template } = await detailDataSource.read(VARIANT_DOCUMENT_ID);
+ expect(template, 'precondition: template document loads').to.exist;
+ const newId = UmbId.new();
+ const newModel = { ...template!, unique: newId };
+
+ const da = UmbVariantId.Create({ culture: 'da', segment: null });
+ const { error } = await publishingDataSource.createAndPublish(newModel, [da], null);
+ expect(error).to.be.undefined;
+
+ const { data: created } = await detailDataSource.read(newId);
+ const daVariant = created!.variants.find((v) => v.culture === 'da');
+ expect(daVariant?.state, 'the requested culture (da) is Published').to.equal('Published');
+ });
+ });
+
+ describe('updateAndPublish', () => {
+ it('publishes only the requested culture', async () => {
+ const { data: model } = await detailDataSource.read(VARIANT_DOCUMENT_ID);
+ expect(model, 'precondition: document loads').to.exist;
+
+ // da starts as Draft; publishing only da should leave en-US untouched.
+ const daVariantId = UmbVariantId.Create({ culture: 'da', segment: null });
+ const { error } = await publishingDataSource.updateAndPublish(model!, [daVariantId]);
+ expect(error).to.be.undefined;
+
+ const { data: updated } = await detailDataSource.read(VARIANT_DOCUMENT_ID);
+ const da = updated!.variants.find((v) => v.culture === 'da');
+ const enUs = updated!.variants.find((v) => v.culture === 'en-US');
+ expect(da?.state, 'da becomes Published').to.equal('Published');
+ expect(enUs?.state, 'en-US is unaffected (was already Published)').to.equal('Published');
+ });
+
+ it('does not publish a culture that was not requested', async () => {
+ // A fresh mock set where da is Draft.
+ const { data: model } = await detailDataSource.read(VARIANT_DOCUMENT_ID);
+ const enUs = UmbVariantId.Create({ culture: 'en-US', segment: null });
+
+ const { error } = await publishingDataSource.updateAndPublish(model!, [enUs]);
+ expect(error).to.be.undefined;
+
+ const { data: updated } = await detailDataSource.read(VARIANT_DOCUMENT_ID);
+ const da = updated!.variants.find((v) => v.culture === 'da');
+ expect(da?.state, 'da stays Draft when only en-US is published').to.equal('Draft');
+ });
+ });
+
+ describe('invariant update-and-publish', () => {
+ it('publishes the invariant variant using an empty culturesToPublish array', async () => {
+ const { data: model } = await detailDataSource.read(INVARIANT_DOCUMENT_ID);
+ expect(model, 'precondition: invariant document loads').to.exist;
+
+ const invariant = UmbVariantId.CreateInvariant();
+ const { error } = await publishingDataSource.updateAndPublish(model!, [invariant]);
+ expect(error).to.be.undefined;
+
+ const { data: updated } = await detailDataSource.read(INVARIANT_DOCUMENT_ID);
+ const variant = updated!.variants.find((v) => v.culture === null);
+ expect(variant?.state, 'the invariant variant is Published').to.equal('Published');
+ });
+ });
+});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts
index 41024e024c38..67b063a2b1ad 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts
@@ -1,10 +1,16 @@
import type { UmbDocumentDetailModel, UmbDocumentVariantPublishModel } from '../../types.js';
import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js';
+import {
+ umbMapDocumentCreateRequestBody,
+ umbMapDocumentUpdateRequestBody,
+} from '../../repository/detail/document-detail-request.mappers.js';
import type {
+ CreateAndPublishDocumentRequestModel,
CultureAndScheduleRequestModel,
PublishDocumentRequestModel,
PublishDocumentWithDescendantsRequestModel,
UnpublishDocumentRequestModel,
+ UpdateAndPublishDocumentRequestModel,
} from '@umbraco-cms/backoffice/external/backend-api';
import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -29,6 +35,67 @@ export class UmbDocumentPublishingServerDataSource {
this.#host = host;
}
+ /**
+ * Creates and publishes a new Document on the server in a single operation
+ * @param {UmbDocumentDetailModel} model - Document Model
+ * @param {Array} variantIds - The variants to publish after creating
+ * @param {string | null} parentUnique - The unique of the parent to create under
+ * @returns {*}
+ * @memberof UmbDocumentPublishingServerDataSource
+ */
+ async createAndPublish(
+ model: UmbDocumentDetailModel,
+ variantIds: Array,
+ parentUnique: string | null = null,
+ ) {
+ if (!model) throw new Error('Document is missing');
+ if (!model.unique) throw new Error('Document unique is missing');
+
+ const body: CreateAndPublishDocumentRequestModel = {
+ ...umbMapDocumentCreateRequestBody(model, parentUnique),
+ culturesToPublish: this.#mapCulturesToPublish(variantIds),
+ };
+
+ // 201 Created returns only the key (no document body). The workspace reloads after this to refresh
+ // its state, so we deliberately do NOT re-read the full document here — that would be a redundant
+ // round-trip on top of the reload.
+ return tryExecute(this.#host, DocumentService.postDocumentCreateAndPublish({ body }));
+ }
+
+ /**
+ * Updates and publishes a Document on the server in a single operation
+ * @param {UmbDocumentDetailModel} model - Document Model
+ * @param {Array} variantIds - The variants to publish after updating
+ * @returns {*}
+ * @memberof UmbDocumentPublishingServerDataSource
+ */
+ async updateAndPublish(model: UmbDocumentDetailModel, variantIds: Array) {
+ if (!model.unique) throw new Error('Unique is missing');
+
+ const body: UpdateAndPublishDocumentRequestModel = {
+ ...umbMapDocumentUpdateRequestBody(model),
+ culturesToPublish: this.#mapCulturesToPublish(variantIds),
+ };
+
+ // 200 returns only a notification header (no document body). The workspace reloads after this to
+ // refresh its state, so we deliberately do NOT re-read the full document here.
+ return tryExecute(
+ this.#host,
+ DocumentService.putDocumentByIdUpdateAndPublish({ path: { id: model.unique }, body }),
+ );
+ }
+
+ /**
+ * Maps the selected variants to the culture codes accepted by the create/update-and-publish endpoints.
+ * Invariant content types require an empty array (cultures cannot be specified), and the server rejects
+ * `null`/`"*"` entries, so invariant variants are filtered out and only distinct culture codes remain.
+ * @param {Array} variantIds - The selected variants to publish
+ * @returns {Array} The distinct culture codes to publish
+ */
+ #mapCulturesToPublish(variantIds: Array): Array {
+ return [...new Set(variantIds.filter((x) => !x.isCultureInvariant()).map((x) => x.toCultureString()))];
+ }
+
/**
* Publish one or more variants of a Document
* @param {string} unique
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts
index 1147abdd20c8..268ebcbb1e0d 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts
@@ -170,29 +170,41 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase implem
return this.#documentWorkspaceContext.validateVariantsAndSubmit(
variantIds,
async () => {
- if (!this.#documentWorkspaceContext) {
- throw new Error('Document workspace context is missing');
- }
-
- // Save the document before scheduling
- await this.#documentWorkspaceContext.performCreateOrUpdate(variantIds, saveData);
-
- // Schedule the document
- const { error } = await this.#publishingRepository.publish(unique, variants);
- if (error) {
+ try {
+ if (!this.#documentWorkspaceContext) {
+ throw new Error('Document workspace context is missing');
+ }
+
+ // Save the document before scheduling
+ await this.#documentWorkspaceContext.performCreateOrUpdate(variantIds, saveData);
+
+ // Schedule the document
+ const { error } = await this.#publishingRepository.publish(unique, variants);
+ if (error) {
+ throw error;
+ }
+
+ const notification = {
+ data: { message: this.#localize.term('speechBubbles_editContentScheduledSavedText') },
+ };
+ this.#notificationContext?.peek('positive', notification);
+
+ // reload the document so all states are updated after the publish operation
+ // TODO: It seems wrong to make a full reload, In this case I think we can just update the variants status? [NL]
+ await this.#documentWorkspaceContext.reload();
+ this.#loadAndProcessLastPublished();
+
+ // request reload of this entity
+ const structureEvent = new UmbRequestReloadStructureForEntityEvent({ entityType, unique });
+ this.#eventContext?.dispatchEvent(structureEvent);
+ } catch (error) {
+ // Notify only on the publish path. The validation-failure path below already
+ // notifies, so a shared top-level .catch would fire a second toast. [JOV]
+ this.#notificationContext?.peek('danger', {
+ data: { message: this.#localize.term('speechBubbles_editContentScheduledNotSavedText') },
+ });
return Promise.reject(error);
}
-
- const notification = { data: { message: this.#localize.term('speechBubbles_editContentScheduledSavedText') } };
- this.#notificationContext?.peek('positive', notification);
-
- // reload the document so all states are updated after the publish operation
- await this.#documentWorkspaceContext.reload();
- this.#loadAndProcessLastPublished();
-
- // request reload of this entity
- const structureEvent = new UmbRequestReloadStructureForEntityEvent({ entityType, unique });
- this.#eventContext?.dispatchEvent(structureEvent);
},
async (reason?: any) => {
const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT);
@@ -290,6 +302,7 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase implem
});
// reload the document so all states are updated after the publish operation
+ // TODO: It seems wrong to make a full reload, I think we should only load the selected variants. [NL]
await this.#documentWorkspaceContext.reload();
await this.#loadAndProcessLastPublished();
@@ -322,6 +335,7 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase implem
await new UmbUnpublishDocumentEntityAction(this, { unique, entityType, meta: {} as never }).execute();
// Reload workspace data to reflect the unpublished state
+ // TODO: It seems wrong to make a full reload, In this case I think we can just update the variants status? [NL]
await this.#documentWorkspaceContext.reload();
await this.#loadAndProcessLastPublished();
}
@@ -368,8 +382,15 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase implem
return this.#documentWorkspaceContext.validateVariantsAndSubmit(
variantIds,
- async () => {
- return this.#performSaveAndPublish(variantIds, saveData);
+ () => {
+ // Notify only on the publish path. The validation-failure path below already
+ // notifies, so a shared top-level .catch would fire a second, contradictory toast. [JOV]
+ return this.#performSaveAndPublish(variantIds, saveData).catch((error) => {
+ this.#notificationContext?.peek('danger', {
+ data: { message: this.#localize.term('speechBubbles_editContentPublishedFailed') },
+ });
+ return Promise.reject(error);
+ });
},
async (reason?: any) => {
// If data of the selection is not valid Then just save:
@@ -399,33 +420,49 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase implem
const entityType = this.#documentWorkspaceContext.getEntityType();
if (!entityType) throw new Error('Entity type is missing');
- await this.#documentWorkspaceContext.performCreateOrUpdate(variantIds, saveData);
+ // The publish already succeeded server-side once we reach the read-back; a failed read-back must not be
+ // reported as a publish failure, so we fall back to the submitted data and flag the editor as stale.
+ let reloadAfterPublishFailed = false;
+ const loadAfterPublish = async (): Promise => {
+ try {
+ return await this.#documentWorkspaceContext!.loadWithoutPersist();
+ } catch {
+ reloadAfterPublishFailed = true;
+ return saveData;
+ }
+ };
- const { error } = await this.#publishingRepository.publish(
- unique,
- variantIds.map((variantId) => ({ variantId })),
- );
+ await this.#documentWorkspaceContext.performCreateOrUpdate(variantIds, saveData, {
+ create: async (data, ids, parent) => {
+ const { error } = await this.#publishingRepository.createAndPublish(data, ids, parent.unique);
+ if (error) throw new Error('Error creating and publishing document', { cause: error });
+ return loadAfterPublish();
+ },
+ update: async (data, ids) => {
+ const { error } = await this.#publishingRepository.updateAndPublish(data, ids);
+ if (error) throw new Error('Error updating and publishing document', { cause: error });
+ return loadAfterPublish();
+ },
+ });
- if (!error) {
- this.#notificationContext?.peek('positive', {
+ this.#notificationContext?.peek('positive', {
+ data: {
+ message: this.#localize.term('speechBubbles_editContentPublishedHeader'),
+ },
+ });
+
+ if (reloadAfterPublishFailed) {
+ this.#notificationContext?.peek('warning', {
data: {
- message: this.#localize.term('speechBubbles_editContentPublishedHeader'),
+ message: this.#localize.term('speechBubbles_editContentPublishedReloadFailed'),
},
});
+ }
- // Clear stale published data and pending changes state so the
- // persistedData observer does not run a comparison against outdated
- // data during reload, which would briefly show a false-positive
- // "pending changes" state.
- this.#clear();
-
- // reload the document so all states are updated after the publish operation
- await this.#documentWorkspaceContext.reload();
- await this.#loadAndProcessLastPublished();
+ await this.#loadAndProcessLastPublished();
- const event = new UmbRequestReloadStructureForEntityEvent({ unique, entityType });
- this.#eventContext?.dispatchEvent(event);
- }
+ const event = new UmbRequestReloadStructureForEntityEvent({ unique, entityType });
+ this.#eventContext?.dispatchEvent(event);
}
#publishableVariantsFilter = (option: UmbDocumentVariantOptionModel) => {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail-request.mappers.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail-request.mappers.ts
new file mode 100644
index 000000000000..864473b33eb4
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail-request.mappers.ts
@@ -0,0 +1,40 @@
+import type { UmbDocumentDetailModel } from '../../types.js';
+import type {
+ CreateDocumentRequestModel,
+ UpdateDocumentRequestModel,
+} from '@umbraco-cms/backoffice/external/backend-api';
+
+/**
+ * Maps a Document detail model to the create request body.
+ * Shared by the detail create endpoint and the publishing create-and-publish endpoint.
+ * @param {UmbDocumentDetailModel} model - The Document to create
+ * @param {string | null} parentUnique - The unique of the parent to create under
+ * @returns {CreateDocumentRequestModel} The create request body
+ */
+export function umbMapDocumentCreateRequestBody(
+ model: UmbDocumentDetailModel,
+ parentUnique: string | null,
+): CreateDocumentRequestModel {
+ return {
+ id: model.unique,
+ parent: parentUnique ? { id: parentUnique } : null,
+ documentType: { id: model.documentType.unique },
+ template: model.template ? { id: model.template.unique } : null,
+ values: model.values,
+ variants: model.variants,
+ };
+}
+
+/**
+ * Maps a Document detail model to the update request body.
+ * Shared by the detail update endpoint and the publishing update-and-publish endpoint.
+ * @param {UmbDocumentDetailModel} model - The Document to update
+ * @returns {UpdateDocumentRequestModel} The update request body
+ */
+export function umbMapDocumentUpdateRequestBody(model: UmbDocumentDetailModel): UpdateDocumentRequestModel {
+ return {
+ template: model.template ? { id: model.template.unique } : null,
+ values: model.values,
+ variants: model.variants,
+ };
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.repository.ts
index e02feda28f64..3c9c26d957f4 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.repository.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.repository.ts
@@ -3,7 +3,10 @@ import { UmbDocumentServerDataSource } from './document-detail.server.data-sourc
import { UMB_DOCUMENT_DETAIL_STORE_CONTEXT } from './document-detail.store.context-token.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbDetailRepositoryBase } from '@umbraco-cms/backoffice/repository';
-export class UmbDocumentDetailRepository extends UmbDetailRepositoryBase {
+export class UmbDocumentDetailRepository extends UmbDetailRepositoryBase<
+ UmbDocumentDetailModel,
+ UmbDocumentServerDataSource
+> {
constructor(host: UmbControllerHost) {
super(host, UmbDocumentServerDataSource, UMB_DOCUMENT_DETAIL_STORE_CONTEXT);
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts
index 21c278fbcd67..b8153489ef6b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts
@@ -11,6 +11,7 @@ import { tryExecute } from '@umbraco-cms/backoffice/resources';
import { umbDeepMerge, type UmbDeepPartialObject } from '@umbraco-cms/backoffice/utils';
import { UmbDocumentTypeDetailServerDataSource } from '@umbraco-cms/backoffice/document-type';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
+import { umbMapDocumentCreateRequestBody, umbMapDocumentUpdateRequestBody } from './document-detail-request.mappers.js';
/**
* A data source for the Document that fetches data from the server
@@ -125,15 +126,7 @@ export class UmbDocumentServerDataSource
if (!model) throw new Error('Document is missing');
if (!model.unique) throw new Error('Document unique is missing');
- // TODO: make data mapper to prevent errors
- const body: CreateDocumentRequestModel = {
- id: model.unique,
- parent: parentUnique ? { id: parentUnique } : null,
- documentType: { id: model.documentType.unique },
- template: model.template ? { id: model.template.unique } : null,
- values: model.values,
- variants: model.variants,
- };
+ const body: CreateDocumentRequestModel = umbMapDocumentCreateRequestBody(model, parentUnique);
const { data, error } = await tryExecute(
this,
@@ -158,12 +151,7 @@ export class UmbDocumentServerDataSource
async update(model: UmbDocumentDetailModel) {
if (!model.unique) throw new Error('Unique is missing');
- // TODO: make data mapper to prevent errors
- const body: UpdateDocumentRequestModel = {
- template: model.template ? { id: model.template.unique } : null,
- values: model.values,
- variants: model.variants,
- };
+ const body: UpdateDocumentRequestModel = umbMapDocumentUpdateRequestBody(model);
const { error } = await tryExecute(
this,
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/context/document-workspace-create-and-publish.context.test.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/context/document-workspace-create-and-publish.context.test.ts
new file mode 100644
index 000000000000..a24018ed73e0
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/context/document-workspace-create-and-publish.context.test.ts
@@ -0,0 +1,110 @@
+import { expect } from '@open-wc/testing';
+import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
+import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
+import { useMockSet } from '@umbraco-cms/internal/mock-manager';
+import { UmbDocumentWorkspaceContext } from './document-workspace.context.js';
+import { TEST_MANIFESTS, UmbTestDocumentWorkspaceHostElement } from './document-workspace-context.test-utils.js';
+import { UmbDocumentPublishingServerDataSource } from '../../publishing/repository/document-publishing.server.data-source.js';
+
+const INVARIANT_DOCUMENT_TYPE_ID = 'variant-documents-invariant-document-type-id';
+const VARIANT_DOCUMENT_TYPE_ID = 'variant-documents-variant-document-type-id';
+const PARENT_ENTITY = { entityType: 'document', unique: null } as const;
+const EN_US = UmbVariantId.Create({ culture: 'en-US', segment: null });
+const DA = UmbVariantId.Create({ culture: 'da', segment: null });
+
+/**
+ * Reproduces the create-and-publish orchestration the publishing workspace context performs: build the
+ * save data, then drive performCreateOrUpdate with a persist strategy that calls the combined
+ * create-and-publish endpoint via the publishing data source. The endpoint does not return the saved
+ * document, so the strategy re-reads the authoritative server state (loadWithoutPersist) and returns it
+ * for the workspace to merge — published variants take the server values, edited-but-unpublished variants
+ * stay dirty.
+ */
+async function createAndPublish(
+ context: UmbDocumentWorkspaceContext,
+ publishingDataSource: UmbDocumentPublishingServerDataSource,
+ variantIds: Array,
+) {
+ const saveData = await context.constructSaveData(variantIds);
+ await context.performCreateOrUpdate(variantIds, saveData, {
+ create: async (data, ids, parent) => {
+ await publishingDataSource.createAndPublish(data, ids, parent.unique);
+ return context.loadWithoutPersist();
+ },
+ update: async (data, ids) => {
+ await publishingDataSource.updateAndPublish(data, ids);
+ return context.loadWithoutPersist();
+ },
+ });
+}
+
+describe('UmbDocumentWorkspaceContext (create-and-publish redirect dirty state)', () => {
+ let hostElement: UmbTestDocumentWorkspaceHostElement;
+ let context: UmbDocumentWorkspaceContext;
+ let publishingDataSource: UmbDocumentPublishingServerDataSource;
+
+ before(() => {
+ umbExtensionsRegistry.registerMany(TEST_MANIFESTS);
+ });
+
+ after(() => {
+ umbExtensionsRegistry.unregisterMany(TEST_MANIFESTS.map((m) => m.alias));
+ });
+
+ beforeEach(async () => {
+ await useMockSet('documents');
+ hostElement = new UmbTestDocumentWorkspaceHostElement();
+ document.body.appendChild(hostElement);
+ await hostElement.init();
+ context = new UmbDocumentWorkspaceContext(hostElement);
+ publishingDataSource = new UmbDocumentPublishingServerDataSource(hostElement);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ // #68071 follow-up (reported by Andy Butland): after create-and-publish the workspace flips
+ // isNew=false, which triggers the new->edit redirect ~500ms later. The reload()+transfer that
+ // reconcile the data state run only after that, so during the redirect window the workspace must
+ // already report itself as clean — otherwise the redirect navigation pops a spurious
+ // "Discard unsaved changes" dialog (most visible with an empty RTE, which keeps current != persisted).
+ it('is not dirty immediately after create-and-publish, before the reload reconciles state', async () => {
+ await context.create(PARENT_ENTITY, INVARIANT_DOCUMENT_TYPE_ID);
+ context.setName('New Test Document');
+ await context.setPropertyValue('text', 'A value');
+
+ await createAndPublish(context, publishingDataSource, [UmbVariantId.CreateInvariant()]);
+
+ expect(context.getIsNew(), 'workspace is no longer new').to.be.false;
+ expect(
+ context.getHasUnpersistedChanges(),
+ 'workspace reports no unpersisted changes right after create-and-publish',
+ ).to.be.false;
+ });
+
+ // Guards the #68071 promise on the create path: the reconcile keeps the edited-but-unpublished variant
+ // dirty. The persist method re-reads the server document; the workspace merges it so the published
+ // variant takes the (clean) server values while the unpublished Danish edit stays in the current data
+ // state and remains dirty. This asserts that end state (no data loss).
+ it('restores an edited-but-unpublished variant after the full create-and-publish flow (no data loss)', async () => {
+ await context.create(PARENT_ENTITY, VARIANT_DOCUMENT_TYPE_ID);
+ context.setName('English name', EN_US);
+ context.setName('Dansk navn', DA);
+ await context.setPropertyValue('variantText', 'English value', EN_US);
+ await context.setPropertyValue('variantText', 'Dansk vaerdi', DA);
+
+ await createAndPublish(context, publishingDataSource, [EN_US]);
+
+ const changed = context.getChangedVariants();
+ expect(
+ changed.some((v) => v.culture === 'en-US'),
+ 'en-US is clean (it was published)',
+ ).to.be.false;
+ expect(
+ changed.some((v) => v.culture === 'da'),
+ 'da stays dirty (edited but not published)',
+ ).to.be.true;
+ expect(context.getPropertyValue('variantText', DA), 'the Danish edit is preserved').to.equal('Dansk vaerdi');
+ });
+});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/context/document-workspace-save-and-publish.context.test.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/context/document-workspace-save-and-publish.context.test.ts
new file mode 100644
index 000000000000..cfebaa1bab55
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/context/document-workspace-save-and-publish.context.test.ts
@@ -0,0 +1,124 @@
+import { expect } from '@open-wc/testing';
+import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
+import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
+import { useMockSet } from '@umbraco-cms/internal/mock-manager';
+import { UmbDocumentWorkspaceContext } from './document-workspace.context.js';
+import { TEST_MANIFESTS, UmbTestDocumentWorkspaceHostElement } from './document-workspace-context.test-utils.js';
+import { UmbDocumentServerDataSource } from '../../repository/detail/document-detail.server.data-source.js';
+import { UmbDocumentPublishingServerDataSource } from '../../publishing/repository/document-publishing.server.data-source.js';
+
+const VARIANT_DOCUMENT_ID = 'variant-documents-variant-document-id';
+const EN_US = UmbVariantId.Create({ culture: 'en-US', segment: null });
+const DA = UmbVariantId.Create({ culture: 'da', segment: null });
+
+const DA_ORIGINAL = 'Dette er den danske varianttekst.';
+
+/**
+ * Reproduces the save-and-publish orchestration that the publishing workspace context performs
+ * (#performSaveAndPublish): build the save data, then drive performCreateOrUpdate with a persist strategy
+ * that performs the combined update-and-publish call. Published variants take the server values, while
+ * only the selected variants will be updated in draft data, leaving the not selected draft variants unchanged. These scenarios use an existing (non-new) document, so the
+ * update path is used.
+ */
+async function saveAndPublish(
+ context: UmbDocumentWorkspaceContext,
+ publishingDataSource: UmbDocumentPublishingServerDataSource,
+ variantIds: Array,
+) {
+ const saveData = await context.constructSaveData(variantIds);
+ await context.performCreateOrUpdate(variantIds, saveData, {
+ update: async (data, ids) => {
+ await publishingDataSource.updateAndPublish(data, ids);
+ return context.loadWithoutPersist();
+ },
+ });
+}
+
+describe('UmbDocumentWorkspaceContext (save & publish data state)', () => {
+ let hostElement: UmbTestDocumentWorkspaceHostElement;
+ let context: UmbDocumentWorkspaceContext;
+ let publishingDataSource: UmbDocumentPublishingServerDataSource;
+
+ before(() => {
+ umbExtensionsRegistry.registerMany(TEST_MANIFESTS);
+ });
+
+ after(() => {
+ umbExtensionsRegistry.unregisterMany(TEST_MANIFESTS.map((m) => m.alias));
+ });
+
+ beforeEach(async () => {
+ await useMockSet('documents');
+ hostElement = new UmbTestDocumentWorkspaceHostElement();
+ document.body.appendChild(hostElement);
+ await hostElement.init();
+ context = new UmbDocumentWorkspaceContext(hostElement);
+ publishingDataSource = new UmbDocumentPublishingServerDataSource(hostElement);
+ await context.load(VARIANT_DOCUMENT_ID);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('keeps an unpublished but edited variant dirty after publishing another variant (#68071)', async () => {
+ await context.setPropertyValue('variantText', 'Edited English', EN_US);
+ await context.setPropertyValue('variantText', 'Redigeret dansk', DA);
+
+ await saveAndPublish(context, publishingDataSource, [EN_US]);
+
+ // Danish was edited but not published, so it must remain dirty in the current data state.
+ expect(context.getPropertyValue('variantText', DA)).to.equal('Redigeret dansk');
+ const changed = context.getChangedVariants();
+ expect(
+ changed.some((v) => v.culture === 'da'),
+ 'da is still reported as a changed variant',
+ ).to.be.true;
+
+ // English was published, so it should now be clean (current matches persisted).
+ expect(context.getPropertyValue('variantText', EN_US)).to.equal('Edited English');
+ expect(
+ changed.some((v) => v.culture === 'en-US'),
+ 'en-US is no longer a changed variant',
+ ).to.be.false;
+ });
+
+ it('persists only the published variant on the server', async () => {
+ await context.setPropertyValue('variantText', 'Edited English', EN_US);
+ await context.setPropertyValue('variantText', 'Redigeret dansk', DA);
+
+ await saveAndPublish(context, publishingDataSource, [EN_US]);
+
+ const freshContext = new UmbDocumentWorkspaceContext(hostElement);
+ await freshContext.load(VARIANT_DOCUMENT_ID);
+ expect(freshContext.getPropertyValue('variantText', EN_US), 'en-US saved on server').to.equal('Edited English');
+ expect(freshContext.getPropertyValue('variantText', DA), 'da not saved on server').to.equal(DA_ORIGINAL);
+ });
+
+ it('uses the combined update-and-publish call, not the save-only update endpoint', async () => {
+ let updateAndPublishCalls = 0;
+ let updateCalls = 0;
+ const originalUpdateAndPublish = UmbDocumentPublishingServerDataSource.prototype.updateAndPublish;
+ const originalUpdate = UmbDocumentServerDataSource.prototype.update;
+ UmbDocumentPublishingServerDataSource.prototype.updateAndPublish = function (...args) {
+ updateAndPublishCalls++;
+ return originalUpdateAndPublish.apply(this, args as never);
+ };
+ UmbDocumentServerDataSource.prototype.update = function (...args) {
+ updateCalls++;
+ return originalUpdate.apply(this, args as never);
+ };
+
+ try {
+ await context.setPropertyValue('variantText', 'Edited English', EN_US);
+ const saveData = await context.constructSaveData([EN_US]);
+ await publishingDataSource.updateAndPublish(saveData, [EN_US]);
+ } finally {
+ UmbDocumentPublishingServerDataSource.prototype.updateAndPublish = originalUpdateAndPublish;
+ UmbDocumentServerDataSource.prototype.update = originalUpdate;
+ }
+
+ expect(updateAndPublishCalls, 'update-and-publish called once').to.equal(1);
+ expect(updateCalls, 'the plain update (save-only) endpoint is not called').to.equal(0);
+ });
+});
diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Document/CreateAndPublishDocumentControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Document/CreateAndPublishDocumentControllerTests.cs
new file mode 100644
index 000000000000..64a33c6202fe
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/ManagementApi/Document/CreateAndPublishDocumentControllerTests.cs
@@ -0,0 +1,90 @@
+using System.Linq.Expressions;
+using System.Net;
+using System.Net.Http.Json;
+using NUnit.Framework;
+using Umbraco.Cms.Api.Management.Controllers.Document;
+using Umbraco.Cms.Api.Management.ViewModels;
+using Umbraco.Cms.Api.Management.ViewModels.Document;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Tests.Common.Builders;
+
+namespace Umbraco.Cms.Tests.Integration.ManagementApi.Document;
+
+public class CreateAndPublishDocumentControllerTests : ManagementApiUserGroupTestBase
+{
+ private ITemplateService TemplateService => GetRequiredService();
+
+ private IContentTypeService ContentTypeService => GetRequiredService();
+
+ private Guid _templateKey;
+ private Guid _contentTypeKey;
+
+ [SetUp]
+ public async Task Setup()
+ {
+ // Template
+ var template = TemplateBuilder.CreateTextPageTemplate(Guid.NewGuid().ToString());
+ await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey);
+ _templateKey = template.Key;
+
+ // Content Type
+ var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id, name: Guid.NewGuid().ToString(), alias: Guid.NewGuid().ToString());
+ contentType.AllowedAsRoot = true;
+ await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+ _contentTypeKey = contentType.Key;
+ }
+
+ protected override Expression> MethodSelector =>
+ x => x.Create(CancellationToken.None, null);
+
+ protected override UserGroupAssertionModel AdminUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.Created
+ };
+
+ protected override UserGroupAssertionModel EditorUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.Created
+ };
+
+ protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.Forbidden
+ };
+
+ protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.Forbidden
+ };
+
+ // Writers can create but not publish, so the combined operation must be forbidden for them.
+ protected override UserGroupAssertionModel WriterUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.Forbidden
+ };
+
+ protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.Unauthorized
+ };
+
+ protected override async Task ClientRequest()
+ {
+ CreateAndPublishDocumentRequestModel createAndPublishDocumentRequestModel = new()
+ {
+ Template = new ReferenceByIdModel(_templateKey),
+ DocumentType = new ReferenceByIdModel(_contentTypeKey),
+ Parent = null,
+ Id = Guid.NewGuid(),
+ Values = [],
+ Variants =
+ [
+ new() { Culture = null, Segment = null, Name = "The en-US name", },
+ ],
+ CulturesToPublish = Array.Empty(),
+ };
+
+ return await Client.PostAsync(Url, JsonContent.Create(createAndPublishDocumentRequestModel));
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Document/UpdateAndPublishDocumentControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Document/UpdateAndPublishDocumentControllerTests.cs
new file mode 100644
index 000000000000..acfaeb7b2a53
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/ManagementApi/Document/UpdateAndPublishDocumentControllerTests.cs
@@ -0,0 +1,97 @@
+using System.Linq.Expressions;
+using System.Net;
+using System.Net.Http.Json;
+using NUnit.Framework;
+using Umbraco.Cms.Api.Management.Controllers.Document;
+using Umbraco.Cms.Api.Management.ViewModels.Document;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Tests.Common.Builders;
+
+namespace Umbraco.Cms.Tests.Integration.ManagementApi.Document;
+
+public class UpdateAndPublishDocumentControllerTests : ManagementApiUserGroupTestBase
+{
+ private IContentEditingService ContentEditingService => GetRequiredService();
+
+ private ITemplateService TemplateService => GetRequiredService();
+
+ private IContentTypeService ContentTypeService => GetRequiredService();
+
+ private Guid _templateKey;
+ private Guid _documentKey;
+
+ [SetUp]
+ public async Task Setup()
+ {
+ // Template
+ var template = TemplateBuilder.CreateTextPageTemplate(Guid.NewGuid().ToString());
+ var templateResponse = await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey);
+ _templateKey = templateResponse.Result.Key;
+
+ // Content Type
+ var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id, name: Guid.NewGuid().ToString(), alias: Guid.NewGuid().ToString());
+ contentType.AllowedAsRoot = true;
+ await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+
+ // Content
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ TemplateKey = _templateKey,
+ ParentKey = Constants.System.RootKey,
+ Variants = [new() { Name = Guid.NewGuid().ToString() }],
+ };
+ var response = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
+ _documentKey = response.Result.Content.Key;
+ }
+
+ protected override Expression> MethodSelector =>
+ x => x.Update(CancellationToken.None, _documentKey, null);
+
+ protected override UserGroupAssertionModel AdminUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.OK
+ };
+
+ protected override UserGroupAssertionModel EditorUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.OK
+ };
+
+ protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.Forbidden
+ };
+
+ protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.Forbidden
+ };
+
+ // Writers can update but not publish, so the combined operation must be forbidden for them.
+ protected override UserGroupAssertionModel WriterUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.Forbidden
+ };
+
+ protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel => new()
+ {
+ ExpectedStatusCode = HttpStatusCode.Unauthorized
+ };
+
+ protected override async Task ClientRequest()
+ {
+ UpdateAndPublishDocumentRequestModel updateAndPublishDocumentRequestModel = new()
+ {
+ Variants =
+ [
+ new() { Culture = null, Segment = null, Name = "The new name", },
+ ],
+ CulturesToPublish = [],
+ };
+
+ return await Client.PutAsync(Url, JsonContent.Create(updateAndPublishDocumentRequestModel));
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs
index 0999a32445aa..68929dec068e 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs
@@ -50,6 +50,7 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent
private IDataTypeService DataTypeService => GetRequiredService();
+
private ILocalizedTextService LocalizedTextService => GetRequiredService();
private ILanguageService LanguageService => GetRequiredService();
@@ -78,6 +79,8 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent
private IValueEditorCache ValueEditorCache => GetRequiredService();
+ private ITemplateService TemplateService => GetRequiredService();
+
protected override void CustomTestSetup(IUmbracoBuilder builder) => builder
.AddNotificationHandler()
.AddNotificationHandler()
@@ -1805,6 +1808,406 @@ public void Can_Save_And_Publish_Content_And_Child_Without_Identity()
Assert.That(childSaved.Success, Is.True);
}
+ [Test]
+ public void Can_SaveAndPublish_Invariant_Content()
+ {
+ var content = ContentService.Create("Home US", -1, "umbTextpage");
+ content.SetValue("author", "Barack Obama");
+
+ var result = ContentService.SaveAndPublish(content, Array.Empty());
+
+ Assert.IsTrue(result.Success);
+ Assert.That(content.HasIdentity, Is.True);
+ Assert.That(content.Published, Is.True);
+ }
+
+ [Test]
+ public void Can_SaveAndPublish_Invariant_Content_Without_Prior_Save()
+ {
+ var content = ContentService.Create("Unsaved Content", -1, "umbTextpage");
+ content.SetValue("author", "Test Author");
+ Assert.IsFalse(content.HasIdentity);
+
+ var result = ContentService.SaveAndPublish(content, Array.Empty());
+
+ Assert.IsTrue(result.Success);
+ Assert.That(content.HasIdentity, Is.True);
+ Assert.That(content.Published, Is.True);
+
+ // re-get to verify persistence
+ var retrieved = ContentService.GetById(content.Id);
+ Assert.IsNotNull(retrieved);
+ Assert.AreEqual("Test Author", retrieved.GetValue("author"));
+ Assert.IsTrue(retrieved.Published);
+ }
+
+ [TestCase(Constants.Security.SuperUserId)]
+ [TestCase(-1)]
+ public void Can_SaveAndPublish_With_Different_User_Ids(int userId)
+ {
+ var content = ContentService.GetById(Textpage.Id);
+ Assert.IsNotNull(content);
+
+ var result = ContentService.SaveAndPublish(content, [], userId);
+
+ Assert.IsTrue(result.Success);
+ Assert.That(content.Published, Is.True);
+ }
+
+ [Test]
+ public void Can_SaveAndPublish_Variant_Content_Multiple_Cultures()
+ {
+ var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out _);
+
+ var result = ContentService.SaveAndPublish(content, [langFr.IsoCode, langUk.IsoCode]);
+
+ Assert.IsTrue(result.Success);
+ Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode));
+ Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode));
+
+ // re-get and verify
+ content = ContentService.GetById(content.Id)!;
+ Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode));
+ Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode));
+ }
+
+ [Test]
+ public void Can_SaveAndPublish_Variant_Content_Single_Culture()
+ {
+ var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out _);
+
+ var result = ContentService.SaveAndPublish(content, [langFr.IsoCode]);
+
+ Assert.IsTrue(result.Success);
+ Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode));
+ Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode));
+
+ // re-get and verify
+ content = ContentService.GetById(content.Id)!;
+ Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode));
+ Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode));
+ }
+
+ [Test]
+ public void Can_SaveAndPublish_And_Child_Without_Identity()
+ {
+ var content = ContentService.Create("Home US", Constants.System.Root, "umbTextpage");
+ content.SetValue("author", "John Doe");
+
+ var published = ContentService.SaveAndPublish(content, []);
+ var childContent = ContentService.Create("Child", content.Id, "umbTextpage");
+
+ // Reset all identity properties
+ childContent.Id = 0;
+ childContent.Path = string.Empty;
+ ((Content)childContent).ResetIdentity();
+ var childPublished = ContentService.SaveAndPublish(childContent, []);
+
+ Assert.That(content.HasIdentity, Is.True);
+ Assert.That(content.Published, Is.True);
+ Assert.That(childContent.HasIdentity, Is.True);
+ Assert.That(childContent.Published, Is.True);
+ Assert.That(published.Success, Is.True);
+ Assert.That(childPublished.Success, Is.True);
+ }
+
+ [Test]
+ public void SaveAndPublish_Fires_Notifications()
+ {
+ var savingWasCalled = false;
+ var publishingWasCalled = false;
+ var contentName = "contentName";
+
+ ContentNotificationHandler.SavingContent = notification =>
+ {
+ savingWasCalled = true;
+ Assert.AreEqual(1, notification.SavedEntities.Count());
+ var entity = notification.SavedEntities.First();
+ Assert.AreEqual(contentName, entity.Name);
+ };
+
+ ContentNotificationHandler.PublishingContent = notification =>
+ {
+ publishingWasCalled = true;
+ Assert.AreEqual(1, notification.PublishedEntities.Count());
+ var entity = notification.PublishedEntities.First();
+ Assert.AreEqual(contentName, entity.Name);
+ };
+
+ try
+ {
+ var content = ContentService.GetById(Textpage.Id);
+ Assert.IsNotNull(content);
+ content.Name = contentName;
+
+ var result = ContentService.SaveAndPublish(content, []);
+
+ Assert.IsTrue(result.Success);
+ Assert.IsTrue(content.Published);
+ Assert.IsTrue(savingWasCalled);
+ Assert.IsTrue(publishingWasCalled);
+ }
+ finally
+ {
+ ContentNotificationHandler.SavingContent = null;
+ ContentNotificationHandler.PublishingContent = null;
+ }
+ }
+
+ [Test]
+ public void SaveAndPublish_Can_Be_Cancelled_By_Saving_Notification()
+ {
+ ContentNotificationHandler.SavingContent = notification =>
+ {
+ notification.Cancel = true;
+ };
+
+ try
+ {
+ var content = ContentService.Create("Cancel Me", -1, "umbTextpage");
+
+ var result = ContentService.SaveAndPublish(content, Array.Empty());
+
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(PublishResultType.FailedPublishCancelledByEvent, result.Result);
+ Assert.IsFalse(content.Published);
+ }
+ finally
+ {
+ ContentNotificationHandler.SavingContent = null;
+ }
+ }
+
+ [Test]
+ public void SaveAndPublish_Rejects_Invalid_Cultures()
+ {
+ var content = CreateEnglishAndFrenchDocument(out _, out _, out _);
+
+ Assert.Throws(() => ContentService.SaveAndPublish(content, ["*"]));
+ Assert.Throws(() => ContentService.SaveAndPublish(content, [null!]));
+ Assert.Throws(() => ContentService.SaveAndPublish(content, ["*", null!]));
+ Assert.Throws(() => ContentService.SaveAndPublish(content, ["en-US", "*", "es-ES"]));
+ }
+
+ [Test]
+ public void SaveAndPublish_Rejects_Whitespace_Cultures()
+ {
+ var content = CreateEnglishAndFrenchDocument(out _, out _, out _);
+
+ Assert.Throws(() => ContentService.SaveAndPublish(content, [string.Empty]));
+ Assert.Throws(() => ContentService.SaveAndPublish(content, [" "]));
+ Assert.Throws(() => ContentService.SaveAndPublish(content, ["en-US", " "]));
+ }
+
+ [Test]
+ public void SaveAndPublish_Rejects_Duplicate_Cultures()
+ {
+ var content = CreateEnglishAndFrenchDocument(out var langUk, out _, out _);
+
+ Assert.Throws(() => ContentService.SaveAndPublish(content, [langUk.IsoCode, langUk.IsoCode]));
+ }
+
+ [Test]
+ public void SaveAndPublish_Rejects_Cultures_On_Invariant_Content()
+ {
+ var content = ContentService.Create("Invariant", -1, "umbTextpage");
+
+ Assert.Throws(() => ContentService.SaveAndPublish(content, ["en-US"]));
+ }
+
+ [Test]
+ public void SaveAndPublish_No_Cultures_On_Variant_Saves_But_Does_Not_Publish()
+ {
+ var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out _);
+
+ // First publish both cultures
+ var published = ContentService.SaveAndPublish(content, [langFr.IsoCode, langUk.IsoCode]);
+ Assert.IsTrue(published.Success);
+
+ // re-get
+ content = ContentService.GetById(content.Id)!;
+
+ // Change some data
+ content.SetCultureName("content-en-updated", langUk.IsoCode);
+
+ // SaveAndPublish with empty cultures - should save data but not publish
+ var result = ContentService.SaveAndPublish(content, Array.Empty());
+
+ // re-get and verify data was saved even though nothing was published
+ content = ContentService.GetById(content.Id)!;
+ Assert.AreEqual("content-en-updated", content.GetCultureName(langUk.IsoCode));
+ }
+
+ [Test]
+ public void Cannot_SaveAndPublish_Trashed_Content()
+ {
+ var content = ContentService.GetById(Trashed.Id);
+ Assert.IsNotNull(content);
+
+ var result = ContentService.SaveAndPublish(content, Array.Empty());
+
+ Assert.IsFalse(result.Success);
+ Assert.IsFalse(content.Published);
+ Assert.IsTrue(content.Trashed);
+ }
+
+ [Test]
+ public void Cannot_SaveAndPublish_Expired_Content()
+ {
+ var content = ContentService.GetById(Subpage.Id);
+ Assert.IsNotNull(content);
+ var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.UtcNow.AddMinutes(-5));
+ ContentService.Save(content, contentSchedule: contentSchedule);
+
+ var parent = ContentService.GetById(Textpage.Id);
+ Assert.IsNotNull(parent);
+ ContentService.SaveAndPublish(parent, Array.Empty());
+
+ var result = ContentService.SaveAndPublish(content, Array.Empty());
+
+ Assert.IsFalse(result.Success);
+ Assert.IsFalse(content.Published);
+ Assert.AreEqual(PublishResultType.FailedPublishHasExpired, result.Result);
+ }
+
+ [Test]
+ public async Task Cannot_SaveAndPublish_Expired_Culture()
+ {
+ var contentType = ContentTypeBuilder.CreateBasicContentType();
+ contentType.Variations = ContentVariation.Culture;
+ await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+
+ var content = ContentBuilder.CreateBasicContent(contentType);
+ content.SetCultureName("Hello", "en-US");
+ var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", null, DateTime.UtcNow.AddMinutes(-5));
+ ContentService.Save(content, contentSchedule: contentSchedule);
+
+ var result = ContentService.SaveAndPublish(content, ["en-US"]);
+
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(PublishResultType.FailedPublishCultureHasExpired, result.Result);
+ Assert.IsFalse(content.Published);
+ }
+
+ [Test]
+ public void Cannot_SaveAndPublish_Content_Awaiting_Release()
+ {
+ var content = ContentService.GetById(Subpage.Id);
+ Assert.IsNotNull(content);
+ var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddHours(2), null);
+ ContentService.Save(content, Constants.Security.SuperUserId, contentSchedule);
+
+ var parent = ContentService.GetById(Textpage.Id);
+ Assert.IsNotNull(parent);
+ ContentService.SaveAndPublish(parent, Array.Empty());
+
+ var result = ContentService.SaveAndPublish(content, Array.Empty());
+
+ Assert.IsFalse(result.Success);
+ Assert.IsFalse(content.Published);
+ Assert.AreEqual(PublishResultType.FailedPublishAwaitingRelease, result.Result);
+ }
+
+ [Test]
+ public async Task Cannot_SaveAndPublish_Culture_Awaiting_Release()
+ {
+ var contentType = ContentTypeBuilder.CreateBasicContentType();
+ contentType.Variations = ContentVariation.Culture;
+ await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+
+ var content = ContentBuilder.CreateBasicContent(contentType);
+ content.SetCultureName("Hello", "en-US");
+ var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", DateTime.UtcNow.AddHours(2), null);
+ ContentService.Save(content, contentSchedule: contentSchedule);
+
+ var result = ContentService.SaveAndPublish(content, ["en-US"]);
+
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(PublishResultType.FailedPublishCultureAwaitingRelease, result.Result);
+ Assert.IsFalse(content.Published);
+ }
+
+ [Test]
+ public async Task SaveAndPublish_Invalid_Content_Still_Saves()
+ {
+ var template = TemplateBuilder.CreateTextPageTemplate();
+ await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey);
+
+ var contentType = ContentTypeBuilder.CreateSimpleContentType(
+ "umbMandatory",
+ "Mandatory Doc Type",
+ mandatoryProperties: true,
+ defaultTemplateId: template.Id);
+ await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+
+ var parentId = Textpage.Id;
+ var parent = ContentService.GetById(parentId);
+ Assert.IsNotNull(parent);
+ ContentService.SaveAndPublish(parent, Array.Empty());
+
+ var content = ContentBuilder.CreateSimpleContent(contentType, "Invalid Content", parentId);
+ content.SetValue("author", string.Empty);
+ Assert.IsFalse(content.HasIdentity);
+
+ var result = ContentService.SaveAndPublish(content, Array.Empty());
+
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(PublishResultType.FailedPublishContentInvalid, result.Result);
+ Assert.IsFalse(content.Published);
+
+ // content IS saved even though publish failed
+ Assert.Greater(content.Id, 0);
+ Assert.IsTrue(content.HasIdentity);
+ }
+
+ [Test]
+ [LongRunning]
+ public async Task Failed_SaveAndPublish_Preserves_Edited_State()
+ {
+ var contentService = GetRequiredService();
+ var contentTypeService = GetRequiredService();
+
+ var contentType = new ContentTypeBuilder()
+ .WithId(0)
+ .AddPropertyType()
+ .WithAlias("header")
+ .WithValueStorageType(ValueStorageType.Integer)
+ .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox)
+ .WithName("header")
+ .Done()
+ .WithContentVariation(ContentVariation.Nothing)
+ .Build();
+
+ await contentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+
+ var content = new ContentBuilder()
+ .WithId(0)
+ .WithName("Home")
+ .WithContentType(contentType)
+ .AddPropertyData()
+ .WithKeyValue("header", "Cool header")
+ .Done()
+ .Build();
+
+ contentService.SaveAndPublish(content, Array.Empty());
+
+ content.Properties[0]!.SetValue("forcedPropertyValue", string.Empty);
+ contentService.Save(content);
+ contentService.PersistContentSchedule(
+ content,
+ ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddHours(2), null));
+
+ var result = contentService.SaveAndPublish(content, Array.Empty());
+
+ Assert.Multiple(() =>
+ {
+ Assert.IsFalse(result.Success);
+ Assert.IsTrue(result.Content.Published);
+ Assert.AreEqual(PublishResultType.FailedPublishAwaitingRelease, result.Result);
+ Assert.IsTrue(result.Content.Edited, "result.Content.Edited");
+ });
+ }
+
[Test]
[LongRunning]
public void Can_Get_Published_Descendant_Versions()
@@ -4268,17 +4671,18 @@ public class ContentNotificationHandler :
INotificationHandler,
INotificationHandler
{
- public static Action PublishingContent { get; set; }
+ public static Action? PublishingContent { get; set; }
- public static Action CopyingContent { get; set; }
+ public static Action? CopyingContent { get; set; }
- public static Action CopiedContent { get; set; }
+ public static Action? CopiedContent { get; set; }
- public static Action SavingContent { get; set; }
+ public static Action? SavingContent { get; set; }
public void Handle(ContentCopiedNotification notification) => CopiedContent?.Invoke(notification);
public void Handle(ContentCopyingNotification notification) => CopyingContent?.Invoke(notification);
+
public void Handle(ContentPublishingNotification notification) => PublishingContent?.Invoke(notification);
public void Handle(ContentSavingNotification notification) => SavingContent?.Invoke(notification);
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.CreateAndPublish.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.CreateAndPublish.cs
new file mode 100644
index 000000000000..1277c5763bd9
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.CreateAndPublish.cs
@@ -0,0 +1,328 @@
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Services.OperationStatus;
+using Umbraco.Cms.Tests.Common.Builders;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
+
+public partial class ContentEditingServiceTests
+{
+ [Test]
+ public async Task Can_CreateAndPublish_Invariant_Content()
+ {
+ var contentType = CreateInvariantContentType();
+
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ ParentKey = Constants.System.RootKey,
+ Variants =
+ [
+ new VariantModel { Name = "Test Create And Publish" }
+ ],
+ Properties =
+ [
+ new PropertyValueModel { Alias = "title", Value = "The title" },
+ new PropertyValueModel { Alias = "text", Value = "The text" }
+ ],
+ };
+
+ var result = await ContentEditingService.CreateAndPublishAsync(createModel, [], Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+ VerifyCreateAndPublish(result.Result.Content);
+
+ // re-get and re-test
+ VerifyCreateAndPublish(await ContentEditingService.GetAsync(result.Result.Content!.Key));
+
+ void VerifyCreateAndPublish(IContent? content)
+ {
+ Assert.IsNotNull(content);
+ Assert.IsTrue(content.HasIdentity);
+ Assert.IsTrue(content.Published);
+ Assert.AreEqual("Test Create And Publish", content.Name);
+ Assert.AreEqual("The title", content.GetValue("title", published: true));
+ Assert.AreEqual("The text", content.GetValue("text", published: true));
+ }
+ }
+
+ [Test]
+ public async Task Can_CreateAndPublish_Culture_Variant_All_Cultures()
+ {
+ var contentType = await CreateVariantContentType();
+
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ ParentKey = Constants.System.RootKey,
+ Properties =
+ [
+ new PropertyValueModel { Alias = "invariantTitle", Value = "The Invariant Title" },
+ new PropertyValueModel { Alias = "variantTitle", Value = "The English Title", Culture = "en-US" },
+ new PropertyValueModel { Alias = "variantTitle", Value = "The Danish Title", Culture = "da-DK" }
+ ],
+ Variants =
+ [
+ new VariantModel { Culture = "en-US", Name = "English Name" },
+ new VariantModel { Culture = "da-DK", Name = "Danish Name" }
+ ],
+ };
+
+ var result = await ContentEditingService.CreateAndPublishAsync(createModel, ["en-US", "da-DK"], Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+ VerifyCreateAndPublish(result.Result.Content);
+
+ // re-get and re-test
+ VerifyCreateAndPublish(await ContentEditingService.GetAsync(result.Result.Content!.Key));
+
+ void VerifyCreateAndPublish(IContent? content)
+ {
+ Assert.IsNotNull(content);
+ Assert.IsTrue(content.Published);
+ Assert.IsTrue(content.IsCulturePublished("en-US"));
+ Assert.IsTrue(content.IsCulturePublished("da-DK"));
+ Assert.AreEqual("English Name", content.GetCultureName("en-US"));
+ Assert.AreEqual("Danish Name", content.GetCultureName("da-DK"));
+ Assert.AreEqual("The Invariant Title", content.GetValue("invariantTitle"));
+ Assert.AreEqual("The English Title", content.GetValue("variantTitle", "en-US", published: true));
+ Assert.AreEqual("The Danish Title", content.GetValue("variantTitle", "da-DK", published: true));
+ }
+ }
+
+ [Test]
+ public async Task Can_CreateAndPublish_Culture_Variant_Single_Culture()
+ {
+ var contentType = await CreateVariantContentType();
+
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ ParentKey = Constants.System.RootKey,
+ Properties =
+ [
+ new PropertyValueModel { Alias = "invariantTitle", Value = "The Invariant Title" },
+ new PropertyValueModel { Alias = "variantTitle", Value = "The English Title", Culture = "en-US" },
+ new PropertyValueModel { Alias = "variantTitle", Value = "The Danish Title", Culture = "da-DK" }
+ ],
+ Variants =
+ [
+ new VariantModel { Culture = "en-US", Name = "English Name" },
+ new VariantModel { Culture = "da-DK", Name = "Danish Name" }
+ ],
+ };
+
+ var result = await ContentEditingService.CreateAndPublishAsync(createModel, ["en-US"], Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+ VerifyCreateAndPublish(result.Result.Content);
+
+ // re-get and re-test
+ VerifyCreateAndPublish(await ContentEditingService.GetAsync(result.Result.Content!.Key));
+
+ void VerifyCreateAndPublish(IContent? content)
+ {
+ Assert.IsNotNull(content);
+ Assert.IsTrue(content.IsCulturePublished("en-US"));
+ Assert.IsFalse(content.IsCulturePublished("da-DK"));
+
+ // both values should still be saved
+ Assert.AreEqual("The English Title", content.GetValue("variantTitle", "en-US", published: true));
+ Assert.AreEqual("The Danish Title", content.GetValue("variantTitle", "da-DK"));
+ }
+ }
+
+ [Test]
+ public async Task Can_CreateAndPublish_With_Template()
+ {
+ var template = TemplateBuilder.CreateTextPageTemplate();
+ await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey);
+
+ var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id);
+ contentType.AllowedAsRoot = true;
+ await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ TemplateKey = template.Key,
+ ParentKey = Constants.System.RootKey,
+ Variants =
+ [
+ new VariantModel { Name = "With Template" }
+ ],
+ Properties =
+ [
+ new PropertyValueModel { Alias = "title", Value = "The title" },
+ new PropertyValueModel { Alias = "bodyText", Value = "The body" }
+ ],
+ };
+
+ var result = await ContentEditingService.CreateAndPublishAsync(createModel, [], Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+
+ var content = result.Result.Content!;
+ Assert.IsTrue(content.Published);
+ Assert.AreEqual(template.Id, content.TemplateId);
+ }
+
+ [Test]
+ public async Task Can_CreateAndPublish_With_Explicit_Key()
+ {
+ var contentType = CreateInvariantContentType();
+ var explicitKey = Guid.NewGuid();
+
+ var createModel = new ContentCreateModel
+ {
+ Key = explicitKey,
+ ContentTypeKey = contentType.Key,
+ ParentKey = Constants.System.RootKey,
+ Variants =
+ [
+ new VariantModel { Name = "Explicit Key" }
+ ],
+ Properties =
+ [
+ new PropertyValueModel { Alias = "title", Value = "The title" }
+ ],
+ };
+
+ var result = await ContentEditingService.CreateAndPublishAsync(createModel, [], Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+
+ var content = result.Result.Content!;
+ Assert.IsTrue(content.Published);
+ Assert.AreEqual(explicitKey, content.Key);
+ }
+
+ [Test]
+ public async Task Cannot_CreateAndPublish_Without_Content_Type()
+ {
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = Guid.NewGuid(),
+ ParentKey = Constants.System.RootKey,
+ Variants =
+ [
+ new VariantModel { Name = "Test" }
+ ],
+ };
+
+ var result = await ContentEditingService.CreateAndPublishAsync(createModel, [], Constants.Security.SuperUserKey);
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(ContentEditingOperationStatus.ContentTypeNotFound, result.Status);
+ }
+
+ [Test]
+ public async Task Cannot_CreateAndPublish_With_Non_Existing_Parent()
+ {
+ var contentType = CreateInvariantContentType();
+
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ ParentKey = Guid.NewGuid(),
+ Variants =
+ [
+ new VariantModel { Name = "Test" }
+ ],
+ };
+
+ var result = await ContentEditingService.CreateAndPublishAsync(createModel, [], Constants.Security.SuperUserKey);
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(ContentEditingOperationStatus.ParentNotFound, result.Status);
+ }
+
+ [Test]
+ public async Task Cannot_CreateAndPublish_With_Non_Existing_Template()
+ {
+ var contentType = CreateInvariantContentType();
+
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ TemplateKey = Guid.NewGuid(),
+ ParentKey = Constants.System.RootKey,
+ Variants =
+ [
+ new VariantModel { Name = "Test" }
+ ],
+ };
+
+ var result = await ContentEditingService.CreateAndPublishAsync(createModel, [], Constants.Security.SuperUserKey);
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(ContentEditingOperationStatus.TemplateNotFound, result.Status);
+ }
+
+ [Test]
+ public async Task Cannot_CreateAndPublish_With_Disallowed_Template()
+ {
+ var template = TemplateBuilder.CreateTextPageTemplate();
+ await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey);
+
+ // content type without allowed templates
+ var contentType = ContentTypeBuilder.CreateBasicContentType();
+ contentType.AllowedAsRoot = true;
+ await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ TemplateKey = template.Key,
+ ParentKey = Constants.System.RootKey,
+ Variants =
+ [
+ new VariantModel { Name = "Test" }
+ ],
+ };
+
+ var result = await ContentEditingService.CreateAndPublishAsync(createModel, [], Constants.Security.SuperUserKey);
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(ContentEditingOperationStatus.TemplateNotAllowed, result.Status);
+ }
+
+ [Test]
+ public async Task Cannot_CreateAndPublish_Invariant_Without_Name()
+ {
+ var contentType = CreateInvariantContentType();
+
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ ParentKey = Constants.System.RootKey,
+ Variants = [],
+ Properties =
+ [
+ new PropertyValueModel { Alias = "title", Value = "The title" }
+ ],
+ };
+
+ var result = await ContentEditingService.CreateAndPublishAsync(createModel, [], Constants.Security.SuperUserKey);
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch, result.Status);
+ }
+
+ [Test]
+ public async Task Cannot_CreateAndPublish_With_Invalid_Culture()
+ {
+ var contentType = await CreateVariantContentType();
+
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ ParentKey = Constants.System.RootKey,
+ Properties =
+ [
+ new PropertyValueModel { Alias = "invariantTitle", Value = "Invariant" },
+ new PropertyValueModel { Alias = "variantTitle", Value = "English", Culture = "en-us" }
+ ],
+ Variants =
+ [
+ new VariantModel { Culture = "en-us", Name = "English" }
+ ],
+ };
+
+ var result = await ContentEditingService.CreateAndPublishAsync(createModel, [], Constants.Security.SuperUserKey);
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(ContentEditingOperationStatus.InvalidCulture, result.Status);
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.UpdateAndPublish.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.UpdateAndPublish.cs
new file mode 100644
index 000000000000..e9f45980b46e
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.UpdateAndPublish.cs
@@ -0,0 +1,309 @@
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Services.OperationStatus;
+using Umbraco.Cms.Tests.Common.Builders;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
+
+public partial class ContentEditingServiceTests
+{
+ [Test]
+ public async Task Can_UpdateAndPublish_Invariant()
+ {
+ var content = await CreateInvariantContent();
+ Assert.IsFalse(content.Published);
+
+ var updateModel = new ContentUpdateModel
+ {
+ Variants =
+ [
+ new VariantModel { Name = "Updated Name" }
+ ],
+ Properties =
+ [
+ new PropertyValueModel { Alias = "title", Value = "The updated title" },
+ new PropertyValueModel { Alias = "text", Value = "The updated text" }
+ ],
+ };
+
+ var result = await ContentEditingService.UpdateAndPublishAsync(content.Key, updateModel, [], Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+ VerifyUpdateAndPublish(result.Result.Content);
+
+ // re-get and re-test
+ VerifyUpdateAndPublish(await ContentEditingService.GetAsync(content.Key));
+
+ void VerifyUpdateAndPublish(IContent? updatedContent)
+ {
+ Assert.IsNotNull(updatedContent);
+ Assert.IsTrue(updatedContent.Published);
+ Assert.AreEqual("Updated Name", updatedContent.Name);
+ Assert.AreEqual("The updated title", updatedContent.GetValue("title", published: true));
+ Assert.AreEqual("The updated text", updatedContent.GetValue("text", published: true));
+ }
+ }
+
+ [Test]
+ public async Task Can_UpdateAndPublish_Culture_Variant()
+ {
+ var content = await CreateCultureVariantContent();
+ Assert.IsFalse(content.Published);
+
+ var updateModel = new ContentUpdateModel
+ {
+ Properties =
+ [
+ new PropertyValueModel { Alias = "invariantTitle", Value = "Updated invariant" },
+ new PropertyValueModel { Alias = "variantTitle", Value = "Updated English", Culture = "en-US" },
+ new PropertyValueModel { Alias = "variantTitle", Value = "Updated Danish", Culture = "da-DK" }
+ ],
+ Variants =
+ [
+ new VariantModel { Culture = "en-US", Name = "Updated English Name" },
+ new VariantModel { Culture = "da-DK", Name = "Updated Danish Name" }
+ ],
+ };
+
+ var result = await ContentEditingService.UpdateAndPublishAsync(content.Key, updateModel, ["en-US", "da-DK"], Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+ VerifyUpdateAndPublish(result.Result.Content);
+
+ // re-get and re-test
+ VerifyUpdateAndPublish(await ContentEditingService.GetAsync(content.Key));
+
+ void VerifyUpdateAndPublish(IContent? updatedContent)
+ {
+ Assert.IsNotNull(updatedContent);
+ Assert.IsTrue(updatedContent.Published);
+ Assert.IsTrue(updatedContent.IsCulturePublished("en-US"));
+ Assert.IsTrue(updatedContent.IsCulturePublished("da-DK"));
+ Assert.AreEqual("Updated English Name", updatedContent.GetPublishName("en-US"));
+ Assert.AreEqual("Updated Danish Name", updatedContent.GetPublishName("da-DK"));
+ Assert.AreEqual("Updated invariant", updatedContent.GetValue("invariantTitle", published: true));
+ Assert.AreEqual("Updated English", updatedContent.GetValue("variantTitle", "en-US", published: true));
+ Assert.AreEqual("Updated Danish", updatedContent.GetValue("variantTitle", "da-DK", published: true));
+ }
+ }
+
+ [Test]
+ public async Task Can_UpdateAndPublish_Single_Culture()
+ {
+ var content = await CreateCultureVariantContent();
+
+ var updateModel = new ContentUpdateModel
+ {
+ Properties =
+ [
+ new PropertyValueModel { Alias = "invariantTitle", Value = "Updated invariant" },
+ new PropertyValueModel { Alias = "variantTitle", Value = "Updated English", Culture = "en-US" },
+ new PropertyValueModel { Alias = "variantTitle", Value = "Updated Danish", Culture = "da-DK" }
+ ],
+ Variants =
+ [
+ new VariantModel { Culture = "en-US", Name = "Updated English Name" },
+ new VariantModel { Culture = "da-DK", Name = "Updated Danish Name" }
+ ],
+ };
+
+ var result = await ContentEditingService.UpdateAndPublishAsync(content.Key, updateModel, ["en-US"], Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+ VerifyUpdateAndPublish(result.Result.Content);
+
+ // re-get and re-test
+ VerifyUpdateAndPublish(await ContentEditingService.GetAsync(content.Key));
+
+ void VerifyUpdateAndPublish(IContent? updatedContent)
+ {
+ Assert.IsNotNull(updatedContent);
+ Assert.IsTrue(updatedContent.IsCulturePublished("en-US"));
+ Assert.IsFalse(updatedContent.IsCulturePublished("da-DK"));
+
+ // both values should still be saved
+ Assert.AreEqual("Updated English", updatedContent.GetValue("variantTitle", "en-US", published: true));
+ Assert.AreNotEqual("Updated Danish", updatedContent.GetValue("variantTitle", "da-DK", published: true));
+ Assert.AreEqual("Updated Danish", updatedContent.GetValue("variantTitle", "da-DK", published: false));
+ }
+ }
+
+ [Test]
+ public async Task Can_UpdateAndPublish_Already_Published_Content()
+ {
+ var contentType = CreateInvariantContentType();
+
+ // create and publish initially
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ ParentKey = Constants.System.RootKey,
+ Variants =
+ [
+ new VariantModel { Name = "Original Name" }
+ ],
+ Properties =
+ [
+ new PropertyValueModel { Alias = "title", Value = "Original title" },
+ new PropertyValueModel { Alias = "text", Value = "Original text" }
+ ],
+ };
+
+ var createResult = await ContentEditingService.CreateAndPublishAsync(createModel, [], Constants.Security.SuperUserKey);
+ Assert.IsTrue(createResult.Success);
+ var content = createResult.Result.Content!;
+ Assert.IsTrue(content.Published);
+
+ // now update and republish
+ var updateModel = new ContentUpdateModel
+ {
+ Variants =
+ [
+ new VariantModel { Name = "Republished Name" }
+ ],
+ Properties =
+ [
+ new PropertyValueModel { Alias = "title", Value = "Republished title" },
+ new PropertyValueModel { Alias = "text", Value = "Republished text" }
+ ],
+ };
+
+ var result = await ContentEditingService.UpdateAndPublishAsync(content.Key, updateModel, [], Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+
+ var updatedContent = await ContentEditingService.GetAsync(content.Key);
+ Assert.IsNotNull(updatedContent);
+ Assert.IsTrue(updatedContent.Published);
+ Assert.AreEqual("Republished Name", updatedContent.Name);
+ Assert.AreEqual("Republished title", updatedContent.GetValue("title", published: true));
+ Assert.AreEqual("Republished text", updatedContent.GetValue("text", published: true));
+ }
+
+ [Test]
+ public async Task Can_UpdateAndPublish_Template()
+ {
+ var template = TemplateBuilder.CreateTextPageTemplate();
+ await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey);
+
+ var template2 = TemplateBuilder.CreateTextPageTemplate("altTemplate");
+ await TemplateService.CreateAsync(template2, Constants.Security.SuperUserKey);
+
+ var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id);
+ contentType.AllowedTemplates = new[] { template, template2 };
+ contentType.AllowedAsRoot = true;
+ await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+
+ var createModel = new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ TemplateKey = template.Key,
+ ParentKey = Constants.System.RootKey,
+ Variants =
+ [
+ new VariantModel { Name = "Template Test" }
+ ],
+ Properties =
+ [
+ new PropertyValueModel { Alias = "title", Value = "Title" },
+ new PropertyValueModel { Alias = "bodyText", Value = "Body" }
+ ],
+ };
+
+ var createResult = await ContentEditingService.CreateAndPublishAsync(createModel, [], Constants.Security.SuperUserKey);
+ Assert.IsTrue(createResult.Success);
+ var content = createResult.Result.Content!;
+ Assert.AreEqual(template.Id, content.TemplateId);
+
+ // update with different template
+ var updateModel = new ContentUpdateModel
+ {
+ TemplateKey = template2.Key,
+ Variants =
+ [
+ new VariantModel { Name = "Template Test Updated" }
+ ],
+ Properties =
+ [
+ new PropertyValueModel { Alias = "title", Value = "Updated Title" },
+ new PropertyValueModel { Alias = "bodyText", Value = "Updated Body" }
+ ],
+ };
+
+ var result = await ContentEditingService.UpdateAndPublishAsync(content.Key, updateModel, [], Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+
+ var updatedContent = await ContentEditingService.GetAsync(content.Key);
+ Assert.IsNotNull(updatedContent);
+ Assert.IsTrue(updatedContent.Published);
+ Assert.AreEqual(template2.Id, updatedContent.TemplateId);
+ }
+
+ [Test]
+ public async Task Cannot_UpdateAndPublish_Non_Existing_Content()
+ {
+ var updateModel = new ContentUpdateModel
+ {
+ Variants =
+ [
+ new VariantModel { Name = "Test" }
+ ],
+ };
+
+ var result = await ContentEditingService.UpdateAndPublishAsync(Guid.NewGuid(), updateModel, [], Constants.Security.SuperUserKey);
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(ContentEditingOperationStatus.NotFound, result.Status);
+ }
+
+ [Test]
+ public async Task Cannot_UpdateAndPublish_With_Invalid_Culture()
+ {
+ var content = await CreateCultureVariantContent();
+
+ var updateModel = new ContentUpdateModel
+ {
+ Properties =
+ [
+ new PropertyValueModel { Alias = "invariantTitle", Value = "Invariant" },
+ new PropertyValueModel { Alias = "variantTitle", Value = "English", Culture = "en-us" }
+ ],
+ Variants =
+ [
+ new VariantModel { Culture = "en-us", Name = "English" }
+ ],
+ };
+
+ var result = await ContentEditingService.UpdateAndPublishAsync(content.Key, updateModel, ["en-us"], Constants.Security.SuperUserKey);
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(ContentEditingOperationStatus.InvalidCulture, result.Status);
+ }
+
+ [Test]
+ public async Task Can_UpdateAndPublish_Readonly_Property_Is_Preserved()
+ {
+ var content = await CreateInvariantContent();
+ var labelValue = content.GetValue("label");
+
+ var updateModel = new ContentUpdateModel
+ {
+ Variants =
+ [
+ new VariantModel { Name = "Updated Name" }
+ ],
+ Properties =
+ [
+ new PropertyValueModel { Alias = "title", Value = "Updated title" },
+ new PropertyValueModel { Alias = "text", Value = "Updated text" },
+ new PropertyValueModel { Alias = "label", Value = "Trying to change label" }
+ ],
+ };
+
+ var result = await ContentEditingService.UpdateAndPublishAsync(content.Key, updateModel, [], Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+ Assert.IsTrue(result.Result.Content!.Published);
+
+ // re-get and verify the label property was not changed
+ var updatedContent = await ContentEditingService.GetAsync(content.Key);
+ Assert.IsNotNull(updatedContent);
+ Assert.AreEqual(labelValue, updatedContent.GetValue("label"));
+ Assert.AreEqual("Updated title", updatedContent.GetValue("title"));
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj
index a4c3202930fb..8374678a0700 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj
@@ -314,5 +314,11 @@
UserServiceTests.cs
+
+ ContentEditingServiceTests.cs
+
+
+ ContentEditingServiceTests.cs
+