@wordpress/media-utils 5.33.1 → 5.34.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.
@@ -0,0 +1,89 @@
1
+ import type { Attachment } from '../../utils/types';
2
+ interface MediaUploadModalProps {
3
+ /**
4
+ * Array of allowed media types.
5
+ * @default ['image']
6
+ */
7
+ allowedTypes?: string[];
8
+ /**
9
+ * Whether multiple files can be selected.
10
+ * @default false
11
+ */
12
+ multiple?: boolean;
13
+ /**
14
+ * The currently selected media item(s).
15
+ * Can be a single ID number or array of IDs for multiple selection.
16
+ */
17
+ value?: number | number[];
18
+ /**
19
+ * Function called when media is selected.
20
+ * Receives single attachment object or array of attachments.
21
+ */
22
+ onSelect: (media: Attachment | Attachment[]) => void;
23
+ /**
24
+ * Function called when the modal is closed without selection.
25
+ */
26
+ onClose?: () => void;
27
+ /**
28
+ * Function to handle media uploads.
29
+ * If not provided, drag and drop will be disabled.
30
+ */
31
+ onUpload?: (args: {
32
+ allowedTypes?: string[];
33
+ filesList: File[];
34
+ onFileChange?: (attachments: Partial<Attachment>[]) => void;
35
+ onError?: (error: Error) => void;
36
+ multiple?: boolean;
37
+ }) => void;
38
+ /**
39
+ * Title for the modal.
40
+ * @default 'Select Media'
41
+ */
42
+ title?: string;
43
+ /**
44
+ * Whether the modal is open.
45
+ */
46
+ isOpen: boolean;
47
+ /**
48
+ * Whether the modal can be closed by clicking outside or pressing escape.
49
+ * @default true
50
+ */
51
+ isDismissible?: boolean;
52
+ /**
53
+ * Additional CSS class for the modal.
54
+ */
55
+ modalClass?: string;
56
+ /**
57
+ * Whether to show a search input.
58
+ * @default true
59
+ */
60
+ search?: boolean;
61
+ /**
62
+ * Label for the search input.
63
+ */
64
+ searchLabel?: string;
65
+ }
66
+ /**
67
+ * MediaUploadModal component that uses Modal and DataViewsPicker for media selection.
68
+ *
69
+ * This is a modern functional component alternative to the legacy MediaUpload class component.
70
+ * It provides a cleaner API and better integration with the WordPress block editor.
71
+ *
72
+ * @param props Component props
73
+ * @param props.allowedTypes Array of allowed media types
74
+ * @param props.multiple Whether multiple files can be selected
75
+ * @param props.value Currently selected media item(s)
76
+ * @param props.onSelect Function called when media is selected
77
+ * @param props.onClose Function called when modal is closed
78
+ * @param props.onUpload Function to handle media uploads
79
+ * @param props.title Title for the modal
80
+ * @param props.isOpen Whether the modal is open
81
+ * @param props.isDismissible Whether modal can be dismissed
82
+ * @param props.modalClass Additional CSS class for modal
83
+ * @param props.search Whether to show search input
84
+ * @param props.searchLabel Label for search input
85
+ * @return JSX element or null
86
+ */
87
+ export declare function MediaUploadModal({ allowedTypes, multiple, value, onSelect, onClose, onUpload, title, isOpen, isDismissible, modalClass, search, searchLabel, }: MediaUploadModalProps): import("react").JSX.Element | null;
88
+ export default MediaUploadModal;
89
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/media-upload-modal/index.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,mBAAmB,CAAC;AAUpE,UAAU,qBAAqB;IAC9B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAE1B;;;OAGG;IACH,QAAQ,EAAE,CAAE,KAAK,EAAE,UAAU,GAAG,UAAU,EAAE,KAAM,IAAI,CAAC;IAEvD;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAE,IAAI,EAAE;QAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;QACxB,SAAS,EAAE,IAAI,EAAE,CAAC;QAClB,YAAY,CAAC,EAAE,CAAE,WAAW,EAAE,OAAO,CAAE,UAAU,CAAE,EAAE,KAAM,IAAI,CAAC;QAChE,OAAO,CAAC,EAAE,CAAE,KAAK,EAAE,KAAK,KAAM,IAAI,CAAC;QACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;KACnB,KAAM,IAAI,CAAC;IAEZ;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAE,EACjC,YAA0B,EAC1B,QAAgB,EAChB,KAAK,EACL,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,KAA4B,EAC5B,MAAM,EACN,aAAoB,EACpB,UAAU,EACV,MAAa,EACb,WAAkC,GAClC,EAAE,qBAAqB,sCAsOvB;AAED,eAAe,gBAAgB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"private-apis.d.ts","sourceRoot":"","sources":["../src/private-apis.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,eAAO,MAAM,WAAW,IAAK,CAAC"}
1
+ {"version":3,"file":"private-apis.d.ts","sourceRoot":"","sources":["../src/private-apis.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,eAAO,MAAM,WAAW,IAAK,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/media-utils",
3
- "version": "5.33.1",
3
+ "version": "5.34.0",
4
4
  "description": "WordPress Media Upload Utils.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -36,14 +36,18 @@
36
36
  "wpScript": true,
37
37
  "types": "build-types",
38
38
  "dependencies": {
39
- "@wordpress/api-fetch": "^7.33.1",
40
- "@wordpress/blob": "^4.33.1",
41
- "@wordpress/element": "^6.33.1",
42
- "@wordpress/i18n": "^6.6.1",
43
- "@wordpress/private-apis": "^1.33.1"
39
+ "@wordpress/api-fetch": "^7.34.0",
40
+ "@wordpress/blob": "^4.34.0",
41
+ "@wordpress/components": "^30.7.0",
42
+ "@wordpress/core-data": "^7.34.0",
43
+ "@wordpress/data": "^10.34.0",
44
+ "@wordpress/dataviews": "^10.2.0",
45
+ "@wordpress/element": "^6.34.0",
46
+ "@wordpress/i18n": "^6.7.0",
47
+ "@wordpress/private-apis": "^1.34.0"
44
48
  },
45
49
  "publishConfig": {
46
50
  "access": "public"
47
51
  },
48
- "gitHead": "5f84bafdec1bed05247c1080c12f6a237951b862"
52
+ "gitHead": "ceebff807958d2e8fc755b5a20473939c78b4d1d"
49
53
  }
@@ -0,0 +1,370 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useState, useCallback, useMemo } from '@wordpress/element';
5
+ import { __ } from '@wordpress/i18n';
6
+ import {
7
+ privateApis as coreDataPrivateApis,
8
+ store as coreStore,
9
+ } from '@wordpress/core-data';
10
+ import { resolveSelect } from '@wordpress/data';
11
+ import { Modal, DropZone } from '@wordpress/components';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import { DataViewsPicker } from '@wordpress/dataviews';
17
+ import type { View, Field, ActionButton } from '@wordpress/dataviews';
18
+ import type { Attachment, RestAttachment } from '../../utils/types';
19
+ import { transformAttachment } from '../../utils/transform-attachment';
20
+ import { uploadMedia } from '../../utils/upload-media';
21
+ import { unlock } from '../../lock-unlock';
22
+
23
+ const { useEntityRecordsWithPermissions } = unlock( coreDataPrivateApis );
24
+
25
+ // Layout constant - matching the picker grid layout type
26
+ const LAYOUT_PICKER_GRID = 'pickerGrid';
27
+
28
+ interface MediaUploadModalProps {
29
+ /**
30
+ * Array of allowed media types.
31
+ * @default ['image']
32
+ */
33
+ allowedTypes?: string[];
34
+
35
+ /**
36
+ * Whether multiple files can be selected.
37
+ * @default false
38
+ */
39
+ multiple?: boolean;
40
+
41
+ /**
42
+ * The currently selected media item(s).
43
+ * Can be a single ID number or array of IDs for multiple selection.
44
+ */
45
+ value?: number | number[];
46
+
47
+ /**
48
+ * Function called when media is selected.
49
+ * Receives single attachment object or array of attachments.
50
+ */
51
+ onSelect: ( media: Attachment | Attachment[] ) => void;
52
+
53
+ /**
54
+ * Function called when the modal is closed without selection.
55
+ */
56
+ onClose?: () => void;
57
+
58
+ /**
59
+ * Function to handle media uploads.
60
+ * If not provided, drag and drop will be disabled.
61
+ */
62
+ onUpload?: ( args: {
63
+ allowedTypes?: string[];
64
+ filesList: File[];
65
+ onFileChange?: ( attachments: Partial< Attachment >[] ) => void;
66
+ onError?: ( error: Error ) => void;
67
+ multiple?: boolean;
68
+ } ) => void;
69
+
70
+ /**
71
+ * Title for the modal.
72
+ * @default 'Select Media'
73
+ */
74
+ title?: string;
75
+
76
+ /**
77
+ * Whether the modal is open.
78
+ */
79
+ isOpen: boolean;
80
+
81
+ /**
82
+ * Whether the modal can be closed by clicking outside or pressing escape.
83
+ * @default true
84
+ */
85
+ isDismissible?: boolean;
86
+
87
+ /**
88
+ * Additional CSS class for the modal.
89
+ */
90
+ modalClass?: string;
91
+
92
+ /**
93
+ * Whether to show a search input.
94
+ * @default true
95
+ */
96
+ search?: boolean;
97
+
98
+ /**
99
+ * Label for the search input.
100
+ */
101
+ searchLabel?: string;
102
+ }
103
+
104
+ /**
105
+ * MediaUploadModal component that uses Modal and DataViewsPicker for media selection.
106
+ *
107
+ * This is a modern functional component alternative to the legacy MediaUpload class component.
108
+ * It provides a cleaner API and better integration with the WordPress block editor.
109
+ *
110
+ * @param props Component props
111
+ * @param props.allowedTypes Array of allowed media types
112
+ * @param props.multiple Whether multiple files can be selected
113
+ * @param props.value Currently selected media item(s)
114
+ * @param props.onSelect Function called when media is selected
115
+ * @param props.onClose Function called when modal is closed
116
+ * @param props.onUpload Function to handle media uploads
117
+ * @param props.title Title for the modal
118
+ * @param props.isOpen Whether the modal is open
119
+ * @param props.isDismissible Whether modal can be dismissed
120
+ * @param props.modalClass Additional CSS class for modal
121
+ * @param props.search Whether to show search input
122
+ * @param props.searchLabel Label for search input
123
+ * @return JSX element or null
124
+ */
125
+ export function MediaUploadModal( {
126
+ allowedTypes = [ 'image' ],
127
+ multiple = false,
128
+ value,
129
+ onSelect,
130
+ onClose,
131
+ onUpload,
132
+ title = __( 'Select Media' ),
133
+ isOpen,
134
+ isDismissible = true,
135
+ modalClass,
136
+ search = true,
137
+ searchLabel = __( 'Search media' ),
138
+ }: MediaUploadModalProps ) {
139
+ const [ selection, setSelection ] = useState< string[] >( () => {
140
+ if ( ! value ) {
141
+ return [];
142
+ }
143
+ return Array.isArray( value )
144
+ ? value.map( String )
145
+ : [ String( value ) ];
146
+ } );
147
+
148
+ // DataViews configuration - allow view updates
149
+ const [ view, setView ] = useState< View >( () => ( {
150
+ type: LAYOUT_PICKER_GRID,
151
+ fields: [],
152
+ titleField: 'title',
153
+ mediaField: 'url',
154
+ search: '',
155
+ page: 1,
156
+ perPage: 20,
157
+ filters: [],
158
+ } ) );
159
+
160
+ // Build query args based on view properties, similar to PostList
161
+ const queryArgs = useMemo( () => {
162
+ const filters: Record< string, any > = {};
163
+
164
+ view.filters?.forEach( ( filter ) => {
165
+ // Handle media type filters
166
+ if ( filter.field === 'media_type' ) {
167
+ filters.media_type = filter.value;
168
+ }
169
+ // Handle author filters
170
+ if ( filter.field === 'author' ) {
171
+ filters.author = filter.value;
172
+ }
173
+ // Handle date filters
174
+ if ( filter.field === 'date' ) {
175
+ filters.after = filter.value?.after;
176
+ filters.before = filter.value?.before;
177
+ }
178
+ // Handle mime type filters
179
+ if ( filter.field === 'mime_type' ) {
180
+ filters.mime_type = filter.value;
181
+ }
182
+ } );
183
+
184
+ // Base media type on allowedTypes if no filter is set
185
+ if ( ! filters.media_type ) {
186
+ filters.media_type = allowedTypes.includes( '*' )
187
+ ? undefined
188
+ : allowedTypes[ 0 ];
189
+ }
190
+
191
+ return {
192
+ per_page: view.perPage || 20,
193
+ page: view.page || 1,
194
+ status: 'inherit',
195
+ order: view.sort?.direction,
196
+ orderby: view.sort?.field,
197
+ search: view.search,
198
+ ...filters,
199
+ };
200
+ }, [ view, allowedTypes ] );
201
+
202
+ // Fetch all media attachments using WordPress core data with permissions
203
+ const {
204
+ records: mediaRecords,
205
+ isResolving: isLoading,
206
+ totalItems,
207
+ totalPages,
208
+ } = useEntityRecordsWithPermissions( 'postType', 'attachment', queryArgs );
209
+
210
+ const fields: Field< RestAttachment >[] = useMemo(
211
+ () => [
212
+ {
213
+ id: 'url',
214
+ type: 'media' as const,
215
+ label: __( 'Media' ),
216
+ render: ( { item }: { item: RestAttachment } ) => (
217
+ <img
218
+ src={ item.source_url }
219
+ alt={ item.alt_text }
220
+ style={ {
221
+ width: '100%',
222
+ height: '100%',
223
+ objectFit: 'cover',
224
+ borderRadius: '4px',
225
+ } }
226
+ />
227
+ ),
228
+ },
229
+ {
230
+ id: 'title',
231
+ type: 'text' as const,
232
+ label: __( 'Title' ),
233
+ getValue: ( { item }: { item: RestAttachment } ) => {
234
+ const titleValue = item.title.raw || item.title.rendered;
235
+ return titleValue || __( '(no title)' );
236
+ },
237
+ },
238
+ {
239
+ id: 'alt',
240
+ type: 'text' as const,
241
+ label: __( 'Alt text' ),
242
+ getValue: ( { item }: { item: RestAttachment } ) =>
243
+ item.alt_text,
244
+ },
245
+ ],
246
+ []
247
+ );
248
+
249
+ const actions: ActionButton< RestAttachment >[] = useMemo(
250
+ () => [
251
+ {
252
+ id: 'select',
253
+ label: multiple ? __( 'Select' ) : __( 'Select' ),
254
+ isPrimary: true,
255
+ supportsBulk: multiple,
256
+ async callback() {
257
+ if ( selection.length === 0 ) {
258
+ return;
259
+ }
260
+
261
+ const selectedPostsQuery = {
262
+ include: selection,
263
+ per_page: -1,
264
+ };
265
+
266
+ const selectedPosts = await resolveSelect(
267
+ coreStore
268
+ ).getEntityRecords(
269
+ 'postType',
270
+ 'attachment',
271
+ selectedPostsQuery
272
+ );
273
+
274
+ // Transform the selected posts to the expected Attachment format
275
+ const transformedPosts =
276
+ selectedPosts?.map( transformAttachment );
277
+
278
+ const selectedItems = multiple
279
+ ? transformedPosts
280
+ : transformedPosts?.[ 0 ];
281
+
282
+ onSelect( selectedItems );
283
+ },
284
+ },
285
+ ],
286
+ [ multiple, onSelect, selection ]
287
+ );
288
+
289
+ const handleModalClose = useCallback( () => {
290
+ onClose?.();
291
+ }, [ onClose ] );
292
+
293
+ const paginationInfo = useMemo(
294
+ () => ( {
295
+ totalItems,
296
+ totalPages,
297
+ } ),
298
+ [ totalItems, totalPages ]
299
+ );
300
+
301
+ const defaultLayouts = useMemo(
302
+ () => ( {
303
+ [ LAYOUT_PICKER_GRID ]: {},
304
+ } ),
305
+ []
306
+ );
307
+
308
+ if ( ! isOpen ) {
309
+ return null;
310
+ }
311
+
312
+ // Use onUpload if provided, otherwise fall back to uploadMedia
313
+ const handleUpload = onUpload || uploadMedia;
314
+
315
+ return (
316
+ <Modal
317
+ title={ title }
318
+ onRequestClose={ handleModalClose }
319
+ isDismissible={ isDismissible }
320
+ className={ modalClass }
321
+ size="fill"
322
+ >
323
+ <DropZone
324
+ onFilesDrop={ ( files ) => {
325
+ let filteredFiles = files;
326
+ // Filter files by allowed types if specified
327
+ if ( allowedTypes && ! allowedTypes.includes( '*' ) ) {
328
+ filteredFiles = files.filter( ( file ) =>
329
+ allowedTypes.some( ( allowedType ) => {
330
+ // Check if the file type matches the allowed MIME type
331
+ return (
332
+ file.type === allowedType ||
333
+ file.type.startsWith(
334
+ allowedType.replace( '*', '' )
335
+ )
336
+ );
337
+ } )
338
+ );
339
+ }
340
+ if ( filteredFiles.length > 0 ) {
341
+ handleUpload( {
342
+ allowedTypes,
343
+ filesList: filteredFiles,
344
+ multiple,
345
+ } );
346
+ }
347
+ } }
348
+ label={ __( 'Drop files to upload' ) }
349
+ />
350
+ <DataViewsPicker
351
+ data={ mediaRecords || [] }
352
+ fields={ fields }
353
+ view={ view }
354
+ onChangeView={ setView }
355
+ actions={ actions }
356
+ selection={ selection }
357
+ onChangeSelection={ setSelection }
358
+ isLoading={ isLoading }
359
+ paginationInfo={ paginationInfo }
360
+ defaultLayouts={ defaultLayouts }
361
+ getItemId={ ( item: RestAttachment ) => String( item.id ) }
362
+ search={ search }
363
+ searchLabel={ searchLabel }
364
+ itemListLabel={ __( 'Media items' ) }
365
+ />
366
+ </Modal>
367
+ );
368
+ }
369
+
370
+ export default MediaUploadModal;
@@ -2,6 +2,7 @@
2
2
  * Internal dependencies
3
3
  */
4
4
  import { sideloadMedia } from './utils/sideload-media';
5
+ import { MediaUploadModal } from './components/media-upload-modal';
5
6
  import { lock } from './lock-unlock';
6
7
 
7
8
  /**
@@ -11,4 +12,5 @@ export const privateApis = {};
11
12
 
12
13
  lock( privateApis, {
13
14
  sideloadMedia,
15
+ MediaUploadModal,
14
16
  } );
package/tsconfig.json CHANGED
@@ -8,6 +8,10 @@
8
8
  "references": [
9
9
  { "path": "../api-fetch" },
10
10
  { "path": "../blob" },
11
+ { "path": "../components" },
12
+ { "path": "../core-data" },
13
+ { "path": "../data" },
14
+ { "path": "../dataviews" },
11
15
  { "path": "../element" },
12
16
  { "path": "../i18n" },
13
17
  { "path": "../private-apis" }