From 142652275344f9ce86b38be1a6c77e3b7dd798c0 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 11 May 2026 12:47:18 +1200 Subject: [PATCH 1/5] Format CustomPostEditorService.swift --- .../CustomPostTypes/CustomPostEditorService.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift index 7aff6a9ee07a..1033f2bb5296 100644 --- a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift +++ b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift @@ -85,9 +85,10 @@ class CustomPostEditorService { let capabilities = PostSettingsCapabilities(from: details) // At the moment, category & tags are separated from custom taxonomies. We can unify them as taxonomies later, // by which point we won't need this filter logic. - self.taxonomies = (try? blog.taxonomies - .filter { capabilities.customTaxonomySlugs.contains($0.slug) } - .sorted(using: KeyPathComparator(\.name))) ?? [] + self.taxonomies = + (try? blog.taxonomies + .filter { capabilities.customTaxonomySlugs.contains($0.slug) } + .sorted(using: KeyPathComparator(\.name))) ?? [] switch self.state { case let .newPost(params): @@ -195,7 +196,8 @@ class CustomPostEditorService { guard try await !hasBeenModified(post: post) else { throw PostUpdateError.conflicts } let endpoint = details.toPostEndpointType() - let updatedPost = try await wpService.posts().updatePost(endpointType: endpoint, postId: post.id, params: params) + let updatedPost = try await wpService.posts() + .updatePost(endpointType: endpoint, postId: post.id, params: params) state = .existingPost(updatedPost) initialSettings = settings @@ -265,7 +267,8 @@ extension PostCreateParams { params.status = .draft if let categoryID = blog.settings?.defaultCategoryID, - categoryID != PostCategory.uncategorized { + categoryID != PostCategory.uncategorized + { params.categories = [TermId(categoryID.int64Value)] } From d2765edff4e6c1b333cc3c41f45ed733fbf2e2ff Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 11 May 2026 12:51:48 +1200 Subject: [PATCH 2/5] Preserve scheduled and private status when publishing custom posts The custom post publish flow flattened the user's selected status to .publish in both the Post Settings and editor-content paths, dropping .future (scheduled) and .private (password/private visibility). The save action now normalizes status only for selections that lack their own publishing semantics (drafts, pending, or unspecified), and preserves .future and .private as the user selected them. --- .../CustomPostEditorService.swift | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift index 1033f2bb5296..18cec86586d9 100644 --- a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift +++ b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift @@ -123,12 +123,12 @@ class CustomPostEditorService { case (.newPost(let existing), true): var params = settings.makeCreateParameters(from: existing, taxonomies: taxonomies) + params.status = params.status?.normalizedPublishStatus() ?? .publish // Update content if let delegate { let hasTitle = details.supports.map[.title] == .bool(true) let editorContent = try await delegate.editorContent(for: self) - params.status = .publish params.title = hasTitle ? editorContent.title : nil params.content = editorContent.content } @@ -141,7 +141,7 @@ class CustomPostEditorService { case (.existingPost(let post, _), true): var params = settings.makeUpdateParameters(from: post, taxonomies: taxonomies) - params.status = .publish + params.status = PostStatus(settings.status).normalizedPublishStatus() // Update content if let delegate { @@ -163,7 +163,7 @@ class CustomPostEditorService { switch state { case .newPost(let existing): var params = existing - params.status = publish ? .publish : .draft + params.status = publish ? (existing.status?.normalizedPublishStatus() ?? .publish) : .draft params.title = hasTitle ? content.title : nil params.content = content.content try await create(params: params) @@ -182,7 +182,7 @@ class CustomPostEditorService { params = PostUpdateParams(meta: nil) } if publish { - params.status = .publish + params.status = pending.map { PostStatus($0.status).normalizedPublishStatus() } ?? .publish } params.title = hasTitle ? content.title : nil params.content = content.content @@ -281,3 +281,18 @@ extension PostCreateParams { return params } } + +private extension PostStatus { + /// Maps a user-selected status to the one used by a publish action. + /// `.future` and `.private` are preserved because they carry their own + /// publishing semantics (scheduled, password/private visibility); every + /// other selection — draft or pending — collapses to `.publish` so the + /// post is published normally. + func normalizedPublishStatus() -> PostStatus { + switch self { + case .future: return .future + case .private: return .private + default: return .publish + } + } +} From b1f82535794c52197f743e5ee6d3820401b856b8 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 11 May 2026 14:59:16 +1200 Subject: [PATCH 3/5] Add a test case --- .../custom-post-preserve-private-status.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Tests/AgentTests/post-editor/custom-post-preserve-private-status.md diff --git a/Tests/AgentTests/post-editor/custom-post-preserve-private-status.md b/Tests/AgentTests/post-editor/custom-post-preserve-private-status.md new file mode 100644 index 000000000000..357477bf599c --- /dev/null +++ b/Tests/AgentTests/post-editor/custom-post-preserve-private-status.md @@ -0,0 +1,32 @@ +# Preserve Private Visibility When Publishing a Custom Post + +Regression test for the bug where publishing a REST custom post from the pre-publishing sheet flattens a user-selected `private` visibility to a public `publish`. + +## Prerequisites +- Logged in to the app with the test account. +- The site has at least one custom post type registered with REST API support, and the custom post types entry is visible on the My Site screen. If no custom post type is available, fail with "Prerequisite not met: site has no REST custom post type". + +## Steps +1. Navigate to the "My Site" tab. +2. From the blog details menu, tap the **"More"** row (uses an ellipsis icon) to open the Custom Post Types list. +3. Tap one of the available custom post types (e.g., "Books"). +4. Tap the FAB (floating "+" button in the bottom-right corner) to create a new custom post. +5. Enter "CPT private preserve" as the post title. +6. Tap the "Publish" button in the top-right corner to open the pre-publish sheet. +7. From the pre-publish sheet, open "Post Settings". +8. Change the visibility setting to "Private". +9. Return to the pre-publish sheet. +10. Tap "Publish" to commit. +11. Dismiss the confirmation screen by tapping "Done". + +## Verification (REST API) +- Use the WordPress REST API endpoint for the chosen custom post type (e.g., `/wp/v2/?search=CPT+private+preserve&status=private`) to look up the post by title. Authenticate with the application password (private posts are not returned to anonymous requests). +- Verify a post titled "CPT private preserve" exists. +- **Regression assertion:** the post's `status` field is exactly `"private"`, not `"publish"`. A `status` of `"publish"` indicates the bug has regressed. + +## Cleanup (REST API) +- Use the WordPress REST API to trash the post created during this test, regardless of pass or fail. + +## Expected Outcome +- The custom post is published with private visibility and the REST API confirms `status: "private"`. +- The user's `private` selection from Post Settings is preserved on the publish path. From 797c79cd47c12cb2d9757440f5ff5fe297655673 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 12:23:56 +1200 Subject: [PATCH 4/5] Add missing `pending` case --- .../CustomPostTypes/CustomPostEditorService.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift index 18cec86586d9..e30c0514d27e 100644 --- a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift +++ b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift @@ -284,14 +284,15 @@ extension PostCreateParams { private extension PostStatus { /// Maps a user-selected status to the one used by a publish action. - /// `.future` and `.private` are preserved because they carry their own - /// publishing semantics (scheduled, password/private visibility); every - /// other selection — draft or pending — collapses to `.publish` so the - /// post is published normally. + /// `.future`, `.private`, and `.pending` are preserved because they carry + /// their own publishing semantics (scheduled, password/private visibility, + /// submit for review); every other selection (draft) collapses to + /// `.publish` so the post is published normally. func normalizedPublishStatus() -> PostStatus { switch self { case .future: return .future case .private: return .private + case .pending: return .pending default: return .publish } } From 87b6edd41b79c5e013de395c7abb2df45493e52a Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 12:43:07 +1200 Subject: [PATCH 5/5] Add more status to the test case --- .../custom-post-preserve-private-status.md | 32 ----------------- .../custom-post-preserve-publish-statuses.md | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+), 32 deletions(-) delete mode 100644 Tests/AgentTests/post-editor/custom-post-preserve-private-status.md create mode 100644 Tests/AgentTests/post-editor/custom-post-preserve-publish-statuses.md diff --git a/Tests/AgentTests/post-editor/custom-post-preserve-private-status.md b/Tests/AgentTests/post-editor/custom-post-preserve-private-status.md deleted file mode 100644 index 357477bf599c..000000000000 --- a/Tests/AgentTests/post-editor/custom-post-preserve-private-status.md +++ /dev/null @@ -1,32 +0,0 @@ -# Preserve Private Visibility When Publishing a Custom Post - -Regression test for the bug where publishing a REST custom post from the pre-publishing sheet flattens a user-selected `private` visibility to a public `publish`. - -## Prerequisites -- Logged in to the app with the test account. -- The site has at least one custom post type registered with REST API support, and the custom post types entry is visible on the My Site screen. If no custom post type is available, fail with "Prerequisite not met: site has no REST custom post type". - -## Steps -1. Navigate to the "My Site" tab. -2. From the blog details menu, tap the **"More"** row (uses an ellipsis icon) to open the Custom Post Types list. -3. Tap one of the available custom post types (e.g., "Books"). -4. Tap the FAB (floating "+" button in the bottom-right corner) to create a new custom post. -5. Enter "CPT private preserve" as the post title. -6. Tap the "Publish" button in the top-right corner to open the pre-publish sheet. -7. From the pre-publish sheet, open "Post Settings". -8. Change the visibility setting to "Private". -9. Return to the pre-publish sheet. -10. Tap "Publish" to commit. -11. Dismiss the confirmation screen by tapping "Done". - -## Verification (REST API) -- Use the WordPress REST API endpoint for the chosen custom post type (e.g., `/wp/v2/?search=CPT+private+preserve&status=private`) to look up the post by title. Authenticate with the application password (private posts are not returned to anonymous requests). -- Verify a post titled "CPT private preserve" exists. -- **Regression assertion:** the post's `status` field is exactly `"private"`, not `"publish"`. A `status` of `"publish"` indicates the bug has regressed. - -## Cleanup (REST API) -- Use the WordPress REST API to trash the post created during this test, regardless of pass or fail. - -## Expected Outcome -- The custom post is published with private visibility and the REST API confirms `status: "private"`. -- The user's `private` selection from Post Settings is preserved on the publish path. diff --git a/Tests/AgentTests/post-editor/custom-post-preserve-publish-statuses.md b/Tests/AgentTests/post-editor/custom-post-preserve-publish-statuses.md new file mode 100644 index 000000000000..37afa42cc243 --- /dev/null +++ b/Tests/AgentTests/post-editor/custom-post-preserve-publish-statuses.md @@ -0,0 +1,35 @@ +# Preserve User-Selected Status When Publishing a Custom Post + +Regression test for the bug where publishing a REST custom post from the pre-publishing sheet flattens a user-selected status (`private`, `pending`, or `future`/scheduled) to a public `publish`, discarding the user's intent. + +## Prerequisites +- Logged in to the app with the test account. +- The site has at least one custom post type registered with REST API support, and the custom post types entry is visible on the My Site screen. If no custom post type is available, fail with "Prerequisite not met: site has no REST custom post type". + +## Status Matrix + +Perform the steps below **once per row**. Each row creates a separate post with its own title. + +| Status | Title | How to set it from the pre-publish sheet | +| --------- | ---------------------- | -------------------------------------------------------------------------------------- | +| `private` | CPT private preserve | Open "Post Settings" and set Visibility to "Private". | +| `pending` | CPT pending preserve | Scroll to "More Options" and toggle "Pending Review" on. | +| `future` | CPT scheduled preserve | Tap the "Date" row and pick a date at least 7 days in the future, then confirm. | + +## Steps (per row) +1. From "My Site", tap **"More"**, then tap one of the available custom post types (e.g., "Books"). +2. Tap the FAB ("+") to create a new custom post and enter the row's **Title**. +3. Tap **"Publish"** in the top-right corner to open the pre-publish sheet. +4. Apply the row's **"How to set it"** action. +5. Return to the pre-publish sheet. For `future`, the primary button changes from "Publish" to "Schedule". +6. Tap the primary button to commit. Dismiss the confirmation screen. + +## Verification (per row, via REST API) +- Look up the post by title against the custom post type's REST endpoint (e.g., `/wp/v2/?search=&status=any`). Authenticate with the application password — private and future posts aren't returned to anonymous requests. +- **Regression assertion:** the post's `status` field is exactly the row's `Status`. Any other value (especially `"publish"`) indicates the bug has regressed. + +## Cleanup (REST API) +- Trash every post created during this test, regardless of pass or fail. + +## Expected Outcome +- For each row, the custom post is saved with the user-selected status preserved through the publish path.