Skip to content

Port restore posts page to React#28767

Open
PedroMarianoAlmeida wants to merge 11 commits into
TryGhost:mainfrom
PedroMarianoAlmeida:migrate-restore-page-to-react
Open

Port restore posts page to React#28767
PedroMarianoAlmeida wants to merge 11 commits into
TryGhost:mainfrom
PedroMarianoAlmeida:migrate-restore-page-to-react

Conversation

@PedroMarianoAlmeida

@PedroMarianoAlmeida PedroMarianoAlmeida commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

This PR ports the Ghost Admin /restore page from Ember to React. The route lets users recover local editor revisions that were saved in the browser, and the new implementation keeps that behavior while moving the route into the React admin shell.

The final diff is a clean cutover: React owns /restore, the temporary /restore-react comparison route is gone, and the old Ember route/controller/template are removed. The Ember local-revisions service stays because the Ember editor still writes the local revision data.

Check live demo

Why Make It

This is part of the Ember to React migration based on routes. /restore is a good candidate because it is small, isolated, and only reachable by direct URL rather than from the main admin navigation.

The main risk was whether restore data came from an API or from the browser. The migration task confirmed it is browser-local: the Ember editor writes revisions to window.localStorage using post-revision-<id>-<timestamp> keys. That means the React route can safely take over the read and restore UI without changing the editor writer path.

What Changed

  • Added a React /restore route that reads local revisions from localStorage, sorted newest-first.
  • Added apps/admin/src/restore/local-revisions.ts as a small read layer for the existing post-revision-* storage contract.
  • Built the Restore Posts UI with Shade ListPage and Table primitives.
  • Kept the existing parity selectors used by tests:
    • data-test-id="restore-post-title"
    • data-test-id="restore-post-button"
  • Added useAddPost to admin-x-framework so restore can create a draft through POST /posts/.
  • Cut the canonical route over by removing /restore from EMBER_ROUTES and registering the React route at /restore.
  • Removed the dead Ember restore-posts route, controller, template, router entry, and old Ember acceptance test.

Restore Behavior

The restore action intentionally follows Ember parity:

  • creates a new draft titled (Restored) <title>
  • preserves lexical content, type, slug when present, authors as {id}, tags, and status: 'draft'
  • shows Post restored successfully or Failed to restore post
  • does not navigate after restore
  • leaves the local revision list unchanged

Why Users And Developers Need It

Users keep the same recovery path for local editor revisions if they lose draft content. Developers get another Ember-owned admin route migrated to React with the old Ember UI removed, focused tests in place, and no change to the editor’s local revision writer.

Migration Notes

  • ghost-revisions is intentionally ignored. Ember reads use a raw post-revision-* prefix scan, so React does the same.
  • No useLocalStorage hook was added. React reads once on mount, matching Ember route model() semantics. React does not own or write this data.
  • No labs flag was added. A flag would keep the Ember route alive and require a later cleanup PR; this route is low-traffic, direct-URL, easy to verify, and easy to revert.
  • User-facing strings remain plain English for parity and consistency with existing React admin surfaces such as Members; this PR does not introduce a new admin React i18n pattern.

Please check your PR against these items:

  • I've read and followed the Contributor Guide
  • I've explained my change
  • I've written an automated test to prove my change works

Testing

  • pnpm --filter @tryghost/admin-x-framework test:unit -- test/unit/api/posts.test.tsx
  • pnpm --filter @tryghost/admin-x-framework test:types
  • pnpm --filter @tryghost/admin-x-framework exec tsc -p tsconfig.declaration.json
  • pnpm --filter @tryghost/shade exec tsc -p tsconfig.declaration.json
  • pnpm --filter @tryghost/shade test:types
  • pnpm --filter @tryghost/shade lint:code
  • pnpm --filter @tryghost/admin test:unit -- src/restore/restore.test.tsx src/restore/local-revisions.test.ts
  • pnpm --filter @tryghost/admin typecheck
  • pnpm --filter @tryghost/admin lint
  • pnpm --filter @tryghost/e2e test:types
  • pnpm --filter @tryghost/e2e lint
  • pnpm --filter @tryghost/e2e test tests/admin/restore.test.ts --timeout=60000

The Playwright restore e2e suite now covers the deleted Ember acceptance-test contract against canonical /restore: seeded local revision renders title + Restore button, clicking Restore shows the success notification, and empty localStorage shows the empty state.

PedroMarianoAlmeida and others added 8 commits June 19, 2026 18:53
- first step of porting /restore from Ember to React: lock in the current
  behavior with a Playwright suite before any React code exists, so the same
  suite can re-validate the React port later
- covers the seeded-revision list (title + Restore button), the restore click
  showing the success notification, and the empty state
- seeds revisions straight into localStorage (load -> setItem -> reload) since
  that is how the Ember editor populates them; reuses the existing
  data-test-id selectors so one suite can target both implementations

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This creates a temporary React-owned restore path so the migration can be built and verified without displacing the existing Ember /restore route.
Add the React restore route probe and extract local revision reads into a tested localStorage module for the Ember-to-React migration.
Render local restore revisions in the React route using Shade components and add RTL coverage for empty and populated states.
Replaces the temporary custom restore revision list with shade table primitives, constrains the restore table to the members page width, and matches the Members Created column date and relative-age styling.
Add the temporary Restore button handler for the selected local revision and cover the click path in the route test.
Remove the Ember restore-posts route, template, controller, and acceptance test now that the React /restore route owns the canonical path.
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ab383d58-c279-4384-9031-529c3822e104

📥 Commits

Reviewing files that changed from the base of the PR and between 60ce99c and 256b961.

📒 Files selected for processing (1)
  • apps/admin/src/restore/restore.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/admin/src/restore/restore.test.tsx

Walkthrough

The pull request migrates the Ghost Admin "Restore Posts" feature from Ember.js to React. On the Ember side, the restore-posts router entry and Handlebars template are removed. On the React side, a new LocalRevision localStorage module is introduced alongside a RestoreRoute component that reads revisions, renders them in a table, and creates draft posts via a new useAddPost mutation hook added to the admin-x-framework posts API. The Post type is expanded with optional fields (authors, lexical, type, tags, feature_image, post_revisions). The /restore path is removed from EMBER_ROUTES and registered as a lazy React route. The sonner wrapper gains a toast re-export, and the state-bridge gains a PostsResponseType'post' mapping to ensure Ember store synchronization. Unit tests (Vitest + MSW) and Playwright E2E tests with page helpers cover the new implementation.

Suggested reviewers

  • rob-ghost
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main objective: migrating the restore posts page from Ember to React, which is the primary change throughout the changeset.
Description check ✅ Passed The description thoroughly explains the rationale, implementation details, and testing approach for porting the restore page to React, directly addressing the changes in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@PedroMarianoAlmeida PedroMarianoAlmeida marked this pull request as ready for review June 21, 2026 03:08

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/admin/src/restore/local-revisions.test.ts (1)

42-61: ⚡ Quick win

Add malformed JSON coverage for revision keys.

Please add a regression test where a post-revision-* key contains invalid JSON and assert findAll() skips it instead of throwing.

Suggested test addition
+    it("ignores malformed JSON for revision keys", () => {
+        window.localStorage.setItem("post-revision-bad-1000", "{bad-json");
+        window.localStorage.setItem("post-revision-good-1001", JSON.stringify({
+            id: "good",
+            revisionTimestamp: 1001,
+            title: "Good revision",
+            type: "post"
+        }));
+
+        expect(findAll()).toEqual([
+            {
+                key: "post-revision-good-1001",
+                id: "good",
+                revisionTimestamp: 1001,
+                title: "Good revision",
+                type: "post"
+            }
+        ]);
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/admin/src/restore/local-revisions.test.ts` around lines 42 - 61, Add a
new test case that verifies findAll() gracefully handles malformed JSON in
revision keys. Create a test where a localStorage key matching the
post-revision-* pattern (e.g., "post-revision-draft-1000") contains invalid JSON
instead of a valid JSON object, then assert that findAll() skips this entry
without throwing an error. This ensures the function properly handles edge cases
where stored revision data may be corrupted or improperly formatted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/admin/src/restore/local-revisions.ts`:
- Around line 39-44: The find function's JSON.parse call can throw an exception
when encountering malformed JSON in localStorage, which crashes the entire
`/restore` route. Wrap the JSON.parse statement in a try-catch block to handle
the parsing error gracefully. When a JSON parsing error occurs, catch the
exception and return null to treat corrupt entries as non-revisions, allowing
findAll() to continue processing other entries without crashing the page.

In `@apps/admin/src/restore/restore.tsx`:
- Around line 121-133: The handleRestore function has a race condition where the
finally block unconditionally clears the restoringRevisionKey state, which can
interfere with overlapping restore mutations. To fix this, capture the specific
revision key at the start of the function (by storing revision.key in a local
variable), and in the finally block, only clear the restoringRevisionKey state
if it still matches the captured key. This prevents one completed mutation from
clearing the loading state while another mutation for a different revision is
still in-flight. Apply the same fix to the similar pattern mentioned at lines
190-204.

In `@e2e/helpers/pages/admin/restore/restore-page.ts`:
- Around line 31-42: The visit() and seedRevisionAndVisit() methods need to
include waitFor() guards to ensure the page has fully rendered before
proceeding. Add await this.waitFor() after the await this.goto() call in the
visit() method, and also add await this.waitFor() after the await
this.page.reload() call in the seedRevisionAndVisit() method. This ensures the
Page Object waits for the route to finish rendering rather than stopping at just
the load event.

---

Nitpick comments:
In `@apps/admin/src/restore/local-revisions.test.ts`:
- Around line 42-61: Add a new test case that verifies findAll() gracefully
handles malformed JSON in revision keys. Create a test where a localStorage key
matching the post-revision-* pattern (e.g., "post-revision-draft-1000") contains
invalid JSON instead of a valid JSON object, then assert that findAll() skips
this entry without throwing an error. This ensures the function properly handles
edge cases where stored revision data may be corrupted or improperly formatted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 486ed3e9-d32d-47de-a5fd-4776b8bfee57

📥 Commits

Reviewing files that changed from the base of the PR and between f32a14f and bcae2a9.

📒 Files selected for processing (19)
  • apps/admin-x-framework/src/api/posts.ts
  • apps/admin-x-framework/test/unit/api/posts.test.tsx
  • apps/admin/src/restore/local-revisions.test.ts
  • apps/admin/src/restore/local-revisions.ts
  • apps/admin/src/restore/restore.test.tsx
  • apps/admin/src/restore/restore.tsx
  • apps/admin/src/routes.tsx
  • apps/shade/src/components/ui/sonner.tsx
  • e2e/helpers/pages/admin/index.ts
  • e2e/helpers/pages/admin/restore/index.ts
  • e2e/helpers/pages/admin/restore/restore-page.ts
  • e2e/tests/admin/restore.test.ts
  • ghost/admin/app/controllers/restore-posts.js
  • ghost/admin/app/router.js
  • ghost/admin/app/routes/restore-posts.js
  • ghost/admin/app/services/state-bridge.js
  • ghost/admin/app/templates/restore-posts.hbs
  • ghost/admin/tests/acceptance/restore-post-test.js
  • ghost/admin/tests/unit/services/state-bridge-test.js
💤 Files with no reviewable changes (5)
  • ghost/admin/app/router.js
  • ghost/admin/app/routes/restore-posts.js
  • ghost/admin/app/templates/restore-posts.hbs
  • ghost/admin/app/controllers/restore-posts.js
  • ghost/admin/tests/acceptance/restore-post-test.js

Comment thread apps/admin/src/restore/local-revisions.ts
Comment thread apps/admin/src/restore/restore.tsx Outdated
Comment thread e2e/helpers/pages/admin/restore/restore-page.ts

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
apps/admin/src/restore/restore.test.tsx (1)

165-213: ⚡ Quick win

Add assertion that error toast was not called.

For consistency with the successful restore test (lines 107-163), this test should verify that mockToastError was not called after the success toast assertion.

✨ Suggested consistency fix
 await waitFor(() => {
     expect(mockToastSuccess).toHaveBeenCalledWith("Post restored successfully");
 });
+
+expect(mockToastError).not.toHaveBeenCalled();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/admin/src/restore/restore.test.tsx` around lines 165 - 213, The
restoreTest function "ignores duplicate restore clicks while the revision is
already restoring" is missing an assertion to verify that mockToastError was not
called. After the existing waitFor block that asserts mockToastSuccess was
called with "Post restored successfully", add another waitFor block or assertion
to ensure that mockToastError was not called, maintaining consistency with the
successful restore test pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/admin/src/restore/restore.test.tsx`:
- Around line 165-213: The restoreTest function "ignores duplicate restore
clicks while the revision is already restoring" is missing an assertion to
verify that mockToastError was not called. After the existing waitFor block that
asserts mockToastSuccess was called with "Post restored successfully", add
another waitFor block or assertion to ensure that mockToastError was not called,
maintaining consistency with the successful restore test pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ddac607d-ae85-45ce-89e4-4e956c2ef09e

📥 Commits

Reviewing files that changed from the base of the PR and between bcae2a9 and 49d85f5.

📒 Files selected for processing (5)
  • apps/admin/src/restore/local-revisions.test.ts
  • apps/admin/src/restore/local-revisions.ts
  • apps/admin/src/restore/restore.test.tsx
  • apps/admin/src/restore/restore.tsx
  • e2e/helpers/pages/admin/restore/restore-page.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • e2e/helpers/pages/admin/restore/restore-page.ts
  • apps/admin/src/restore/local-revisions.test.ts
  • apps/admin/src/restore/local-revisions.ts
  • apps/admin/src/restore/restore.tsx

Handle malformed local revision JSON, prevent duplicate restore creates, and add the restore page object wait guard.
@PedroMarianoAlmeida PedroMarianoAlmeida force-pushed the migrate-restore-page-to-react branch from 49d85f5 to 743400f Compare June 21, 2026 03:33
Use moment-timezone for restore revision dates so the absolute and relative labels follow the same pattern as the members page.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant