@wordpress/editor 14.49.1 → 14.51.1-next.v.202607070741.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.
- package/CHANGELOG.md +12 -0
- package/build/components/collab-sidebar/format.cjs +83 -0
- package/build/components/collab-sidebar/format.cjs.map +7 -0
- package/build/components/collab-sidebar/hooks.cjs +117 -16
- package/build/components/collab-sidebar/hooks.cjs.map +2 -2
- package/build/components/collab-sidebar/index.cjs +18 -2
- package/build/components/collab-sidebar/index.cjs.map +2 -2
- package/build/components/collab-sidebar/note-highlight-styles.cjs +67 -0
- package/build/components/collab-sidebar/note-highlight-styles.cjs.map +7 -0
- package/build/components/collab-sidebar/utils.cjs +139 -0
- package/build/components/collab-sidebar/utils.cjs.map +2 -2
- package/build/components/collaborators-presence/use-collaborator-notifications.cjs +55 -30
- package/build/components/collaborators-presence/use-collaborator-notifications.cjs.map +2 -2
- package/build/components/commands/index.cjs +99 -3
- package/build/components/commands/index.cjs.map +2 -2
- package/build/components/document-bar/index.cjs +28 -34
- package/build/components/document-bar/index.cjs.map +2 -2
- package/build/components/entities-saved-states/index.cjs +1 -4
- package/build/components/entities-saved-states/index.cjs.map +2 -2
- package/build/components/header/index.cjs +1 -8
- package/build/components/header/index.cjs.map +2 -2
- package/build/components/media-categories/index.cjs +92 -6
- package/build/components/media-categories/index.cjs.map +2 -2
- package/build/components/page-attributes/parent.cjs +0 -1
- package/build/components/page-attributes/parent.cjs.map +2 -2
- package/build/components/post-author/combobox.cjs +0 -1
- package/build/components/post-author/combobox.cjs.map +2 -2
- package/build/components/post-author/select.cjs +0 -1
- package/build/components/post-author/select.cjs.map +2 -2
- package/build/components/post-preview-button/index.cjs +29 -2
- package/build/components/post-preview-button/index.cjs.map +2 -2
- package/build/components/post-publish-panel/maybe-upload-media.cjs +15 -26
- package/build/components/post-publish-panel/maybe-upload-media.cjs.map +3 -3
- package/build/components/post-revisions-preview/revisions-slider.cjs +3 -1
- package/build/components/post-revisions-preview/revisions-slider.cjs.map +2 -2
- package/build/components/post-taxonomies/flat-term-selector.cjs +0 -1
- package/build/components/post-taxonomies/flat-term-selector.cjs.map +2 -2
- package/build/components/post-taxonomies/hierarchical-term-selector.cjs +0 -2
- package/build/components/post-taxonomies/hierarchical-term-selector.cjs.map +2 -2
- package/build/components/post-template/classic-theme.cjs +0 -1
- package/build/components/post-template/classic-theme.cjs.map +2 -2
- package/build/components/preferences-modal/index.cjs +54 -28
- package/build/components/preferences-modal/index.cjs.map +2 -2
- package/build/components/preview-dropdown/index.cjs +38 -9
- package/build/components/preview-dropdown/index.cjs.map +3 -3
- package/build/components/provider/use-block-editor-settings.cjs +23 -9
- package/build/components/provider/use-block-editor-settings.cjs.map +3 -3
- package/build/components/resizable-editor/index.cjs +54 -9
- package/build/components/resizable-editor/index.cjs.map +3 -3
- package/build/components/sidebar/dataform-post-summary.cjs +5 -1
- package/build/components/sidebar/dataform-post-summary.cjs.map +3 -3
- package/build/components/sidebar/index.cjs +29 -38
- package/build/components/sidebar/index.cjs.map +3 -3
- package/build/components/sidebar/post-summary.cjs +0 -13
- package/build/components/sidebar/post-summary.cjs.map +3 -3
- package/build/components/visual-editor/index.cjs +15 -11
- package/build/components/visual-editor/index.cjs.map +3 -3
- package/build/dataviews/store/private-actions.cjs +24 -19
- package/build/dataviews/store/private-actions.cjs.map +2 -2
- package/build/store/actions.cjs +5 -3
- package/build/store/actions.cjs.map +2 -2
- package/build/store/constants.cjs +26 -0
- package/build/store/constants.cjs.map +2 -2
- package/build/store/private-actions.cjs +25 -18
- package/build/store/private-actions.cjs.map +3 -3
- package/build/store/private-selectors.cjs +5 -0
- package/build/store/private-selectors.cjs.map +2 -2
- package/build/store/reducer.cjs +6 -6
- package/build/store/reducer.cjs.map +2 -2
- package/build/store/selectors.cjs +3 -1
- package/build/store/selectors.cjs.map +2 -2
- package/build/utils/device-type.cjs +52 -0
- package/build/utils/device-type.cjs.map +7 -0
- package/build/utils/media-sideload-from-url/index.cjs +63 -0
- package/build/utils/media-sideload-from-url/index.cjs.map +7 -0
- package/build-module/components/collab-sidebar/format.mjs +57 -0
- package/build-module/components/collab-sidebar/format.mjs.map +7 -0
- package/build-module/components/collab-sidebar/hooks.mjs +121 -16
- package/build-module/components/collab-sidebar/hooks.mjs.map +2 -2
- package/build-module/components/collab-sidebar/index.mjs +21 -5
- package/build-module/components/collab-sidebar/index.mjs.map +2 -2
- package/build-module/components/collab-sidebar/note-highlight-styles.mjs +41 -0
- package/build-module/components/collab-sidebar/note-highlight-styles.mjs.map +7 -0
- package/build-module/components/collab-sidebar/utils.mjs +133 -0
- package/build-module/components/collab-sidebar/utils.mjs.map +2 -2
- package/build-module/components/collaborators-presence/use-collaborator-notifications.mjs +55 -30
- package/build-module/components/collaborators-presence/use-collaborator-notifications.mjs.map +2 -2
- package/build-module/components/commands/index.mjs +99 -3
- package/build-module/components/commands/index.mjs.map +2 -2
- package/build-module/components/document-bar/index.mjs +28 -34
- package/build-module/components/document-bar/index.mjs.map +2 -2
- package/build-module/components/entities-saved-states/index.mjs +1 -4
- package/build-module/components/entities-saved-states/index.mjs.map +2 -2
- package/build-module/components/header/index.mjs +1 -13
- package/build-module/components/header/index.mjs.map +2 -2
- package/build-module/components/media-categories/index.mjs +93 -7
- package/build-module/components/media-categories/index.mjs.map +2 -2
- package/build-module/components/page-attributes/parent.mjs +0 -1
- package/build-module/components/page-attributes/parent.mjs.map +2 -2
- package/build-module/components/post-author/combobox.mjs +0 -1
- package/build-module/components/post-author/combobox.mjs.map +2 -2
- package/build-module/components/post-author/select.mjs +0 -1
- package/build-module/components/post-author/select.mjs.map +2 -2
- package/build-module/components/post-preview-button/index.mjs +29 -2
- package/build-module/components/post-preview-button/index.mjs.map +2 -2
- package/build-module/components/post-publish-panel/maybe-upload-media.mjs +19 -27
- package/build-module/components/post-publish-panel/maybe-upload-media.mjs.map +2 -2
- package/build-module/components/post-revisions-preview/revisions-slider.mjs +3 -1
- package/build-module/components/post-revisions-preview/revisions-slider.mjs.map +2 -2
- package/build-module/components/post-taxonomies/flat-term-selector.mjs +0 -1
- package/build-module/components/post-taxonomies/flat-term-selector.mjs.map +2 -2
- package/build-module/components/post-taxonomies/hierarchical-term-selector.mjs +0 -2
- package/build-module/components/post-taxonomies/hierarchical-term-selector.mjs.map +2 -2
- package/build-module/components/post-template/classic-theme.mjs +0 -1
- package/build-module/components/post-template/classic-theme.mjs.map +2 -2
- package/build-module/components/preferences-modal/index.mjs +54 -28
- package/build-module/components/preferences-modal/index.mjs.map +2 -2
- package/build-module/components/preview-dropdown/index.mjs +38 -9
- package/build-module/components/preview-dropdown/index.mjs.map +2 -2
- package/build-module/components/provider/use-block-editor-settings.mjs +23 -9
- package/build-module/components/provider/use-block-editor-settings.mjs.map +2 -2
- package/build-module/components/resizable-editor/index.mjs +55 -10
- package/build-module/components/resizable-editor/index.mjs.map +2 -2
- package/build-module/components/sidebar/dataform-post-summary.mjs +6 -2
- package/build-module/components/sidebar/dataform-post-summary.mjs.map +2 -2
- package/build-module/components/sidebar/index.mjs +29 -42
- package/build-module/components/sidebar/index.mjs.map +2 -2
- package/build-module/components/sidebar/post-summary.mjs +0 -13
- package/build-module/components/sidebar/post-summary.mjs.map +2 -2
- package/build-module/components/visual-editor/index.mjs +16 -13
- package/build-module/components/visual-editor/index.mjs.map +2 -2
- package/build-module/dataviews/store/private-actions.mjs +26 -19
- package/build-module/dataviews/store/private-actions.mjs.map +2 -2
- package/build-module/store/actions.mjs +5 -3
- package/build-module/store/actions.mjs.map +2 -2
- package/build-module/store/constants.mjs +25 -0
- package/build-module/store/constants.mjs.map +2 -2
- package/build-module/store/private-actions.mjs +27 -17
- package/build-module/store/private-actions.mjs.map +2 -2
- package/build-module/store/private-selectors.mjs +4 -0
- package/build-module/store/private-selectors.mjs.map +2 -2
- package/build-module/store/reducer.mjs +5 -5
- package/build-module/store/reducer.mjs.map +2 -2
- package/build-module/store/selectors.mjs +3 -1
- package/build-module/store/selectors.mjs.map +2 -2
- package/build-module/utils/device-type.mjs +25 -0
- package/build-module/utils/device-type.mjs.map +7 -0
- package/build-module/utils/media-sideload-from-url/index.mjs +32 -0
- package/build-module/utils/media-sideload-from-url/index.mjs.map +7 -0
- package/build-style/style-rtl.css +84 -39
- package/build-style/style.css +84 -39
- package/build-types/components/collab-sidebar/format.d.ts +17 -0
- package/build-types/components/collab-sidebar/format.d.ts.map +1 -0
- package/build-types/components/collab-sidebar/hooks.d.ts.map +1 -1
- package/build-types/components/collab-sidebar/index.d.ts.map +1 -1
- package/build-types/components/collab-sidebar/note-highlight-styles.d.ts +31 -0
- package/build-types/components/collab-sidebar/note-highlight-styles.d.ts.map +1 -0
- package/build-types/components/collab-sidebar/utils.d.ts +82 -2
- package/build-types/components/collab-sidebar/utils.d.ts.map +1 -1
- package/build-types/components/collaborators-presence/use-collaborator-notifications.d.ts.map +1 -1
- package/build-types/components/commands/index.d.ts.map +1 -1
- package/build-types/components/document-bar/index.d.ts.map +1 -1
- package/build-types/components/entities-saved-states/index.d.ts.map +1 -1
- package/build-types/components/header/index.d.ts.map +1 -1
- package/build-types/components/media-categories/index.d.ts +12 -3
- package/build-types/components/media-categories/index.d.ts.map +1 -1
- package/build-types/components/page-attributes/parent.d.ts.map +1 -1
- package/build-types/components/post-author/combobox.d.ts.map +1 -1
- package/build-types/components/post-author/select.d.ts.map +1 -1
- package/build-types/components/post-preview-button/index.d.ts.map +1 -1
- package/build-types/components/post-publish-panel/maybe-upload-media.d.ts.map +1 -1
- package/build-types/components/post-revisions-preview/revisions-slider.d.ts.map +1 -1
- package/build-types/components/post-taxonomies/flat-term-selector.d.ts.map +1 -1
- package/build-types/components/post-taxonomies/hierarchical-term-selector.d.ts.map +1 -1
- package/build-types/components/post-template/classic-theme.d.ts.map +1 -1
- package/build-types/components/preview-dropdown/index.d.ts.map +1 -1
- package/build-types/components/provider/use-block-editor-settings.d.ts.map +1 -1
- package/build-types/components/resizable-editor/index.d.ts.map +1 -1
- package/build-types/components/sidebar/dataform-post-summary.d.ts.map +1 -1
- package/build-types/components/sidebar/index.d.ts.map +1 -1
- package/build-types/components/sidebar/post-summary.d.ts.map +1 -1
- package/build-types/components/visual-editor/index.d.ts.map +1 -1
- package/build-types/dataviews/store/private-actions.d.ts.map +1 -1
- package/build-types/store/actions.d.ts.map +1 -1
- package/build-types/store/constants.d.ts +6 -0
- package/build-types/store/constants.d.ts.map +1 -1
- package/build-types/store/index.d.ts +2 -2
- package/build-types/store/private-actions.d.ts +9 -15
- package/build-types/store/private-actions.d.ts.map +1 -1
- package/build-types/store/private-selectors.d.ts +7 -0
- package/build-types/store/private-selectors.d.ts.map +1 -1
- package/build-types/store/reducer.d.ts +6 -7
- package/build-types/store/reducer.d.ts.map +1 -1
- package/build-types/store/selectors.d.ts +1 -1
- package/build-types/store/selectors.d.ts.map +1 -1
- package/build-types/utils/device-type.d.ts +23 -0
- package/build-types/utils/device-type.d.ts.map +1 -0
- package/build-types/utils/get-template-part-icon.d.ts.map +1 -1
- package/build-types/utils/media-sideload-from-url/index.d.ts +18 -0
- package/build-types/utils/media-sideload-from-url/index.d.ts.map +1 -0
- package/package.json +48 -46
- package/src/components/collab-sidebar/format.js +72 -0
- package/src/components/collab-sidebar/hooks.js +203 -22
- package/src/components/collab-sidebar/index.js +19 -5
- package/src/components/collab-sidebar/note-highlight-styles.js +83 -0
- package/src/components/collab-sidebar/style.scss +9 -0
- package/src/components/collab-sidebar/test/note-highlight-styles.js +106 -0
- package/src/components/collab-sidebar/test/utils.js +471 -0
- package/src/components/collab-sidebar/utils.js +225 -2
- package/src/components/collaborators-presence/test/use-collaborator-notifications.ts +265 -262
- package/src/components/collaborators-presence/use-collaborator-notifications.ts +70 -34
- package/src/components/commands/index.js +142 -0
- package/src/components/document-bar/index.js +0 -9
- package/src/components/document-bar/style.scss +0 -17
- package/src/components/entities-saved-states/index.js +1 -5
- package/src/components/header/index.js +1 -15
- package/src/components/header/style.scss +7 -3
- package/src/components/media-categories/index.js +160 -6
- package/src/components/media-categories/test/index.js +202 -0
- package/src/components/page-attributes/parent.js +0 -1
- package/src/components/post-author/combobox.js +0 -1
- package/src/components/post-author/select.js +0 -1
- package/src/components/post-preview-button/index.js +68 -2
- package/src/components/post-publish-panel/maybe-upload-media.js +25 -30
- package/src/components/post-publish-panel/style.scss +1 -1
- package/src/components/post-publish-panel/test/__snapshots__/index.js.snap +11 -127
- package/src/components/post-revisions-preview/revisions-slider.js +4 -1
- package/src/components/post-revisions-preview/test/block-diff.js +2 -2
- package/src/components/post-taxonomies/flat-term-selector.js +0 -1
- package/src/components/post-taxonomies/hierarchical-term-selector.js +0 -2
- package/src/components/post-template/classic-theme.js +0 -1
- package/src/components/preferences-modal/index.js +40 -22
- package/src/components/preferences-modal/test/index.js +83 -0
- package/src/components/preview-dropdown/index.js +42 -3
- package/src/components/provider/use-block-editor-settings.js +42 -7
- package/src/components/resizable-editor/index.js +71 -9
- package/src/components/resizable-editor/style.scss +7 -0
- package/src/components/sidebar/dataform-post-summary.js +13 -1
- package/src/components/sidebar/index.js +32 -47
- package/src/components/sidebar/post-summary.js +0 -17
- package/src/components/styles-canvas/style.scss +1 -2
- package/src/components/visual-editor/index.js +19 -15
- package/src/components/visual-editor/style.scss +11 -4
- package/src/dataviews/store/private-actions.ts +30 -26
- package/src/store/actions.js +6 -3
- package/src/store/constants.ts +45 -0
- package/src/store/private-actions.js +38 -28
- package/src/store/private-selectors.js +10 -0
- package/src/store/reducer.js +7 -9
- package/src/store/selectors.js +3 -1
- package/src/store/test/actions.js +31 -22
- package/src/utils/device-type.js +46 -0
- package/src/utils/media-sideload-from-url/index.js +54 -0
- package/src/utils/media-sideload-from-url/test/index.js +162 -0
- package/build/components/post-publish-panel/media-util.cjs +0 -72
- package/build/components/post-publish-panel/media-util.cjs.map +0 -7
- package/build-module/components/post-publish-panel/media-util.mjs +0 -46
- package/build-module/components/post-publish-panel/media-util.mjs.map +0 -7
- package/build-types/components/post-publish-panel/media-util.d.ts +0 -20
- package/build-types/components/post-publish-panel/media-util.d.ts.map +0 -1
- package/src/components/post-publish-panel/media-util.js +0 -87
- package/src/components/post-publish-panel/test/media-util.js +0 -118
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* WordPress dependencies
|
|
11
11
|
*/
|
|
12
12
|
import { __, sprintf, _x } from '@wordpress/i18n';
|
|
13
|
-
import { resolveSelect } from '@wordpress/data';
|
|
13
|
+
import { dispatch, resolveSelect } from '@wordpress/data';
|
|
14
14
|
import { decodeEntities } from '@wordpress/html-entities';
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -124,14 +124,16 @@ const getOpenverseCaption = ( item ) => {
|
|
|
124
124
|
return _caption.replace( /\s{2}/g, ' ' );
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
const getCoreMediaQuery = ( query = {} ) => ( {
|
|
128
|
+
...query,
|
|
129
|
+
orderBy: !! query?.search ? 'relevance' : 'date',
|
|
130
|
+
} );
|
|
131
|
+
|
|
127
132
|
const coreMediaFetch = async ( query = {} ) => {
|
|
128
133
|
const mediaItems = await resolveSelect( coreStore ).getEntityRecords(
|
|
129
134
|
'postType',
|
|
130
135
|
'attachment',
|
|
131
|
-
|
|
132
|
-
...query,
|
|
133
|
-
orderBy: !! query?.search ? 'relevance' : 'date',
|
|
134
|
-
}
|
|
136
|
+
getCoreMediaQuery( query )
|
|
135
137
|
);
|
|
136
138
|
return mediaItems.map( ( mediaItem ) => ( {
|
|
137
139
|
...mediaItem,
|
|
@@ -142,6 +144,129 @@ const coreMediaFetch = async ( query = {} ) => {
|
|
|
142
144
|
} ) );
|
|
143
145
|
};
|
|
144
146
|
|
|
147
|
+
const getAttachedImagesQuery = ( postId, query = {} ) => ( {
|
|
148
|
+
...query,
|
|
149
|
+
media_type: 'image',
|
|
150
|
+
parent: postId,
|
|
151
|
+
} );
|
|
152
|
+
|
|
153
|
+
const normalizePostId = ( postId ) => {
|
|
154
|
+
const parsedPostId = typeof postId === 'number' ? postId : Number( postId );
|
|
155
|
+
|
|
156
|
+
return Number.isInteger( parsedPostId ) && parsedPostId > 0
|
|
157
|
+
? parsedPostId
|
|
158
|
+
: undefined;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const saveAttachmentParent = ( attachmentId, postId ) =>
|
|
162
|
+
// `throwOnError` so a failed REST write rejects (rather than being silently
|
|
163
|
+
// swallowed), letting the attach/detach handlers surface an error notice
|
|
164
|
+
// instead of a false success.
|
|
165
|
+
dispatch( coreStore ).saveEntityRecord(
|
|
166
|
+
'postType',
|
|
167
|
+
'attachment',
|
|
168
|
+
{
|
|
169
|
+
id: attachmentId,
|
|
170
|
+
post: postId,
|
|
171
|
+
},
|
|
172
|
+
{ throwOnError: true }
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// A selected media item's coarse type is exposed differently by each picker.
|
|
176
|
+
// The classic media modal puts the media type directly on `type` (e.g. 'image').
|
|
177
|
+
// The DataViews-driven modal passes REST attachment records, where `type` is the
|
|
178
|
+
// *post* type ('attachment') and the media type lives in `media_type`
|
|
179
|
+
// ('image'|'file') / `mime_type`. So the REST fields must be read first, with
|
|
180
|
+
// `type` as the classic-modal fallback — otherwise a REST image reads as
|
|
181
|
+
// 'attachment' and gets gated out.
|
|
182
|
+
const getMediaItemType = ( mediaItem ) =>
|
|
183
|
+
mediaItem?.media_type ||
|
|
184
|
+
mediaItem?.mime_type?.split( '/' )[ 0 ] ||
|
|
185
|
+
mediaItem?.type;
|
|
186
|
+
|
|
187
|
+
// The picker's "Upload files" tab accepts any file type, so the selection can
|
|
188
|
+
// include non-images. Gate to images only: a non-image would be reparented to
|
|
189
|
+
// the post but never appear in the image-filtered grid, and would wrongly count
|
|
190
|
+
// toward the "images attached" notice.
|
|
191
|
+
const getImageAttachmentIds = ( mediaItems ) => [
|
|
192
|
+
...new Set(
|
|
193
|
+
( Array.isArray( mediaItems ) ? mediaItems : [ mediaItems ] )
|
|
194
|
+
.filter(
|
|
195
|
+
( mediaItem ) => getMediaItemType( mediaItem ) === 'image'
|
|
196
|
+
)
|
|
197
|
+
.map( ( mediaItem ) => mediaItem?.id )
|
|
198
|
+
.filter( Boolean )
|
|
199
|
+
),
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const invalidateAttachedImagesQueries = ( postId, query = {} ) => {
|
|
203
|
+
const { invalidateResolution } = dispatch( coreStore );
|
|
204
|
+
// Invalidate the resolution backing the visible grid so it refetches after
|
|
205
|
+
// an attach/detach and reflects the updated set of attached images. The tab
|
|
206
|
+
// is always shown (via `emptyMessage`), so there's no separate visibility
|
|
207
|
+
// probe to invalidate.
|
|
208
|
+
invalidateResolution( 'getEntityRecords', [
|
|
209
|
+
'postType',
|
|
210
|
+
'attachment',
|
|
211
|
+
getCoreMediaQuery( getAttachedImagesQuery( postId, query ) ),
|
|
212
|
+
] );
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Builds the "Attachments" media category for a given post. It behaves like
|
|
217
|
+
* any other inserter media source (e.g. Openverse): it appears in the tab list
|
|
218
|
+
* and renders through the shared media panel. In addition to `fetch`, it exposes
|
|
219
|
+
* optional `attach`/`detach`/`invalidate` capabilities that the shared panel
|
|
220
|
+
* picks up to offer an "Attach images" button and a per-item "Detach from post"
|
|
221
|
+
* action in the same dropdown Openverse uses for "Report image".
|
|
222
|
+
*
|
|
223
|
+
* @param {number} postId The current post id.
|
|
224
|
+
* @param {string|null} [typeLabel] The post type's singular label to use in copy (e.g. "Page"),
|
|
225
|
+
* or null to fall back to the generic "post".
|
|
226
|
+
* @return {InserterMediaCategory} The Attachments media category.
|
|
227
|
+
*/
|
|
228
|
+
const getAttachedImagesCategory = ( postId, typeLabel ) => ( {
|
|
229
|
+
name: 'attached-images',
|
|
230
|
+
labels: {
|
|
231
|
+
name: __( 'Attached images' ),
|
|
232
|
+
search_items: __( 'Search attachments' ),
|
|
233
|
+
},
|
|
234
|
+
mediaType: 'image',
|
|
235
|
+
// The post type's singular label (e.g. "Page"), threaded through so the
|
|
236
|
+
// shared panel can word its attach/detach copy for the current post type.
|
|
237
|
+
postTypeLabel: typeLabel,
|
|
238
|
+
// Empty-state message. Providing this also keeps the source in the tab list
|
|
239
|
+
// when it has no items, so it stays discoverable and the first image can be
|
|
240
|
+
// attached even with none yet.
|
|
241
|
+
emptyMessage: typeLabel
|
|
242
|
+
? sprintf(
|
|
243
|
+
// translators: %s: Name of the post type e.g: "Page".
|
|
244
|
+
__( 'No images attached to this %s.' ),
|
|
245
|
+
typeLabel
|
|
246
|
+
)
|
|
247
|
+
: __( 'No images attached to this post.' ),
|
|
248
|
+
async fetch( query = {} ) {
|
|
249
|
+
return coreMediaFetch( getAttachedImagesQuery( postId, query ) );
|
|
250
|
+
},
|
|
251
|
+
async attach( mediaItems ) {
|
|
252
|
+
const attachmentIds = getImageAttachmentIds( mediaItems );
|
|
253
|
+
|
|
254
|
+
await Promise.all(
|
|
255
|
+
attachmentIds.map( ( attachmentId ) =>
|
|
256
|
+
saveAttachmentParent( attachmentId, postId )
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
return attachmentIds.length;
|
|
261
|
+
},
|
|
262
|
+
async detach( mediaItem ) {
|
|
263
|
+
await saveAttachmentParent( mediaItem.id, 0 );
|
|
264
|
+
},
|
|
265
|
+
invalidate( query = {} ) {
|
|
266
|
+
invalidateAttachedImagesQueries( postId, query );
|
|
267
|
+
},
|
|
268
|
+
} );
|
|
269
|
+
|
|
145
270
|
/** @type {InserterMediaCategory[]} */
|
|
146
271
|
const inserterMediaCategories = [
|
|
147
272
|
{
|
|
@@ -226,4 +351,33 @@ const inserterMediaCategories = [
|
|
|
226
351
|
},
|
|
227
352
|
];
|
|
228
353
|
|
|
229
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Returns the inserter media categories for a given post. The "Attachments"
|
|
356
|
+
* category is prepended only when editing real, front-end-rendered content
|
|
357
|
+
* (posts, pages, public custom post types). It is omitted for synced patterns,
|
|
358
|
+
* navigation menus and templates, which aren't the entity that actually gets
|
|
359
|
+
* rendered, so attaching media to them is meaningless.
|
|
360
|
+
*
|
|
361
|
+
* @param {number|string} postId The current post id.
|
|
362
|
+
* @param {string} [viewablePostTypeLabel] Singular label of the post type, set only when it is front-end viewable (post, page, public CPT).
|
|
363
|
+
* @return {InserterMediaCategory[]} The inserter media categories.
|
|
364
|
+
*/
|
|
365
|
+
export default function getInserterMediaCategories(
|
|
366
|
+
postId,
|
|
367
|
+
viewablePostTypeLabel
|
|
368
|
+
) {
|
|
369
|
+
const currentPostId = normalizePostId( postId );
|
|
370
|
+
|
|
371
|
+
// A falsy label means either a non-viewable post type (synced pattern,
|
|
372
|
+
// navigation, template) or that the record hasn't resolved yet — in both
|
|
373
|
+
// cases the category is omitted. A numeric id is also required since it
|
|
374
|
+
// backs the attachment `parent` query.
|
|
375
|
+
if ( ! currentPostId || ! viewablePostTypeLabel ) {
|
|
376
|
+
return inserterMediaCategories;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return [
|
|
380
|
+
getAttachedImagesCategory( currentPostId, viewablePostTypeLabel ),
|
|
381
|
+
...inserterMediaCategories,
|
|
382
|
+
];
|
|
383
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { dispatch, resolveSelect } from '@wordpress/data';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import getInserterMediaCategories from '..';
|
|
10
|
+
|
|
11
|
+
jest.mock( '@wordpress/data', () => ( {
|
|
12
|
+
dispatch: jest.fn(),
|
|
13
|
+
resolveSelect: jest.fn(),
|
|
14
|
+
} ) );
|
|
15
|
+
|
|
16
|
+
jest.mock( '@wordpress/core-data', () => ( {
|
|
17
|
+
store: 'core',
|
|
18
|
+
} ) );
|
|
19
|
+
|
|
20
|
+
describe( 'getInserterMediaCategories', () => {
|
|
21
|
+
beforeEach( () => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
it( 'does not include attached images for non-numeric post IDs', () => {
|
|
26
|
+
const categories = getInserterMediaCategories(
|
|
27
|
+
'wp_template//theme//home'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(
|
|
31
|
+
categories.some(
|
|
32
|
+
( category ) => category.name === 'attached-images'
|
|
33
|
+
)
|
|
34
|
+
).toBe( false );
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
it( 'fetches images attached to the current post', async () => {
|
|
38
|
+
const getEntityRecords = jest.fn().mockResolvedValue( [
|
|
39
|
+
{
|
|
40
|
+
id: 10,
|
|
41
|
+
source_url: 'https://example.com/image.jpg',
|
|
42
|
+
alt_text: 'Alt text',
|
|
43
|
+
media_details: {
|
|
44
|
+
sizes: {
|
|
45
|
+
medium: {
|
|
46
|
+
source_url: 'https://example.com/image-medium.jpg',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
caption: {
|
|
51
|
+
raw: 'Caption',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
] );
|
|
55
|
+
resolveSelect.mockReturnValue( { getEntityRecords } );
|
|
56
|
+
|
|
57
|
+
const [ attachedImagesCategory ] = getInserterMediaCategories(
|
|
58
|
+
42,
|
|
59
|
+
'Post'
|
|
60
|
+
);
|
|
61
|
+
const results = await attachedImagesCategory.fetch( { per_page: 20 } );
|
|
62
|
+
|
|
63
|
+
expect( getEntityRecords ).toHaveBeenCalledWith(
|
|
64
|
+
'postType',
|
|
65
|
+
'attachment',
|
|
66
|
+
{
|
|
67
|
+
per_page: 20,
|
|
68
|
+
media_type: 'image',
|
|
69
|
+
parent: 42,
|
|
70
|
+
orderBy: 'date',
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
expect( results ).toEqual( [
|
|
74
|
+
expect.objectContaining( {
|
|
75
|
+
id: 10,
|
|
76
|
+
url: 'https://example.com/image.jpg',
|
|
77
|
+
previewUrl: 'https://example.com/image-medium.jpg',
|
|
78
|
+
alt: 'Alt text',
|
|
79
|
+
caption: 'Caption',
|
|
80
|
+
} ),
|
|
81
|
+
] );
|
|
82
|
+
} );
|
|
83
|
+
|
|
84
|
+
it( 'attaches and detaches attachment records', async () => {
|
|
85
|
+
const saveEntityRecord = jest.fn().mockResolvedValue( {} );
|
|
86
|
+
dispatch.mockReturnValue( { saveEntityRecord } );
|
|
87
|
+
|
|
88
|
+
const [ attachedImagesCategory ] = getInserterMediaCategories(
|
|
89
|
+
42,
|
|
90
|
+
'Post'
|
|
91
|
+
);
|
|
92
|
+
const attachedCount = await attachedImagesCategory.attach( [
|
|
93
|
+
// Classic media modal items expose `type`.
|
|
94
|
+
{ id: 10, type: 'image' },
|
|
95
|
+
{ id: 11, type: 'image' },
|
|
96
|
+
{ id: 10, type: 'image' },
|
|
97
|
+
// Non-image (e.g. a PDF uploaded via the picker) is skipped.
|
|
98
|
+
{ id: 12, type: 'application' },
|
|
99
|
+
{},
|
|
100
|
+
// DataViews-driven modal items are REST attachments: `type` is the
|
|
101
|
+
// post type ('attachment'), and the media type lives in
|
|
102
|
+
// `media_type`/`mime_type`. These must still gate to images.
|
|
103
|
+
{
|
|
104
|
+
id: 13,
|
|
105
|
+
type: 'attachment',
|
|
106
|
+
media_type: 'image',
|
|
107
|
+
mime_type: 'image/png',
|
|
108
|
+
},
|
|
109
|
+
{ id: 14, type: 'attachment', mime_type: 'image/jpeg' },
|
|
110
|
+
{
|
|
111
|
+
id: 15,
|
|
112
|
+
type: 'attachment',
|
|
113
|
+
media_type: 'file',
|
|
114
|
+
mime_type: 'application/pdf',
|
|
115
|
+
},
|
|
116
|
+
] );
|
|
117
|
+
await attachedImagesCategory.detach( { id: 11 } );
|
|
118
|
+
|
|
119
|
+
expect( attachedCount ).toBe( 4 );
|
|
120
|
+
expect( saveEntityRecord ).toHaveBeenCalledWith(
|
|
121
|
+
'postType',
|
|
122
|
+
'attachment',
|
|
123
|
+
{
|
|
124
|
+
id: 10,
|
|
125
|
+
post: 42,
|
|
126
|
+
},
|
|
127
|
+
{ throwOnError: true }
|
|
128
|
+
);
|
|
129
|
+
expect( saveEntityRecord ).toHaveBeenCalledWith(
|
|
130
|
+
'postType',
|
|
131
|
+
'attachment',
|
|
132
|
+
{
|
|
133
|
+
id: 11,
|
|
134
|
+
post: 42,
|
|
135
|
+
},
|
|
136
|
+
{ throwOnError: true }
|
|
137
|
+
);
|
|
138
|
+
expect( saveEntityRecord ).toHaveBeenCalledWith(
|
|
139
|
+
'postType',
|
|
140
|
+
'attachment',
|
|
141
|
+
{
|
|
142
|
+
id: 11,
|
|
143
|
+
post: 0,
|
|
144
|
+
},
|
|
145
|
+
{ throwOnError: true }
|
|
146
|
+
);
|
|
147
|
+
expect( saveEntityRecord ).toHaveBeenCalledWith(
|
|
148
|
+
'postType',
|
|
149
|
+
'attachment',
|
|
150
|
+
{
|
|
151
|
+
id: 13,
|
|
152
|
+
post: 42,
|
|
153
|
+
},
|
|
154
|
+
{ throwOnError: true }
|
|
155
|
+
);
|
|
156
|
+
expect( saveEntityRecord ).toHaveBeenCalledWith(
|
|
157
|
+
'postType',
|
|
158
|
+
'attachment',
|
|
159
|
+
{
|
|
160
|
+
id: 14,
|
|
161
|
+
post: 42,
|
|
162
|
+
},
|
|
163
|
+
{ throwOnError: true }
|
|
164
|
+
);
|
|
165
|
+
expect( saveEntityRecord ).not.toHaveBeenCalledWith(
|
|
166
|
+
'postType',
|
|
167
|
+
'attachment',
|
|
168
|
+
expect.objectContaining( { id: 12 } ),
|
|
169
|
+
expect.anything()
|
|
170
|
+
);
|
|
171
|
+
expect( saveEntityRecord ).not.toHaveBeenCalledWith(
|
|
172
|
+
'postType',
|
|
173
|
+
'attachment',
|
|
174
|
+
expect.objectContaining( { id: 15 } ),
|
|
175
|
+
expect.anything()
|
|
176
|
+
);
|
|
177
|
+
expect( saveEntityRecord ).toHaveBeenCalledTimes( 5 );
|
|
178
|
+
} );
|
|
179
|
+
|
|
180
|
+
it( 'words the empty state from the post type label', () => {
|
|
181
|
+
const [ attachedImagesCategory ] = getInserterMediaCategories(
|
|
182
|
+
42,
|
|
183
|
+
'Page'
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect( attachedImagesCategory.emptyMessage ).toBe(
|
|
187
|
+
'No images attached to this Page.'
|
|
188
|
+
);
|
|
189
|
+
} );
|
|
190
|
+
|
|
191
|
+
it( 'excludes attached images when there is no viewable post type label', () => {
|
|
192
|
+
// The label is empty for non-viewable post types (synced patterns,
|
|
193
|
+
// navigation, templates) and before the record resolves.
|
|
194
|
+
const categories = getInserterMediaCategories( 42, undefined );
|
|
195
|
+
|
|
196
|
+
expect(
|
|
197
|
+
categories.some(
|
|
198
|
+
( category ) => category.name === 'attached-images'
|
|
199
|
+
)
|
|
200
|
+
).toBe( false );
|
|
201
|
+
} );
|
|
202
|
+
} );
|
|
@@ -14,7 +14,7 @@ import { VisuallyHidden } from '@wordpress/ui';
|
|
|
14
14
|
*/
|
|
15
15
|
import { store as editorStore } from '../../store';
|
|
16
16
|
|
|
17
|
-
function
|
|
17
|
+
function buildInterstitialMarkup() {
|
|
18
18
|
let markup = renderToString(
|
|
19
19
|
<div className="editor-post-preview-button__interstitial-message">
|
|
20
20
|
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
|
@@ -95,11 +95,77 @@ function writeInterstitialMessage( targetDocument ) {
|
|
|
95
95
|
*/
|
|
96
96
|
markup = applyFilters( 'editor.PostPreview.interstitialMarkup', markup );
|
|
97
97
|
|
|
98
|
+
return markup;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function writeInterstitialMessage( targetDocument, markup ) {
|
|
98
102
|
targetDocument.write( markup );
|
|
99
103
|
targetDocument.title = __( 'Generating preview…' );
|
|
100
104
|
targetDocument.close();
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Resolves the preview window's `document`, working around
|
|
109
|
+
* `Document-Isolation-Policy` (DIP) isolation.
|
|
110
|
+
*
|
|
111
|
+
* The editor screen is served with `Document-Isolation-Policy:
|
|
112
|
+
* isolate-and-credentialless` to enable cross-origin isolation. This places the
|
|
113
|
+
* editor tab and an already-open preview tab in separate agent clusters, so
|
|
114
|
+
* synchronous access to a reused preview tab's `document` throws a
|
|
115
|
+
* `SecurityError`. Navigating the reused tab back to `about:blank` returns it to
|
|
116
|
+
* the opener's agent cluster and restores access. That navigation is
|
|
117
|
+
* asynchronous and we can't attach a cross-isolation `load` listener, so poll
|
|
118
|
+
* the `document` access (the operation that throws) until it succeeds, up to a
|
|
119
|
+
* short timeout.
|
|
120
|
+
*
|
|
121
|
+
* @param {Window} previewWindow The preview window/tab.
|
|
122
|
+
*
|
|
123
|
+
* @return {?Document} The reachable preview document, or `null` if it never
|
|
124
|
+
* becomes reachable within the timeout.
|
|
125
|
+
*/
|
|
126
|
+
async function getPreviewDocument( previewWindow ) {
|
|
127
|
+
// A freshly opened tab is already on `about:blank` and accessible, so this
|
|
128
|
+
// succeeds on the first preview without any reset.
|
|
129
|
+
try {
|
|
130
|
+
return previewWindow.document;
|
|
131
|
+
} catch {
|
|
132
|
+
// The reused preview tab is isolated from the editor; reset it below.
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
previewWindow.location = 'about:blank';
|
|
136
|
+
|
|
137
|
+
const timeoutMs = 1000;
|
|
138
|
+
const intervalMs = 50;
|
|
139
|
+
const deadline = Date.now() + timeoutMs;
|
|
140
|
+
do {
|
|
141
|
+
await new Promise( ( resolve ) => setTimeout( resolve, intervalMs ) );
|
|
142
|
+
try {
|
|
143
|
+
return previewWindow.document;
|
|
144
|
+
} catch {
|
|
145
|
+
// Navigation to `about:blank` hasn't completed yet; keep polling.
|
|
146
|
+
}
|
|
147
|
+
} while ( Date.now() < deadline );
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Writes the preview interstitial into the preview window, working around
|
|
154
|
+
* `Document-Isolation-Policy` (DIP) isolation.
|
|
155
|
+
*
|
|
156
|
+
* The interstitial is a progressive enhancement: if the document never becomes
|
|
157
|
+
* reachable we simply skip it, and the caller still navigates the preview to the
|
|
158
|
+
* real content.
|
|
159
|
+
*
|
|
160
|
+
* @param {Window} previewWindow The preview window/tab.
|
|
161
|
+
*/
|
|
162
|
+
async function writeInterstitialIntoPreviewWindow( previewWindow ) {
|
|
163
|
+
const previewDocument = await getPreviewDocument( previewWindow );
|
|
164
|
+
if ( previewDocument ) {
|
|
165
|
+
writeInterstitialMessage( previewDocument, buildInterstitialMarkup() );
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
103
169
|
/**
|
|
104
170
|
* Renders a button that opens a new window or tab for the preview,
|
|
105
171
|
* writes the interstitial message to this window, and then navigates
|
|
@@ -168,7 +234,7 @@ export default function PostPreviewButton( {
|
|
|
168
234
|
// https://html.spec.whatwg.org/multipage/interaction.html#dom-window-focus
|
|
169
235
|
previewWindow.focus();
|
|
170
236
|
|
|
171
|
-
|
|
237
|
+
await writeInterstitialIntoPreviewWindow( previewWindow );
|
|
172
238
|
|
|
173
239
|
const link = await __unstableSaveForPreview( { forceIsAutosaveable } );
|
|
174
240
|
|
|
@@ -10,14 +10,18 @@ import {
|
|
|
10
10
|
} from '@wordpress/components';
|
|
11
11
|
import { useSelect, useDispatch } from '@wordpress/data';
|
|
12
12
|
import { __, _x } from '@wordpress/i18n';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
store as blockEditorStore,
|
|
15
|
+
privateApis as blockEditorPrivateApis,
|
|
16
|
+
} from '@wordpress/block-editor';
|
|
14
17
|
import { useState } from '@wordpress/element';
|
|
15
|
-
import { isBlobURL } from '@wordpress/blob';
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Internal dependencies
|
|
19
21
|
*/
|
|
20
|
-
import {
|
|
22
|
+
import { unlock } from '../../lock-unlock';
|
|
23
|
+
|
|
24
|
+
const { mediaSideloadFromUrlKey } = unlock( blockEditorPrivateApis );
|
|
21
25
|
|
|
22
26
|
function flattenBlocks( blocks ) {
|
|
23
27
|
const result = [];
|
|
@@ -113,10 +117,13 @@ export default function MaybeUploadMediaPanel() {
|
|
|
113
117
|
const [ isUploading, setIsUploading ] = useState( false );
|
|
114
118
|
const [ isAnimating, setIsAnimating ] = useState( false );
|
|
115
119
|
const [ hadUploadError, setHadUploadError ] = useState( false );
|
|
116
|
-
const { editorBlocks,
|
|
120
|
+
const { editorBlocks, mediaSideloadFromUrl } = useSelect(
|
|
117
121
|
( select ) => ( {
|
|
118
122
|
editorBlocks: select( blockEditorStore ).getBlocks(),
|
|
119
|
-
|
|
123
|
+
mediaSideloadFromUrl:
|
|
124
|
+
select( blockEditorStore ).getSettings()[
|
|
125
|
+
mediaSideloadFromUrlKey
|
|
126
|
+
],
|
|
120
127
|
} ),
|
|
121
128
|
[]
|
|
122
129
|
);
|
|
@@ -127,7 +134,7 @@ export default function MaybeUploadMediaPanel() {
|
|
|
127
134
|
);
|
|
128
135
|
const { updateBlockAttributes } = useDispatch( blockEditorStore );
|
|
129
136
|
|
|
130
|
-
if ( !
|
|
137
|
+
if ( ! mediaSideloadFromUrl || ! blocksWithExternalMedia.length ) {
|
|
131
138
|
return null;
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -178,32 +185,20 @@ export default function MaybeUploadMediaPanel() {
|
|
|
178
185
|
);
|
|
179
186
|
|
|
180
187
|
// Create an upload promise for each URL, that we can wait for in all
|
|
181
|
-
// blocks that make use of that media.
|
|
188
|
+
// blocks that make use of that media. The server sideloads each URL,
|
|
189
|
+
// so this works even when the editor is cross-origin isolated.
|
|
182
190
|
const uploadPromises = Object.fromEntries(
|
|
183
|
-
|
|
184
|
-
(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if ( isBlobURL( media.url ) ) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
resolve( media );
|
|
196
|
-
},
|
|
197
|
-
onError() {
|
|
198
|
-
reject();
|
|
199
|
-
},
|
|
200
|
-
} );
|
|
201
|
-
} )
|
|
202
|
-
);
|
|
191
|
+
[ ...mediaUrls ].map( ( url ) => {
|
|
192
|
+
const uploadPromise = new Promise( ( resolve, reject ) => {
|
|
193
|
+
mediaSideloadFromUrl( {
|
|
194
|
+
url,
|
|
195
|
+
onSuccess: ( media ) => resolve( media ),
|
|
196
|
+
onError: () => reject(),
|
|
197
|
+
} );
|
|
198
|
+
} );
|
|
203
199
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
200
|
+
return [ url, uploadPromise ];
|
|
201
|
+
} )
|
|
207
202
|
);
|
|
208
203
|
|
|
209
204
|
// Wait for all blocks to be updated with library media.
|