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, + }; +}