feat(demo): edit existing posts (Android + iOS)#433
Merged
Conversation
Base automatically changed from
fix/android-rest-base-namespace-payload
to
trunk
April 8, 2026 15:12
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 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.
77d6234 to
59025fc
Compare
This was referenced Apr 8, 2026
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>
This was referenced Apr 8, 2026
jkmassel
approved these changes
Apr 8, 2026
Contributor
jkmassel
left a comment
There was a problem hiding this comment.
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>
Member
Author
|
Pushed one additional change in ae9b4f9. This reverts bumping |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
Adds a cross-platform "edit existing posts" feature to the iOS and Android demo apps:
PostTypeDetailsstruct, carrying the slug + REST base + namespace together. Threaded throughEditorConfiguration,EditorPreloadList,GBKitGlobal, andEditorServiceso custom post types get the correct REST routing instead of a heuristic pluralizationWhy?
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
PostTypeDetails.ktdata class withpost/pagecompanion constants, mirroring iOS 1:1.EditorConfiguration.postTypeis nowPostTypeDetailsinstead ofString.EditorConfiguration.builder(siteURL, siteApiRoot, postType)now takes aPostTypeDetailsparameter. Host apps will need to passPostTypeDetails.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'srestNamespace/restBaseinstead of hardcoded/wp/v2/posts/\$id, fixing a latent bug where editing a page would preload the wrong path.GBKitGlobal.PostpopulatesrestBase/restNamespacedirectly from the post type details instead of therestBaseFor()heuristic introduced in fix(android): include restBase and restNamespace in GBKit post payload #432.Android demo changes
PostsListActivitythat fetches posts for the selected post type and launches the editor with the chosen post.persistPost()helper inEditorActivitythat reads title/content viagetTitleAndContent()and PUTs viawordpress-rs.SitePreparationViewModelbacked by real REST data (/wp/v2/types), with a fallback to Post/Page when fetching fails.iOS demo changes
pr-build/1270(the PR that exportsPostUpdateParams).EditorViewModel.saveHandlerto a newpersistPost()helper on_EditorView.Testing Instructions
Accessibility Testing Instructions
N/A, only demo app navigation changes.
Screenshots or screencast
Screen.Recording.2026-04-08.at.12.11.54.mov