import React, {useEffect, useState} from 'react'; import {IconVideo} from '@tabler/icons-react'; import {Button, Icon, IconButton, Label, Message, Modal, Panel, Stack} from '../ui'; import {deleteFileGroups} from '../../api'; import {humanFileSize, isoDatetimeToString} from '../Common'; import {TaggedDeleteConfirmModal} from '../TaggedDeleteConfirmModal'; /** * Card component for a single conflicting file. * * Manages its own loading and error state for deletion. * Only calls onDelete when deletion succeeds. * * @param {Object} file - File info object from backend containing: * - file_group_id: number - ID of the FileGroup * - current_path: string - Current file path * - title: string - File title * - model_type: 'video'|'archive' - Type of content * - size: number - File size in bytes * - archive_id: number|null - Archive ID if model_type is 'archive' * - poster_path: string|null - Path to thumbnail/poster image * - published_datetime: string|null - ISO datetime string * - source_id: string|null - External source identifier * - quality_rank: number|null - Quality score for recommendations * @param {Function} onDelete - Called with (file) when deletion succeeds * @param {boolean} isRecommended - Whether this file is recommended to keep */ function ConflictFileCard({file, onDelete, isRecommended}) { const [isDeleting, setIsDeleting] = useState(false); const [localError, setLocalError] = useState(null); const [taggedFileGroups, setTaggedFileGroups] = useState(null); const performDelete = async (force) => { if (file.file_group_id) { return await deleteFileGroups([file.file_group_id], force); } return null; }; const handleDelete = async (force = false) => { setIsDeleting(true); setLocalError(null); try { const result = await performDelete(force); if (result && result.tagged) { setTaggedFileGroups(result.file_groups); return; } onDelete(file); } catch (err) { setLocalError(err.message || 'Failed to delete'); } finally { setIsDeleting(false); } }; return ( {localError && (
{localError}
)}
{/* Poster/Thumbnail column */}
{file.poster_path ? ( ) : (
{file.model_type === 'video' ? : }
)}
{/* File info column */}
{file.title || 'Untitled'} {isRecommended && ( )}
{file.current_path}
{file.size > 0 && ( )} {file.published_datetime && ( )} {file.source_id && ( )} {file.quality_rank !== null && file.quality_rank !== undefined && ( )}
{/* Delete button column */}
handleDelete(false)} />
setTaggedFileGroups(null)} onConfirm={async () => { setTaggedFileGroups(null); await handleDelete(true); }} />
); } /** * Modal component for resolving reorganization conflicts. * * Shows files that would have the same destination path and allows users * to delete duplicates before proceeding with reorganization. * * @param {boolean} open - Whether the modal is open * @param {Function} onClose - Callback when modal is closed * @param {Array} conflicts - Array of ConflictDetail objects from the backend * @param {string} collectionKind - 'channel' or 'domain' * @param {Function} onResolved - Callback when a file is deleted (to refresh preview) */ export function ConflictResolutionModal({ open, onClose, conflicts = [], collectionKind, onResolved, }) { const [localConflicts, setLocalConflicts] = useState(conflicts); const [hasChanges, setHasChanges] = useState(false); // Sync local conflicts with props when they change (e.g., when modal opens with new data) useEffect(() => { setLocalConflicts(conflicts); setHasChanges(false); }, [conflicts]); /** * Handle successful file deletion from a ConflictFileCard. * Only called when deletion succeeds (errors are handled in the card). * @param {Object} file - The file that was deleted */ const handleFileDelete = (file) => { // Update local conflicts: remove the deleted file setLocalConflicts(prev => { const updated = prev.map(conflict => ({ ...conflict, conflicting_files: conflict.conflicting_files.filter( f => f.file_group_id !== file.file_group_id ), })).filter(conflict => conflict.conflicting_files.length > 1); // Keep conflicts that still have 2+ files return updated; }); setHasChanges(true); }; const handleClose = () => { if (hasChanges && onResolved) { onResolved(); } onClose(); }; const totalConflicts = localConflicts.length; return ( Resolve Conflicts Before Reorganizing

{totalConflicts} destination {totalConflicts === 1 ? 'path has' : 'paths have'}{' '} multiple files that would be moved there. This typically happens when the same{' '} {collectionKind === 'channel' ? 'video' : 'page'} was downloaded multiple times.

Delete the duplicates you don't want to keep before reorganizing. For each conflict, keep only one file.

{localConflicts.map((conflict, idx) => { // Find the highest quality rank in this conflict (for video recommendations) const hasRanks = conflict.conflicting_files.some( f => f.quality_rank !== null && f.quality_rank !== undefined ); const highestRank = hasRanks ? Math.max(...conflict.conflicting_files.map(f => f.quality_rank || 0)) : null; // Check if there are multiple files with the same highest rank const highestRankCount = hasRanks ? conflict.conflicting_files.filter(f => f.quality_rank === highestRank).length : 0; return (
Destination: {conflict.destination_path}
{conflict.conflicting_files.map((file, fileIdx) => { // Only show "Recommended" if there's a clear winner (one file with highest rank) const isRecommended = hasRanks && highestRankCount === 1 && file.quality_rank === highestRank; return ( ); })}
); })}
{localConflicts.length === 0 && (

You can now proceed with the reorganization.

)}
); }