@wordpress/media-utils
Version:
WordPress Media Upload Utils.
457 lines (456 loc) • 13.6 kB
JavaScript
// 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