Skip to content

Content: Reintroduce create/save-and-publish as a single operation (closes #22577) - #22812

Merged
AndyButland merged 18 commits into
v17/devfrom
v17/improvement/save-and-publish-take-three
Jul 2, 2026
Merged

Content: Reintroduce create/save-and-publish as a single operation (closes #22577)#22812
AndyButland merged 18 commits into
v17/devfrom
v17/improvement/save-and-publish-take-three

Conversation

@kjac

@kjac kjac commented May 12, 2026

Copy link
Copy Markdown
Contributor

Description

  • Adds SaveAndPublish to IContentService / ContentService — a single operation that saves and publishes content in one scope, avoiding the race conditions and double-notification overhead of calling Save + Publish separately. Supports both invariant and culture-variant content.
  • Adds CreateAndPublishAsync and UpdateAndPublishAsync to IContentEditingService / ContentEditingService, backed by the new SaveAndPublish operation.
  • Adds Management API endpoints for create-and-publish and update-and-publish documents.

Testing

A bumch of tests have been added to cover all the things they did in v13 and more.

Fixes #22577

@Migaroez

Migaroez commented May 21, 2026

Copy link
Copy Markdown
Contributor

@kjac I think this is good to go in, I have a plan for the merge upto 18 rdy to go.

@kjac
kjac marked this pull request as ready for review May 22, 2026 14:37
@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown

Claude finished @kjac's task in 6m 54s —— View job


PR Review

Target: origin/v17/dev · Based on commit: 04c715dd · Skipped: 1 file out of 13 total (OpenApi.json — noise)

Reintroduces create/save-and-publish as a single atomic operation at all three layers: IContentService.SaveAndPublish (new default interface method), IContentEditingService.CreateAndPublishAsync/UpdateAndPublishAsync (new with default impls), two new Management API controllers, and matching request models — with good integration test coverage at both service levels.

  • Modified public API: IContentService (new SaveAndPublish method), IContentEditingService (new CreateAndPublishAsync, UpdateAndPublishAsync), ContentService (new public SaveAndPublish override), ContentEditingService (new CreateAndPublishAsync/UpdateAndPublishAsync + private HandleCreateAsync/HandleUpdateAsync/SaveAndPublish)
  • Affected implementations (outside this PR): Any external class implementing IContentService or IContentEditingService — protected by default interface implementations
  • Breaking changes: None — all new interface methods have default implementations per the correct pattern
  • Other changes: Two new REST endpoints added — POST /document/create-and-publish and PUT /document/{id}/update-and-publish

Critical

None

Important

  • src/Umbraco.Core/Services/ContentEditingService.cs:420: Commented-out dead code block left inside Save method (lines 420–430). This was likely from an exploratory commit and should be removed before merge. Fix this →

Suggestions

  • src/Umbraco.Core/Services/ContentEditingService.cs:460: SaveAndPublish switch only maps FailedPublishCancelledByEvent — all other failure types silently become Unknown. Important cases like FailedPublishContentInvalidPropertyValidationError, FailedPublishMandatoryCultureMissingInvalidCulture, FailedPublishIsTrashedInTrash are swallowed. Callers (including API consumers) can't act on these.

  • src/Umbraco.Core/Services/IContentEditingService.cs:53: Default impls for CreateAndPublishAsync / UpdateAndPublishAsync return Unknown silently. Per CLAUDE.md guidance, if no reasonable default exists, throw new NotImplementedException() is preferred — a silent Unknown result is indistinguishable from a genuine service failure.

  • src/Umbraco.Core/Services/IContentService.cs:576: TODO version inconsistency in this PR: IContentService.SaveAndPublish uses // TODO (V19) while IContentEditingService.CreateAndPublishAsync uses // TODO (V18). Both are new default interface methods added in v17. Suggest aligning to V19 (current+2) for consistency with the removal policy.

  • Missing E2E tests: Per coding preferences, every API endpoint must have E2E tests. The new POST /document/create-and-publish and PUT /document/{id}/update-and-publish endpoints have solid integration test coverage for the service layer, but no E2E tests for the REST endpoints themselves.

  • src/Umbraco.Cms.Api.Management/Controllers/Document/CreateAndPublishDocumentController.cs:57: CodeQL CSRF alert is a known false positive for this API — OpenIddict tokens in __Host- cookies with SameSite=Strict provide CSRF protection at the cookie level, consistent with all other Document controllers.


Approved with Suggestions for improvement

Good to go — the core implementation is clean and well-tested at the service layer. The commented-out dead code block (Important) should be cleaned up before merge. The other items are suggestions worth considering but not blockers.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test

Comment thread src/Umbraco.Core/Services/ContentEditingService.cs Outdated
Comment thread src/Umbraco.Core/Services/ContentEditingService.cs
Comment thread src/Umbraco.Core/Services/IContentEditingService.cs Outdated
Comment thread src/Umbraco.Core/Services/IContentService.cs

@AndyButland AndyButland left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All looks good to me. Test coverage very thorough, and manual tests check out.

I did spot one security related issue where we needed to validate the users can create/update AND publish, not just create/update, but I've applied that change. Along with a few other smaller things I found in review.

We'll keep this PR open for the FE work to be applied, so approval is for the C# code (management API, service layer and tests).

iOvergaard and others added 3 commits June 22, 2026 13:13
… (FE) (#23031)

* Documents: Use single-transaction create/update-and-publish endpoints (FE)

Replace the separate save+publish calls on the document save-and-publish path
with the new create-and-publish / update-and-publish endpoints, and keep
unpublished but edited variants dirty after reload via a current-state re-merge.

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

* Documents: Address review feedback on save-and-publish FE

- Deduplicate request-body mapping in the document data source (shared
  #mapCreateRequestBody / #mapUpdateRequestBody) to remove create vs
  create-and-publish duplication (CodeScene).
- Split performCreateOrUpdateAndPublish into #createAndPublish / #updateAndPublish
  to reduce method complexity (CodeScene), and clarify its post-condition in JSDoc.
- Default parentUnique to null on the repository createAndPublish overload (Claude).
- De-duplicate segments before expanding variant lists (Copilot).
- Add a createAndPublish data-source unit test (Copilot).

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

* Documents: Drop redundant document re-read on save-and-publish

The create/update-and-publish data-source methods re-read the full document
after the mutation, but the publishing workspace context immediately calls
reload() (which re-fetches and refreshes state) and discards that result.
Remove the data-source re-read so save-and-publish fetches the document once
(the reload) instead of twice.

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

* Documents: Fix spurious discard dialog after create-and-publish

After create-and-publish the workspace flipped isNew=false before its data
state was reconciled (that happens in the subsequent reload + transfer). The
flip schedules the new->edit redirect, whose navigation guard then saw a
transient dirty state and popped a "Discard unsaved changes" dialog — most
visible on an invariant document with an empty RTE (reported by Andy Butland).

Reconcile persisted to the just-saved data before flipping isNew. Using
saveData (not the full current data) keeps published variants clean while
edited-but-unpublished variants stay dirty, so their edits remain preserved
and navigation-guarded during the brief pre-reload window. Update-and-publish
is unaffected (no redirect).

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

* Documents: Move create/update-and-publish into the publishing domain

Following review feedback (Mads Rasmussen), keep all publishing-related HTTP in
the publishing repository/data source rather than the document detail layer:

- Move createAndPublish/updateAndPublish from UmbDocumentServerDataSource +
  UmbDocumentDetailRepository to UmbDocumentPublishingServerDataSource +
  UmbDocumentPublishingRepository. Extract the shared create/update request-body
  mapping to document-detail-request.mappers.ts so it is not duplicated.
- The publishing workspace context orchestrates the combined call and asks the
  document workspace context to apply the create/update lifecycle via new public
  finalizeCreate/finalizeUpdate methods. This keeps setIsNew and
  _workspaceEventUnique private — no shared workspace base-class changes.
- finalizeCreate carries the create-and-publish fix (reconcile persisted to the
  saved data before the new->edit redirect) so an edited-but-unpublished variant
  stays dirty while published variants are clean.

Move the and-publish data-source test to the publishing folder; rework the
save-and-publish/create-and-publish context tests to drive the publishing data
source + finalize methods.

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

* Documents: Drop redundant context guard in save-and-publish orchestrator

#createOrUpdateAndPublish re-checked #documentWorkspaceContext although its only
caller (#performSaveAndPublish) already guards it; pass the narrowed reference in.

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

* Documents: Reconcile current as well as persisted in finalizeCreate

Andy Butland found the create-and-publish "Discard unsaved changes" dialog
still appeared for some document types. The dirty guard's jsonStringComparison
is order-sensitive, and savedData (a merge-processed projection of the draft)
can order its values array differently from current — so reconciling only
persisted still left a spurious mismatch. Set both persisted and current to
savedData before flipping isNew, so the new->edit redirect can never observe a
dirty state. Edited-but-unpublished variants are restored (dirty again) by the
caller's reload + transferPublishedVariantsToCurrent.

Update the create-and-publish "no data loss" test to assert the end state
(after reload + transfer) instead of the intermediate redirect window, since
the unpublished variant's edit is briefly absent during it by design.

Diagnosis and fix proposed by Andy Butland.

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

* Documents: Align finalizeUpdate with finalizeCreate and tighten method docs

Review feedback from Niels: finalizeUpdate now takes the saved data and
reconciles persisted/current like finalizeCreate (mirroring the base
_update), the finalize*/transfer JSDocs describe only the method's own
responsibility, and the transfer parameter is renamed to currentData to
match the method name.

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

* initial correction

* second round of refactor

* url-pattern-to-string tests

* url-pattern-to-string jsdocs

* avoid discard changes dialog when navigating between create and edit

* keep track of the absolute route as well

* check absolute path as part of dirty check

* check navigation util

* added TODOs

* clean up

* error handling

* error handling for schedule

* remove unused import

* rename to performDefault...

* rename

* rename and export interface

* simplify #applyPersistedData

* fix(documents): avoid double notification on publish/schedule validation failure

The validation-failure path already notifies and re-rejects; the shared
top-level .catch then fired a second, contradictory toast ("saved for you"
followed by "could not be saved"). Scope the failure notification to the
publish path so each outcome shows a single message.

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

* fix(documents): don't report publish failure when only the read-back fails

After a successful create/update-and-publish the workspace re-reads the
document (the endpoints return key-only). If that read-back fails, the publish
has already succeeded server-side, so falling through to the publish-failed
toast is misleading. Fall back to the submitted data so the workspace lifecycle
completes, and surface a soft warning that the editor could not be refreshed.

Also widen the loadWithoutPersist() error message from "document" to "entity"
since the method lives on the shared entity-detail base.

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

* simplify comment

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
@AndyButland
AndyButland merged commit 4351388 into v17/dev Jul 2, 2026
28 of 29 checks passed
@AndyButland
AndyButland deleted the v17/improvement/save-and-publish-take-three branch July 2, 2026 07:39
@AndyButland AndyButland changed the title Content: Reintroduce create/save-and-publish as a single operation Content: Reintroduce create/save-and-publish as a single operation (closes #22577) Jul 2, 2026
kjac pushed a commit that referenced this pull request Aug 4, 2026
…ublish (closes #23523) (#23528)

* fix(core): raise ContentSavedNotification on save-and-publish (closes #23523)

The combined SaveAndPublish operation introduced in #22812 published the
Saving, Publishing and Published notifications but never the paired Saved
notification that the standalone Save path raises. Restore it by raising
ContentSavedNotification once the document is persisted within
CommitDocumentChangesInternal, opted into only by the save-and-publish
entry points so the unpublish, branch and scheduled paths are unaffected.

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

* fix(core): address review feedback on save-and-publish Saved notification

- Capture the Saved notification's cultures using the same expression and
  timing as the standalone Save path (before persistence resets change
  tracking), so it honours the documented SavedCultures contract rather
  than reporting "*" unconditionally for invariant content.
- Document the new raiseSavedNotification parameter (CS1573).
- Use the concrete Content type for the test document variable (CA1859).

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

* Additional test verifying cultures saved for variant save and publish.

* Extracted nested ternary operation into an independent statement.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants