@wordpress/editor 14.41.2-next.v.202603161435.0 → 14.42.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 (154) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/build/components/collaborators-overlay/compute-selection.cjs +10 -10
  3. package/build/components/collaborators-overlay/compute-selection.cjs.map +2 -2
  4. package/build/components/collaborators-overlay/overlay.cjs +11 -0
  5. package/build/components/collaborators-overlay/overlay.cjs.map +2 -2
  6. package/build/components/collaborators-overlay/timing-utils.cjs +46 -0
  7. package/build/components/collaborators-overlay/timing-utils.cjs.map +7 -0
  8. package/build/components/collaborators-overlay/use-render-cursors.cjs +1 -1
  9. package/build/components/collaborators-overlay/use-render-cursors.cjs.map +2 -2
  10. package/build/components/post-locked-modal/index.cjs +16 -3
  11. package/build/components/post-locked-modal/index.cjs.map +2 -2
  12. package/build/components/post-revisions-preview/block-diff.cjs +39 -11
  13. package/build/components/post-revisions-preview/block-diff.cjs.map +2 -2
  14. package/build/components/post-revisions-preview/diff-markers.cjs +2 -2
  15. package/build/components/post-revisions-preview/diff-markers.cjs.map +2 -2
  16. package/build/components/post-revisions-preview/revisions-canvas.cjs +1 -1
  17. package/build/components/post-revisions-preview/revisions-canvas.cjs.map +2 -2
  18. package/build/components/post-template/block-theme.cjs +7 -4
  19. package/build/components/post-template/block-theme.cjs.map +2 -2
  20. package/build/components/post-template/hooks.cjs +39 -2
  21. package/build/components/post-template/hooks.cjs.map +2 -2
  22. package/build/components/post-template/panel.cjs +5 -42
  23. package/build/components/post-template/panel.cjs.map +3 -3
  24. package/build/components/provider/use-block-editor-settings.cjs +2 -0
  25. package/build/components/provider/use-block-editor-settings.cjs.map +3 -3
  26. package/build/components/revision-block-diff/index.cjs +84 -0
  27. package/build/components/revision-block-diff/index.cjs.map +7 -0
  28. package/build/components/sidebar/dataform-post-summary.cjs +17 -2
  29. package/build/components/sidebar/dataform-post-summary.cjs.map +2 -2
  30. package/build/components/sidebar/index.cjs +5 -1
  31. package/build/components/sidebar/index.cjs.map +3 -3
  32. package/build/components/{sync-connection-modal → sync-connection-error-modal}/index.cjs +87 -78
  33. package/build/components/sync-connection-error-modal/index.cjs.map +7 -0
  34. package/build/components/{sync-connection-modal → sync-connection-error-modal}/use-retry-countdown.cjs +14 -27
  35. package/build/components/sync-connection-error-modal/use-retry-countdown.cjs.map +7 -0
  36. package/build/components/visual-editor/index.cjs +2 -2
  37. package/build/components/visual-editor/index.cjs.map +2 -2
  38. package/build/store/actions.cjs +1 -3
  39. package/build/store/actions.cjs.map +2 -2
  40. package/build/utils/media-finalize/index.cjs +43 -0
  41. package/build/utils/media-finalize/index.cjs.map +7 -0
  42. package/build/utils/sync-error-messages.cjs +29 -16
  43. package/build/utils/sync-error-messages.cjs.map +3 -3
  44. package/build-module/components/collaborators-overlay/compute-selection.mjs +10 -10
  45. package/build-module/components/collaborators-overlay/compute-selection.mjs.map +2 -2
  46. package/build-module/components/collaborators-overlay/overlay.mjs +11 -0
  47. package/build-module/components/collaborators-overlay/overlay.mjs.map +2 -2
  48. package/build-module/components/collaborators-overlay/timing-utils.mjs +21 -0
  49. package/build-module/components/collaborators-overlay/timing-utils.mjs.map +7 -0
  50. package/build-module/components/collaborators-overlay/use-render-cursors.mjs +1 -1
  51. package/build-module/components/collaborators-overlay/use-render-cursors.mjs.map +2 -2
  52. package/build-module/components/post-locked-modal/index.mjs +16 -3
  53. package/build-module/components/post-locked-modal/index.mjs.map +2 -2
  54. package/build-module/components/post-revisions-preview/block-diff.mjs +39 -11
  55. package/build-module/components/post-revisions-preview/block-diff.mjs.map +2 -2
  56. package/build-module/components/post-revisions-preview/diff-markers.mjs +2 -2
  57. package/build-module/components/post-revisions-preview/diff-markers.mjs.map +2 -2
  58. package/build-module/components/post-revisions-preview/revisions-canvas.mjs +1 -1
  59. package/build-module/components/post-revisions-preview/revisions-canvas.mjs.map +2 -2
  60. package/build-module/components/post-template/block-theme.mjs +7 -4
  61. package/build-module/components/post-template/block-theme.mjs.map +2 -2
  62. package/build-module/components/post-template/hooks.mjs +37 -1
  63. package/build-module/components/post-template/hooks.mjs.map +2 -2
  64. package/build-module/components/post-template/panel.mjs +5 -42
  65. package/build-module/components/post-template/panel.mjs.map +2 -2
  66. package/build-module/components/provider/use-block-editor-settings.mjs +2 -0
  67. package/build-module/components/provider/use-block-editor-settings.mjs.map +2 -2
  68. package/build-module/components/revision-block-diff/index.mjs +53 -0
  69. package/build-module/components/revision-block-diff/index.mjs.map +7 -0
  70. package/build-module/components/sidebar/dataform-post-summary.mjs +17 -2
  71. package/build-module/components/sidebar/dataform-post-summary.mjs.map +2 -2
  72. package/build-module/components/sidebar/index.mjs +5 -1
  73. package/build-module/components/sidebar/index.mjs.map +2 -2
  74. package/build-module/components/sync-connection-error-modal/index.mjs +177 -0
  75. package/build-module/components/sync-connection-error-modal/index.mjs.map +7 -0
  76. package/build-module/components/sync-connection-error-modal/use-retry-countdown.mjs +36 -0
  77. package/build-module/components/sync-connection-error-modal/use-retry-countdown.mjs.map +7 -0
  78. package/build-module/components/visual-editor/index.mjs +2 -2
  79. package/build-module/components/visual-editor/index.mjs.map +2 -2
  80. package/build-module/store/actions.mjs +1 -3
  81. package/build-module/store/actions.mjs.map +2 -2
  82. package/build-module/utils/media-finalize/index.mjs +12 -0
  83. package/build-module/utils/media-finalize/index.mjs.map +7 -0
  84. package/build-module/utils/sync-error-messages.mjs +24 -16
  85. package/build-module/utils/sync-error-messages.mjs.map +3 -3
  86. package/build-style/style-rtl.css +24 -8
  87. package/build-style/style.css +24 -8
  88. package/build-types/components/collaborators-overlay/overlay.d.ts.map +1 -1
  89. package/build-types/components/collaborators-overlay/timing-utils.d.ts +11 -0
  90. package/build-types/components/collaborators-overlay/timing-utils.d.ts.map +1 -0
  91. package/build-types/components/post-locked-modal/index.d.ts +2 -2
  92. package/build-types/components/post-locked-modal/index.d.ts.map +1 -1
  93. package/build-types/components/post-revisions-preview/block-diff.d.ts.map +1 -1
  94. package/build-types/components/post-template/block-theme.d.ts +1 -3
  95. package/build-types/components/post-template/block-theme.d.ts.map +1 -1
  96. package/build-types/components/post-template/hooks.d.ts +1 -0
  97. package/build-types/components/post-template/hooks.d.ts.map +1 -1
  98. package/build-types/components/post-template/panel.d.ts.map +1 -1
  99. package/build-types/components/provider/use-block-editor-settings.d.ts.map +1 -1
  100. package/build-types/components/revision-block-diff/index.d.ts +6 -0
  101. package/build-types/components/revision-block-diff/index.d.ts.map +1 -0
  102. package/build-types/components/sidebar/dataform-post-summary.d.ts.map +1 -1
  103. package/build-types/components/sidebar/index.d.ts.map +1 -1
  104. package/build-types/components/sync-connection-error-modal/index.d.ts +22 -0
  105. package/build-types/components/sync-connection-error-modal/index.d.ts.map +1 -0
  106. package/build-types/components/sync-connection-error-modal/use-retry-countdown.d.ts +11 -0
  107. package/build-types/components/sync-connection-error-modal/use-retry-countdown.d.ts.map +1 -0
  108. package/build-types/store/actions.d.ts.map +1 -1
  109. package/build-types/utils/media-finalize/index.d.ts +2 -0
  110. package/build-types/utils/media-finalize/index.d.ts.map +1 -0
  111. package/build-types/utils/sync-error-messages.d.ts +17 -3
  112. package/build-types/utils/sync-error-messages.d.ts.map +1 -1
  113. package/package.json +44 -44
  114. package/src/components/collaborators-overlay/compute-selection.ts +13 -13
  115. package/src/components/collaborators-overlay/overlay.tsx +19 -0
  116. package/src/components/collaborators-overlay/timing-utils.ts +30 -0
  117. package/src/components/collaborators-overlay/use-render-cursors.ts +1 -1
  118. package/src/components/post-locked-modal/index.js +21 -3
  119. package/src/components/post-revisions-preview/block-diff.js +59 -20
  120. package/src/components/post-revisions-preview/diff-markers.js +2 -2
  121. package/src/components/post-revisions-preview/revisions-canvas.js +1 -1
  122. package/src/components/post-revisions-preview/test/block-diff.js +69 -31
  123. package/src/components/post-template/block-theme.js +4 -1
  124. package/src/components/post-template/hooks.js +42 -0
  125. package/src/components/post-template/panel.js +5 -59
  126. package/src/components/provider/use-block-editor-settings.js +2 -0
  127. package/src/components/revision-block-diff/index.js +74 -0
  128. package/src/components/revision-block-diff/style.scss +13 -0
  129. package/src/components/sidebar/dataform-post-summary.js +37 -13
  130. package/src/components/sidebar/index.js +2 -0
  131. package/src/components/sync-connection-error-modal/index.tsx +265 -0
  132. package/src/components/sync-connection-error-modal/style.scss +14 -0
  133. package/src/components/sync-connection-error-modal/use-retry-countdown.ts +57 -0
  134. package/src/components/visual-editor/index.js +2 -2
  135. package/src/store/actions.js +1 -4
  136. package/src/style.scss +2 -1
  137. package/src/utils/media-finalize/index.js +11 -0
  138. package/src/utils/media-finalize/test/index.js +34 -0
  139. package/src/utils/sync-error-messages.ts +72 -0
  140. package/src/utils/test/sync-error-messages.js +9 -32
  141. package/build/components/sync-connection-modal/index.cjs.map +0 -7
  142. package/build/components/sync-connection-modal/use-retry-countdown.cjs.map +0 -7
  143. package/build-module/components/sync-connection-modal/index.mjs +0 -170
  144. package/build-module/components/sync-connection-modal/index.mjs.map +0 -7
  145. package/build-module/components/sync-connection-modal/use-retry-countdown.mjs +0 -49
  146. package/build-module/components/sync-connection-modal/use-retry-countdown.mjs.map +0 -7
  147. package/build-types/components/sync-connection-modal/index.d.ts +0 -8
  148. package/build-types/components/sync-connection-modal/index.d.ts.map +0 -1
  149. package/build-types/components/sync-connection-modal/use-retry-countdown.d.ts +0 -9
  150. package/build-types/components/sync-connection-modal/use-retry-countdown.d.ts.map +0 -1
  151. package/src/components/sync-connection-modal/index.js +0 -206
  152. package/src/components/sync-connection-modal/style.scss +0 -14
  153. package/src/components/sync-connection-modal/use-retry-countdown.js +0 -70
  154. package/src/utils/sync-error-messages.js +0 -58
@@ -1,13 +1,7 @@
1
- /**
2
- * WordPress dependencies
3
- */
4
- import { useSelect } from '@wordpress/data';
5
- import { store as coreStore } from '@wordpress/core-data';
6
-
7
1
  /**
8
2
  * Internal dependencies
9
3
  */
10
- import { store as editorStore } from '../../store';
4
+ import { usePostTemplatePanelMode } from './hooks';
11
5
  import ClassicThemeControl from './classic-theme';
12
6
  import BlockThemeControl from './block-theme';
13
7
 
@@ -17,60 +11,12 @@ import BlockThemeControl from './block-theme';
17
11
  * @return {React.ReactNode} The rendered PostTemplatePanel component.
18
12
  */
19
13
  export default function PostTemplatePanel() {
20
- const { templateId, isBlockTheme } = useSelect( ( select ) => {
21
- const { getCurrentTemplateId, getEditorSettings } =
22
- select( editorStore );
23
- return {
24
- templateId: getCurrentTemplateId(),
25
- isBlockTheme: getEditorSettings().__unstableIsBlockBasedTheme,
26
- };
27
- }, [] );
28
-
29
- const isVisible = useSelect( ( select ) => {
30
- const postTypeSlug = select( editorStore ).getCurrentPostType();
31
- const postType = select( coreStore ).getPostType( postTypeSlug );
32
- if ( ! postType?.viewable ) {
33
- return false;
34
- }
35
-
36
- const settings = select( editorStore ).getEditorSettings();
37
- const hasTemplates =
38
- !! settings.availableTemplates &&
39
- Object.keys( settings.availableTemplates ).length > 0;
40
- if ( hasTemplates ) {
41
- return true;
42
- }
43
-
44
- if ( ! settings.supportsTemplateMode ) {
45
- return false;
46
- }
47
-
48
- const canCreateTemplates =
49
- select( coreStore ).canUser( 'create', {
50
- kind: 'postType',
51
- name: 'wp_template',
52
- } ) ?? false;
53
- return canCreateTemplates;
54
- }, [] );
55
-
56
- const canViewTemplates = useSelect(
57
- ( select ) => {
58
- return isVisible
59
- ? select( coreStore ).canUser( 'read', {
60
- kind: 'postType',
61
- name: 'wp_template',
62
- } )
63
- : false;
64
- },
65
- [ isVisible ]
66
- );
67
-
68
- if ( ( ! isBlockTheme || ! canViewTemplates ) && isVisible ) {
14
+ const mode = usePostTemplatePanelMode();
15
+ if ( mode === 'classic' ) {
69
16
  return <ClassicThemeControl />;
70
17
  }
71
-
72
- if ( isBlockTheme && !! templateId ) {
73
- return <BlockThemeControl id={ templateId } />;
18
+ if ( mode === 'block-theme' ) {
19
+ return <BlockThemeControl />;
74
20
  }
75
21
  return null;
76
22
  }
@@ -25,6 +25,7 @@ import inserterMediaCategories from '../media-categories';
25
25
  import { mediaUpload } from '../../utils';
26
26
  import mediaUploadOnSuccess from '../../utils/media-upload/on-success';
27
27
  import { default as mediaSideload } from '../../utils/media-sideload';
28
+ import { default as mediaFinalize } from '../../utils/media-finalize';
28
29
  import { store as editorStore } from '../../store';
29
30
  import { unlock } from '../../lock-unlock';
30
31
  import { useGlobalStylesContext } from '../global-styles-provider';
@@ -343,6 +344,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
343
344
  ? mediaUploadOnSuccess
344
345
  : undefined,
345
346
  mediaSideload: hasUploadPermissions ? mediaSideload : undefined,
347
+ mediaFinalize: hasUploadPermissions ? mediaFinalize : undefined,
346
348
  __experimentalBlockPatterns: blockPatterns,
347
349
  [ selectBlockPatternsKey ]: ( select ) => {
348
350
  const { hasFinishedResolution, getBlockPatternsForPostType } =
@@ -0,0 +1,74 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { PanelBody } from '@wordpress/components';
5
+ import { store as blockEditorStore } from '@wordpress/block-editor';
6
+ import { useSelect } from '@wordpress/data';
7
+ import { __ } from '@wordpress/i18n';
8
+
9
+ /**
10
+ * Internal dependencies
11
+ */
12
+ import PostPanelRow from '../post-panel-row';
13
+
14
+ /**
15
+ * Panel that shows changed block attributes for the selected block
16
+ * when viewing revisions.
17
+ */
18
+ export default function RevisionBlockDiffPanel() {
19
+ const { block } = useSelect( ( select ) => {
20
+ const { getSelectedBlock } = select( blockEditorStore );
21
+ return {
22
+ block: getSelectedBlock(),
23
+ };
24
+ }, [] );
25
+
26
+ if ( ! block ) {
27
+ return null;
28
+ }
29
+
30
+ const diffInfo = block.attributes?.__revisionDiffStatus;
31
+ const changedAttributes = diffInfo?.changedAttributes;
32
+
33
+ if ( ! changedAttributes ) {
34
+ return null;
35
+ }
36
+
37
+ const fields = Object.entries( changedAttributes ).map(
38
+ ( [ key, parts ] ) => (
39
+ <PostPanelRow key={ key } label={ key }>
40
+ <span className="editor-revision-fields-diff__value">
41
+ { parts.map( ( part, index ) => {
42
+ if ( part.added ) {
43
+ return (
44
+ <ins
45
+ key={ index }
46
+ className="editor-revision-fields-diff__added"
47
+ >
48
+ { part.value }
49
+ </ins>
50
+ );
51
+ }
52
+ if ( part.removed ) {
53
+ return (
54
+ <del
55
+ key={ index }
56
+ className="editor-revision-fields-diff__removed"
57
+ >
58
+ { part.value }
59
+ </del>
60
+ );
61
+ }
62
+ return <span key={ index }>{ part.value }</span>;
63
+ } ) }
64
+ </span>
65
+ </PostPanelRow>
66
+ )
67
+ );
68
+
69
+ return (
70
+ <PanelBody title={ __( 'Changed attributes' ) } initialOpen>
71
+ { fields }
72
+ </PanelBody>
73
+ );
74
+ }
@@ -0,0 +1,13 @@
1
+ .editor-revision-fields-diff__value {
2
+ word-break: break-word;
3
+ }
4
+
5
+ .editor-revision-fields-diff__added {
6
+ background-color: color-mix(in srgb, currentColor 5%, #00a32a 15%);
7
+ text-decoration: none;
8
+ }
9
+
10
+ .editor-revision-fields-diff__removed {
11
+ text-decoration: line-through;
12
+ color: #d63638;
13
+ }
@@ -17,6 +17,7 @@ import { store as editorStore } from '../../store';
17
17
  import PostTrash from '../post-trash';
18
18
  import usePostFields from '../post-fields';
19
19
  import { unlock } from '../../lock-unlock';
20
+ import { usePostTemplatePanelMode } from '../post-template/hooks';
20
21
 
21
22
  const form = {
22
23
  layout: {
@@ -93,7 +94,8 @@ export default function DataFormPostSummary( { onActionPerformed } ) {
93
94
  [ postType, postId ]
94
95
  );
95
96
 
96
- // Fetch classic theme templates from editor settings.
97
+ const templatePanelMode = usePostTemplatePanelMode();
98
+
97
99
  const availableTemplates = useSelect( ( select ) => {
98
100
  if ( select( coreDataStore ).getCurrentTheme()?.is_block_theme ) {
99
101
  return null;
@@ -119,18 +121,40 @@ export default function DataFormPostSummary( { onActionPerformed } ) {
119
121
  const _fields = usePostFields( { postType } );
120
122
  const fields = useMemo(
121
123
  () =>
122
- _fields?.map( ( field ) => {
123
- if ( field.id === 'status' ) {
124
- return {
125
- ...field,
126
- elements: field.elements.filter(
127
- ( element ) => element.value !== 'trash'
128
- ),
129
- };
130
- }
131
- return field;
132
- } ),
133
- [ _fields ]
124
+ _fields
125
+ ?.map( ( field ) => {
126
+ if ( field.id === 'status' ) {
127
+ return {
128
+ ...field,
129
+ elements: field.elements.filter(
130
+ ( element ) => element.value !== 'trash'
131
+ ),
132
+ };
133
+ }
134
+ if ( field.id === 'template' ) {
135
+ // `usePostTemplatePanelMode` is reused in the Post Template panel to match
136
+ // the existing behavior. If the panel rendered nothing we should exclude the
137
+ // template field from the form.
138
+ if ( ! templatePanelMode ) {
139
+ return null;
140
+ }
141
+ // In classic themes without available templates we need to make the field read-only.
142
+ if (
143
+ templatePanelMode === 'classic' &&
144
+ Object.keys( availableTemplates ?? {} ).length === 0
145
+ ) {
146
+ return {
147
+ ...field,
148
+ readOnly: true,
149
+ render: () => __( 'Default template' ),
150
+ };
151
+ }
152
+ return field;
153
+ }
154
+ return field;
155
+ } )
156
+ .filter( Boolean ),
157
+ [ _fields, templatePanelMode, availableTemplates ]
134
158
  );
135
159
 
136
160
  const onChange = ( edits ) => {
@@ -32,6 +32,7 @@ import SidebarHeader from './header';
32
32
  import TemplateContentPanel from '../template-content-panel';
33
33
  import TemplatePartContentPanel from '../template-part-content-panel';
34
34
  import { MediaMetadataPanel } from '../media';
35
+ import RevisionBlockDiffPanel from '../revision-block-diff';
35
36
  import useAutoSwitchEditorSidebars from '../provider/use-auto-switch-editor-sidebars';
36
37
  import { sidebars } from './constants';
37
38
  import { unlock } from '../../lock-unlock';
@@ -144,6 +145,7 @@ const SidebarContent = ( {
144
145
  { ! isAttachment && (
145
146
  <Tabs.TabPanel tabId={ sidebars.block } focusable={ false }>
146
147
  <BlockInspector />
148
+ { isRevisionsMode && <RevisionBlockDiffPanel /> }
147
149
  </Tabs.TabPanel>
148
150
  ) }
149
151
  </Tabs.Context.Provider>
@@ -0,0 +1,265 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useSelect, select } from '@wordpress/data';
5
+ import { useCopyToClipboard } from '@wordpress/compose';
6
+ // @ts-ignore No exported types.
7
+ import { serialize } from '@wordpress/blocks';
8
+ import {
9
+ store as coreDataStore,
10
+ privateApis as coreDataPrivateApis,
11
+ type ConnectionError,
12
+ } from '@wordpress/core-data';
13
+ // @ts-expect-error - No type declarations available for @wordpress/block-editor
14
+ // prettier-ignore
15
+ import { privateApis, store as blockEditorStore } from '@wordpress/block-editor';
16
+ import {
17
+ Button,
18
+ Modal,
19
+ withFilters,
20
+ __experimentalHStack as HStack,
21
+ __experimentalVStack as VStack,
22
+ } from '@wordpress/components';
23
+ import { useState, useEffect } from '@wordpress/element';
24
+ import { __, sprintf, _n } from '@wordpress/i18n';
25
+
26
+ /**
27
+ * Internal dependencies
28
+ */
29
+ import { getSyncErrorMessages } from '../../utils/sync-error-messages';
30
+ import { store as editorStore } from '../../store';
31
+ import { unlock } from '../../lock-unlock';
32
+ import { useRetryCountdown } from './use-retry-countdown';
33
+
34
+ const { BlockCanvasCover } = unlock( privateApis );
35
+ const { retrySyncConnection } = unlock( coreDataPrivateApis );
36
+
37
+ // Debounce time for initial disconnected status to allow connection to establish.
38
+ const INITIAL_DISCONNECTED_DEBOUNCE_MS = 5000;
39
+
40
+ // Debounce time for showing the disconnect dialog after the intial connection,
41
+ // allowing brief network interruptions to resolve.
42
+ const DISCONNECTED_DEBOUNCE_MS = 2000;
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
+ /**
179
+ * Sync connection modal that displays when any entity reports a disconnection.
180
+ * Uses BlockCanvasCover.Fill to render in the block canvas.
181
+ *
182
+ * @return The modal component or null if not disconnected.
183
+ */
184
+ export function SyncConnectionErrorModal() {
185
+ const [ hasInitialized, setHasInitialized ] = useState( false );
186
+ const [ showModal, setShowModal ] = useState( false );
187
+
188
+ const { connectionStatus, isCollaborationEnabled, postType } = useSelect(
189
+ ( selectFn ) => {
190
+ const currentPostType =
191
+ selectFn( editorStore ).getCurrentPostType();
192
+ return {
193
+ connectionStatus:
194
+ selectFn( coreDataStore ).getSyncConnectionStatus() || null,
195
+ isCollaborationEnabled:
196
+ selectFn(
197
+ editorStore
198
+ ).isCollaborationEnabledForCurrentPost(),
199
+ postType: currentPostType
200
+ ? selectFn( coreDataStore ).getPostType( currentPostType )
201
+ : null,
202
+ };
203
+ },
204
+ []
205
+ );
206
+
207
+ const { onManualRetry, secondsRemaining } =
208
+ useRetryCountdown( connectionStatus );
209
+
210
+ const isConnected = 'connected' === connectionStatus?.status;
211
+
212
+ // Set hasInitialized after a debounce to give extra time on initial load.
213
+ useEffect( () => {
214
+ const timeout = setTimeout( () => {
215
+ setHasInitialized( true );
216
+ }, INITIAL_DISCONNECTED_DEBOUNCE_MS );
217
+
218
+ return () => clearTimeout( timeout );
219
+ }, [] );
220
+
221
+ useEffect( () => {
222
+ if ( isConnected ) {
223
+ setShowModal( false );
224
+ return;
225
+ }
226
+
227
+ const timeout = setTimeout( () => {
228
+ setShowModal( true );
229
+ }, DISCONNECTED_DEBOUNCE_MS );
230
+
231
+ return () => clearTimeout( timeout );
232
+ }, [ isConnected ] );
233
+
234
+ if ( ! isCollaborationEnabled || ! hasInitialized || ! showModal ) {
235
+ return null;
236
+ }
237
+
238
+ const error =
239
+ connectionStatus && 'error' in connectionStatus
240
+ ? connectionStatus?.error
241
+ : undefined;
242
+ const manualRetry =
243
+ connectionStatus &&
244
+ 'canManuallyRetry' in connectionStatus &&
245
+ connectionStatus.canManuallyRetry
246
+ ? () => {
247
+ onManualRetry();
248
+ retrySyncConnection();
249
+ }
250
+ : undefined;
251
+ const messages = getSyncErrorMessages( error );
252
+
253
+ return (
254
+ <BlockCanvasCover.Fill>
255
+ <FilteredSyncConnectionErrorModal
256
+ description={ messages.description }
257
+ error={ error }
258
+ manualRetry={ manualRetry }
259
+ postType={ postType }
260
+ secondsRemainingUntilAutoRetry={ secondsRemaining }
261
+ title={ messages.title }
262
+ />
263
+ </BlockCanvasCover.Fill>
264
+ );
265
+ }
@@ -0,0 +1,14 @@
1
+ @use "@wordpress/base-styles/colors" as *;
2
+ @use "@wordpress/base-styles/z-index" as *;
3
+
4
+ .editor-sync-connection-error-modal {
5
+ z-index: z-index(".editor-sync-connection-error-modal");
6
+ }
7
+
8
+ .editor-sync-connection-error-modal p {
9
+ margin: 0;
10
+ }
11
+
12
+ .editor-sync-connection-error-modal__retry-countdown {
13
+ color: $gray-700;
14
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import type { ConnectionStatus } from '@wordpress/core-data';
5
+ import { useState, useEffect } from '@wordpress/element';
6
+
7
+ interface UseRetryCountdownResult {
8
+ onManualRetry: () => void;
9
+ secondsRemaining?: number;
10
+ }
11
+
12
+ export function useRetryCountdown(
13
+ connectionStatus?: ConnectionStatus | null
14
+ ): UseRetryCountdownResult {
15
+ const [ secondsRemaining, setSecondsRemaining ] = useState< number >();
16
+
17
+ useEffect( () => {
18
+ if ( ! connectionStatus ) {
19
+ return;
20
+ }
21
+
22
+ // Only clear countdown when explicitly connected.
23
+ if ( 'connected' === connectionStatus.status ) {
24
+ setSecondsRemaining( undefined );
25
+ return;
26
+ }
27
+
28
+ // For transient states (e.g. 'connecting' during a retry attempt)
29
+ // or when retryInMs is not yet available, keep the previous
30
+ // countdown value to avoid a brief flash.
31
+ if (
32
+ 'disconnected' !== connectionStatus.status ||
33
+ ! connectionStatus.willAutoRetryInMs
34
+ ) {
35
+ return;
36
+ }
37
+
38
+ const { willAutoRetryInMs: retryInMs } = connectionStatus;
39
+ const retryAt = Date.now() + retryInMs;
40
+ setSecondsRemaining( Math.ceil( retryInMs / 1000 ) );
41
+
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 );
47
+ }
48
+ }, 1000 );
49
+
50
+ return () => clearInterval( intervalId );
51
+ }, [ connectionStatus ] );
52
+
53
+ return {
54
+ onManualRetry: () => setSecondsRemaining( 0 ),
55
+ secondsRemaining,
56
+ };
57
+ }
@@ -41,7 +41,7 @@ import {
41
41
  import { useZoomOutModeExit } from './use-zoom-out-mode-exit';
42
42
  import { usePaddingAppender } from './use-padding-appender';
43
43
  import { useEditContentOnlySectionExit } from './use-edit-content-only-section-exit';
44
- import { SyncConnectionModal } from '../sync-connection-modal';
44
+ import { SyncConnectionErrorModal } from '../sync-connection-error-modal';
45
45
 
46
46
  const {
47
47
  LayoutStyle,
@@ -427,7 +427,7 @@ function VisualEditor( {
427
427
  }
428
428
  ) }
429
429
  >
430
- <SyncConnectionModal />
430
+ <SyncConnectionErrorModal />
431
431
  <ResizableEditor enableResizing={ enableResizing } height="100%">
432
432
  <BlockCanvas
433
433
  shouldIframe={ ! disableIframe }
@@ -189,10 +189,7 @@ export const savePost =
189
189
  }
190
190
 
191
191
  const content = select.getEditedPostContent();
192
-
193
- if ( ! options.isAutosave ) {
194
- dispatch.editPost( { content }, { undoIgnore: true } );
195
- }
192
+ dispatch.editPost( { content }, { undoIgnore: true } );
196
193
 
197
194
  const previousRecord = select.getCurrentPost();
198
195
  let edits = {
package/src/style.scss CHANGED
@@ -39,13 +39,14 @@
39
39
  @use "./components/post-panel-section/style.scss" as *;
40
40
  @use "./components/post-publish-panel/style.scss" as *;
41
41
  @use "./components/post-revisions-preview/style.scss" as *;
42
+ @use "./components/revision-block-diff/style.scss" as *;
42
43
  @use "./components/post-saved-state/style.scss" as *;
43
44
  @use "./components/post-schedule/style.scss" as *;
44
45
  @use "./components/post-status/style.scss" as *;
45
46
  @use "./components/post-sticky/style.scss" as *;
46
47
  @use "./components/post-sync-status/style.scss" as *;
47
48
  @use "./components/post-taxonomies/style.scss" as *;
48
- @use "./components/sync-connection-modal/style.scss" as *;
49
+ @use "./components/sync-connection-error-modal/style.scss" as *;
49
50
  @use "./components/post-template/style.scss" as *;
50
51
  @use "./components/post-text-editor/style.scss" as *;
51
52
  @use "./components/post-title/style.scss" as *;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import apiFetch from '@wordpress/api-fetch';
5
+
6
+ export default async function mediaFinalize( id ) {
7
+ await apiFetch( {
8
+ path: `/wp/v2/media/${ id }/finalize`,
9
+ method: 'POST',
10
+ } );
11
+ }