@wordpress/editor 14.7.0 → 14.7.1-next.5368f64a9.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 (146) hide show
  1. package/README.md +4 -0
  2. package/build/bindings/post-meta.js +2 -1
  3. package/build/bindings/post-meta.js.map +1 -1
  4. package/build/components/editor/index.js +2 -2
  5. package/build/components/editor/index.js.map +1 -1
  6. package/build/components/global-styles-provider/index.js +3 -4
  7. package/build/components/global-styles-provider/index.js.map +1 -1
  8. package/build/components/header/index.js +4 -4
  9. package/build/components/header/index.js.map +1 -1
  10. package/build/components/inserter-sidebar/index.js +2 -5
  11. package/build/components/inserter-sidebar/index.js.map +1 -1
  12. package/build/components/post-publish-panel/maybe-upload-media.js +151 -32
  13. package/build/components/post-publish-panel/maybe-upload-media.js.map +1 -1
  14. package/build/components/post-publish-panel/media-util.js +79 -0
  15. package/build/components/post-publish-panel/media-util.js.map +1 -0
  16. package/build/components/post-trash/check.js +2 -1
  17. package/build/components/post-trash/check.js.map +1 -1
  18. package/build/components/post-trash/index.js +22 -8
  19. package/build/components/post-trash/index.js.map +1 -1
  20. package/build/components/preferences-modal/index.js +28 -1
  21. package/build/components/preferences-modal/index.js.map +1 -1
  22. package/build/components/provider/index.js +34 -8
  23. package/build/components/provider/index.js.map +1 -1
  24. package/build/components/provider/use-block-editor-settings.js +9 -4
  25. package/build/components/provider/use-block-editor-settings.js.map +1 -1
  26. package/build/components/sidebar/post-summary.js +3 -0
  27. package/build/components/sidebar/post-summary.js.map +1 -1
  28. package/build/components/start-page-options/index.js +16 -19
  29. package/build/components/start-page-options/index.js.map +1 -1
  30. package/build/components/start-template-options/index.js +2 -4
  31. package/build/components/start-template-options/index.js.map +1 -1
  32. package/build/components/table-of-contents/index.js +2 -4
  33. package/build/components/table-of-contents/index.js.map +1 -1
  34. package/build/components/text-editor/index.js +2 -4
  35. package/build/components/text-editor/index.js.map +1 -1
  36. package/build/components/visual-editor/edit-template-blocks-notification.js +1 -1
  37. package/build/components/visual-editor/edit-template-blocks-notification.js.map +1 -1
  38. package/build/dataviews/actions/reset-post.js +4 -8
  39. package/build/dataviews/actions/reset-post.js.map +1 -1
  40. package/build/dataviews/actions/trash-post.js +4 -8
  41. package/build/dataviews/actions/trash-post.js.map +1 -1
  42. package/build/store/selectors.js +7 -3
  43. package/build/store/selectors.js.map +1 -1
  44. package/build-module/bindings/post-meta.js +2 -1
  45. package/build-module/bindings/post-meta.js.map +1 -1
  46. package/build-module/components/editor/index.js +2 -2
  47. package/build-module/components/editor/index.js.map +1 -1
  48. package/build-module/components/global-styles-provider/index.js +3 -4
  49. package/build-module/components/global-styles-provider/index.js.map +1 -1
  50. package/build-module/components/header/index.js +4 -4
  51. package/build-module/components/header/index.js.map +1 -1
  52. package/build-module/components/inserter-sidebar/index.js +2 -5
  53. package/build-module/components/inserter-sidebar/index.js.map +1 -1
  54. package/build-module/components/post-publish-panel/maybe-upload-media.js +150 -31
  55. package/build-module/components/post-publish-panel/maybe-upload-media.js.map +1 -1
  56. package/build-module/components/post-publish-panel/media-util.js +72 -0
  57. package/build-module/components/post-publish-panel/media-util.js.map +1 -0
  58. package/build-module/components/post-trash/check.js +2 -1
  59. package/build-module/components/post-trash/check.js.map +1 -1
  60. package/build-module/components/post-trash/index.js +23 -11
  61. package/build-module/components/post-trash/index.js.map +1 -1
  62. package/build-module/components/preferences-modal/index.js +28 -1
  63. package/build-module/components/preferences-modal/index.js.map +1 -1
  64. package/build-module/components/provider/index.js +35 -9
  65. package/build-module/components/provider/index.js.map +1 -1
  66. package/build-module/components/provider/use-block-editor-settings.js +9 -4
  67. package/build-module/components/provider/use-block-editor-settings.js.map +1 -1
  68. package/build-module/components/sidebar/post-summary.js +3 -0
  69. package/build-module/components/sidebar/post-summary.js.map +1 -1
  70. package/build-module/components/start-page-options/index.js +17 -21
  71. package/build-module/components/start-page-options/index.js.map +1 -1
  72. package/build-module/components/start-template-options/index.js +2 -4
  73. package/build-module/components/start-template-options/index.js.map +1 -1
  74. package/build-module/components/table-of-contents/index.js +2 -4
  75. package/build-module/components/table-of-contents/index.js.map +1 -1
  76. package/build-module/components/text-editor/index.js +2 -4
  77. package/build-module/components/text-editor/index.js.map +1 -1
  78. package/build-module/components/visual-editor/edit-template-blocks-notification.js +1 -1
  79. package/build-module/components/visual-editor/edit-template-blocks-notification.js.map +1 -1
  80. package/build-module/dataviews/actions/reset-post.js +4 -8
  81. package/build-module/dataviews/actions/reset-post.js.map +1 -1
  82. package/build-module/dataviews/actions/trash-post.js +4 -8
  83. package/build-module/dataviews/actions/trash-post.js.map +1 -1
  84. package/build-module/store/selectors.js +6 -2
  85. package/build-module/store/selectors.js.map +1 -1
  86. package/build-style/style-rtl.css +2 -2
  87. package/build-style/style.css +2 -2
  88. package/build-types/components/global-styles-provider/index.d.ts.map +1 -1
  89. package/build-types/components/header/back-button.d.ts.map +1 -1
  90. package/build-types/components/inserter-sidebar/index.d.ts.map +1 -1
  91. package/build-types/components/more-menu/tools-more-menu-group.d.ts.map +1 -1
  92. package/build-types/components/more-menu/view-more-menu-group.d.ts.map +1 -1
  93. package/build-types/components/plugin-document-setting-panel/index.d.ts.map +1 -1
  94. package/build-types/components/plugin-post-publish-panel/index.d.ts.map +1 -1
  95. package/build-types/components/plugin-post-status-info/index.d.ts.map +1 -1
  96. package/build-types/components/plugin-pre-publish-panel/index.d.ts.map +1 -1
  97. package/build-types/components/post-excerpt/plugin.d.ts.map +1 -1
  98. package/build-types/components/post-publish-panel/maybe-upload-media.d.ts +1 -1
  99. package/build-types/components/post-publish-panel/maybe-upload-media.d.ts.map +1 -1
  100. package/build-types/components/post-publish-panel/media-util.d.ts +20 -0
  101. package/build-types/components/post-publish-panel/media-util.d.ts.map +1 -0
  102. package/build-types/components/post-trash/check.d.ts.map +1 -1
  103. package/build-types/components/post-trash/index.d.ts +4 -1
  104. package/build-types/components/post-trash/index.d.ts.map +1 -1
  105. package/build-types/components/preferences-modal/enable-plugin-document-setting-panel.d.ts.map +1 -1
  106. package/build-types/components/preferences-modal/index.d.ts.map +1 -1
  107. package/build-types/components/provider/index.d.ts.map +1 -1
  108. package/build-types/components/provider/use-block-editor-settings.d.ts.map +1 -1
  109. package/build-types/components/save-publish-panels/index.d.ts.map +1 -1
  110. package/build-types/components/sidebar/post-summary.d.ts.map +1 -1
  111. package/build-types/components/start-page-options/index.d.ts +1 -0
  112. package/build-types/components/start-page-options/index.d.ts.map +1 -1
  113. package/build-types/components/start-template-options/index.d.ts.map +1 -1
  114. package/build-types/components/text-editor/index.d.ts.map +1 -1
  115. package/build-types/components/visual-editor/edit-template-blocks-notification.d.ts.map +1 -1
  116. package/build-types/dataviews/actions/reset-post.d.ts.map +1 -1
  117. package/build-types/dataviews/actions/trash-post.d.ts.map +1 -1
  118. package/build-types/store/selectors.d.ts +8 -8
  119. package/build-types/store/selectors.d.ts.map +1 -1
  120. package/package.json +36 -36
  121. package/src/bindings/post-meta.js +1 -1
  122. package/src/components/editor/index.js +1 -1
  123. package/src/components/global-styles-provider/index.js +11 -7
  124. package/src/components/header/index.js +3 -3
  125. package/src/components/header/style.scss +1 -1
  126. package/src/components/inserter-sidebar/index.js +2 -5
  127. package/src/components/post-publish-panel/maybe-upload-media.js +149 -48
  128. package/src/components/post-publish-panel/media-util.js +87 -0
  129. package/src/components/post-publish-panel/test/media-util.js +118 -0
  130. package/src/components/post-trash/check.js +5 -2
  131. package/src/components/post-trash/index.js +23 -12
  132. package/src/components/preferences-modal/index.js +227 -172
  133. package/src/components/provider/index.js +42 -10
  134. package/src/components/provider/use-block-editor-settings.js +11 -6
  135. package/src/components/sidebar/post-summary.js +4 -0
  136. package/src/components/start-page-options/index.js +28 -26
  137. package/src/components/start-template-options/index.js +1 -2
  138. package/src/components/table-of-contents/index.js +1 -2
  139. package/src/components/text-editor/index.js +1 -2
  140. package/src/components/text-editor/style.scss +1 -1
  141. package/src/components/visual-editor/edit-template-blocks-notification.js +4 -1
  142. package/src/dataviews/actions/reset-post.tsx +2 -4
  143. package/src/dataviews/actions/trash-post.tsx +2 -4
  144. package/src/store/selectors.js +9 -3
  145. package/tsconfig.json +1 -0
  146. package/tsconfig.tsbuildinfo +1 -1
@@ -14,6 +14,11 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
14
14
  import { useState } from '@wordpress/element';
15
15
  import { isBlobURL } from '@wordpress/blob';
16
16
 
17
+ /**
18
+ * Internal dependencies
19
+ */
20
+ import { fetchMedia } from './media-util';
21
+
17
22
  function flattenBlocks( blocks ) {
18
23
  const result = [];
19
24
 
@@ -25,7 +30,53 @@ function flattenBlocks( blocks ) {
25
30
  return result;
26
31
  }
27
32
 
28
- function Image( block ) {
33
+ /**
34
+ * Determine whether a block has external media.
35
+ *
36
+ * Different blocks use different attribute names (and potentially
37
+ * different logic as well) in determining whether the media is
38
+ * present, and whether it's external.
39
+ *
40
+ * @param {{name: string, attributes: Object}} block The block.
41
+ * @return {boolean?} Whether the block has external media
42
+ */
43
+ function hasExternalMedia( block ) {
44
+ if ( block.name === 'core/image' || block.name === 'core/cover' ) {
45
+ return block.attributes.url && ! block.attributes.id;
46
+ }
47
+
48
+ if ( block.name === 'core/media-text' ) {
49
+ return block.attributes.mediaUrl && ! block.attributes.mediaId;
50
+ }
51
+
52
+ return undefined;
53
+ }
54
+
55
+ /**
56
+ * Retrieve media info from a block.
57
+ *
58
+ * Different blocks use different attribute names, so we need this
59
+ * function to normalize things into a consistent naming scheme.
60
+ *
61
+ * @param {{name: string, attributes: Object}} block The block.
62
+ * @return {{url: ?string, alt: ?string, id: ?number}} The media info for the block.
63
+ */
64
+ function getMediaInfo( block ) {
65
+ if ( block.name === 'core/image' || block.name === 'core/cover' ) {
66
+ const { url, alt, id } = block.attributes;
67
+ return { url, alt, id };
68
+ }
69
+
70
+ if ( block.name === 'core/media-text' ) {
71
+ const { mediaUrl: url, mediaAlt: alt, mediaId: id } = block.attributes;
72
+ return { url, alt, id };
73
+ }
74
+
75
+ return {};
76
+ }
77
+
78
+ // Image component to represent a single image in the upload dialog.
79
+ function Image( { clientId, alt, url } ) {
29
80
  const { selectBlock } = useDispatch( blockEditorStore );
30
81
  return (
31
82
  <motion.img
@@ -33,17 +84,17 @@ function Image( block ) {
33
84
  role="button"
34
85
  aria-label={ __( 'Select image block.' ) }
35
86
  onClick={ () => {
36
- selectBlock( block.clientId );
87
+ selectBlock( clientId );
37
88
  } }
38
89
  onKeyDown={ ( event ) => {
39
90
  if ( event.key === 'Enter' || event.key === ' ' ) {
40
- selectBlock( block.clientId );
91
+ selectBlock( clientId );
41
92
  event.preventDefault();
42
93
  }
43
94
  } }
44
- key={ block.clientId }
45
- alt={ block.attributes.alt }
46
- src={ block.attributes.url }
95
+ key={ clientId }
96
+ alt={ alt }
97
+ src={ url }
47
98
  animate={ { opacity: 1 } }
48
99
  exit={ { opacity: 0, scale: 0 } }
49
100
  style={ {
@@ -58,7 +109,7 @@ function Image( block ) {
58
109
  );
59
110
  }
60
111
 
61
- export default function PostFormatPanel() {
112
+ export default function MaybeUploadMediaPanel() {
62
113
  const [ isUploading, setIsUploading ] = useState( false );
63
114
  const [ isAnimating, setIsAnimating ] = useState( false );
64
115
  const [ hadUploadError, setHadUploadError ] = useState( false );
@@ -69,15 +120,14 @@ export default function PostFormatPanel() {
69
120
  } ),
70
121
  []
71
122
  );
72
- const externalImages = flattenBlocks( editorBlocks ).filter(
73
- ( block ) =>
74
- block.name === 'core/image' &&
75
- block.attributes.url &&
76
- ! block.attributes.id
123
+
124
+ // Get a list of blocks with external media.
125
+ const blocksWithExternalMedia = flattenBlocks( editorBlocks ).filter(
126
+ ( block ) => hasExternalMedia( block )
77
127
  );
78
128
  const { updateBlockAttributes } = useDispatch( blockEditorStore );
79
129
 
80
- if ( ! mediaUpload || ! externalImages.length ) {
130
+ if ( ! mediaUpload || ! blocksWithExternalMedia.length ) {
81
131
  return null;
82
132
  }
83
133
 
@@ -88,43 +138,86 @@ export default function PostFormatPanel() {
88
138
  </span>,
89
139
  ];
90
140
 
141
+ /**
142
+ * Update an individual block to point to newly-added library media.
143
+ *
144
+ * Different blocks use different attribute names, so we need this
145
+ * function to ensure we modify the correct attributes for each type.
146
+ *
147
+ * @param {{name: string, attributes: Object}} block The block.
148
+ * @param {{id: number, url: string}} media Media library file info.
149
+ */
150
+ function updateBlockWithUploadedMedia( block, media ) {
151
+ if ( block.name === 'core/image' || block.name === 'core/cover' ) {
152
+ updateBlockAttributes( block.clientId, {
153
+ id: media.id,
154
+ url: media.url,
155
+ } );
156
+ }
157
+
158
+ if ( block.name === 'core/media-text' ) {
159
+ updateBlockAttributes( block.clientId, {
160
+ mediaId: media.id,
161
+ mediaUrl: media.url,
162
+ } );
163
+ }
164
+ }
165
+
166
+ // Handle fetching and uploading all external media in the post.
91
167
  function uploadImages() {
92
168
  setIsUploading( true );
93
169
  setHadUploadError( false );
94
- Promise.all(
95
- externalImages.map( ( image ) =>
96
- window
97
- .fetch(
98
- image.attributes.url.includes( '?' )
99
- ? image.attributes.url
100
- : image.attributes.url + '?'
101
- )
102
- .then( ( response ) => response.blob() )
103
- .then( ( blob ) =>
104
- new Promise( ( resolve, reject ) => {
105
- mediaUpload( {
106
- filesList: [ blob ],
107
- onFileChange: ( [ media ] ) => {
108
- if ( isBlobURL( media.url ) ) {
109
- return;
110
- }
111
-
112
- updateBlockAttributes( image.clientId, {
113
- id: media.id,
114
- url: media.url,
115
- } );
116
- resolve();
117
- },
118
- onError() {
119
- reject();
120
- },
121
- } );
122
- } ).then( () => setIsAnimating( true ) )
123
- )
124
- .catch( () => {
125
- setHadUploadError( true );
126
- } )
170
+
171
+ // Multiple blocks can be using the same URL, so we
172
+ // should ensure we only fetch and upload each of them once.
173
+ const mediaUrls = new Set(
174
+ blocksWithExternalMedia.map( ( block ) => {
175
+ const { url } = getMediaInfo( block );
176
+ return url;
177
+ } )
178
+ );
179
+
180
+ // Create an upload promise for each URL, that we can wait for in all
181
+ // blocks that make use of that media.
182
+ const uploadPromises = Object.fromEntries(
183
+ Object.entries( fetchMedia( [ ...mediaUrls ] ) ).map(
184
+ ( [ url, filePromise ] ) => {
185
+ const uploadPromise = filePromise.then(
186
+ ( blob ) =>
187
+ new Promise( ( resolve, reject ) => {
188
+ mediaUpload( {
189
+ filesList: [ blob ],
190
+ onFileChange: ( [ media ] ) => {
191
+ if ( isBlobURL( media.url ) ) {
192
+ return;
193
+ }
194
+
195
+ resolve( media );
196
+ },
197
+ onError() {
198
+ reject();
199
+ },
200
+ } );
201
+ } )
202
+ );
203
+
204
+ return [ url, uploadPromise ];
205
+ }
127
206
  )
207
+ );
208
+
209
+ // Wait for all blocks to be updated with library media.
210
+ Promise.allSettled(
211
+ blocksWithExternalMedia.map( ( block ) => {
212
+ const { url } = getMediaInfo( block );
213
+
214
+ return uploadPromises[ url ]
215
+ .then( ( media ) =>
216
+ updateBlockWithUploadedMedia( block, media )
217
+ )
218
+ .then( () => setIsAnimating( true ) )
219
+ .catch( () => setHadUploadError( true ) );
220
+ } )
128
221
  ).finally( () => {
129
222
  setIsUploading( false );
130
223
  } );
@@ -147,8 +240,16 @@ export default function PostFormatPanel() {
147
240
  <AnimatePresence
148
241
  onExitComplete={ () => setIsAnimating( false ) }
149
242
  >
150
- { externalImages.map( ( image ) => {
151
- return <Image key={ image.clientId } { ...image } />;
243
+ { blocksWithExternalMedia.map( ( block ) => {
244
+ const { url, alt } = getMediaInfo( block );
245
+ return (
246
+ <Image
247
+ key={ block.clientId }
248
+ clientId={ block.clientId }
249
+ url={ url }
250
+ alt={ alt }
251
+ />
252
+ );
152
253
  } ) }
153
254
  </AnimatePresence>
154
255
  { isUploading || isAnimating ? (
@@ -0,0 +1,87 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { v4 as uuid } from 'uuid';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { getFilename } from '@wordpress/url';
10
+
11
+ /**
12
+ * Generate a list of unique basenames given a list of URLs.
13
+ *
14
+ * We want all basenames to be unique, since sometimes the extension
15
+ * doesn't reflect the mime type, and may end up getting changed by
16
+ * the server, on upload.
17
+ *
18
+ * @param {string[]} urls The list of URLs
19
+ * @return {Record< string, string >} A URL => basename record.
20
+ */
21
+ export function generateUniqueBasenames( urls ) {
22
+ const basenames = new Set();
23
+
24
+ return Object.fromEntries(
25
+ urls.map( ( url ) => {
26
+ // We prefer to match the remote filename, if possible.
27
+ const filename = getFilename( url );
28
+ let basename = '';
29
+
30
+ if ( filename ) {
31
+ const parts = filename.split( '.' );
32
+ if ( parts.length > 1 ) {
33
+ // Assume the last part is the extension.
34
+ parts.pop();
35
+ }
36
+ basename = parts.join( '.' );
37
+ }
38
+
39
+ if ( ! basename ) {
40
+ // It looks like we don't have a basename, so let's use a UUID.
41
+ basename = uuid();
42
+ }
43
+
44
+ if ( basenames.has( basename ) ) {
45
+ // Append a UUID to deduplicate the basename.
46
+ // The server will try to deduplicate on its own if we don't do this,
47
+ // but it may run into a race condition
48
+ // (see https://github.com/WordPress/gutenberg/issues/64899).
49
+ // Deduplicating the filenames before uploading is safer.
50
+ basename = `${ basename }-${ uuid() }`;
51
+ }
52
+
53
+ basenames.add( basename );
54
+
55
+ return [ url, basename ];
56
+ } )
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Fetch a list of URLs, turning those into promises for files with
62
+ * unique filenames.
63
+ *
64
+ * @param {string[]} urls The list of URLs
65
+ * @return {Record< string, Promise< File > >} A URL => File promise record.
66
+ */
67
+ export function fetchMedia( urls ) {
68
+ return Object.fromEntries(
69
+ Object.entries( generateUniqueBasenames( urls ) ).map(
70
+ ( [ url, basename ] ) => {
71
+ const filePromise = window
72
+ .fetch( url.includes( '?' ) ? url : url + '?' )
73
+ .then( ( response ) => response.blob() )
74
+ .then( ( blob ) => {
75
+ // The server will reject the upload if it doesn't have an extension,
76
+ // even though it'll rewrite the file name to match the mime type.
77
+ // Here we provide it with a safe extension to get it past that check.
78
+ return new File( [ blob ], `${ basename }.png`, {
79
+ type: blob.type,
80
+ } );
81
+ } );
82
+
83
+ return [ url, filePromise ];
84
+ }
85
+ )
86
+ );
87
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { generateUniqueBasenames } from '../media-util';
5
+
6
+ describe( 'generateUniqueBasenames', () => {
7
+ it( 'should prefer the original basenames', () => {
8
+ const urls = [
9
+ 'https://example.com/images/image1.jpg',
10
+ 'https://example.com/images/image2.jpg',
11
+ 'https://example.com/images/image3.jpg',
12
+ 'https://example.com/images/image4.jpg',
13
+ ];
14
+
15
+ expect( generateUniqueBasenames( urls ) ).toEqual( {
16
+ 'https://example.com/images/image1.jpg': 'image1',
17
+ 'https://example.com/images/image2.jpg': 'image2',
18
+ 'https://example.com/images/image3.jpg': 'image3',
19
+ 'https://example.com/images/image4.jpg': 'image4',
20
+ } );
21
+ } );
22
+
23
+ it( 'should handle filenames with no extensions', () => {
24
+ const urls = [
25
+ 'https://example.com/images/image1',
26
+ 'https://example.com/images/image2',
27
+ 'https://example.com/images/image3',
28
+ 'https://example.com/images/image4',
29
+ ];
30
+
31
+ expect( generateUniqueBasenames( urls ) ).toEqual( {
32
+ 'https://example.com/images/image1': 'image1',
33
+ 'https://example.com/images/image2': 'image2',
34
+ 'https://example.com/images/image3': 'image3',
35
+ 'https://example.com/images/image4': 'image4',
36
+ } );
37
+ } );
38
+
39
+ it( 'should handle query parameters correctly', () => {
40
+ const urls = [
41
+ 'https://example.com/images/image1.jpg?a=notafile.npg',
42
+ 'https://example.com/images/image2.jpg?a=notafile.npg',
43
+ 'https://example.com/images/image3.jpg?a=notafile.npg',
44
+ 'https://example.com/images/image4.jpg?a=notafile.npg',
45
+ ];
46
+
47
+ expect( generateUniqueBasenames( urls ) ).toEqual( {
48
+ 'https://example.com/images/image1.jpg?a=notafile.npg': 'image1',
49
+ 'https://example.com/images/image2.jpg?a=notafile.npg': 'image2',
50
+ 'https://example.com/images/image3.jpg?a=notafile.npg': 'image3',
51
+ 'https://example.com/images/image4.jpg?a=notafile.npg': 'image4',
52
+ } );
53
+ } );
54
+
55
+ it( 'should deduplicate identical filenames', () => {
56
+ const urls = [
57
+ 'https://example.com/image1/image.jpg',
58
+ 'https://example.com/image2/image.jpg',
59
+ 'https://example.com/image3/image.jpg',
60
+ 'https://example.com/image4/image.jpg',
61
+ ];
62
+
63
+ const results = generateUniqueBasenames( urls );
64
+ const resultLength = Object.entries( results ).length;
65
+ expect( resultLength ).toBe( urls.length );
66
+
67
+ const basenames = new Set( Object.values( results ) );
68
+ expect( basenames.size ).toBe( resultLength );
69
+ } );
70
+
71
+ it( 'should deduplicate identical basenames', () => {
72
+ const urls = [
73
+ 'https://example.com/images/image.jpg',
74
+ 'https://example.com/images/image.png',
75
+ 'https://example.com/images/image.webp',
76
+ 'https://example.com/images/image.avif',
77
+ ];
78
+
79
+ const results = generateUniqueBasenames( urls );
80
+ const resultLength = Object.entries( results ).length;
81
+ expect( resultLength ).toBe( urls.length );
82
+
83
+ const basenames = new Set( Object.values( results ) );
84
+ expect( basenames.size ).toBe( resultLength );
85
+ } );
86
+
87
+ it( 'should deduplicate filenames without extensions', () => {
88
+ const urls = [
89
+ 'https://example.com/image1/image',
90
+ 'https://example.com/image2/image',
91
+ 'https://example.com/image3/image',
92
+ 'https://example.com/image4/image',
93
+ ];
94
+
95
+ const results = generateUniqueBasenames( urls );
96
+ const resultLength = Object.entries( results ).length;
97
+ expect( resultLength ).toBe( urls.length );
98
+
99
+ const basenames = new Set( Object.values( results ) );
100
+ expect( basenames.size ).toBe( resultLength );
101
+ } );
102
+
103
+ it( 'should deduplicate paths with no filename', () => {
104
+ const urls = [
105
+ 'https://example.com/image1/dir/',
106
+ 'https://example.com/image2/dir/',
107
+ 'https://example.com/image3/dir/',
108
+ 'https://example.com/image4/dir/',
109
+ ];
110
+
111
+ const results = generateUniqueBasenames( urls );
112
+ const resultLength = Object.entries( results ).length;
113
+ expect( resultLength ).toBe( urls.length );
114
+
115
+ const basenames = new Set( Object.values( results ) );
116
+ expect( basenames.size ).toBe( resultLength );
117
+ } );
118
+ } );
@@ -8,6 +8,7 @@ import { store as coreStore } from '@wordpress/core-data';
8
8
  * Internal dependencies
9
9
  */
10
10
  import { store as editorStore } from '../../store';
11
+ import { GLOBAL_POST_TYPES } from '../../store/constants';
11
12
 
12
13
  /**
13
14
  * Wrapper component that renders its children only if the post can trashed.
@@ -34,10 +35,12 @@ export default function PostTrashCheck( { children } ) {
34
35
  : false;
35
36
 
36
37
  return {
37
- canTrashPost: ( ! isNew || postId ) && canUserDelete,
38
+ canTrashPost:
39
+ ( ! isNew || postId ) &&
40
+ canUserDelete &&
41
+ ! GLOBAL_POST_TYPES.includes( postType ),
38
42
  };
39
43
  }, [] );
40
-
41
44
  if ( ! canTrashPost ) {
42
45
  return null;
43
46
  }
@@ -1,31 +1,35 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { __ } from '@wordpress/i18n';
4
+ import { __, sprintf } from '@wordpress/i18n';
5
5
  import {
6
6
  Button,
7
7
  __experimentalConfirmDialog as ConfirmDialog,
8
8
  } from '@wordpress/components';
9
- import { useSelect, useDispatch } from '@wordpress/data';
9
+ import { useSelect, useDispatch, useRegistry } from '@wordpress/data';
10
10
  import { useState } from '@wordpress/element';
11
11
 
12
12
  /**
13
13
  * Internal dependencies
14
14
  */
15
15
  import { store as editorStore } from '../../store';
16
+ import PostTrashCheck from './check';
16
17
 
17
18
  /**
18
19
  * Displays the Post Trash Button and Confirm Dialog in the Editor.
19
20
  *
21
+ * @param {?{onActionPerformed: Object}} An object containing the onActionPerformed function.
20
22
  * @return {JSX.Element|null} The rendered PostTrash component.
21
23
  */
22
- export default function PostTrash() {
23
- const { isNew, isDeleting, postId } = useSelect( ( select ) => {
24
+ export default function PostTrash( { onActionPerformed } ) {
25
+ const registry = useRegistry();
26
+ const { isNew, isDeleting, postId, title } = useSelect( ( select ) => {
24
27
  const store = select( editorStore );
25
28
  return {
26
29
  isNew: store.isEditedPostNew(),
27
30
  isDeleting: store.isDeletingPost(),
28
31
  postId: store.getCurrentPostId(),
32
+ title: store.getCurrentPostAttribute( 'title' ),
29
33
  };
30
34
  }, [] );
31
35
  const { trashPost } = useDispatch( editorStore );
@@ -35,13 +39,18 @@ export default function PostTrash() {
35
39
  return null;
36
40
  }
37
41
 
38
- const handleConfirm = () => {
42
+ const handleConfirm = async () => {
39
43
  setShowConfirmDialog( false );
40
- trashPost();
44
+ await trashPost();
45
+ const item = await registry
46
+ .resolveSelect( editorStore )
47
+ .getCurrentPost();
48
+ // After the post is trashed, we want to trigger the onActionPerformed callback, so the user is redirect
49
+ // to the post view depending on if the user is on post editor or site editor.
50
+ onActionPerformed?.( 'move-to-trash', [ item ] );
41
51
  };
42
-
43
52
  return (
44
- <>
53
+ <PostTrashCheck>
45
54
  <Button
46
55
  __next40pxDefaultSize
47
56
  className="editor-post-trash"
@@ -60,12 +69,14 @@ export default function PostTrash() {
60
69
  onConfirm={ handleConfirm }
61
70
  onCancel={ () => setShowConfirmDialog( false ) }
62
71
  confirmButtonText={ __( 'Move to trash' ) }
63
- size="medium"
72
+ size="small"
64
73
  >
65
- { __(
66
- 'Are you sure you want to move this post to the trash?'
74
+ { sprintf(
75
+ // translators: %s: The item's title.
76
+ __( 'Are you sure you want to move "%s" to the trash?' ),
77
+ title
67
78
  ) }
68
79
  </ConfirmDialog>
69
- </>
80
+ </PostTrashCheck>
70
81
  );
71
82
  }