Ember to React YOLO migration#28486
Conversation
- ghost/admin has workspace:* deps on apps/* (e.g. @tryghost/admin-x-framework) and pnpm 11 strict-validates the workspace graph at install time - compose.dev.yaml already mounts apps/ into the ghost-dev container for this reason, but the e2e worker containers were never given the same bind, so every dev-mode e2e run failed during global setup - applied the same apps/ bind in the e2e ghost-manager
…tations no ref - groundwork for migrating the tag detail screen from Ember to React: the same Playwright suite (same page objects and selectors) will run against both implementations, gated by the tagDetailsX labs flag - normalized page-object selectors to semantic locators and data-testid, adding the matching data-testid attributes to the Ember templates (kept the existing data-test-* attributes for Ember acceptance tests) - extracted the tag editor tests into a shared, order-independent suite using factories with unique names, since per-file isolation shares DB state across tests in a file and the previous tests were order-dependent - added missing coverage for the unsaved-changes guard and meta data round-trip - opted list.test.ts into per-test isolation: it relies on pristine default content (single 'News' tag) and was order-dependent under per-file reuse - added DEVIATIONS.md as a running log for the Ember-to-React migration
no ref - gates the upcoming React implementation of the tag detail/new screen; the Ember implementation remains the default while the flag is off - also lets the shared e2e suite run against both implementations
no ref - dev-mode Ghost containers run a pnpm install check at boot which can take several minutes on slower machines; the 60x1s health-check window flipped containers to 'unhealthy' (and waitForReady's 120s default timed out) even though boots complete successfully shortly after - waitForHealthy still bails out early if the container exits, so the wider window only extends the success path
no ref
- the sidebar rendered role=navigation with no accessible name; with the new
React tag detail screen adding a semantic breadcrumb <nav>, the e2e
SidebarPage locator (getByRole('navigation')) became ambiguous
- naming the landmark is the a11y-correct fix when multiple navigation
landmarks exist, and the page object now targets it explicitly
no ref - ports the Ember tag detail/new screen to React in apps/posts (where the tags list already lives), built with Shade + react-hook-form + zod: main fields, meta data / X card / Facebook card / code injection sections with previews, image uploads, auto-slug generation (incl. hash- prefix for internal tags), save states, delete confirmation and an unsaved-changes guard via react-router's useBlocker - the admin shell routes /tags/new and /tags/:tagSlug through FlagGatedRoute: React when the tagDetailsX labs flag is on, EmberFallback otherwise - the Ember tag route hands its URL to the react-fallback catch-all when the flag is on, so the hidden Ember app doesn't load data or register transition guards that fight React's navigation (this broke delete-then-navigate) - added TagsResponseType to the state bridge's emberDataTypeMapping so React tag mutations keep the Ember store in sync - extended the framework tags API with full snake_case fields, slug lookup and add/edit mutations; exported useBlocker; exported shade's Accordion - the shared e2e suite now also runs against React (editor-react.test.ts); all 26 tests pass against both implementations with the same page objects
…ndings no ref Fixes from the /code-review + Codex passes on the tagDetailsX slice: - visibility is derived from the name on save (renaming '#tag' to 'tag' now demotes it to public; the server only ever promotes) - re-entrant submit guard (cmd+S key repeat could create duplicate tags; Ember used a drop-concurrency task) - a save resolving after the user navigated away no longer yanks them back (guarded by an unmount ref that re-arms on mount for StrictMode) - server validation messages surface in the save/delete error toasts - slug generation uses @tryghost/string like Ember (transliteration parity; the hand-rolled version produced empty slugs for non-Latin names) - bare hex accent colors are normalized with a leading # on blur - leading-comma names are rejected client-side (matches server validation) - useConfirmUnload guards browser unload like in-app navigation - the unsaved-changes blocker reads live form state (no bypass ref) and lets in-flight saves complete without a spurious dialog - the View button prefers the canonical URL (Ember parity) - transient refetch errors no longer unmount the form as a 404 - tag mutations use updateQueries instead of invalidateQueries: invalidation refetched every loaded page of the tags list and its Ember-side handling (store.unloadAll) stripped tags out of loaded posts' embedded relationships - tag routes no longer set allowInForceUpgrade: force-upgraded sites must not be able to manage tags via the React screen - scoped useWatch in the accordion sections (code-injection keystrokes no longer re-render every preview) and deduplicated the X/Facebook card markup - e2e: factory tags use unique names (faker department names collided in per-file environments), the Ember run pins tagDetailsX off so a future flag GA fails loudly, added a visibility-demotion test, and NAV_ITEMS URL patterns tolerate dev-mode's missing /ghost trailing slash
no ref - groundwork for migrating the posts/pages list from Ember to React: the same suite will run against both implementations, gated by a postsListX labs flag - covers the gaps the Ember acceptance tests had over e2e: status grouping, type/visibility/tag/order filters, URL-driven filters, multi-select with context-menu bulk actions (feature, add tag, unpublish, delete, duplicate) and the pages list - added a createPageFactory (pages share the posts schema; only the API resource differs) - added data-testids to the Ember context menu, bulk-action modals and status badges (kept existing data-test-* attributes for Ember acceptance tests) - PostsPage gained row-selection/context-menu helpers and a PagesPage subclass; getPostByTitle now tolerates the star icon featured posts prepend to the heading's accessible name
no ref - gates the upcoming React implementation of the posts and pages list screens; the Ember implementation remains the default while the flag is off - lets the shared e2e list suite run against both implementations
no ref - ports the Ember posts/pages list screens to React in apps/posts following the members-list architecture: three status sections (scheduled, drafts, published+sent) backed by infinite queries, Ember-compatible URL query params (type/visibility/author/tag/order), filter dropdowns, status badges, featured stars and editor links - Ember-parity selection model (ctrl/meta toggle, shift ranges, invert via cmd+A, Escape clears) and right-click context menu with bulk actions (feature/unfeature, add tag, change access, unpublish, unschedule, duplicate, delete) as pure, unit-tested modules - bulk operations go through new framework hooks (PUT /posts/bulk, DELETE /posts/?filter=, POST /posts/:id/copy) shared by posts and pages - the admin shell routes /posts and /pages through FlagGatedRoute; the Ember routes stay active so URL query params survive, but their model hooks and templates are gated on the flag — hidden Ember markup would otherwise duplicate the React list's test ids and break shared-locator e2e - the shared e2e suite now also runs against React (list-react.test.ts); all 26 tests pass against both implementations with the same page objects - custom views UI and analytics columns land separately before flag GA
no ref - custom views (saved filters) with Ember parity: default Drafts/Scheduled/ Published views, active view name as the page title, Save as view / Edit current view buttons, a Shade dialog matching the shared e2e selectors (View name label, Ember's color swatch labels and validation messages), persisted in the shared_views setting so the admin sidebar updates live - analytics columns on published rows gated by web_analytics_enabled and members_track_sources (visitor counts, opens/sent, member conversions) batched per loaded page via the stats endpoints - fixed sidebar active-state for list views: React Router navigates with pushState which fires no hashchange, so the hidden Ember router's query params go stale and the Ember-bridge isRouteActive matching silently broke whenever React owned the navigation — custom view, Drafts/Scheduled/ Published and Pages active states are now computed React-side from the URL (same approach the rest of the sidebar already used via useMatch) - extracted the three custom-views e2e files into shared suites that run against Ember (flag pinned off) and React (postsListX on); save/edit view buttons in the Ember template gained aria-labels (their accessible name came from the inlined svg title in dev builds, breaking the locators) - restored apps/posts eslint devDeps that pnpm dep changes had un-hoisted - full posts/pages e2e set (list + custom views, both implementations): 60/60 passing
…dings no ref Fixes from the /code-review + Codex passes on the postsListX slice: - bulk mutations invalidate both posts and pages query caches at the framework layer (other cached filter combinations went stale for the whole session) - bulk actions surface success/error toasts with real API messages; modals stay open on failure (errors were silently swallowed as unhandled rejections) - change-access modal gained 'Specific tiers' with a tier picker and preselects the selection's current visibility (it silently flipped paid posts to public) - add-tags modal and the tag filter search server-side and support inline tag creation (only the first 100 tags were reachable) - context-menu parity: Unpublish hidden for sent email posts, feature/unfeature uses Ember's majority rule (selections that were mostly featured re-featured everything instead of unfeaturing) - publish success modal restored on the list routes (the scheduled/published localStorage handoff from the publish flow went unconsumed and could leak a stale modal into the analytics screen) - filter changes replace history entries (Ghost#11057 parity), Escape no longer wipes the selection while a modal or the context menu is open, 'Featured pages' restored for pages - NQL hygiene: selection ids must be ObjectId hex, author/tag URL params must be slug-shaped, bulk select-all filters are scoped with type:post|page - dedup: shared_views save/delete scaffold shared between members and posts views; view-filter equality and the color palette exported once from @tryghost/posts/api and consumed by the admin sidebar; stats count endpoints moved into the framework api; list rows memoized so selection clicks don't re-render every row - e2e: the feature bulk action waits for the bulk response before filtering (race); Ember store sync mappings for PostsResponseType/PagesResponseType set to null (lists are React-owned while the flag is on; unloading posts could disturb records held by the still-Ember editor) - documented pre-existing upstream issues found during review in DEVIATIONS.md (server-side bulk authorization gap, stats endpoint ownership, shared_views last-write-wins)
no ref - gates the upcoming React implementation of the member detail screen and member activity; the Ember implementations remain the default while off
no ref - groundwork for migrating the member detail screen and member activity from Ember to React: the same suites run against both implementations, gated by the memberDetailsX labs flag - extracted the members-legacy detail tests (CRUD/validation, impersonation, disable-commenting, activity events) into order-independent suite factories with unique factory data, removing the per-test isolation requirement - added missing coverage: unsaved-changes guard (stay/leave), newsletter subscription toggle round-trip, note round-trip, and a new members-activity suite (event listing, member filter, event-type exclusion) - normalized fragile selectors to data-testid (members-back link, labels input) with matching testids added to the Ember templates - members-activity feed only shows events created strictly before the page's load timestamp at second precision; the suite polls the events API with the same cursor filter before navigating to avoid same-second flakiness - 56 tests passing against Ember (React wrappers pass against the ungated Ember screens until the implementation lands)
…etailsX no ref - ports the Ember member detail screen to React in apps/posts: identity sidebar (avatar, geolocation, last seen, signup info, attribution, engagement stats, comments-disabled indicator), form (name/email/labels with typeahead+create/note/newsletter toggles incl. email suppression re-enable), subscriptions section (tier cards, status badges, Stripe links, add/remove complimentary with expiry, cancel/continue), actions menu (impersonate with signin URL copy, sign out of all devices, disable/enable commenting, delete with optional Stripe cancellation), embedded activity feed, and Ember-parity save semantics - ports members-activity: full event table with cursor-paginated infinite fetch, Ember-compatible member/excludedEvents query params, event-type filter, member context card; the event parsing helper is a complete pure port with unit tests - extracts the unsaved-changes blocker + dialog into a shared module (the tag detail form now uses it, as promised in DEVIATIONS) - framework: member edit/delete (with Stripe cancel param), signin URLs, session signout, suppression delete, subscription edit, and cursor-based member events hooks - admin shell routes /members/new, /members/:memberId and /members-activity through FlagGatedRoute; the Ember member route hands over to react-fallback (it registers unsaved-changes guards) while members-activity is gated at the template level to preserve its query params - the shared e2e suites pass 54/54 against both implementations
no ref Fixes from the /code-review + Codex passes on the memberDetailsX slice: - members-activity sanitizes its URL params before building NQL filters (member must be ObjectId hex, excludedEvents whitelisted against known event types — crafted params could widen the events query) - the impersonate dialog refetches the signin URL on every open (the cached URL could be five minutes stale after an email change) - member save invalidates the labels cache (labels created inline during a save were missing from typeaheads and filters for up to five minutes) - the add-complimentary dialog disables confirm while a custom expiry date is empty/invalid (it silently degraded to a forever subscription) - server validation errors map onto the offending form field inline (Ember parity) instead of only flashing a toast - members-activity gained the member search picker (Ember parity — the only member-scoped entry point was a hand-crafted URL) and automatic infinite scroll via an observer sentinel alongside the Load more fallback - documented in DEVIATIONS: force-upgrade handle removal, same-second cursor parity, email-preview-link deferral, flag-flip limitation - full member dual suites + tags React regression: 67/67 passing
no ref - groundwork for migrating the auth screens to React behind an authX flag: suite factories with Ember/React wrapper pairs, fully semantic locators plus flow-notification testids added to the Ember templates - new coverage: signin validation errors, forgot-password errors, wrong 2FA code; 2FA and reset suites pin security.staffDeviceVerification via config env (a local config.local.json disabled it, silently breaking 2FA locally) - reset wrappers use per-test isolation (completing a reset rotates the session and invalidates the per-file cached auth state); MailPit count assertions made baseline-relative - documents the BetterAuth feasibility decision in DEVIATIONS.md: the React screens will call Ghost's existing session/authentication endpoints; full BetterAuth server adoption is a separate platform project (sourced assessment included)
The pnpm dev container was exposing a broader workspace at runtime than the Docker image installed, causing pnpm 11 to repair dependencies before starting. Narrowing the source mounts keeps the runtime graph aligned with the image install and disables script-time dependency repair for this dev container.
Dev-mode E2E worker containers use the same Ghost dev image as compose.dev.yaml, so they need the same narrowed backend workspace mounts and pnpm verifier override. This keeps the worker container runtime graph aligned with the image install instead of exposing ghost/admin.
no ref - the admin-x-framework Post type now ships PostAuthor[] (added with the React posts list), which made the stats app's narrower local authors override incompatible and broke tsc on @tryghost/stats:build - the override is redundant now that the framework type includes name
no ref - ports signin, 2FA verify, password reset, signup, setup and signout to the React admin shell against Ghost's existing session/authentication endpoints (deliberate deviation from the BetterAuth instruction — see DEVIATIONS.md for the full justification) - authX is exposed on the public site payload so both shells can read the flag before a session exists - both Ember route bases park the hidden Ember app when the flag is on: AuthenticatedRoute's replaceLocation fallback wiped deep-link URLs and UnauthenticatedRoute's prohibitAuthentication rewrote the shared URL after the post-signin reload - the post-signin deep-link redirect captures its target once (query refetches re-render the gate after the key is cleared) and navigates via location.replace, since router pushState fires no hashchange and would leave the parked Ember app showing an empty content area - e2e auth suites run against both implementations with the same page objects (26 tests, flag off/on)
no ref - review findings from the multi-angle code review + Codex adversarial pass on the authX slice: - flag-off parity: the auth gates now reveal the Ember fallback as soon as the site payload says authX is off, instead of also waiting for the /users/me query to settle (pre-slice Ember never gated on it) - signin redirects to /setup on fresh installs, restoring Ember's UnauthenticatedRoute setup-status check - invalid or expired invite links explain themselves on the signup screen instead of silently bouncing to signin - the API's minimum password length lives in one shared constant instead of three inline literals
no ref
- ports the editor screen (posts and pages) to the React admin shell:
title + Koenig lexical body, autosave, publish/update/preview flows,
settings menu (slug, publish date, tags, excerpt, feature image,
delete) and unsaved-changes guards
- per the plan, state management lives in a discrete, independently
testable state machine (apps/admin/src/editor/state/) — a pure
transition(state, event) -> {state, effects} reducer covering scratch
values, dirty detection, the save queue (manual supersedes background,
latest-wins queueing, queue-drain completion), status transitions and
leave decisions, with ~100 unit tests and no React/DOM/network imports
- Koenig is imported as workspace ESM (no UMD loader); the Ember
lexical-editor route hands over via react-fallback when the flag is on
- new framework editor/slugs APIs (full-post payloads with Ember's
ALL_POST_INCLUDES, 409 UpdateCollisionError, newsletter/email_segment
publish params, slug generation)
- cross-shell navigations (publish complete, delete, editor backlink)
use real location hash changes so a parked flag-off Ember list wakes;
the shade share modal carries the publish-complete test hooks shared
with Ember's complete step
- e2e: editor, publishing, publish-flow, post-preview and post-updates
suites converted to shared dual-flag suites — 36 tests green against
both implementations, plus 19-test flag-off baseline before the port
no ref - review findings from the multi-angle code review + Codex adversarial pass on the editorX slice, all covered by new unit tests: - failed publish/schedule saves disarm the publish intent so a later plain save can't publish unexpectedly; queued saves carry their full intent (saveType/publishedAt) instead of just a kind - scratch edits made while a save is in flight survive the response resync and failure reverts; email extras are per-request - slug generation races are token-guarded and slug blur commits scratch before the async sanitize, closing a leave-guard data-loss hole - contributor/author access gates ported from Ember's edit route - DST-gap time conversions verified as fixpoints, impossible calendar dates rejected; excerpt length validated before the publish flow opens - the Ember bridge maps page model events so Ember-side page edits invalidate React page caches
no ref - ports the remaining Ember-owned screens to the React admin shell: restore-posts (plus the localStorage local-revisions store, written by the React editor with Ember's exact schema so revisions are restorable from either shell), site preview, billing (/pro), explore and migrate iframe wrappers, and the home/dashboard redirects - home/dashboard carry no labs flag: they are pure redirects with no UI; the home redirect is effect-based and guarded by the live hash so a navigation right after landing on "/" isn't clobbered (Ember's ran synchronously inside the route transition) - removed the dead /launch, /mentions and /posts/analytics/:postId/ mentions entries from EMBER_ROUTES and Ember's router; only the email debug screen and the (deliberately unported) designsandbox remain Ember-owned - e2e: redirects, restore and site suites (dual wrappers where flagged)
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
| const trimmedQuery = query.trim(); | ||
| const [debouncedQuery] = useDebounce(trimmedQuery, 300); | ||
| // Mirrors Ember's tagsManager.searchTagsTask filter, including the quote escaping | ||
| const safeTerm = debouncedQuery.replace(/'/g, '\\\''); |
| const [search, setSearch] = useState(''); | ||
| const [debouncedSearch] = useDebounce(search.trim(), 300); | ||
| // Mirrors Ember's tagsManager.searchTagsTask filter, including the quote escaping | ||
| const safeTerm = debouncedSearch.replace(/'/g, '\\\''); |
no ref - per review feedback on the migration so far: - posts/pages list filters sit inline with the New button (right-aligned toolbar, wrapping below on narrow viewports like Ember) - tag/member detail and members-activity share a PageCanvas matching Ember's gh-canvas metrics (max-width 1200, measured paddings), so content no longer jumps when switching implementations - the editor's populated states restyled to Ember parity: header bar (green Publish, ghost Preview, breadcrumb, panel icon), 419px settings column, the publish/update flows' full designs (green headline, pill radios, gradient confirm), preview chrome, and exact gh-editor-title typography in a 740px column - tag code injection now uses the admin-x-design-system CodeEditor (CodeMirror with HTML highlighting), reversing the slice-1 deviation per direction; the editor preview gained Ember's Email tab (rendered via a new email-previews framework hook) with newsletter and free/paid segment selection - the React shell normalizes a slashless /ghost admin path at boot (Ember's location API used to), keeping hash-history URLs canonical
no ref - per review feedback on the migration: - the editor settings sidebar gained Ember's remaining PSM sections: post access/tiers, authors, template select, featured and show-title-and-feature-image toggles, and collapsible code injection (CodeMirror), meta data, X card and Facebook card sections — all flowing through the editor state machine (new PostSettings record, SETTINGS_CHANGED event, per-field resync that preserves in-flight edits) with Ember's role gates and serializer rules - the settings menu module is lazy-loaded: its design-system/CodeMirror graph was loading eagerly and pushed the editor's first render from ~150ms to ~7s, which also broke the reload e2e budget - posts/pages list rows match Ember: feature-image thumbnails (with placeholder), colored status lines and Ember's exact wording, gh-format-post-time dates, the pencil/stats hover button, and no status group headers (Ember has none) - framework: Theme.templates typed as ThemeTemplate[] (was string[])
no ref - ports the last functional Ember screen: the post email debug panel (batches, temporary/permanent recipient failures, analytics status with custom scheduling) behind the postDebugX flag, with new framework email hooks; only the Ember-only designsandbox remains, deliberately - removed the boot-time /ghost path normalization: rewriting history state before the routers captured the location broke the post-signin redirect chain in slashless-serving environments; the sidebar e2e assertions are slash-tolerant instead (the slice-2 precedent) - the deep-link signin e2e waits for the redirect back to the signin screen before interacting: the deep link is a same-document hash navigation and the form remounts when the shell bounces back — under load the fills landed in the old mount and the form submitted empty - the current-user query now retries transient failures (network/5xx) while keeping 4xx definitive: this query decides authenticated vs signed-out for the whole shell, and one dropped request must not strand a logged-in user on the signin screen - build-mode type fixes (editor email-preview null coercion, machine settings-resync cast, test resource literals) - full admin e2e suite: 352 passed
no ref - final-slice review pass (multi-angle + Codex adversarial), all fixes unit-tested: - the migrate iframe's apiUrl reply is deferred until the integration key has loaded (it could send apiKey undefined); the explore screen validates postMessage origins by exact origin, not substring; the billing iframe receives owner details for non-owner force-upgrade sessions (Ember billing-service parity) - local revisions now carry the full serialized post in Ember's field names (authors, feature image, visibility/tiers, template, meta/ social/code-injection) and restore end-to-end; the pending throttled revision is flushed on editor unmount; corrupt stored entries are skipped instead of throwing - the debug screen's permission redirect uses a real hash navigation (cross-shell safe) and the template default normalizes to null - the post-updates e2e date assertion tolerates the midnight boundary between the host and container clocks - DEVIATIONS.md final summary: migration end-state, flag-GA cleanup list, superseded notes
E2E Tests FailedTo view the Playwright test report locally, run: REPORT_DIR=$(mktemp -d) && gh run download 27325881035 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR" |
no ref - the per-screen parity audit (AUDIT-REPORT.local.md) surfaced correctness bugs the dual e2e missed; all fixed with unit tests and verified live: - new posts no longer persist a slug derived from the first few title keystrokes: draft saves regenerate the slug server-side whenever the title changed (Ember beforeSaveTask parity, uniqueness-incrementor aware), replacing the racy pending-slug mechanism - the settings panel's date field no longer stamps published_at onto never-published drafts or rewrites seconds on published posts - the title input no longer shows the literal "(Untitled)" the API payload uses for empty titles - setup starts the owner onboarding checklist before booting the admin (fresh installs were skipping onboarding entirely) - contributors get Ember's Preview + Save controls instead of the publish/unpublish buttons their role can't use - breakout Koenig cards respect the open settings panel via Ember's --editor-sidebar-width mechanism, and the title shares the body's 740px column (a leaked Ember textarea max-width capped it at 500px) - the shared unsaved-changes blocker holds navigation until in-flight saves settle and surfaces failures (tag + member detail could lose edits silently mid-save) - full admin e2e after the fixes: 352 passed
no ref - ports the two remaining high-severity parity gaps from the audit: - the publish flow gained Ember's email recipients controls (free/paid/ specific-people with label and tier segment pills, Stripe-gated paid option, forceSpecific behavior), persisting through the publish save's email_segment param - the editor canvas gained Ember's feature image above the title: add/ replace/remove with hover overlay, alt text toggle and a Koenig-backed caption editor; alt and caption flow through the editor machine and local revisions; published posts defer the save to Update like Ember - ports Ember's caption HTML normalization with a fix for Chrome's white-space shorthand expansion that silently broke the original check
no ref - the publish flow's member-count query (which gates email availability) could read a cached zero from before members were added, wrongly disabling the email publish types until a manual refresh - refetch the count whenever the flow opens, matching Ember's publishOptions.setup, which re-fetched it every time - surfaced as an intermittent e2e failure on the email/scheduled publishing tests once the recipients selector added load to the flow
E2E Tests FailedTo view the Playwright test report locally, run: REPORT_DIR=$(mktemp -d) && gh run download 27343208778 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR" |
Fable