@wordpress/editor 14.43.0 → 14.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +8 -0
  3. package/build/components/autocompleters/index.cjs +3 -0
  4. package/build/components/autocompleters/index.cjs.map +2 -2
  5. package/build/components/autocompleters/link.cjs +71 -0
  6. package/build/components/autocompleters/link.cjs.map +7 -0
  7. package/build/components/collab-sidebar/index.cjs.map +2 -2
  8. package/build/components/collaborators-overlay/cursor-dom-utils.cjs +1 -1
  9. package/build/components/collaborators-overlay/cursor-dom-utils.cjs.map +2 -2
  10. package/build/components/collaborators-overlay/timing-utils.cjs +1 -1
  11. package/build/components/collaborators-overlay/timing-utils.cjs.map +2 -2
  12. package/build/components/collaborators-overlay/use-render-cursors.cjs.map +2 -2
  13. package/build/components/error-boundary/index.cjs +1 -1
  14. package/build/components/error-boundary/index.cjs.map +2 -2
  15. package/build/components/post-revisions-panel/index.cjs +44 -42
  16. package/build/components/post-revisions-panel/index.cjs.map +2 -2
  17. package/build/components/post-revisions-preview/revisions-slider.cjs +9 -17
  18. package/build/components/post-revisions-preview/revisions-slider.cjs.map +2 -2
  19. package/build/components/post-title/index.cjs +2 -2
  20. package/build/components/post-title/index.cjs.map +2 -2
  21. package/build/components/sidebar/index.cjs +7 -1
  22. package/build/components/sidebar/index.cjs.map +2 -2
  23. package/build/components/sidebar/post-revision-summary.cjs +11 -2
  24. package/build/components/sidebar/post-revision-summary.cjs.map +2 -2
  25. package/build/components/sidebar/post-summary.cjs +0 -18
  26. package/build/components/sidebar/post-summary.cjs.map +2 -2
  27. package/build/components/style-book/categories.cjs.map +2 -2
  28. package/build/components/style-book/examples.cjs +1 -1
  29. package/build/components/style-book/examples.cjs.map +2 -2
  30. package/build/components/style-book/types.cjs.map +1 -1
  31. package/build/components/styles-canvas/revisions.cjs +2 -2
  32. package/build/components/styles-canvas/revisions.cjs.map +1 -1
  33. package/build/components/sync-connection-error-modal/index.cjs +66 -74
  34. package/build/components/sync-connection-error-modal/index.cjs.map +3 -3
  35. package/build/components/sync-connection-error-modal/use-retry-countdown.cjs +32 -9
  36. package/build/components/sync-connection-error-modal/use-retry-countdown.cjs.map +2 -2
  37. package/build/hooks/default-autocompleters.cjs +1 -1
  38. package/build/hooks/default-autocompleters.cjs.map +2 -2
  39. package/build/store/private-actions.cjs +1 -6
  40. package/build/store/private-actions.cjs.map +2 -2
  41. package/build/store/private-selectors.cjs +4 -6
  42. package/build/store/private-selectors.cjs.map +2 -2
  43. package/build/store/reducer.cjs +1 -1
  44. package/build/store/reducer.cjs.map +2 -2
  45. package/build-module/components/autocompleters/index.mjs +4 -2
  46. package/build-module/components/autocompleters/index.mjs.map +2 -2
  47. package/build-module/components/autocompleters/link.mjs +40 -0
  48. package/build-module/components/autocompleters/link.mjs.map +7 -0
  49. package/build-module/components/collab-sidebar/index.mjs.map +2 -2
  50. package/build-module/components/collaborators-overlay/cursor-dom-utils.mjs +1 -1
  51. package/build-module/components/collaborators-overlay/cursor-dom-utils.mjs.map +2 -2
  52. package/build-module/components/collaborators-overlay/timing-utils.mjs +1 -1
  53. package/build-module/components/collaborators-overlay/timing-utils.mjs.map +2 -2
  54. package/build-module/components/collaborators-overlay/use-render-cursors.mjs.map +2 -2
  55. package/build-module/components/error-boundary/index.mjs +1 -1
  56. package/build-module/components/error-boundary/index.mjs.map +2 -2
  57. package/build-module/components/post-revisions-panel/index.mjs +44 -42
  58. package/build-module/components/post-revisions-panel/index.mjs.map +2 -2
  59. package/build-module/components/post-revisions-preview/revisions-slider.mjs +9 -17
  60. package/build-module/components/post-revisions-preview/revisions-slider.mjs.map +2 -2
  61. package/build-module/components/post-title/index.mjs +2 -2
  62. package/build-module/components/post-title/index.mjs.map +2 -2
  63. package/build-module/components/sidebar/index.mjs +7 -1
  64. package/build-module/components/sidebar/index.mjs.map +2 -2
  65. package/build-module/components/sidebar/post-revision-summary.mjs +15 -3
  66. package/build-module/components/sidebar/post-revision-summary.mjs.map +2 -2
  67. package/build-module/components/sidebar/post-summary.mjs +1 -18
  68. package/build-module/components/sidebar/post-summary.mjs.map +2 -2
  69. package/build-module/components/style-book/categories.mjs.map +2 -2
  70. package/build-module/components/style-book/examples.mjs +1 -1
  71. package/build-module/components/style-book/examples.mjs.map +2 -2
  72. package/build-module/components/styles-canvas/revisions.mjs +2 -2
  73. package/build-module/components/styles-canvas/revisions.mjs.map +1 -1
  74. package/build-module/components/sync-connection-error-modal/index.mjs +66 -75
  75. package/build-module/components/sync-connection-error-modal/index.mjs.map +2 -2
  76. package/build-module/components/sync-connection-error-modal/use-retry-countdown.mjs +33 -10
  77. package/build-module/components/sync-connection-error-modal/use-retry-countdown.mjs.map +2 -2
  78. package/build-module/hooks/default-autocompleters.mjs +2 -2
  79. package/build-module/hooks/default-autocompleters.mjs.map +2 -2
  80. package/build-module/store/private-actions.mjs +1 -6
  81. package/build-module/store/private-actions.mjs.map +2 -2
  82. package/build-module/store/private-selectors.mjs +4 -6
  83. package/build-module/store/private-selectors.mjs.map +2 -2
  84. package/build-module/store/reducer.mjs +1 -1
  85. package/build-module/store/reducer.mjs.map +2 -2
  86. package/build-style/style-rtl.css +40 -30
  87. package/build-style/style.css +40 -30
  88. package/build-types/bindings/post-data.d.ts +3 -3
  89. package/build-types/bindings/term-data.d.ts +14 -14
  90. package/build-types/components/autocompleters/index.d.ts +1 -0
  91. package/build-types/components/autocompleters/link.d.ts +12 -0
  92. package/build-types/components/autocompleters/link.d.ts.map +1 -0
  93. package/build-types/components/collab-sidebar/index.d.ts.map +1 -1
  94. package/build-types/components/collaborators-overlay/use-render-cursors.d.ts.map +1 -1
  95. package/build-types/components/keyboard-shortcut-help-modal/config.d.ts +11 -11
  96. package/build-types/components/post-actions/set-as-homepage.d.ts +1 -1
  97. package/build-types/components/post-actions/set-as-posts-page.d.ts +1 -1
  98. package/build-types/components/post-format/index.d.ts +10 -10
  99. package/build-types/components/post-locked-modal/index.d.ts +2 -2
  100. package/build-types/components/post-revisions-panel/index.d.ts.map +1 -1
  101. package/build-types/components/post-revisions-preview/revisions-slider.d.ts.map +1 -1
  102. package/build-types/components/post-status/index.d.ts +10 -10
  103. package/build-types/components/post-visibility/utils.d.ts +6 -6
  104. package/build-types/components/sidebar/index.d.ts.map +1 -1
  105. package/build-types/components/sidebar/post-revision-summary.d.ts.map +1 -1
  106. package/build-types/components/sidebar/post-summary.d.ts +0 -3
  107. package/build-types/components/sidebar/post-summary.d.ts.map +1 -1
  108. package/build-types/components/style-book/categories.d.ts.map +1 -1
  109. package/build-types/components/style-book/examples.d.ts.map +1 -1
  110. package/build-types/components/style-book/types.d.ts +1 -13
  111. package/build-types/components/style-book/types.d.ts.map +1 -1
  112. package/build-types/components/sync-connection-error-modal/index.d.ts +0 -14
  113. package/build-types/components/sync-connection-error-modal/index.d.ts.map +1 -1
  114. package/build-types/components/sync-connection-error-modal/use-retry-countdown.d.ts.map +1 -1
  115. package/build-types/hooks/custom-sources-backwards-compatibility.d.ts +1 -1
  116. package/build-types/hooks/custom-sources-backwards-compatibility.d.ts.map +1 -1
  117. package/build-types/hooks/pattern-overrides.d.ts +1 -1
  118. package/build-types/hooks/pattern-overrides.d.ts.map +1 -1
  119. package/build-types/store/private-actions.d.ts.map +1 -1
  120. package/build-types/store/private-selectors.d.ts.map +1 -1
  121. package/build-types/store/reducer.d.ts +10 -10
  122. package/build-types/store/reducer.d.ts.map +1 -1
  123. package/build-types/utils/pageTypeBadge.d.ts +1 -1
  124. package/build-types/utils/pageTypeBadge.d.ts.map +1 -1
  125. package/package.json +45 -45
  126. package/src/components/autocompleters/index.js +1 -0
  127. package/src/components/autocompleters/link.js +47 -0
  128. package/src/components/autocompleters/style.scss +6 -0
  129. package/src/components/collab-sidebar/index.js +1 -0
  130. package/src/components/collaborators-overlay/cursor-dom-utils.ts +1 -1
  131. package/src/components/collaborators-overlay/timing-utils.ts +1 -1
  132. package/src/components/collaborators-overlay/use-render-cursors.ts +4 -2
  133. package/src/components/error-boundary/index.js +1 -1
  134. package/src/components/error-boundary/index.native.js +1 -1
  135. package/src/components/post-revisions-panel/index.js +46 -44
  136. package/src/components/post-revisions-preview/revisions-slider.js +9 -27
  137. package/src/components/post-title/index.js +3 -3
  138. package/src/components/sidebar/index.js +7 -1
  139. package/src/components/sidebar/post-revision-summary.js +13 -3
  140. package/src/components/sidebar/post-summary.js +1 -18
  141. package/src/components/style-book/categories.ts +0 -1
  142. package/src/components/style-book/examples.tsx +6 -12
  143. package/src/components/style-book/types.ts +1 -18
  144. package/src/components/styles-canvas/revisions.js +2 -2
  145. package/src/components/sync-connection-error-modal/index.tsx +151 -163
  146. package/src/components/sync-connection-error-modal/use-retry-countdown.ts +46 -10
  147. package/src/hooks/default-autocompleters.js +2 -2
  148. package/src/hooks/test/default-autocompleters.js +2 -2
  149. package/src/store/private-actions.js +1 -6
  150. package/src/store/private-selectors.js +4 -13
  151. package/src/store/reducer.js +9 -8
@@ -124,7 +124,7 @@ const PostTitle = forwardRef( ( _, forwardedRef ) => {
124
124
  try {
125
125
  plainText = clipboardData.getData( 'text/plain' );
126
126
  html = clipboardData.getData( 'text/html' );
127
- } catch ( error ) {
127
+ } catch {
128
128
  // Some browsers like UC Browser paste plain text by default and
129
129
  // don't support clipboardData at all, so allow default
130
130
  // behaviour.
@@ -181,7 +181,7 @@ const PostTitle = forwardRef( ( _, forwardedRef ) => {
181
181
  const style = isEditingContentOnlySection ? { opacity: 0.2 } : undefined;
182
182
 
183
183
  return (
184
- /* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
184
+ /* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */
185
185
  <h1
186
186
  ref={ useMergeRefs( [ richTextRef, focusRef ] ) }
187
187
  contentEditable={ ! isEditingContentOnlySection && ! isPreview }
@@ -195,7 +195,7 @@ const PostTitle = forwardRef( ( _, forwardedRef ) => {
195
195
  onPaste={ onPaste }
196
196
  style={ style }
197
197
  />
198
- /* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
198
+ /* eslint-enable jsx-a11y/no-noninteractive-element-to-interactive-role */
199
199
  );
200
200
  } );
201
201
 
@@ -112,7 +112,13 @@ const SidebarContent = ( {
112
112
  <PluginDocumentSettingPanel.Slot />
113
113
  <TemplateContentPanel />
114
114
  { window?.__experimentalDataFormInspector &&
115
- [ 'post', 'page' ].includes( postType ) && (
115
+ [
116
+ 'post',
117
+ 'page',
118
+ 'wp_template',
119
+ 'wp_template_part',
120
+ 'wp_block',
121
+ ].includes( postType ) && (
116
122
  <>
117
123
  <TemplateActionsPanel />
118
124
  <PostRevisionsPanel />
@@ -2,7 +2,12 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { useSelect } from '@wordpress/data';
5
- import { __experimentalVStack as VStack } from '@wordpress/components';
5
+ import {
6
+ ExternalLink,
7
+ __experimentalVStack as VStack,
8
+ } from '@wordpress/components';
9
+ import { __ } from '@wordpress/i18n';
10
+ import { addQueryArgs } from '@wordpress/url';
6
11
 
7
12
  /**
8
13
  * Internal dependencies
@@ -15,7 +20,6 @@ import { PostContentInformationUI } from '../post-content-information';
15
20
  import RevisionFieldsDiffPanel from '../revision-fields-diff';
16
21
  import PostPanelSection from '../post-panel-section';
17
22
  import PostCardPanel from '../post-card-panel';
18
- import { OpenRevisionsClassicScreen } from './post-summary';
19
23
 
20
24
  export default function PostRevisionSummary() {
21
25
  const { revisionId, postId, postContent } = useSelect( ( select ) => {
@@ -40,7 +44,13 @@ export default function PostRevisionSummary() {
40
44
  <PostContentInformationUI postContent={ postContent } />
41
45
  <RevisionCreatedPanel />
42
46
  </VStack>
43
- <OpenRevisionsClassicScreen revisionId={ revisionId } />
47
+ <ExternalLink
48
+ href={ addQueryArgs( 'revision.php', {
49
+ revision: revisionId,
50
+ } ) }
51
+ >
52
+ { __( 'Open classic revisions screen' ) }
53
+ </ExternalLink>
44
54
  <RevisionAuthorPanel />
45
55
  </VStack>
46
56
  </PostPanelSection>
@@ -1,13 +1,8 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import {
5
- __experimentalVStack as VStack,
6
- ExternalLink,
7
- } from '@wordpress/components';
4
+ import { __experimentalVStack as VStack } from '@wordpress/components';
8
5
  import { useSelect } from '@wordpress/data';
9
- import { __ } from '@wordpress/i18n';
10
- import { addQueryArgs } from '@wordpress/url';
11
6
 
12
7
  /**
13
8
  * Internal dependencies
@@ -41,18 +36,6 @@ import PostTrash from '../post-trash';
41
36
  */
42
37
  const PANEL_NAME = 'post-status';
43
38
 
44
- export function OpenRevisionsClassicScreen( { revisionId } ) {
45
- return (
46
- <ExternalLink
47
- href={ addQueryArgs( 'revision.php', {
48
- revision: revisionId,
49
- } ) }
50
- >
51
- { __( 'Open classic revisions screen' ) }
52
- </ExternalLink>
53
- );
54
- }
55
-
56
39
  export default function PostSummary( { onActionPerformed } ) {
57
40
  const postType = useSelect(
58
41
  ( select ) => select( editorStore ).getCurrentPostType(),
@@ -2,7 +2,6 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  // @wordpress/blocks imports are not typed.
5
- // @ts-expect-error
6
5
  import { getCategories } from '@wordpress/blocks';
7
6
 
8
7
  /**
@@ -2,24 +2,18 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { __, sprintf } from '@wordpress/i18n';
5
+ import type { Block } from '@wordpress/blocks';
5
6
  import {
6
7
  getBlockType,
7
8
  getBlockTypes,
8
9
  getBlockFromExample,
9
10
  createBlock,
10
- // @wordpress/blocks imports are not typed.
11
- // @ts-expect-error
12
11
  } from '@wordpress/blocks';
13
12
 
14
13
  /**
15
14
  * Internal dependencies
16
15
  */
17
- import type {
18
- BlockExample,
19
- ColorOrigin,
20
- MultiOriginPalettes,
21
- BlockType,
22
- } from './types';
16
+ import type { BlockExample, ColorOrigin, MultiOriginPalettes } from './types';
23
17
  import ColorExamples from './color-examples';
24
18
  import DuotoneExamples from './duotone-examples';
25
19
  import { STYLE_BOOK_COLOR_GROUPS } from './constants';
@@ -111,7 +105,7 @@ function getOverviewBlockExamples(
111
105
  }
112
106
 
113
107
  // Get examples for typography blocks.
114
- const typographyBlockExamples: BlockType[] = [];
108
+ const typographyBlockExamples: Block[] = [];
115
109
 
116
110
  if ( getBlockType( 'core/heading' ) ) {
117
111
  const headingBlock = createBlock( 'core/heading', {
@@ -212,7 +206,7 @@ function getOverviewBlockExamples(
212
206
  */
213
207
  export function getExamples( colors: MultiOriginPalettes ): BlockExample[] {
214
208
  const nonHeadingBlockExamples = getBlockTypes()
215
- .filter( ( blockType: BlockType ) => {
209
+ .filter( ( blockType ) => {
216
210
  const { name, example, supports } = blockType;
217
211
  return (
218
212
  name !== 'core/heading' &&
@@ -220,7 +214,7 @@ export function getExamples( colors: MultiOriginPalettes ): BlockExample[] {
220
214
  supports?.inserter !== false
221
215
  );
222
216
  } )
223
- .map( ( blockType: BlockType ) => ( {
217
+ .map( ( blockType ) => ( {
224
218
  name: blockType.name,
225
219
  title: blockType.title,
226
220
  category: blockType.category,
@@ -232,7 +226,7 @@ export function getExamples( colors: MultiOriginPalettes ): BlockExample[] {
232
226
  blocks: getBlockFromExample( blockType.name, {
233
227
  ...blockType.example,
234
228
  attributes: {
235
- ...blockType.example.attributes,
229
+ ...blockType.example?.attributes,
236
230
  style: undefined,
237
231
  },
238
232
  } ),
@@ -1,8 +1,4 @@
1
- export type Block = {
2
- name: string;
3
- attributes: Record< string, unknown >;
4
- innerBlocks?: Block[];
5
- };
1
+ import type { Block } from '@wordpress/blocks';
6
2
 
7
3
  export type StyleBookCategory = {
8
4
  title: string;
@@ -65,16 +61,3 @@ export type MultiOriginPalettes = {
65
61
  duotones: Omit< ColorOrigin, 'colors' | 'gradients' >;
66
62
  gradients: Omit< ColorOrigin, 'colors' | 'duotones' >;
67
63
  };
68
-
69
- /*
70
- * Typing the items from getBlockTypes from '@wordpress/blocks'
71
- * to appease the TS linter.
72
- */
73
- export type BlockType = {
74
- name: string;
75
- title: string;
76
- category: string;
77
- example: BlockType;
78
- attributes: Record< string, unknown >;
79
- supports: Record< string, unknown >;
80
- };
@@ -23,7 +23,7 @@ import { unlock } from '../../lock-unlock';
23
23
 
24
24
  const {
25
25
  ExperimentalBlockEditorProvider,
26
- __unstableBlockStyleVariationOverridesWithConfig,
26
+ BlockStyleVariationOverridesWithConfig,
27
27
  } = unlock( blockEditorPrivateApis );
28
28
 
29
29
  function isObjectEmpty( object ) {
@@ -133,7 +133,7 @@ function StylesCanvasRevisions( { path }, ref ) {
133
133
  * so they can access any registered style overrides.
134
134
  */ }
135
135
  <EditorStyles styles={ editorStyles } />
136
- <__unstableBlockStyleVariationOverridesWithConfig
136
+ <BlockStyleVariationOverridesWithConfig
137
137
  config={ mergedConfig }
138
138
  />
139
139
  </ExperimentalBlockEditorProvider>
@@ -8,7 +8,6 @@ import { serialize } from '@wordpress/blocks';
8
8
  import {
9
9
  store as coreDataStore,
10
10
  privateApis as coreDataPrivateApis,
11
- type ConnectionError,
12
11
  } from '@wordpress/core-data';
13
12
  // @ts-expect-error - No type declarations available for @wordpress/block-editor
14
13
  // prettier-ignore
@@ -16,10 +15,10 @@ import { privateApis, store as blockEditorStore } from '@wordpress/block-editor'
16
15
  import {
17
16
  Button,
18
17
  Modal,
19
- withFilters,
20
18
  __experimentalHStack as HStack,
21
19
  __experimentalVStack as VStack,
22
20
  } from '@wordpress/components';
21
+ import { applyFilters } from '@wordpress/hooks';
23
22
  import { useState, useEffect } from '@wordpress/element';
24
23
  import { __, sprintf, _n } from '@wordpress/i18n';
25
24
 
@@ -37,144 +36,6 @@ const { retrySyncConnection } = unlock( coreDataPrivateApis );
37
36
  // Debounce time for initial disconnected status to allow connection to establish.
38
37
  const INITIAL_DISCONNECTED_DEBOUNCE_MS = 20000;
39
38
 
40
- // Debounce time for showing the disconnect dialog after the intial connection,
41
- // allowing brief network interruptions to resolve.
42
- const DISCONNECTED_DEBOUNCE_MS = 8000;
43
-
44
- export interface SyncConnectionErrorModalProps {
45
- description: string; // Modal description.
46
- error?: ConnectionError; // Error object with a `code` property.
47
- manualRetry?: () => void; // Callback for when the retry button is clicked.
48
- postType?: { slug?: string; labels?: { name?: string } } | null; // Current post type object.
49
- secondsRemainingUntilAutoRetry?: number; // Seconds remaining until the next automatic retry attempt, if applicable.
50
- title: string; // Modal title.
51
- }
52
-
53
- /**
54
- * Default sync connection modal component.
55
- *
56
- * Can be replaced or wrapped via the `editor.SyncConnectionErrorModal` filter.
57
- *
58
- * @param props - SyncConnectionErrorModalProps.
59
- */
60
- function DefaultSyncConnectionErrorModal(
61
- props: SyncConnectionErrorModalProps
62
- ) {
63
- const {
64
- description,
65
- manualRetry,
66
- postType,
67
- secondsRemainingUntilAutoRetry,
68
- title,
69
- } = props;
70
- const copyButtonRef = useCopyToClipboard( () => {
71
- const blocks = select( blockEditorStore ).getBlocks();
72
- return serialize( blocks );
73
- } );
74
-
75
- let retryCountdownText: string = '';
76
- let isRetrying = false;
77
- if (
78
- secondsRemainingUntilAutoRetry &&
79
- secondsRemainingUntilAutoRetry > 0
80
- ) {
81
- retryCountdownText = sprintf(
82
- /* translators: %d: number of seconds until retry */
83
- _n(
84
- 'Retrying connection in %d second\u2026',
85
- 'Retrying connection in %d seconds\u2026',
86
- secondsRemainingUntilAutoRetry
87
- ),
88
- secondsRemainingUntilAutoRetry
89
- );
90
- } else if ( 0 === secondsRemainingUntilAutoRetry ) {
91
- isRetrying = true;
92
- retryCountdownText = __( 'Retrying\u2026' );
93
- }
94
-
95
- let editPostHref = 'edit.php';
96
- if ( postType?.slug ) {
97
- editPostHref = `edit.php?post_type=${ postType.slug }`;
98
- }
99
-
100
- return (
101
- <Modal
102
- overlayClassName="editor-sync-connection-error-modal"
103
- isDismissible={ false }
104
- onRequestClose={ () => {} }
105
- shouldCloseOnClickOutside={ false }
106
- shouldCloseOnEsc={ false }
107
- size="medium"
108
- title={ title }
109
- >
110
- <VStack spacing={ 6 }>
111
- <p>{ description }</p>
112
- { retryCountdownText && (
113
- <p className="editor-sync-connection-error-modal__retry-countdown">
114
- { retryCountdownText }
115
- </p>
116
- ) }
117
- <HStack justify="right">
118
- <Button
119
- __next40pxDefaultSize
120
- href={ editPostHref }
121
- isDestructive
122
- variant="tertiary"
123
- >
124
- { sprintf(
125
- /* translators: %s: Post type name (e.g., "Posts", "Pages"). */
126
- __( 'Back to %s' ),
127
- postType?.labels?.name ?? __( 'Posts' )
128
- ) }
129
- </Button>
130
- <Button
131
- __next40pxDefaultSize
132
- ref={ copyButtonRef }
133
- variant={ manualRetry ? 'secondary' : 'primary' }
134
- >
135
- { __( 'Copy Post Content' ) }
136
- </Button>
137
- { manualRetry && (
138
- <Button
139
- __next40pxDefaultSize
140
- accessibleWhenDisabled
141
- aria-disabled={ isRetrying }
142
- disabled={ isRetrying }
143
- isBusy={ isRetrying }
144
- variant="primary"
145
- onClick={ manualRetry }
146
- >
147
- { __( 'Retry' ) }
148
- </Button>
149
- ) }
150
- </HStack>
151
- </VStack>
152
- </Modal>
153
- );
154
- }
155
-
156
- /**
157
- * Filtered version of the sync connection modal, allowing third-party
158
- * plugins to replace the default modal via:
159
- *
160
- * ```js
161
- * wp.hooks.addFilter(
162
- * 'editor.SyncConnectionErrorModal',
163
- * 'my-plugin/custom-sync-connection-error-modal',
164
- * ( OriginalComponent ) => ( props ) => {
165
- * // Return a custom component or wrap the original.
166
- * return <OriginalComponent { ...props } />;
167
- * }
168
- * );
169
- * ```
170
- */
171
- // @ts-ignore
172
- const FilteredSyncConnectionErrorModal = globalThis.IS_GUTENBERG_PLUGIN
173
- ? withFilters( 'editor.SyncConnectionErrorModal' )(
174
- DefaultSyncConnectionErrorModal
175
- )
176
- : DefaultSyncConnectionErrorModal;
177
-
178
39
  /**
179
40
  * Sync connection modal that displays when any entity reports a disconnection.
180
41
  * Uses BlockCanvasCover.Fill to render in the block canvas.
@@ -184,6 +45,8 @@ const FilteredSyncConnectionErrorModal = globalThis.IS_GUTENBERG_PLUGIN
184
45
  export function SyncConnectionErrorModal() {
185
46
  const [ hasInitialized, setHasInitialized ] = useState( false );
186
47
  const [ showModal, setShowModal ] = useState( false );
48
+ const [ isManualRetryAvailable, setIsManualRetryAvailable ] =
49
+ useState( false );
187
50
 
188
51
  const { connectionStatus, isCollaborationEnabled, postType } = useSelect(
189
52
  ( selectFn ) => {
@@ -207,7 +70,10 @@ export function SyncConnectionErrorModal() {
207
70
  const { onManualRetry, secondsRemaining } =
208
71
  useRetryCountdown( connectionStatus );
209
72
 
210
- const isConnected = 'connected' === connectionStatus?.status;
73
+ const copyButtonRef = useCopyToClipboard( () => {
74
+ const blocks = select( blockEditorStore ).getBlocks();
75
+ return serialize( blocks );
76
+ } );
211
77
 
212
78
  // Set hasInitialized after a debounce to give extra time on initial load.
213
79
  useEffect( () => {
@@ -218,18 +84,46 @@ export function SyncConnectionErrorModal() {
218
84
  return () => clearTimeout( timeout );
219
85
  }, [] );
220
86
 
87
+ // Track retry availability separately from the raw connection status.
88
+ // The polling manager briefly emits `{ status: 'connecting' }` without
89
+ // `canManuallyRetry` when a retry is kicked off, which would otherwise
90
+ // unmount the Retry button briefly.
221
91
  useEffect( () => {
222
- if ( isConnected ) {
92
+ if ( 'connecting' === connectionStatus?.status ) {
93
+ return;
94
+ }
95
+
96
+ setIsManualRetryAvailable(
97
+ connectionStatus !== null &&
98
+ 'canManuallyRetry' in connectionStatus &&
99
+ connectionStatus.canManuallyRetry === true
100
+ );
101
+ }, [ connectionStatus ] );
102
+
103
+ // Show the modal when disconnected and either retries are exhausted or
104
+ // no retry is available (unrecoverable error). Hide on reconnect.
105
+ // The 'connecting' state is ignored so the modal preserves its current
106
+ // visibility during active retry attempts.
107
+ const canRetry =
108
+ connectionStatus &&
109
+ 'disconnected' === connectionStatus.status &&
110
+ ( connectionStatus.canManuallyRetry ||
111
+ connectionStatus.willAutoRetryInMs );
112
+
113
+ useEffect( () => {
114
+ if ( 'connected' === connectionStatus?.status ) {
223
115
  setShowModal( false );
224
116
  return;
225
117
  }
226
118
 
227
- const timeout = setTimeout( () => {
119
+ if (
120
+ connectionStatus?.status &&
121
+ 'connecting' !== connectionStatus.status &&
122
+ ( ! canRetry || connectionStatus.backgroundRetriesFailed )
123
+ ) {
228
124
  setShowModal( true );
229
- }, DISCONNECTED_DEBOUNCE_MS );
230
-
231
- return () => clearTimeout( timeout );
232
- }, [ isConnected ] );
125
+ }
126
+ }, [ connectionStatus, canRetry ] );
233
127
 
234
128
  if ( ! isCollaborationEnabled || ! hasInitialized || ! showModal ) {
235
129
  return null;
@@ -239,27 +133,121 @@ export function SyncConnectionErrorModal() {
239
133
  connectionStatus && 'error' in connectionStatus
240
134
  ? connectionStatus?.error
241
135
  : undefined;
242
- const manualRetry =
243
- connectionStatus &&
244
- 'canManuallyRetry' in connectionStatus &&
245
- connectionStatus.canManuallyRetry
246
- ? () => {
247
- onManualRetry();
248
- retrySyncConnection();
249
- }
250
- : undefined;
136
+
137
+ // For unrecoverable errors (no retry available), allow plugins to handle
138
+ // the error themselves. If a plugin returns a value other than false, it
139
+ // signals that it has taken over error display and the default modal is
140
+ // suppressed.
141
+ //
142
+ // @example
143
+ // ```js
144
+ // wp.hooks.addFilter(
145
+ // 'editor.isSyncConnectionErrorHandled',
146
+ // 'my-plugin/handle-sync-error',
147
+ // ( isHandled, errorCode ) => {
148
+ // if ( errorCode === 'connection-limit-exceeded' ) {
149
+ // return true; // Plugin handles this error via its own UI.
150
+ // }
151
+ // return isHandled;
152
+ // }
153
+ // );
154
+ // ```
155
+ if (
156
+ ! canRetry &&
157
+ applyFilters(
158
+ 'editor.isSyncConnectionErrorHandled',
159
+ false,
160
+ error?.code
161
+ ) !== false
162
+ ) {
163
+ return null;
164
+ }
165
+
166
+ const manualRetry = isManualRetryAvailable
167
+ ? () => {
168
+ onManualRetry();
169
+ retrySyncConnection();
170
+ }
171
+ : undefined;
172
+
251
173
  const messages = getSyncErrorMessages( error );
252
174
 
175
+ let retryCountdownText: string = '';
176
+ let isRetrying = false;
177
+ if ( secondsRemaining && secondsRemaining > 0 ) {
178
+ retryCountdownText = sprintf(
179
+ /* translators: %d: number of seconds until retry */
180
+ _n(
181
+ 'Retrying connection in %d second\u2026',
182
+ 'Retrying connection in %d seconds\u2026',
183
+ secondsRemaining
184
+ ),
185
+ secondsRemaining
186
+ );
187
+ } else if ( 0 === secondsRemaining ) {
188
+ isRetrying = true;
189
+ retryCountdownText = __( 'Retrying\u2026' );
190
+ }
191
+
192
+ let editPostHref = 'edit.php';
193
+ if ( postType?.slug ) {
194
+ editPostHref = `edit.php?post_type=${ postType.slug }`;
195
+ }
196
+
253
197
  return (
254
198
  <BlockCanvasCover.Fill>
255
- <FilteredSyncConnectionErrorModal
256
- description={ messages.description }
257
- error={ error }
258
- manualRetry={ manualRetry }
259
- postType={ postType }
260
- secondsRemainingUntilAutoRetry={ secondsRemaining }
199
+ <Modal
200
+ overlayClassName="editor-sync-connection-error-modal"
201
+ isDismissible={ false }
202
+ onRequestClose={ () => {} }
203
+ shouldCloseOnClickOutside={ false }
204
+ shouldCloseOnEsc={ false }
205
+ size="medium"
261
206
  title={ messages.title }
262
- />
207
+ >
208
+ <VStack spacing={ 6 }>
209
+ <p>{ messages.description }</p>
210
+ { retryCountdownText && (
211
+ <p className="editor-sync-connection-error-modal__retry-countdown">
212
+ { retryCountdownText }
213
+ </p>
214
+ ) }
215
+ <HStack justify="right">
216
+ <Button
217
+ __next40pxDefaultSize
218
+ href={ editPostHref }
219
+ isDestructive
220
+ variant="tertiary"
221
+ >
222
+ { sprintf(
223
+ /* translators: %s: Post type name (e.g., "Posts", "Pages"). */
224
+ __( 'Back to %s' ),
225
+ postType?.labels?.name ?? __( 'Posts' )
226
+ ) }
227
+ </Button>
228
+ <Button
229
+ __next40pxDefaultSize
230
+ ref={ copyButtonRef }
231
+ variant={ manualRetry ? 'secondary' : 'primary' }
232
+ >
233
+ { __( 'Copy Post Content' ) }
234
+ </Button>
235
+ { manualRetry && (
236
+ <Button
237
+ __next40pxDefaultSize
238
+ accessibleWhenDisabled
239
+ aria-disabled={ isRetrying }
240
+ disabled={ isRetrying }
241
+ isBusy={ isRetrying }
242
+ variant="primary"
243
+ onClick={ manualRetry }
244
+ >
245
+ { __( 'Retry' ) }
246
+ </Button>
247
+ ) }
248
+ </HStack>
249
+ </VStack>
250
+ </Modal>
263
251
  </BlockCanvasCover.Fill>
264
252
  );
265
253
  }
@@ -2,7 +2,7 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import type { ConnectionStatus } from '@wordpress/core-data';
5
- import { useState, useEffect } from '@wordpress/element';
5
+ import { useState, useEffect, useRef } from '@wordpress/element';
6
6
 
7
7
  interface UseRetryCountdownResult {
8
8
  onManualRetry: () => void;
@@ -13,6 +13,7 @@ export function useRetryCountdown(
13
13
  connectionStatus?: ConnectionStatus | null
14
14
  ): UseRetryCountdownResult {
15
15
  const [ secondsRemaining, setSecondsRemaining ] = useState< number >();
16
+ const hasRetriedRef = useRef( false );
16
17
 
17
18
  useEffect( () => {
18
19
  if ( ! connectionStatus ) {
@@ -22,6 +23,7 @@ export function useRetryCountdown(
22
23
  // Only clear countdown when explicitly connected.
23
24
  if ( 'connected' === connectionStatus.status ) {
24
25
  setSecondsRemaining( undefined );
26
+ hasRetriedRef.current = false;
25
27
  return;
26
28
  }
27
29
 
@@ -37,21 +39,55 @@ export function useRetryCountdown(
37
39
 
38
40
  const { willAutoRetryInMs: retryInMs } = connectionStatus;
39
41
  const retryAt = Date.now() + retryInMs;
40
- setSecondsRemaining( Math.ceil( retryInMs / 1000 ) );
41
42
 
42
- const intervalId = setInterval( () => {
43
- const remaining = Math.ceil( ( retryAt - Date.now() ) / 1000 );
44
- setSecondsRemaining( Math.max( 0, remaining ) );
45
- if ( remaining <= 0 ) {
46
- clearInterval( intervalId );
43
+ // After a retry attempt (manual or automatic), show "Retrying..."
44
+ // for 500ms before starting the next countdown. Skip the delay on
45
+ // the very first disconnect so the countdown starts immediately.
46
+ const hasRetried = hasRetriedRef.current;
47
+ hasRetriedRef.current = true;
48
+
49
+ if ( hasRetried ) {
50
+ setSecondsRemaining( 0 );
51
+ }
52
+
53
+ let countdownIntervalId: ReturnType< typeof setInterval > | null = null;
54
+
55
+ const startCountdown = () => {
56
+ setSecondsRemaining( Math.ceil( ( retryAt - Date.now() ) / 1000 ) );
57
+
58
+ countdownIntervalId = setInterval( () => {
59
+ const remaining = Math.ceil( ( retryAt - Date.now() ) / 1000 );
60
+ setSecondsRemaining( Math.max( 0, remaining ) );
61
+
62
+ if ( remaining <= 0 && countdownIntervalId ) {
63
+ clearInterval( countdownIntervalId );
64
+ }
65
+ }, 1000 );
66
+ };
67
+
68
+ const retryingDelayId = hasRetried
69
+ ? setTimeout( startCountdown, 500 )
70
+ : null;
71
+
72
+ if ( ! retryingDelayId ) {
73
+ startCountdown();
74
+ }
75
+
76
+ return () => {
77
+ if ( retryingDelayId ) {
78
+ clearTimeout( retryingDelayId );
47
79
  }
48
- }, 1000 );
49
80
 
50
- return () => clearInterval( intervalId );
81
+ if ( countdownIntervalId ) {
82
+ clearInterval( countdownIntervalId );
83
+ }
84
+ };
51
85
  }, [ connectionStatus ] );
52
86
 
53
87
  return {
54
- onManualRetry: () => setSecondsRemaining( 0 ),
88
+ onManualRetry: () => {
89
+ setSecondsRemaining( 0 );
90
+ },
55
91
  secondsRemaining,
56
92
  };
57
93
  }