Skip to content

feat(demo): edit existing posts (Android + iOS)#433

Merged
dcalhoun merged 22 commits intotrunkfrom
feat/demo-edit-existing-posts
Apr 8, 2026
Merged

feat(demo): edit existing posts (Android + iOS)#433
dcalhoun merged 22 commits intotrunkfrom
feat/demo-edit-existing-posts

Conversation

@dcalhoun
Copy link
Copy Markdown
Member

@dcalhoun dcalhoun commented Apr 8, 2026

What?

Adds a cross-platform "edit existing posts" feature to the iOS and Android demo apps:

  • A Posts list screen for browsing existing posts of the currently selected post type
  • A Post type picker on the site preparation screen (Post / Page, plus any custom post types discovered via REST)
  • A Save button in the editor toolbar that PUTs the edited title/content back to the site via the WordPress REST API
  • PostTypeDetails — a Kotlin data class mirroring the iOS PostTypeDetails struct, carrying the slug + REST base + namespace together. Threaded through EditorConfiguration, EditorPreloadList, GBKitGlobal, and EditorService so custom post types get the correct REST routing instead of a heuristic pluralization
  • Demo-app cleanup: removes permanently-disabled toolbar items and overflow menu placeholders on both platforms

Why?

GutenbergKit's demo apps previously only supported creating new posts — there was no way to load an existing post, edit it, and persist the changes back to the server. That makes it hard to dogfood the editor against real content and hard to reproduce bugs that only occur on existing posts.

How?

Save flow (REST-only in this PR)

Both demos persist title/content via wordpress-rs' posts.update() directly.

Android library changes

  • New: PostTypeDetails.kt data class with post/page companion constants, mirroring iOS 1:1.
  • Breaking: EditorConfiguration.postType is now PostTypeDetails instead of String. EditorConfiguration.builder(siteURL, siteApiRoot, postType) now takes a PostTypeDetails parameter. Host apps will need to pass PostTypeDetails.post / PostTypeDetails.page (or a custom-constructed instance) instead of a string slug. This mirrors iOS' current state after #299.
  • EditorPreloadList.buildPostPath() now uses the post type's restNamespace/restBase instead of hardcoded /wp/v2/posts/\$id, fixing a latent bug where editing a page would preload the wrong path.
  • GBKitGlobal.Post populates restBase/restNamespace directly from the post type details instead of the restBaseFor() heuristic introduced in fix(android): include restBase and restNamespace in GBKit post payload #432.

Android demo changes

  • New PostsListActivity that fetches posts for the selected post type and launches the editor with the chosen post.
  • New Save button wired to a persistPost() helper in EditorActivity that reads title/content via getTitleAndContent() and PUTs via wordpress-rs.
  • Post type picker in SitePreparationViewModel backed by real REST data (/wp/v2/types), with a fallback to Post/Page when fetching fails.
  • Assorted UI cleanups: browse button repositioned, overflow-menu placeholders removed, save button uppercased.

iOS demo changes

  • wordpress-rs SPM dependency bumped to pr-build/1270 (the PR that exports PostUpdateParams).
  • Save button added to the editor toolbar, wired through EditorViewModel.saveHandler to a new persistPost() helper on _EditorView.
  • Overflow menu stripped to just the Code/Visual editor toggle (placeholders removed).

Testing Instructions

  1. Run the iOS/Android demo app.
  2. Add a test site.
  3. Tap Browse.
  4. Select an existing post, confirm title/content load, make an edit, tap Save.
  5. Exit the editor, exist the post list view.
  6. Navigate back to the same post.
  7. Verify the edits persisted.
  8. Repeat steps 3-7 for a Page post type.

Accessibility Testing Instructions

N/A, only demo app navigation changes.

Screenshots or screencast

Screen.Recording.2026-04-08.at.12.11.54.mov

@github-actions github-actions bot added the [Type] Enhancement A suggestion for improvement. label Apr 8, 2026
Base automatically changed from fix/android-rest-base-namespace-payload to trunk April 8, 2026 15:12
dcalhoun and others added 20 commits April 8, 2026 11:25
Adds a new PostsListActivity that fetches and displays posts from a
WordPress site so the user can pick one to edit. Mirrors the iOS
PostsListView in functionality.

- GutenbergKitApplication.createApiClient() builds a WpApiClient from
  a stored Account (works for both self-hosted Application Passwords
  and WP.com OAuth flows).
- PostsListActivity uses the client to call posts.listWithEditContext()
  with pagination, then launches EditorActivity with the selected
  post's ID, title, and content pre-filled.
- Registered in AndroidManifest.xml.

Required so the Android demo can exercise the Save button flow added
in a follow-up commit, which needs a real post ID to PUT to the REST
API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wires the Save button in EditorActivity to:

1. Call GutenbergView.savePost() to trigger the editor store's save
   lifecycle so plugins (e.g., VideoPress) fire their side-effect API
   calls.
2. Read the latest title/content via getTitleAndContent() and persist
   the post via WpApiClient's posts().update() call.

Also adds a "Browse" button to SitePreparationActivity (visible only
for authenticated sites) that launches PostsListActivity to pick an
existing post to edit. The selected post's ID is threaded through to
EditorActivity via a new EXTRA_ACCOUNT_ID intent extra so the Save
handler can reconstruct the API client.

The Save button is disabled for new drafts (where postId is null)
since updating requires an existing post ID.

Requires the PostUpdateParams export from wordpress-rs PR
Automattic/wordpress-rs#1270 (already exposed via uniffi for Kotlin).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PostListParams.status defaults to publish-only on the server side,
hiding drafts from the demo's posts list. Pass PostStatus.Any
explicitly so all statuses (draft, pending, future, etc.) appear in
the picker, matching the iOS demo's behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Matches the iOS demo's layout where the "Browse" action sits below the
Post Type picker in the Feature Configuration card, rather than in the
top app bar alongside Start.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The toolbar previously showed both "Save" and a non-functional "PUBLISH"
button. Removes the disabled Publish button so Save is the only
top-level action.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Change R.string.save to "SAVE" so the toolbar button matches the
  styling of the previous PUBLISH button it replaced.
- Remove the unused R.string.publish (the disabled Publish button was
  removed in an earlier commit).
- Remove the disabled "Save" item from the editor's overflow menu so
  the toolbar Save button is the sole save action.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes the placeholder Preview, Post Settings, and Help dropdown menu
items from the editor toolbar. Only the working "Code Editor / Visual
Editor" toggle remains. Also drops the now-unused string resources.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ports the iOS `PostTypeDetails` struct to Android as a Kotlin data
class with `post`/`page` companion constants. `EditorConfiguration`,
`EditorPreloadList`, `GBKitGlobal`, and `EditorService` now carry the
full post type details (slug + restBase + restNamespace) instead of
just the slug, matching the iOS API surface 1:1.

This deletes the `restBaseFor()` heuristic in `GBKitGlobal` that
incorrectly pluralized custom post-type slugs, and fixes a related
bug in `EditorPreloadList.buildPostPath()` where the preload key was
hardcoded to `/wp/v2/posts/$id` even when editing a page.

The demo app still threads a string slug internally; a small
`slugToPostTypeDetails()` helper bridges to the new API. Chunk 3
will replace that helper with real REST data from `wordpress-rs`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`RESTAPIRepository.buildPostUrl` was hardcoded to `/wp/v2/posts/$id`,
so opening a `page` (or any non-post type) hit the wrong endpoint and
the WordPress REST API returned `rest_post_invalid_id`. Use the
configuration's `restNamespace`/`restBase` instead, matching iOS
(`RESTAPIRepository.swift:110-120`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the hardcoded `post`/`page` picker with a dynamic list
fetched via `wordpress-rs`'s `postTypes.listWithEditContext()`,
mirroring the iOS `SitePreparationView.loadPostTypes()` flow.

The view-model now stores `postTypes: List<PostTypeDetails>` and
`selectedPostType: PostTypeDetails?`, deletes the
`slugToPostTypeDetails` placeholder, and threads the selected type
straight into `EditorConfiguration` and `PostsListActivity`. The
post-type filter matches iOS: always include `post`/`page`, include
custom types only when `viewable && visibility.showUi`, exclude all
internal built-ins (`Attachment`, `WpBlock`, etc.).

`PostsListActivity` now takes a `PostTypeDetails` extra (Parcelable)
instead of a string slug, and dispatches the endpoint type from
`postType.postType` so the existing `Posts`/`Pages`/`Custom`
`when` keeps working for the standard cases. The picker shows
"Loading post types…" while the list is empty and falls back to
`PostTypeDetails.post` if the REST call fails so the editor can
still launch.

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

- Drop the manual pagination loop in `PostsListViewModel.loadPosts` —
  iOS doesn't paginate either, and a single 20-post page is plenty for
  the demo. Removes ~25 lines and a hidden N-request fan-out on busy
  sites.
- Inline the `PostsListViewModelFactory` as an anonymous object in
  `onCreate`. The standalone factory class was 12 lines of pure
  boilerplate for a one-shot screen with immutable args.
- Move the activity's hardcoded UI strings ("Posts", "Back", "Browse",
  "No posts found", "Error loading posts", "Failed to save post") to
  `strings.xml`. Save error toast messages also use string resources
  with a positional argument for the underlying error.
- Clean up fully-qualified imports introduced earlier in this branch:
  `android.content.Context`, `androidx.compose.ui.platform.LocalContext`,
  and `org.wordpress.gutenberg.model.EditorDependencies`. These now use
  short names with proper imports at the top of each file.
- Refresh `app/detekt-baseline.xml` to absorb the legitimate
  `LongMethod`/`LongParameterList`/`TooGenericExceptionCaught` cases
  added by this branch's earlier commits — they're inherent to the
  Compose composables and the demo's intentionally permissive error
  handling. Three pre-existing `UseCheckOrError` warnings are gone now
  that the catch sites use `error()` instead of `throw IllegalStateException`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After fetching post types from the REST API, prefer `post` over the
first item in the alphabetically-sorted list. Without this the picker
defaulted to `page` (alphabetical first), which was surprising — `post`
is the conventional default and matches what users expect from the
prior hardcoded picker.

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

The demo's persistPost() wrapper used GutenbergView.savePost() to fire
the editor store save lifecycle before reading content and PUTting via
the REST API. That bridge method is introduced in a follow-up PR, so
strip the savePostAwait() helper and its call site here — the demo's
save flow now goes directly from reading title/content to the REST
persistence step. The bridge call will be re-introduced in the
savePost() lifecycle PR along with its resilient error handling.

Also migrate the GBKitGlobal restBase tests from the previous PR to
use PostTypeDetails, since the earlier String-slug arguments no longer
compile against the refactored EditorConfiguration.postType field.
Updates the wordpress-rs SPM dependency to track the pr-build/1270
branch, which contains the PostUpdateParams export needed by the
demo app's Save button to persist posts via the REST API.

See: Automattic/wordpress-rs#1270

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a "Save" button to the demo app's editor toolbar. Tapping it:

1. Calls EditorViewController.savePost() to trigger the editor store's
   save lifecycle, so plugins (e.g., VideoPress) fire their side-effect
   API calls.
2. Reads the latest title/content via getTitleAndContent() and persists
   the post via WordPressAPI's posts.updateCancellation() call.

The API client is threaded from PostsListView through RunnableEditor
and into EditorView. The Save button is disabled for new drafts
(where postID is nil) since updating requires an existing post ID.

Requires the PostUpdateParams export added in wordpress-rs
feat/export-post-update-params branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes the placeholder Preview, Revisions, Post Settings, Help, and
the static block/word/character count footer from the editor's more
menu. Only the working "Code Editor / Visual Editor" toggle remains.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move the long save closure body out of `viewModel.saveHandler =`
  and into a `private func persistPost(...)` on `_EditorView`. The
  saveHandler closure is now a one-liner that delegates, mirroring
  how `viewModel.perform` is wired for undo/redo while keeping the
  actual save logic readable as a regular method.
- Add an inline comment to `RunnableEditor` explaining why
  `apiClient` is excluded from `==` and `hash(into:)`: `WordPressAPI`
  isn't `Hashable`/`Equatable` (it owns native Rust state), and two
  editors with the same configuration but different client instances
  should be treated as equal for navigation/identity purposes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
createApiClient() previously constructed every WpApiClient via
wpOrgSiteApiRootUrl, which resolves paths assuming the self-hosted
/wp/v2/... layout. For WP.com accounts that produced double-prefixed
URLs like:

    https://public-api.wordpress.com/wp/v2/sites/229672404/wp/v2/posts

— and the WP.com REST API responded with rest_no_route, breaking both
the post type picker (only "Post" appeared) and the posts list (404
on browse). Same root cause for both screens: they both go through
createApiClient().

Switch to constructing WpApiClient with an explicit ApiUrlResolver:

- WP.com accounts use WpComDotOrgApiUrlResolver(siteId, .Production),
  matching how the iOS demo wires its WordPressAPI client. The site id
  is extracted from the existing siteApiRoot via the same regex used
  by SitePreparationViewModel.
- Self-hosted accounts continue to use WpOrgSiteApiUrlResolver against
  the site api root.

After this change the post type picker fetches from the correct
/wp/v2/sites/{id}/types path and the posts list fetches from
/wp/v2/sites/{id}/posts.
…anch

Locks the SPM resolution to the wordpress-rs pr-build/1270 commit so
fresh Xcode checkouts don't see a dirty Package.resolved on first
open. Companion to the SPM dependency bump introduced earlier in
this branch.
The list view rendered title?.rendered, which is the WordPress-encoded
form intended for HTML insertion — so titles containing non-breaking
spaces showed up as e.g. \`A new post&nbsp;2\`. The editor handoff was
already correctly using title?.raw; this aligns the display path on
both Android and iOS to do the same, falling back to rendered only if
raw is missing.
The save flow previously fell through to `.posts` for any post type that
wasn't `page`, so custom post types would hit the wrong REST route.
Mirror the Android `EditorActivity.persistPost` mapping and the existing
`PostsListView` load flow by using `.custom(restBase)` for non-standard
types.

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

@jkmassel jkmassel left a comment

Choose a reason for hiding this comment

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

Tested on iOS and Android against a self-hosted site, a Jetpack site, and a WordPress.com site.

Everything seems to work, though I filed a few GitHub Issues for future improvement.

Reverts the wordpress-rs SPM pin back to the alpha-20260313 tag and
reaches PostUpdateParams through the WordPressAPIInternal module
instead. The public typealias that exports PostUpdateParams from
WordPressAPI only exists on wordpress-rs trunk (via #1270), not on any
published tag, so the previous approach forced the demo onto a PR
build branch. Importing the internal module mirrors the workaround
WordPress-iOS uses in Modules/Sources/WordPressCore/ApiCache.swift and
lets us stay on the tagged release until a tag including #1270 is cut.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dcalhoun
Copy link
Copy Markdown
Member Author

dcalhoun commented Apr 8, 2026

Pushed one additional change in ae9b4f9. This reverts bumping wordpress-rs as we can use WordPressAPIInternal for now until a new wordpress-rs tag including Automattic/wordpress-rs#1270 is available.

@dcalhoun dcalhoun merged commit 7af6862 into trunk Apr 8, 2026
16 checks passed
@dcalhoun dcalhoun deleted the feat/demo-edit-existing-posts branch April 8, 2026 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants