UNPKG

@wordpress/media-utils

Version:
457 lines (456 loc) 13.6 kB
// packages/media-utils/src/components/media-upload-modal/index.tsx import clsx from "clsx"; import { createPortal, useState, useCallback, useMemo, useRef, useEffect } from "@wordpress/element"; import { __, sprintf, _n } from "@wordpress/i18n"; import { privateApis as coreDataPrivateApis, store as coreStore } from "@wordpress/core-data"; import { resolveSelect, useDispatch } from "@wordpress/data"; import { Modal, DropZone, FormFileUpload, Button } from "@wordpress/components"; import { upload as uploadIcon } from "@wordpress/icons"; import { DataViewsPicker } from "@wordpress/dataviews"; import { Stack } from "@wordpress/ui"; import { altTextField, attachedToField, authorField, captionField, dateAddedField, dateModifiedField, descriptionField, filenameField, filesizeField, mediaDimensionsField, mediaThumbnailField, mimeTypeField } from "@wordpress/media-fields"; import { store as noticesStore, SnackbarNotices } from "@wordpress/notices"; import { transformAttachment } from "../../utils/transform-attachment.mjs"; import { uploadMedia } from "../../utils/upload-media.mjs"; import { unlock } from "../../lock-unlock.mjs"; import { UploadStatusPopover } from "./upload-status-popover.mjs"; import { useInvalidateAttachmentResolutions } from "./use-invalidate-attachment-resolutions.mjs"; import { useUploadStatus } from "./use-upload-status.mjs"; import { jsx, jsxs } from "react/jsx-runtime"; var { useEntityRecordsWithPermissions } = unlock(coreDataPrivateApis); var LAYOUT_PICKER_GRID = "pickerGrid"; var LAYOUT_PICKER_TABLE = "pickerTable"; var NOTICES_CONTEXT = "media-modal"; var NOTICE_ID_UPLOAD_PROGRESS = "media-modal-upload-progress"; function MediaUploadModal({ allowedTypes, multiple = false, value, onSelect, onClose, onUpload, title = __("Select Media"), isOpen, isDismissible = true, modalClass, search = true, searchLabel = __("Search media") }) { const [selection, setSelection] = useState(() => { if (!value) { return []; } return Array.isArray(value) ? value.map(String) : [String(value)]; }); const { createSuccessNotice, removeAllNotices } = useDispatch(noticesStore); const invalidateAttachmentResolutions = useInvalidateAttachmentResolutions(); const [view, setView] = useState(() => ({ type: LAYOUT_PICKER_GRID, fields: [], showTitle: false, titleField: "title", mediaField: "media_thumbnail", search: "", page: 1, perPage: 50, filters: [], layout: { previewSize: 170, density: "compact" } })); const queryArgs = useMemo(() => { const filters = {}; view.filters?.forEach((filter) => { if (filter.field === "media_type") { filters.media_type = filter.value; } if (filter.field === "author") { if (filter.operator === "isAny") { filters.author = filter.value; } else if (filter.operator === "isNone") { filters.author_exclude = filter.value; } } if (filter.field === "date" || filter.field === "modified") { if (filter.operator === "before") { filters.before = filter.value; } else if (filter.operator === "after") { filters.after = filter.value; } } if (filter.field === "mime_type") { filters.mime_type = filter.value; } }); if (!filters.media_type) { filters.media_type = allowedTypes?.includes("*") ? void 0 : allowedTypes; } return { per_page: view.perPage || 20, page: view.page || 1, status: "inherit", order: view.sort?.direction, orderby: view.sort?.field, search: view.search, _embed: "author,wp:attached-to", ...filters }; }, [view, allowedTypes]); const handleBatchComplete = useCallback( (attachments) => { const uploadedIds = attachments.map((attachment) => String(attachment.id)).filter(Boolean); if (multiple) { setSelection((prev) => { const existing = new Set(prev); const newIds = uploadedIds.filter( (id) => !existing.has(id) ); return [...prev, ...newIds]; }); } else { setSelection(uploadedIds.slice(0, 1)); } invalidateAttachmentResolutions(); }, [multiple, invalidateAttachmentResolutions] ); const { uploadingFiles, registerBatch, dismissError, clearCompleted, allComplete } = useUploadStatus({ onBatchComplete: handleBatchComplete }); const isPopoverOpenRef = useRef(false); const handlePopoverOpenChange = useCallback( (open) => { isPopoverOpenRef.current = open; if (!open) { clearCompleted(); } }, [clearCompleted] ); const { records: mediaRecords, isResolving: isLoading, totalItems, totalPages } = useEntityRecordsWithPermissions("postType", "attachment", queryArgs); const fields = useMemo( () => [ // Media field definitions from @wordpress/media-fields // Cast is safe because RestAttachment has the same properties as Attachment { ...mediaThumbnailField, enableHiding: false // Within the modal, the thumbnail should always be shown. }, { id: "title", type: "text", label: __("Title"), getValue: ({ item }) => { const titleValue = item.title.raw || item.title.rendered; return titleValue || __("(no title)"); } }, altTextField, captionField, descriptionField, dateAddedField, dateModifiedField, authorField, filenameField, filesizeField, mediaDimensionsField, mimeTypeField, attachedToField ], [] ); const actions = useMemo( () => [ { id: "select", label: __("Select"), isPrimary: true, supportsBulk: multiple, async callback() { if (selection.length === 0) { return; } const selectedPostsQuery = { include: selection, per_page: -1 }; const selectedPosts = await resolveSelect( coreStore ).getEntityRecords( "postType", "attachment", selectedPostsQuery ); const transformedPosts = (selectedPosts ?? []).map(transformAttachment).filter(Boolean); const selectedItems = multiple ? transformedPosts : transformedPosts?.[0]; removeAllNotices("snackbar", NOTICES_CONTEXT); onSelect(selectedItems); } } ], [multiple, onSelect, selection, removeAllNotices] ); const handleModalClose = useCallback(() => { removeAllNotices("snackbar", NOTICES_CONTEXT); onClose?.(); }, [removeAllNotices, onClose]); const handleUpload = onUpload || uploadMedia; const prevAllCompleteRef = useRef(false); useEffect(() => { if (allComplete && !prevAllCompleteRef.current) { const completeCount = uploadingFiles.filter( (file) => file.status === "uploaded" ).length; if (completeCount > 0) { createSuccessNotice( sprintf( // translators: %s: number of files _n( "Uploaded %s file", "Uploaded %s files", completeCount ), completeCount.toLocaleString() ), { type: "snackbar", context: NOTICES_CONTEXT, id: NOTICE_ID_UPLOAD_PROGRESS } ); } if (!isPopoverOpenRef.current) { clearCompleted(); } } prevAllCompleteRef.current = allComplete; }, [allComplete, uploadingFiles, createSuccessNotice, clearCompleted]); const handleFileSelect = useCallback( (event) => { const files = event.target.files; if (files && files.length > 0) { const filesArray = Array.from(files); const { onFileChange, onError } = registerBatch(filesArray); handleUpload({ allowedTypes, filesList: filesArray, onFileChange, onError }); } }, [allowedTypes, handleUpload, registerBatch] ); const paginationInfo = useMemo( () => ({ totalItems, totalPages }), [totalItems, totalPages] ); const defaultLayouts = useMemo( () => ({ [LAYOUT_PICKER_GRID]: { fields: [], showTitle: false }, [LAYOUT_PICKER_TABLE]: { fields: [ "filename", "filesize", "media_dimensions", "author", "date" ], showTitle: true } }), [] ); const acceptTypes = useMemo(() => { if (allowedTypes?.includes("*")) { return void 0; } return allowedTypes?.join(","); }, [allowedTypes]); if (!isOpen) { return null; } return /* @__PURE__ */ jsxs( Modal, { title, onRequestClose: handleModalClose, isDismissible, className: modalClass, overlayClassName: "media-upload-modal", size: "fill", headerActions: /* @__PURE__ */ jsx( FormFileUpload, { accept: acceptTypes, multiple: true, onChange: handleFileSelect, __next40pxDefaultSize: true, render: ({ openFileDialog }) => /* @__PURE__ */ jsx( Button, { onClick: openFileDialog, icon: uploadIcon, __next40pxDefaultSize: true, children: __("Upload media") } ) } ), children: [ /* @__PURE__ */ jsx( DropZone, { onFilesDrop: (files) => { let filteredFiles = files; if (allowedTypes && !allowedTypes.includes("*")) { filteredFiles = files.filter( (file) => allowedTypes.some((allowedType) => { return file.type === allowedType || file.type.startsWith( allowedType.replace("*", "") ); }) ); } if (filteredFiles.length > 0) { const { onFileChange, onError } = registerBatch(filteredFiles); handleUpload({ allowedTypes, filesList: filteredFiles, onFileChange, onError }); } }, label: __("Drop files to upload") } ), /* @__PURE__ */ jsxs( DataViewsPicker, { data: mediaRecords || [], fields, view, onChangeView: setView, actions, selection, onChangeSelection: setSelection, isLoading, paginationInfo, defaultLayouts, getItemId: (item) => String(item.id), itemListLabel: __("Media items"), children: [ /* @__PURE__ */ jsxs( Stack, { direction: "row", align: "top", justify: "space-between", className: "dataviews__view-actions", gap: "xs", children: [ /* @__PURE__ */ jsxs( Stack, { direction: "row", gap: "sm", justify: "start", className: "dataviews__search", children: [ search && /* @__PURE__ */ jsx(DataViewsPicker.Search, { label: searchLabel }), /* @__PURE__ */ jsx(DataViewsPicker.FiltersToggle, {}) ] } ), /* @__PURE__ */ jsxs(Stack, { direction: "row", gap: "xs", style: { flexShrink: 0 }, children: [ /* @__PURE__ */ jsx(DataViewsPicker.LayoutSwitcher, {}), /* @__PURE__ */ jsx(DataViewsPicker.ViewConfig, {}) ] }) ] } ), /* @__PURE__ */ jsx(DataViewsPicker.FiltersToggled, { className: "dataviews-filters__container" }), /* @__PURE__ */ jsx(DataViewsPicker.Layout, {}), /* @__PURE__ */ jsxs( "div", { className: clsx("media-upload-modal__footer", { "is-uploading": uploadingFiles.length > 0 }), children: [ /* @__PURE__ */ jsx( UploadStatusPopover, { uploadingFiles, onDismissError: dismissError, onOpenChange: handlePopoverOpenChange } ), /* @__PURE__ */ jsx(DataViewsPicker.BulkActionToolbar, {}) ] } ) ] } ), createPortal( /* @__PURE__ */ jsx( SnackbarNotices, { className: "media-upload-modal__snackbar", context: NOTICES_CONTEXT } ), document.body ) ] } ); } var media_upload_modal_default = MediaUploadModal; export { MediaUploadModal, media_upload_modal_default as default }; //# sourceMappingURL=index.mjs.map