Automattic / Automattic/jetpack

Forms: clean up dashboard component duplication and decide the fate of the legacy dashboard (converted to project)

Open
#50,712 1 comment 0 reactions 1 assignee Claimed by @CGastrell View on GitHub
[Package] Forms
Dominant language
PHP
Stars
1.8k
Forks
898
Avg merge
1d 18h
Merged PRs (30d)
774

Description

This issue has been converted to a project [Forms: clean up dashboard component duplication and decide the fate of the lega…](https://linear.app/a8c/project/a1f47c5e-beb0-4847-b31c-0d88cf24e335/overview)

---

Forms ships **two dashboards**. Most "duplicated components" are legacy-vs-wp-build pairs, not copies of unreleased packages. This tracks cleaning up the duplication and retiring the legacy tree.

| Tree | Routing | Active when |
| -- | -- | -- |
| `src/dashboard/` (legacy SPA) | `react-router` hash | `jetpack_forms_alpha` → false |
| `routes/` + `src/dashboard/wp-build/` | `@wordpress/route` modules | `jetpack_forms_alpha` → true (default since 7.18.0) |

Legacy is reachable **only** via the filter — set false on WPCOM by the `disable-central-forms-management` / `a8c-e2e-test-blog` stickers (`jetpack-mu-wpcom/.../wpcom-contact-form-flags.php`), or by self-hosted plugins. No back door: `add_admin_submenu()` registers one slug, `redirect_dashboard_url_cross_variant()` bounces the other.

## Plan

Ordered by "survives retiring legacy?". Track B is deleted by step 4 — don't start it before step 3.

| Step | What | Track | Status |
| -- | -- | -- | -- |
| 1 | Delete 3 dead components (`layout/header`, `forms-logo`, `actions-dropdown-menu`) | A | ✅ [#50713]() |
| 0 | Blank-dashboard safeguard (see below) | A | ✅ Automattic/jetpack#51151 |
| 6a | Dedupe **shared** helpers (`copy-clipboard-button`, `use-empty-spam`/`-trash`) | A | ✅ [#50788]() |
| 9 | Remove redundant `fix_boot_import_map_ordering()` workaround (see below) | A | ✅ Automattic/jetpack#50780 — 3 pkgs |
| 3a | Adapt Calypso Forms e2e to CFM (see below) | gate | ✅ wp-calypso#113202, merged 2026-08-11 |
| 3b-i | Remove the `a8c-e2e-test-blog` gate (Jetpack) | gate | ✅ Automattic/jetpack#51149 — live on WPCOM 2026-08-17 |
| 3b-ii | Strip the spec's legacy branches (wp-calypso) | cleanup | not started — no longer a gate, see below |
| **3** | **Retire legacy?** sticker rollout ✅ ([FORMS-668](https://linear.app/a8c/issue/FORMS-668/clean-up-related-stickers-after-rollout) closed) | gate | blocked on 3b |
| 4a-pre | Retire the `disable-central-forms-management` sticker gating (mu-wpcom) | B | ✅ Automattic/jetpack#51397 — merged 2026-08-25 |
| 4a | Retire the `jetpack_forms_alpha` filter (see below) | B | in review — Automattic/jetpack#51395; 4a-pre ✅, waiting on the 16.2 cut |
| 4b | ~~Relocate the shared modules~~ — mostly dissolved, see below | B | probably unnecessary |
| 4c | Delete the 30 legacy-only files | B | next after 4a — an earlier attempt's file list is preserved under *Notes* |
| 2 | `components/page/` → `@wordpress/admin-ui` (legacy-only) | B | blocked on 3 |
| 5 | Collapse legacy↔wp-build pairs | B | blocked on 3 |
| 6b | `__experimental*` → `@wordpress/ui` sweep in legacy files | B | blocked on 3 |

## Duplication inventory

* **A — redundant unreleased-package copy:** `components/page/*` = hand-rolled `@wordpress/admin-ui` `Page` + `@wordpress/ui` `Stack`. Already a dep, already used by `wp-build/components/page`. → step 2.
* **B — dead code:** `layout/header.tsx`, `forms-logo/`, `actions-dropdown-menu/`. → step 1 ([#50713]()).
* **C — legacy↔wp-build pairs:** `dataviews-header-row`, `inbox-status-toggle`, `integrations-button`/`manage-integrations-button`. → step 5.
* **D — intra-package:** `copy-clipboard-button` (also in `form-editor/plugins/copy-code-row`), `empty-spam`/`empty-trash` (127-line near-dup hooks), `__experimental*` vs `@wordpress/ui`. → steps 6a/6b.
* **E — SCSS/token dup:** `page/style.scss`, `_mixins.scss`, `--jp-forms-*` shadowing `--wpds-*`. → folded into 2/5.

## Step 3 — retire legacy

Sticker was a rollback valve for [FORMS-667](https://linear.app/a8c/issue/FORMS-667/jetpack-forms-central-management-shows-black-screen-with-the-events) (import-map race → blank dashboard with TEC active); root cause fixed Apr 2026 (Automattic/jetpack#47823/Automattic/jetpack#47824). **Verified 2026-07-22: no longer reproduces** on WP 6.9 + 7.0, workaround on/off, across Forms/Newsletter/Social/VideoPress — the `@wordpress/build` loader defers boot to `DOMContentLoaded`.

**Sticker rollout complete 2026-07-31** — all 24 sites de-stickered in four monitored batches, no re-adds, no support reports. [FORMS-668](https://linear.app/a8c/issue/FORMS-668/clean-up-related-stickers-after-rollout) **closed.** Not customer breakage.

## Step 3a — Calypso Forms e2e on CFM

**The** `a8c-e2e-test-blog` **gate is not reliably in force on Atomic.** `wpcomsh_is_site_sticker_active()` reads `Atomic_Persistent_Data->site_sticker_{$name}`, and **that copy is per-site** — verified with `wp wpcomsh persistent-data site_sticker_a8c-e2e-test-blog`, which returns `1` for `jetpackAtomicDefaultUser` and `null` for `gutenbergAtomicSiteUser`. Both carry the sticker on their report card, so **the report card does not tell you whether the gate bites on Atomic** — check persistent data, or the served bundle:

| Account | Bundle | Dashboard |
| -- | -- | -- |
| `gutenbergAtomicSiteUser` | `build/pages/*` + `build/routes/*` | **CFM** |
| `jetpackAtomicDefaultUser`, Simple accounts | `dist/dashboard/*` | legacy |

Consequences: which dashboard a run gets is decided by the **test account**, not a flag (`TEST_ON_ATOMIC=true` → CFM; adding `JETPACK_TARGET=wpcom-deployment` → legacy). At least one account has been on CFM while the spec looked for the old dashboard's folder tabs, which is why the Atomic deployment build has been failing `Feedback: Form Submission` unmuted (e.g. build 18670818, 2026-07-27). That build only runs as a release-candidate gate, so it failed quietly between runs.

**Why the two accounts disagree.** The persisted copy is written when a sticker is added while it is listed in `atomic_site_stickers()`, and re-pushed on transfer, clone **and reset**. `a8c-e2e-test-blog` only entered that list in March 2026, long after these sites were stickered, so a site has the persisted value only if it has been reset since. There is no reconciliation job.

`disable-central-forms-management` **is not involved.** No e2e site carries it, and nothing in wpcom code adds it — it appears once in the whole repo, in `atomic_site_stickers()`. It has only ever been applied by hand, which is why de-stickering the 24 rollout sites was a manual job.

### Rule for step 4

Do not delete the legacy dashboard while the e2e suite still depends on it. Order:

1. **3a** — the spec passes on **both** dashboards. ✅
2. **3b-i** — remove the `a8c-e2e-test-blog` check from `wpcom_is_central_forms_management_enabled()` (`wpcom-contact-form-flags.php`). **Required, not optional** — e2e sites keep this sticker for unrelated reasons, so without this they stay on legacy and the Forms spec breaks permanently once step 4 lands. Then confirm both e2e builds stay green: this is the first point at which the sites actually change dashboards.
3. **3b-ii** — strip the spec's legacy branches, once nothing serves legacy any more.
4. **4** — delete the legacy tree.

**Neither the sticker nor the Atomic persistent data is touched.** An earlier draft of this plan called for dropping `a8c-e2e-test-blog` from `atomic_site_stickers()` — the list of stickers mirrored into Atomic persistent data — and clearing the persisted values. Neither is needed:

* After 3b-i nothing reads the persisted key. It has no readers in wpcom, and its one Jetpack reader is the check being deleted.
* Clearing it by hand would not stick. `woa_transfer_persist_data_blog_stickers()` re-pushes every listed sticker on transfer, clone and reset, and e2e sites get reset.
* The sticker itself predates Forms by years, is auto-applied to every e2e-created site, and has many unrelated consumers (CircleCI e2e site lists, empty-blog protection, Reader exclusion, JITM, memberships, and more). Removing it is out of the question.

Dropping the entry from `atomic_site_stickers()` stays available as optional tidy-up — it stops future writes of a key nobody reads — but it changes no behaviour and gates nothing.

### Status — 2026-08-17 (live)

The gate removal is **live on [WordPress.com]()**, and Simple is confirmed on CFM.

* **wp-calypso#113202 merged 2026-08-11 12:26 UTC** — the spec finds response folders on both dashboards.
* Automattic/jetpack#51149 **merged 2026-08-17 12:28 UTC**, six days later, so the ordering rule held.
* **Reached WPCOM the same day** through the vendored mu-wpcom sync: moon at 13:20 UTC, sun at 18:30 UTC. Confirmed by locating the commit that removed the check from each ring's vendored copy, not by assuming the sync ran.
* **Simple is serving CFM in production** — verified directly in a browser on a free Simple site.

**Simple deployment builds are green.** Three ran after both rings had the change; the Forms spec passed in each sampled run, first attempt. The run before the change took ~57s and needed a retry; the runs after take ~68s with no retry — consistent with CFM being the slower dashboard and with the legacy flakiness noted below going away.

**Atomic does not receive this change through the sun/moon rings.** Those rings are the Simple deployment path. On Atomic the same code ships inside the `wpcomsh` plugin, which bundles `automattic/jetpack-mu-wpcom` (the "[WordPress.com]() Features" / Site Helper plugin does too). That is a separate release cadence, so **Atomic sites may still be running the old gate** — treat "live on WPCOM" above as Simple-only until someone checks an Atomic site directly.

The check is the same one used for Simple: open the Forms dashboard on an Atomic e2e site and see which bundle loads — `build/pages/*` + `build/routes/*` is CFM, `dist/dashboard/*` is legacy.

**Atomic also has no usable post-flip build signal.** Two smoke runs have happened since the sync, and both landed on the `Atomic: private` variation, which is broken for reasons unrelated to Forms:

* `Feedback: Form Submission` fails at **setup**, in `createPost()`, with `unauthorized: API calls to this blog have been disabled`. It never loads a dashboard, so it cannot be reporting anything about CFM.
* The rest of the suite fails on that variation too — Jetpack dashboard smoke, the block smoke tests, page and post editors, FSE, phpMyAdmin — with wp-admin timeouts and "Timed out waiting for an app shell".
* The same `API calls to this blog have been disabled` failure appears in a run from **2026-08-10**, a week before this change. Pre-existing, and worth its own ticket.

So Atomic still needs one healthy-variation run to close the loop.

**The e2e safety net is currently off.** The whole `Feedback: Form Submission` test is `test.fixme()` on wp-calypso trunk, with a comment naming the cause: CFM's row "View" action now opens a standalone response page (Automattic/jetpack#51127), while `FeedbackInboxPage` still drives the DataViews inspector — it waits on `.jp-forms-response-header` and clicks controls that page does not render. *"Porting the page object to the new route is the fix."*

**That port is not a blocker for this issue and is not in its scope.** It is fallout from Automattic/jetpack#51127 and belongs to whoever owns that change. Accepted consequence: 4c will land without e2e coverage of the responses dashboard unless the port happens first.

**Atomic is confirmed on CFM** (verified 2026-08-19 with a headed run against `jetpackAtomicDefaultUser`, the account that was previously on legacy). Both infrastructures now serve the wp-build dashboard.

**The Atomic risk is low either way.** The spec auto-detects its dashboard (see below), so it passes on legacy *and* on CFM. A green Atomic run therefore does not prove the flip happened, and the only genuine failure mode is Atomic flipping while something about CFM there is broken. Worth noting that CFM on Atomic is not untested: one Atomic account has served CFM since before this work, and the merged spec was verified against it repeatedly.

**Note on which build to watch.** `jetpack_atomic_deployment_e2e_desktop` is a release-candidate gate and runs rarely — it had not run for a week at the time of the flip. The build that runs many times a day is `jetpack_atomic_build_smoke_e2e_desktop`, and each of its runs exercises a single Atomic variation. Check that one for day-to-day signal.

Both dashboards pass: legacy ~46s on Simple and ~66s on Atomic-legacy; CFM verified over repeated runs including the `pixel`, `galaxy` and `iphone` projects.

### Step 4 is not blocked on 3b-ii

Recorded because the plan above implies otherwise. The spec chooses its dashboard by **runtime auto-detection** — `FeedbackInboxPage.isCentralFormManagement()` checks the page URL or the CFM-only "Forms" tab and caches the result. There is no flag or pinned configuration.

So deleting the legacy tree does not break the spec: detection returns CFM and the legacy branches simply stop executing. 3b-ii removes dead code and one flake mode — a false-negative detection would send the spec down the legacy path against a CFM-only site — but it gates nothing. **Step 4's real precondition is a healthy Atomic run confirming CFM, not 3b-ii.**

### Retiring the dashboard gates

Forms carries two dashboard filters. Only one is obsolete — see the note below the table, which corrects an earlier reading of this issue that treated both as the same thing.

| Filter | Since | Reads | What it gates |
| -- | -- | -- | -- |
| `jetpack_forms_dashboard_enable` | 0.3.0 | 1 | Whether **any** Forms dashboard initialises |
| `jetpack_forms_alpha` | 2025-12 | 6 | **Which** dashboard renders |

`jetpack_forms_alpha` was a development flag — Automattic/jetpack#46206, *"Introduce wp-build on dashboard under feature flag"*. "alpha" meant alpha-stage. Its default flipped to `true` in [#47826](), *"Enable Central Forms Management by default for all sites"*, and since then its only job has been the rollback valve. That valve is now empty: the sole remaining writer is the `disable-central-forms-management` branch in `wpcom-contact-form-flags.php`. Enumerated **2026-08-24** — `wp blog-stickers get-blogs-with-sticker disable-central-forms-management` returns zero blogs. That closes the gap the 2026-07-31 rollout left: the rollout verified 24 known sites, and `get-blog-report-card` is per-blog, so it could never rule out a later re-add or a new site.

It is read in six places — `Dashboard::init()`, `redirect_dashboard_url_cross_variant()`, `is_wp_build_dashboard_page()`, `add_admin_submenu()`, `get_forms_admin_url()`, and `Contact_Form_Plugin::unread_count()` for the menu-badge slug. It is not documented anywhere outside the code.

**Why this has to land before the delete.** Today a `false` return registers the *legacy* slug and renders an empty `

` that the legacy bundle fills. Delete the legacy tree with the gate still in place and any site still getting `false` lands on a blank Forms dashboard — the same failure class as step 0, on the other branch.

**Plan for 4a:**

* Remove all six `jetpack_forms_alpha` branches. Always wp-build.
* Announce the retirement with `_deprecated_hook()`, guarded by `has_filter()` so it stays silent for everyone who never used the filter. **Not** `apply_filters_deprecated()` — that still applies the filter and returns its value, and once legacy is gone a `false` cannot be honoured, so returning it would lie. Both helpers have monorepo precedent.
* `jetpack_forms_dashboard_enable` **is kept.** It looks like a second obsolete gate but it is not the same kind of thing: `jetpack_forms_alpha` chose *which* dashboard rendered, while this one gates *whether* the dashboard is registered at all — no menu entry, no dashboard. That capability survives retiring the legacy tree and is worth keeping. Its docblock is corrected in Automattic/jetpack#51395: it claimed the filter defaults to `false` while the code has always passed `true`.

**Correction, 2026-08-24 — the mu-wpcom cleanup is a prerequisite, not optional tidy-up.**

An earlier revision of this section said 4a made it optional, because `__return_false` goes inert once nothing reads the filter. That treated the sticker as one thing. It has two legs, and only one of them writes `jetpack_forms_alpha`:

| Leg | Hook | Inert after 4a? |
| -- | -- | -- |
| `wpcom_maybe_disable_central_forms_management()` | `jetpack_forms_alpha` | yes |
| `wpcom_contact_form_set_editor_feature_flags()` | `jetpack_block_editor_feature_flags` | **no** |

The second leg sets `central-form-management`, which gates `Contact_Form::register_post_type()`, `Form_Editor::init()` and `Form_Preview::init()` in `class-contact-form-plugin.php`. So a stickered site running 4a without 4a-pre gets the wp-build dashboard — whose Forms tab lists `jetpack_form` posts — with no `jetpack_form` post type registered. That combination was unreachable before, because the sticker also forced the legacy dashboard, which has no Forms tab.

**Order:** Automattic/jetpack#51397 **lands before** Automattic/jetpack#51395**.** Merging them together does not help. `packages/forms` reaches sites through `plugins/jetpack`; `packages/jetpack-mu-wpcom` reaches them through `wpcomsh` and `mu-wpcom-plugin`. Those trains do not arrive together, so only sequencing closes the window.

No site carries the sticker today, so nothing is broken in practice either way. The order removes the hazard from the code rather than from the site list.

**Status, 2026-08-25 —** Automattic/jetpack#51397 **is merged** (`d0c3311`), so the ordering requirement above is satisfied and 4a is clear on this dependency. Merged to trunk, not deployed: `packages/jetpack-mu-wpcom` reaches Simple through the vendored sync and Atomic through `wpcomsh`, and neither has been checked. Nothing waits on that — no site carries the sticker, and 4a's remaining gate is the 16.2 cut.

**What still holds from the earlier reading:** once 4a-pre has landed everywhere, a stale Atomic `wpcomsh` is harmless — both legs are gone from it — so 4c no longer waits on an Atomic deploy that cannot be observed from this repo.

### Open

* **CI does not exercise CFM.** The PR matrix and the Simple deployment build run legacy; only the Atomic deployment build reaches CFM, as a release-candidate gate. After step 4 that is the sole automated coverage.
* **The spec is flaky on legacy in CI** — the Simple build passes it on retry. Pre-existing, not addressed by wp-calypso#113202.
* **Low-priority Forms nit:** the CFM page logs `Store "FORM_RESPONSES" is already registered.` once at load — `src/dashboard/store/index.js` calls `register( store )` at module scope and is imported from several route chunks. Harmless (it appears in passing runs), but real.

## Notes

* **Step 0 (blank-dash safeguard):** `is_wp_build_dashboard_page()` checks flag+slug only; if `build/` is missing or `jetpack_forms_load_wp_build` is false, `add_admin_submenu()` renders an empty `

` while `load_admin_scripts()` skips the legacy bundle — a blank page. Automattic/jetpack#51151 renders an error notice instead. Found by code reading, not reproduced live.
* **Step 9:** the boot-ordering workaround (`class-dashboard.php`, `@todo`-flagged) is dead weight now the bundled loader self-defers. wp-build code, survives step 4. Two identical copies live in `packages/scan` and `jetpack-mu-wpcom/ai-launchpad`; Automattic/jetpack#50780 removes all three (dhasilva review).
* **A pre-4a attempt at 4c exists, and its value is the inventory, not the diff.** Recovered 2026-08-31 from a detached-HEAD worktree during a cleanup sweep, onto a local-only branch (`rescue/retire-legacy-dashboard`, commit `467ebb54c61`). **Never pushed**, so the list below is the durable copy — the branch may not survive.

It is **not rebasable**. Its base is trunk at 2026-07-06, ~1244 commits back, and it predates Automattic/jetpack#51395 entirely, so it takes its own run at `class-dashboard.php` (−203 lines) while Automattic/jetpack#51395 rewrites the same file. Four files collide head-on: `class-dashboard.php`, `class-contact-form-plugin.php`, `Dashboard_Test.php`, `Contact_Form_Plugin_Test.php`. `git merge-tree` against trunk conflicts. Two further parts are already dead: its `jetpack-mu-wpcom` hunk deletes `wpcom_maybe_disable_central_forms_management()` outright, which the merged Automattic/jetpack#51397 supersedes by keeping it as a deprecated no-op, and its docblock there still names the e2e-site gate that Automattic/jetpack#51149 removed. Both its changelog files also put `Comment:` *after* the blank line, which would publish that word as entry text, and there is no `projects/plugins/jetpack/` entry at all.

**Redo 4c on top of** Automattic/jetpack#51395**.** Use the list only to seed the file set, and re-verify it — it deleted 32 files against a July tree, while the 2026-08-19 measurement below counts 30 legacy-only files. The difference is six weeks of drift, not disagreement.

32 files the earlier attempt deleted
* `projects/packages/forms/src/dashboard/class-dashboard-view-switch.php`
* `projects/packages/forms/src/dashboard/components/actions-dropdown-menu/index.tsx`
* `projects/packages/forms/src/dashboard/components/back-to-forms-button/index.tsx`
* `projects/packages/forms/src/dashboard/components/dataviews-header-row/index.tsx`
* `projects/packages/forms/src/dashboard/components/dataviews-header-row/style.scss`
* `projects/packages/forms/src/dashboard/components/forms-responses-tabs/index.tsx`
* `projects/packages/forms/src/dashboard/components/inspector/index.tsx`
* `projects/packages/forms/src/dashboard/components/inspector/mobile.tsx`
* `projects/packages/forms/src/dashboard/components/inspector/response-actions/index.tsx`
* `projects/packages/forms/src/dashboard/components/inspector/response-navigation/index.tsx`
* `projects/packages/forms/src/dashboard/components/inspector/single.tsx`
* `projects/packages/forms/src/dashboard/components/inspector/style.scss`
* `projects/packages/forms/src/dashboard/components/integrations-button/index.tsx`
* `projects/packages/forms/src/dashboard/components/layout/header.tsx`
* `projects/packages/forms/src/dashboard/components/layout/style.scss`
* `projects/packages/forms/src/dashboard/forms/single/index.tsx`
* `projects/packages/forms/src/dashboard/forms/views.ts`
* `projects/packages/forms/src/dashboard/hooks/use-response-navigation.ts`
* `projects/packages/forms/src/dashboard/inbox/index.js`
* `projects/packages/forms/src/dashboard/inbox/stage/actions.tsx`
* `projects/packages/forms/src/dashboard/inbox/stage/index.js`
* `projects/packages/forms/src/dashboard/inbox/stage/process-status-change.ts`
* `projects/packages/forms/src/dashboard/inbox/style.scss`
* `projects/packages/forms/src/dashboard/index.tsx`
* `projects/packages/forms/src/dashboard/integrations/index.tsx`
* `projects/packages/forms/src/dashboard/notices-list.tsx`
* `projects/packages/forms/src/dashboard/router/react-router-dashboard-search-params-provider.tsx`
* `projects/packages/forms/tests/js/dashboard/hooks/use-response-navigation.test.jsx`
* `projects/packages/forms/tests/js/dashboard/inbox/actions-menu-counter.test.js`
* `projects/packages/forms/tests/js/dashboard/inbox/optimistically-update-unread-count.test.js`
* `projects/packages/forms/tests/php/dashboard/Dashboard_View_Switch_Test.php`
* `projects/packages/forms/tools/webpack.config.dashboard.js`


* **Step 4 trap (measured 2026-08-19):** the shared surface is far larger than the earlier "~20 modules" estimate. Walking the import graph transitively from `routes/` into `src/dashboard/`: **69 of 99 code files are shared**, 10 are the `wp-build/` subtree, and only **30 are legacy-only**. Direct imports alone were 32; the rest come in through those.

That inverts the plan. Relocating 69 files and rewriting every `routes/` import is a large, risky change bought for a naming benefit. Deleting the 30 legacy-only files and leaving everything else where it is needs no import rewrites at all — `src/dashboard/` simply stops being "the legacy dashboard" and becomes what it already mostly is: shared Forms dashboard code plus `wp-build/`. Renaming the directory afterwards is cosmetic and can be its own PR, or never.

**Before deleting, verify beyond the import graph.** The measurement follows relative JS/TS imports only, so it does not see SCSS imports, dynamic or string-built specifiers, or PHP references such as the `dist/dashboard/…` bundle path. Those need their own pass.
* **Step 2 note:** admin-ui `Page` renders no content wrapper when `hasPadding={false}`; the local `.admin-ui-page__content` is the scroll container, so the layout contract must be rebuilt (`wp-build/style.scss` is the precedent). `hasBorder`/`contentWidth` are dead props.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.