#68897·woocommerce

Email editor: review drawer says structural changes are "applied automatically" when they are never applied

Author: vladolaruCreated Sep 18, 2026Updated Sep 18, 2026
LabelsBugEmail

Prerequisites

  • I have carried out troubleshooting steps and I believe I have found a bug.
  • I have searched for similar bugs in both open and closed issues and cannot find a duplicate.

Describe the bug

In the email template review drawer, every entry under "Auto-resolved" that comes from structural_changes is badged "Apply core" and captioned "Structural change applied automatically." Neither is true. The applier skips structural changes at merge time, by design and by explicit comment, so nothing is applied and the merchant is told the opposite.

The summary code says so directly at WCEmailTemplateChangeSummary.php:806-809, explaining why structural wrappers are routed to structural_changes rather than added_blocks:

structural_changes instead — the selective applier skips them at merge time, and surfacing them as added_blocks would advertise an "Added Group block" the apply will never apply.

The drawer then advertises exactly that, one field over.

One kind is self-contradictory on its face. For kind: 'merchant_removed' the entry's own title reads "You removed Paragraph; core still has it." — a statement that the merchant's removal is being respected — while its badge says "Apply core" and its caption says the change was applied automatically. The code's intent is the opposite of the badge (WCEmailTemplateChangeSummary.php:755-756: "Yours removed it; core kept it. Don't re-add — respect merchant intent.").

Reproduced on trunk at 0f7de959fb (11.3.0-dev) with only WooCommerce active.

Expected behavior

An entry the apply will not act on is not badged "Apply core" and does not claim to have been applied. Wording matches what actually happens: for merchant_removed, that the merchant's removal is preserved; for the nest kinds, that core changed the structure and this update will not apply that change.

If the API reports structural_skipped: true, the merchant is told something after the apply as well.

Actual behavior

Two scenarios, both showing the caption is wrong.

Core adds a Group wrapper. The summary returns {"kind":"nest","description":"Added Group wrapper","path":[0]}; the drawer shows it as "Apply core" / "Structural change applied automatically". Apply returns "structural_skipped": true and the post content is byte-identical to before. No wrapper is added, and nothing tells the merchant.

Merchant removed a block core still has. The drawer shows:

Title Badge Caption
You removed Paragraph; core still has it. Apply core Structural change applied automatically.

Nothing is applied for that entry, correctly — but the merchant was told it was.

structural_skipped is returned by the apply endpoint and never used. It appears in the client only as a field on the response interface (hooks/use-apply-update.ts:29); no code reads it and nothing is surfaced.

Steps to reproduce

Setup is identical to the companion issue on auto-resolved content updates — a clean pnpm env:e2e:start environment with the wc-email-template-sync-test-helper plugin from tests/e2e/test-plugins/, the block email editor feature enabled, and the same helper REST routes used to pin an old canonical template, seed a customised post, then move core forward.

For the merchant_removed case, seed:

Old canonical <p>OLD BLOCK A</p><p>OLD BLOCK B</p><p>OLD BLOCK C</p>
Merchant post <p>OLD BLOCK A</p><p>MERCHANT EDITED C</p> (removed B, edited C)
New canonical <p>NEW CORE A</p><p>OLD BLOCK B</p><p>OLD BLOCK C</p>

Then:

  1. Run the detection sweep; the post classifies as core_updated_customized.
  2. Open the New order email in the editor. Because there are no conflicts, the banner offers a direct Apply button alongside Review — worth noting, since a merchant can apply without ever seeing this list.
  3. Click Review.
  4. Observe the "Auto-resolved · 2 blocks" group. The second entry is titled "You removed Paragraph; core still has it.", badged Apply core, captioned "Structural change applied automatically."
  5. Click Apply (2). The request body is {"choices":[]}.
  6. Reload. OLD BLOCK B was not re-added — correct behaviour, opposite of what step 4 promised.

For the wrapper case, use the same old canonical, a merchant post that edits only C, and a new canonical that wraps A in a core/group. Apply returns structural_skipped: true and changes nothing.

WordPress Environment

Reproduced on a clean wp-env environment rather than a merchant site.

WooCommerce 11.3.0-dev, trunk at 0f7de959fb
WordPress latest, clean install via wp-env
PHP 8.1
Theme Twenty Twenty-Three
Other plugins none active beyond the E2E helper

Isolating the problem

  • I have deactivated other plugins and confirmed this bug occurs when only WooCommerce plugin is active.
  • This bug happens with a default WordPress theme active.
  • I can reproduce this bug consistently using the steps above.

Technical findings

AutoResolvedGroup renders every structural_changes entry with a hardcoded tag="apply_core" and a single caption, regardless of kind (review-drawer.tsx:316-326):

typescript
{ summary.structural_changes.map( ( change, idx ) => (
    <AutoResolvedItem
        title={ change.description }
        sub={ __( 'Structural change applied automatically.', 'woocommerce' ) }
        tag="apply_core"
    />
) ) }

The three-way summary emits three kinds, none of which the applier acts on:

kind Description text What apply does
merchant_removed "You removed %s; core still has it." Nothing, deliberately — respects the merchant's removal
nest "Added %s wrapper" Nothing — is_structural_block() short-circuits Pass 2 and sets structural_skipped
nest "Removed %s wrapper" Nothing

On the applier side, structural blocks are skipped in Pass 1 and Pass 2 and only set $structural_skipped = true (WCEmailTemplateSelectiveApplier.php, is_structural_block() at line 848).

Proposal

Two changes, both on the client.

Give each kind honest wording and the right badge. structural_changes entries already carry kind, so the drawer can switch on it instead of using one caption for all three. merchant_removed is a keep-yours outcome and should carry the keep_yours badge with wording like "Core still has this block. Your removal is kept." The nest kinds should say core changed the structure and that this update does not apply structural changes, rather than claiming it did.

Use structural_skipped. The apply endpoint already returns it and the client already types it. Surfacing it after an apply — a notice that some structural changes were not applied — closes the gap for merchants who need to know the template's shape has diverged from core.

Neither change touches the merge. This is a classification and copy bug, not a behaviour bug, which is why it is filed separately from the auto-resolved content issue even though both surface in the same group of the same dialog.

Test coverage

structural_skipped appears in the client only as fixture values set to false (__tests__/use-update-banner.test.ts, __tests__/use-apply-update-suppress-snackbar.test.ts), so no test exercises the true branch. The drawer's own Jest tests do not render a structural_changes entry at all. A fix should add drawer tests asserting badge and caption per kind.

Related issues

Found while verifying #68896. All three surface in the same dialog; the root causes are different.

  • #68896 — auto-resolved content updates are dropped on Apply.
  • #68898 — Apply duplicates a block when the merchant has deleted one.