diff --git a/frontend/src/static/js/components/BulkActionsModals.jsx b/frontend/src/static/js/components/BulkActionsModals.jsx
new file mode 100644
index 00000000..e1227d68
--- /dev/null
+++ b/frontend/src/static/js/components/BulkActionsModals.jsx
@@ -0,0 +1,201 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { BulkActionConfirmModal } from './BulkActionConfirmModal';
+import { BulkActionPermissionModal } from './BulkActionPermissionModal';
+import { BulkActionPlaylistModal } from './BulkActionPlaylistModal';
+import { BulkActionChangeOwnerModal } from './BulkActionChangeOwnerModal';
+import { BulkActionPublishStateModal } from './BulkActionPublishStateModal';
+import { BulkActionCategoryModal } from './BulkActionCategoryModal';
+import { BulkActionTagModal } from './BulkActionTagModal';
+
+/**
+ * Renders all bulk action modals
+ * This component is reusable across different pages
+ */
+export function BulkActionsModals({
+ // Confirm modal props
+ showConfirmModal,
+ confirmMessage,
+ onConfirmCancel,
+ onConfirmProceed,
+
+ // Permission modal props
+ showPermissionModal,
+ permissionType,
+ selectedMediaIds,
+ onPermissionModalCancel,
+ onPermissionModalSuccess,
+ onPermissionModalError,
+
+ // Playlist modal props
+ showPlaylistModal,
+ onPlaylistModalCancel,
+ onPlaylistModalSuccess,
+ onPlaylistModalError,
+ username,
+
+ // Change owner modal props
+ showChangeOwnerModal,
+ onChangeOwnerModalCancel,
+ onChangeOwnerModalSuccess,
+ onChangeOwnerModalError,
+
+ // Publish state modal props
+ showPublishStateModal,
+ onPublishStateModalCancel,
+ onPublishStateModalSuccess,
+ onPublishStateModalError,
+
+ // Category modal props
+ showCategoryModal,
+ onCategoryModalCancel,
+ onCategoryModalSuccess,
+ onCategoryModalError,
+
+ // Tag modal props
+ showTagModal,
+ onTagModalCancel,
+ onTagModalSuccess,
+ onTagModalError,
+
+ // Common props
+ csrfToken,
+
+ // Notification
+ showNotification,
+ notificationMessage,
+ notificationType,
+}) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showNotification && (
+
+ {notificationMessage}
+
+ )}
+ >
+ );
+}
+
+BulkActionsModals.propTypes = {
+ showConfirmModal: PropTypes.bool.isRequired,
+ confirmMessage: PropTypes.string.isRequired,
+ onConfirmCancel: PropTypes.func.isRequired,
+ onConfirmProceed: PropTypes.func.isRequired,
+
+ showPermissionModal: PropTypes.bool.isRequired,
+ permissionType: PropTypes.oneOf(['viewer', 'editor', 'owner', null]),
+ selectedMediaIds: PropTypes.array.isRequired,
+ onPermissionModalCancel: PropTypes.func.isRequired,
+ onPermissionModalSuccess: PropTypes.func.isRequired,
+ onPermissionModalError: PropTypes.func.isRequired,
+
+ showPlaylistModal: PropTypes.bool.isRequired,
+ onPlaylistModalCancel: PropTypes.func.isRequired,
+ onPlaylistModalSuccess: PropTypes.func.isRequired,
+ onPlaylistModalError: PropTypes.func.isRequired,
+ username: PropTypes.string,
+
+ showChangeOwnerModal: PropTypes.bool.isRequired,
+ onChangeOwnerModalCancel: PropTypes.func.isRequired,
+ onChangeOwnerModalSuccess: PropTypes.func.isRequired,
+ onChangeOwnerModalError: PropTypes.func.isRequired,
+
+ showPublishStateModal: PropTypes.bool.isRequired,
+ onPublishStateModalCancel: PropTypes.func.isRequired,
+ onPublishStateModalSuccess: PropTypes.func.isRequired,
+ onPublishStateModalError: PropTypes.func.isRequired,
+
+ showCategoryModal: PropTypes.bool.isRequired,
+ onCategoryModalCancel: PropTypes.func.isRequired,
+ onCategoryModalSuccess: PropTypes.func.isRequired,
+ onCategoryModalError: PropTypes.func.isRequired,
+
+ showTagModal: PropTypes.bool.isRequired,
+ onTagModalCancel: PropTypes.func.isRequired,
+ onTagModalSuccess: PropTypes.func.isRequired,
+ onTagModalError: PropTypes.func.isRequired,
+
+ csrfToken: PropTypes.string.isRequired,
+
+ showNotification: PropTypes.bool.isRequired,
+ notificationMessage: PropTypes.string.isRequired,
+ notificationType: PropTypes.oneOf(['success', 'error']).isRequired,
+};
diff --git a/frontend/src/static/js/utils/hooks/useBulkActions.js b/frontend/src/static/js/utils/hooks/useBulkActions.js
new file mode 100644
index 00000000..32dfb4b7
--- /dev/null
+++ b/frontend/src/static/js/utils/hooks/useBulkActions.js
@@ -0,0 +1,516 @@
+import { useState } from 'react';
+import { translateString } from '../helpers';
+
+/**
+ * Custom hook for managing bulk actions on media items
+ * Provides state management and handlers for selecting media and executing bulk actions
+ */
+export function useBulkActions() {
+ const [selectedMedia, setSelectedMedia] = useState(new Set());
+ const [availableMediaIds, setAvailableMediaIds] = useState([]);
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
+ const [pendingAction, setPendingAction] = useState(null);
+ const [confirmMessage, setConfirmMessage] = useState('');
+ const [listKey, setListKey] = useState(0);
+ const [notificationMessage, setNotificationMessage] = useState('');
+ const [showNotification, setShowNotification] = useState(false);
+ const [notificationType, setNotificationType] = useState('success');
+ const [showPermissionModal, setShowPermissionModal] = useState(false);
+ const [permissionType, setPermissionType] = useState(null);
+ const [showPlaylistModal, setShowPlaylistModal] = useState(false);
+ const [showChangeOwnerModal, setShowChangeOwnerModal] = useState(false);
+ const [showPublishStateModal, setShowPublishStateModal] = useState(false);
+ const [showCategoryModal, setShowCategoryModal] = useState(false);
+ const [showTagModal, setShowTagModal] = useState(false);
+
+ // Get CSRF token from cookies
+ const getCsrfToken = () => {
+ const name = 'csrftoken';
+ let cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ const cookies = document.cookie.split(';');
+ for (let i = 0; i < cookies.length; i++) {
+ const cookie = cookies[i].trim();
+ if (cookie.substring(0, name.length + 1) === name + '=') {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+ };
+
+ // Show notification
+ const showNotificationMessage = (message, type = 'success') => {
+ setNotificationMessage(message);
+ setShowNotification(true);
+ setNotificationType(type);
+
+ setTimeout(() => {
+ setShowNotification(false);
+ }, 5000);
+ };
+
+ // Handle media selection toggle
+ const handleMediaSelection = (mediaId, isSelected) => {
+ setSelectedMedia((prevState) => {
+ const newSelectedMedia = new Set(prevState);
+ if (isSelected) {
+ newSelectedMedia.add(mediaId);
+ } else {
+ newSelectedMedia.delete(mediaId);
+ }
+ return newSelectedMedia;
+ });
+ };
+
+ // Handle items update from list
+ const handleItemsUpdate = (items) => {
+ const mediaIds = items.map((item) => item.friendly_token || item.uid || item.id);
+ setAvailableMediaIds(mediaIds);
+ };
+
+ // Select all available media
+ const handleSelectAll = () => {
+ setSelectedMedia(new Set(availableMediaIds));
+ };
+
+ // Deselect all media
+ const handleDeselectAll = () => {
+ setSelectedMedia(new Set());
+ };
+
+ // Clear selection
+ const clearSelection = () => {
+ setSelectedMedia(new Set());
+ };
+
+ // Clear selection and refresh list
+ const clearSelectionAndRefresh = () => {
+ setSelectedMedia(new Set());
+ setListKey((prev) => prev + 1);
+ };
+
+ // Handle bulk action button clicks
+ const handleBulkAction = (action) => {
+ const selectedCount = selectedMedia.size;
+
+ if (selectedCount === 0) {
+ return;
+ }
+
+ if (action === 'delete-media') {
+ setShowConfirmModal(true);
+ setPendingAction(action);
+ setConfirmMessage(translateString('You are going to delete') + ` ${selectedCount} ` + translateString('media, are you sure?'));
+ } else if (action === 'enable-comments') {
+ setShowConfirmModal(true);
+ setPendingAction(action);
+ setConfirmMessage(translateString('You are going to enable comments to') + ` ${selectedCount} ` + translateString('media, are you sure?'));
+ } else if (action === 'disable-comments') {
+ setShowConfirmModal(true);
+ setPendingAction(action);
+ setConfirmMessage(translateString('You are going to disable comments to') + ` ${selectedCount} ` + translateString('media, are you sure?'));
+ } else if (action === 'enable-download') {
+ setShowConfirmModal(true);
+ setPendingAction(action);
+ setConfirmMessage(translateString('You are going to enable download for') + ` ${selectedCount} ` + translateString('media, are you sure?'));
+ } else if (action === 'disable-download') {
+ setShowConfirmModal(true);
+ setPendingAction(action);
+ setConfirmMessage(translateString('You are going to disable download for') + ` ${selectedCount} ` + translateString('media, are you sure?'));
+ } else if (action === 'copy-media') {
+ setShowConfirmModal(true);
+ setPendingAction(action);
+ setConfirmMessage(translateString('You are going to copy') + ` ${selectedCount} ` + translateString('media, are you sure?'));
+ } else if (action === 'add-remove-coviewers') {
+ setShowPermissionModal(true);
+ setPermissionType('viewer');
+ } else if (action === 'add-remove-coeditors') {
+ setShowPermissionModal(true);
+ setPermissionType('editor');
+ } else if (action === 'add-remove-coowners') {
+ setShowPermissionModal(true);
+ setPermissionType('owner');
+ } else if (action === 'add-remove-playlist') {
+ setShowPlaylistModal(true);
+ } else if (action === 'change-owner') {
+ setShowChangeOwnerModal(true);
+ } else if (action === 'publish-state') {
+ setShowPublishStateModal(true);
+ } else if (action === 'add-remove-category') {
+ setShowCategoryModal(true);
+ } else if (action === 'add-remove-tags') {
+ setShowTagModal(true);
+ }
+ };
+
+ // Cancel confirm modal
+ const handleConfirmCancel = () => {
+ setShowConfirmModal(false);
+ setPendingAction(null);
+ setConfirmMessage('');
+ };
+
+ // Proceed with confirmed action
+ const handleConfirmProceed = () => {
+ const action = pendingAction;
+ setShowConfirmModal(false);
+ setPendingAction(null);
+ setConfirmMessage('');
+
+ if (action === 'delete-media') {
+ executeDeleteMedia();
+ } else if (action === 'enable-comments') {
+ executeEnableComments();
+ } else if (action === 'disable-comments') {
+ executeDisableComments();
+ } else if (action === 'enable-download') {
+ executeEnableDownload();
+ } else if (action === 'disable-download') {
+ executeDisableDownload();
+ } else if (action === 'copy-media') {
+ executeCopyMedia();
+ }
+ };
+
+ // Execute delete media
+ const executeDeleteMedia = () => {
+ const selectedIds = Array.from(selectedMedia);
+ const selectedCount = selectedIds.length;
+
+ fetch('/api/v1/media/user/bulk_actions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': getCsrfToken(),
+ },
+ body: JSON.stringify({
+ action: 'delete_media',
+ media_ids: selectedIds,
+ }),
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error('Failed to delete media');
+ }
+ return response.json();
+ })
+ .then((data) => {
+ const message = selectedCount === 1
+ ? translateString('The media was deleted successfully.')
+ : translateString('Successfully deleted') + ` ${selectedCount} ` + translateString('media.');
+ showNotificationMessage(message);
+ clearSelectionAndRefresh();
+ })
+ .catch((error) => {
+ showNotificationMessage(translateString('Failed to delete media. Please try again.'), 'error');
+ clearSelectionAndRefresh();
+ });
+ };
+
+ // Execute enable comments
+ const executeEnableComments = () => {
+ const selectedIds = Array.from(selectedMedia);
+
+ fetch('/api/v1/media/user/bulk_actions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': getCsrfToken(),
+ },
+ body: JSON.stringify({
+ action: 'enable_comments',
+ media_ids: selectedIds,
+ }),
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error('Failed to enable comments');
+ }
+ return response.json();
+ })
+ .then((data) => {
+ showNotificationMessage(translateString('Successfully Enabled comments'));
+ clearSelection();
+ })
+ .catch((error) => {
+ showNotificationMessage(translateString('Failed to enable comments.'), 'error');
+ clearSelection();
+ });
+ };
+
+ // Execute disable comments
+ const executeDisableComments = () => {
+ const selectedIds = Array.from(selectedMedia);
+
+ fetch('/api/v1/media/user/bulk_actions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': getCsrfToken(),
+ },
+ body: JSON.stringify({
+ action: 'disable_comments',
+ media_ids: selectedIds,
+ }),
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error('Failed to disable comments');
+ }
+ return response.json();
+ })
+ .then((data) => {
+ showNotificationMessage(translateString('Successfully Disabled comments'));
+ clearSelection();
+ })
+ .catch((error) => {
+ showNotificationMessage(translateString('Failed to disable comments.'), 'error');
+ clearSelection();
+ });
+ };
+
+ // Execute enable download
+ const executeEnableDownload = () => {
+ const selectedIds = Array.from(selectedMedia);
+
+ fetch('/api/v1/media/user/bulk_actions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': getCsrfToken(),
+ },
+ body: JSON.stringify({
+ action: 'enable_download',
+ media_ids: selectedIds,
+ }),
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error('Failed to enable download');
+ }
+ return response.json();
+ })
+ .then((data) => {
+ showNotificationMessage(translateString('Successfully Enabled Download'));
+ clearSelection();
+ })
+ .catch((error) => {
+ showNotificationMessage(translateString('Failed to enable download.'), 'error');
+ clearSelection();
+ });
+ };
+
+ // Execute disable download
+ const executeDisableDownload = () => {
+ const selectedIds = Array.from(selectedMedia);
+
+ fetch('/api/v1/media/user/bulk_actions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': getCsrfToken(),
+ },
+ body: JSON.stringify({
+ action: 'disable_download',
+ media_ids: selectedIds,
+ }),
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error('Failed to disable download');
+ }
+ return response.json();
+ })
+ .then((data) => {
+ showNotificationMessage(translateString('Successfully Disabled Download'));
+ clearSelection();
+ })
+ .catch((error) => {
+ showNotificationMessage(translateString('Failed to disable download.'), 'error');
+ clearSelection();
+ });
+ };
+
+ // Execute copy media
+ const executeCopyMedia = () => {
+ const selectedIds = Array.from(selectedMedia);
+
+ fetch('/api/v1/media/user/bulk_actions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': getCsrfToken(),
+ },
+ body: JSON.stringify({
+ action: 'copy_media',
+ media_ids: selectedIds,
+ }),
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error('Failed to copy media');
+ }
+ return response.json();
+ })
+ .then((data) => {
+ showNotificationMessage(translateString('Successfully Copied'));
+ clearSelectionAndRefresh();
+ })
+ .catch((error) => {
+ showNotificationMessage(translateString('Failed to copy media.'), 'error');
+ clearSelection();
+ });
+ };
+
+ // Permission modal handlers
+ const handlePermissionModalCancel = () => {
+ setShowPermissionModal(false);
+ setPermissionType(null);
+ };
+
+ const handlePermissionModalSuccess = (message) => {
+ showNotificationMessage(message);
+ clearSelection();
+ setShowPermissionModal(false);
+ setPermissionType(null);
+ };
+
+ const handlePermissionModalError = (message) => {
+ showNotificationMessage(message, 'error');
+ setShowPermissionModal(false);
+ setPermissionType(null);
+ };
+
+ // Playlist modal handlers
+ const handlePlaylistModalCancel = () => {
+ setShowPlaylistModal(false);
+ };
+
+ const handlePlaylistModalSuccess = (message) => {
+ showNotificationMessage(message);
+ clearSelection();
+ setShowPlaylistModal(false);
+ };
+
+ const handlePlaylistModalError = (message) => {
+ showNotificationMessage(message, 'error');
+ setShowPlaylistModal(false);
+ };
+
+ // Change owner modal handlers
+ const handleChangeOwnerModalCancel = () => {
+ setShowChangeOwnerModal(false);
+ };
+
+ const handleChangeOwnerModalSuccess = (message) => {
+ showNotificationMessage(message);
+ clearSelectionAndRefresh();
+ setShowChangeOwnerModal(false);
+ };
+
+ const handleChangeOwnerModalError = (message) => {
+ showNotificationMessage(message, 'error');
+ setShowChangeOwnerModal(false);
+ };
+
+ // Publish state modal handlers
+ const handlePublishStateModalCancel = () => {
+ setShowPublishStateModal(false);
+ };
+
+ const handlePublishStateModalSuccess = (message) => {
+ showNotificationMessage(message);
+ clearSelectionAndRefresh();
+ setShowPublishStateModal(false);
+ };
+
+ const handlePublishStateModalError = (message) => {
+ showNotificationMessage(message, 'error');
+ setShowPublishStateModal(false);
+ };
+
+ // Category modal handlers
+ const handleCategoryModalCancel = () => {
+ setShowCategoryModal(false);
+ };
+
+ const handleCategoryModalSuccess = (message) => {
+ showNotificationMessage(message);
+ clearSelection();
+ setShowCategoryModal(false);
+ };
+
+ const handleCategoryModalError = (message) => {
+ showNotificationMessage(message, 'error');
+ setShowCategoryModal(false);
+ };
+
+ // Tag modal handlers
+ const handleTagModalCancel = () => {
+ setShowTagModal(false);
+ };
+
+ const handleTagModalSuccess = (message) => {
+ showNotificationMessage(message);
+ clearSelection();
+ setShowTagModal(false);
+ };
+
+ const handleTagModalError = (message) => {
+ showNotificationMessage(message, 'error');
+ setShowTagModal(false);
+ };
+
+ return {
+ // State
+ selectedMedia,
+ availableMediaIds,
+ listKey,
+ showConfirmModal,
+ confirmMessage,
+ notificationMessage,
+ showNotification,
+ notificationType,
+ showPermissionModal,
+ permissionType,
+ showPlaylistModal,
+ showChangeOwnerModal,
+ showPublishStateModal,
+ showCategoryModal,
+ showTagModal,
+
+ // Handlers
+ handleMediaSelection,
+ handleItemsUpdate,
+ handleSelectAll,
+ handleDeselectAll,
+ handleBulkAction,
+ handleConfirmCancel,
+ handleConfirmProceed,
+ handlePermissionModalCancel,
+ handlePermissionModalSuccess,
+ handlePermissionModalError,
+ handlePlaylistModalCancel,
+ handlePlaylistModalSuccess,
+ handlePlaylistModalError,
+ handleChangeOwnerModalCancel,
+ handleChangeOwnerModalSuccess,
+ handleChangeOwnerModalError,
+ handlePublishStateModalCancel,
+ handlePublishStateModalSuccess,
+ handlePublishStateModalError,
+ handleCategoryModalCancel,
+ handleCategoryModalSuccess,
+ handleCategoryModalError,
+ handleTagModalCancel,
+ handleTagModalSuccess,
+ handleTagModalError,
+
+ // Utility
+ getCsrfToken,
+ clearSelection,
+ clearSelectionAndRefresh,
+ };
+}