Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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/<cpt-rest-base>?search=<title>&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.
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -122,12 +123,12 @@ class CustomPostEditorService {

case (.newPost(let existing), true):
var params = settings.makeCreateParameters(from: existing, taxonomies: taxonomies)
params.status = params.status?.normalizedPublishStatus() ?? .publish
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.

Maybe not related to this PR, but can params.status ever be nil?

If not, maybe it doesn't need to be optional and we could flatten this out? Or maybe it should be able to be nil to represent "the user hasn't told us what they want"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm okay with it being non-optional. We can always give new posts a default "draft" status, which sounds very reasonable to me.


// 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
}
Expand All @@ -140,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 {
Expand All @@ -162,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)
Expand All @@ -181,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
Expand All @@ -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

Expand Down Expand Up @@ -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)]
}

Expand All @@ -278,3 +281,19 @@ extension PostCreateParams {
return params
}
}

private extension PostStatus {
/// Maps a user-selected status to the one used by a publish action.
/// `.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 {
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.

What happens if a user without publishing permission goes through this flow? Will it preserve pending?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Addressed in the latest commit.

switch self {
case .future: return .future
case .private: return .private
case .pending: return .pending
default: return .publish
}
}
}