VideoPress: fix legacy admin crash on video upload/delete (base-ui ref-merge)#49340
Conversation
…merge The legacy VideoPress admin dashboard crashed with an uncaught TypeError from base-ui@1.4.1's useMergedRefs/useRenderElement path whenever a video was uploaded or deleted (regression from #48909, which migrated UpgradeTrigger to a @wordpress/ui Notice compound tree). UpgradeTrigger conditionally mounted <Notice.Description> based on hasUsedVideo (which flips on upload/delete). That changed the Notice.Root subtree shape across renders; base-ui@1.4.1's useRenderElement conditionally swaps between two different ref-merge hooks (separate useRef slots), so the shifting shape misaligned the stored fork-ref and didChange read .refs off null. Fix: always render <Notice.Description>, varying only its text — mirroring the modern dashboard's free-tier-notice.tsx, which uses the same Notice and never crashes because its Description is static. A non-empty fallback nudge covers the pre-first-upload case so we never render an empty styled row. Keeps the JETPACK-1543 @wordpress/ui Notice migration; does not revert #48909. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! Jetpack plugin: No scheduled milestone found for this plugin. If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. Videopress plugin: No scheduled milestone found for this plugin. If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. |
…-fold
The previous fix put both translated strings as the two arms of an inline
ternary. Terser folds `cond ? __('a',d) : __('b',d)` into a single
`__(cond?'a':'b', d)`, whose msgid is no longer a string literal, so the
i18n-check-webpack-plugin fails the production build (makepot can't extract
it). Hoist each string to its own module-scope constant so the calls stay
separate literal statements.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Coverage SummaryCoverage changed in 1 file.
|
| // If they live as the two arms of an inline ternary, terser folds them into a | ||
| // single `__( cond ? 'a' : 'b', domain )`, which breaks string-literal POT | ||
| // extraction (the i18n-check-webpack-plugin fails the production build). |
There was a problem hiding this comment.
Ugh, that's annoying, I ran into that today, too.
Fixes #49230
Proposed changes
The legacy VideoPress admin dashboard threw an uncaught
TypeErrorfrom@base-ui/react@1.4.1'suseMergedRefs/useRenderElementpath on every video upload or delete, crashing the React tree and leaving the admin unresponsive. This was a regression from #48909 (27bf601935), which migratedUpgradeTriggerfromContextualUpgradeTriggerto a@wordpress/uiNoticecompound tree.Root cause. In
UpgradeTrigger, theNotice.Descriptionchild was mounted conditionally:descriptionderives fromhasUsedVideo(=hasVideos), which flips on upload/delete. Mounting/unmounting that child changes theNotice.Rootsubtree shape across renders. base-ui@1.4.1'suseRenderElementconditionally swaps between two different ref-merge hook functions (useMergedRefsvsuseMergedRefsN, separateuseRefslots) depending on subtree shape, so the changing shape misaligns the stored fork-ref anddidChangereads.refsoffnull. Crash.The modern wp-build dashboard's equivalent (
src/dashboard/components/overview/free-tier-notice.tsx) uses the same@wordpress/uiNotice but never crashes because it always renders<Notice.Description>(static, never conditional).The fix. Make the
Notice.Rootchildren shape invariant across thehasVideosflip: always render<Notice.Description>, varying only its text — mirroringfree-tier-notice.tsx. No more&&-gated direct child ofNotice.Root.projects/packages/videopress/src/client/admin/components/admin-page/index.tsx—UpgradeTriggernow renders<Notice.Description>unconditionally.Empty-description edge case. Before the first upload on a free site,
descriptionwas previously''— that's why it was conditional (an empty<Notice.Description>would render an empty styled row). Rather than gating the child (which reintroduces the crash) or rendering blank, I gave the no-videos branch a sensible non-empty fallback nudge:This keeps the children shape constant in both states while never rendering an empty row. (
.upgrade-triggerSCSS only sets amargin-toponNotice.Root;Notice.Descriptioncarries its own internal spacing, so a blank one would have looked broken — hence the copy, not an empty string.)This keeps the JETPACK-1543
@wordpress/uiNotice migration — it does not revert #48909.Related product discussion/links
27bf601935)Does this pull request change what data or activity we track or use?
No. Analytics (
jetpack_videopress_upgrade_trigger_link_click) are unchanged.Testing instructions
The legacy dashboard renders by default — the modern dashboard is gated behind
apply_filters( 'rsm_jetpack_ui_modernization_videopress', false )insrc/class-admin-ui.php(defaultfalse, nothing in shipped code enables it). So the default VideoPress admin is the legacy/crashing one; no filter is needed to reproduce.UpgradeTriggerrenders.wp-admin/admin.php?page=jetpack-videopress.Before
TypeError: Cannot read properties of null (reading 'refs')(on delete) /...undefined (reading 'length')(on upload). The admin becomes unresponsive.After
🤖 Generated with Claude Code