add tree-list
This commit is contained in:
parent
1fda6d5cda
commit
e86f4148f1
|
|
@ -80,6 +80,7 @@ class CreateUploadRequest(BaseModel):
|
|||
original_filename: str = Field(max_length=512)
|
||||
content_type: str = Field(max_length=100)
|
||||
size_bytes: int = Field(gt=0, le=21474836480) # Max 20GB
|
||||
folder_id: Optional[str] = Field(None, max_length=36)
|
||||
|
||||
|
||||
class CreateUploadResponse(BaseModel):
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ async def create_upload(
|
|||
original_filename=data.original_filename,
|
||||
content_type=data.content_type,
|
||||
size_bytes=data.size_bytes,
|
||||
folder_id=data.folder_id,
|
||||
)
|
||||
|
||||
return CreateUploadResponse(
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class AssetService:
|
|||
original_filename: str,
|
||||
content_type: str,
|
||||
size_bytes: int,
|
||||
folder_id: Optional[str] = None,
|
||||
) -> tuple[Asset, dict]:
|
||||
"""
|
||||
Create an asset and generate pre-signed upload URL.
|
||||
|
|
@ -59,6 +60,7 @@ class AssetService:
|
|||
original_filename: Original filename
|
||||
content_type: MIME type
|
||||
size_bytes: File size in bytes
|
||||
folder_id: Optional folder ID to upload to
|
||||
|
||||
Returns:
|
||||
Tuple of (asset, presigned_post_data)
|
||||
|
|
@ -74,6 +76,7 @@ class AssetService:
|
|||
content_type=content_type,
|
||||
size_bytes=size_bytes,
|
||||
storage_key_original="", # Will be set after upload
|
||||
folder_id=folder_id,
|
||||
)
|
||||
|
||||
# Generate storage key
|
||||
|
|
|
|||
|
|
@ -11,32 +11,112 @@ import {
|
|||
ListItemText,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
Collapse,
|
||||
} from '@mui/material';
|
||||
import { Folder as FolderIcon, Home as HomeIcon } from '@mui/icons-material';
|
||||
import {
|
||||
Folder as FolderIcon,
|
||||
Home as HomeIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
ChevronRight as ChevronRightIcon,
|
||||
FolderOpen as FolderOpenIcon,
|
||||
} from '@mui/icons-material';
|
||||
import api from '../services/api';
|
||||
|
||||
interface Folder {
|
||||
id: string;
|
||||
name: string;
|
||||
parent_folder_id: string | null;
|
||||
children?: Folder[];
|
||||
}
|
||||
|
||||
interface MoveFolderDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onMove: (folderId: string | null) => void;
|
||||
}
|
||||
|
||||
interface FolderTreeItemProps {
|
||||
folder: Folder;
|
||||
level: number;
|
||||
selectedFolder: string | null;
|
||||
expanded: Set<string>;
|
||||
onSelect: (folderId: string) => void;
|
||||
onToggle: (folderId: string) => void;
|
||||
}
|
||||
|
||||
function FolderTreeItem({ folder, level, selectedFolder, expanded, onSelect, onToggle }: FolderTreeItemProps) {
|
||||
const hasChildren = folder.children && folder.children.length > 0;
|
||||
const isExpanded = expanded.has(folder.id);
|
||||
const isSelected = selectedFolder === folder.id;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton
|
||||
selected={isSelected}
|
||||
onClick={() => onSelect(folder.id)}
|
||||
sx={{ pl: 2 + level * 3 }}
|
||||
>
|
||||
<ListItemIcon onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (hasChildren) {
|
||||
onToggle(folder.id);
|
||||
}
|
||||
}} sx={{ minWidth: 36, cursor: hasChildren ? 'pointer' : 'default' }}>
|
||||
{hasChildren ? (
|
||||
isExpanded ? <ExpandMoreIcon /> : <ChevronRightIcon />
|
||||
) : (
|
||||
<span style={{ width: 24, display: 'inline-block' }} />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemIcon sx={{ minWidth: 40 }}>
|
||||
{isExpanded && hasChildren ? <FolderOpenIcon /> : <FolderIcon />}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={folder.name} />
|
||||
</ListItemButton>
|
||||
|
||||
{hasChildren && (
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
{folder.children!.map((child) => (
|
||||
<FolderTreeItem
|
||||
key={child.id}
|
||||
folder={child}
|
||||
level={level + 1}
|
||||
selectedFolder={selectedFolder}
|
||||
expanded={expanded}
|
||||
onSelect={onSelect}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MoveFolderDialog({ open, onClose, onMove }: MoveFolderDialogProps) {
|
||||
const [folders, setFolders] = useState<any[]>([]);
|
||||
const [folders, setFolders] = useState<Folder[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadFolders();
|
||||
loadAllFolders();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const loadFolders = async () => {
|
||||
const loadAllFolders = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Load all folders (no parent_id filter to get all)
|
||||
const response = await api.listFolders();
|
||||
setFolders(response.items || []);
|
||||
const allFolders: Folder[] = response.items || [];
|
||||
|
||||
// Build tree structure
|
||||
const tree = buildTree(allFolders);
|
||||
setFolders(tree);
|
||||
} catch (error) {
|
||||
console.error('Failed to load folders:', error);
|
||||
} finally {
|
||||
|
|
@ -44,14 +124,56 @@ export default function MoveFolderDialog({ open, onClose, onMove }: MoveFolderDi
|
|||
}
|
||||
};
|
||||
|
||||
const buildTree = (flatFolders: Folder[]): Folder[] => {
|
||||
const folderMap = new Map<string, Folder>();
|
||||
const roots: Folder[] = [];
|
||||
|
||||
// Create map and initialize children array
|
||||
flatFolders.forEach((folder) => {
|
||||
folderMap.set(folder.id, { ...folder, children: [] });
|
||||
});
|
||||
|
||||
// Build tree
|
||||
flatFolders.forEach((folder) => {
|
||||
const node = folderMap.get(folder.id)!;
|
||||
if (folder.parent_folder_id === null) {
|
||||
roots.push(node);
|
||||
} else {
|
||||
const parent = folderMap.get(folder.parent_folder_id);
|
||||
if (parent) {
|
||||
parent.children!.push(node);
|
||||
} else {
|
||||
// Parent not found, treat as root
|
||||
roots.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
};
|
||||
|
||||
const handleToggle = (folderId: string) => {
|
||||
setExpanded((prev) => {
|
||||
const newExpanded = new Set(prev);
|
||||
if (newExpanded.has(folderId)) {
|
||||
newExpanded.delete(folderId);
|
||||
} else {
|
||||
newExpanded.add(folderId);
|
||||
}
|
||||
return newExpanded;
|
||||
});
|
||||
};
|
||||
|
||||
const handleMove = () => {
|
||||
onMove(selectedFolder);
|
||||
setSelectedFolder(null);
|
||||
setExpanded(new Set());
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setSelectedFolder(null);
|
||||
setExpanded(new Set());
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
|
@ -67,7 +189,7 @@ export default function MoveFolderDialog({ open, onClose, onMove }: MoveFolderDi
|
|||
selected={selectedFolder === null}
|
||||
onClick={() => setSelectedFolder(null)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ListItemIcon sx={{ minWidth: 40 }}>
|
||||
<HomeIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Библиотека (корень)" />
|
||||
|
|
@ -79,16 +201,15 @@ export default function MoveFolderDialog({ open, onClose, onMove }: MoveFolderDi
|
|||
</Typography>
|
||||
) : (
|
||||
folders.map((folder) => (
|
||||
<ListItemButton
|
||||
<FolderTreeItem
|
||||
key={folder.id}
|
||||
selected={selectedFolder === folder.id}
|
||||
onClick={() => setSelectedFolder(folder.id)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<FolderIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={folder.name} />
|
||||
</ListItemButton>
|
||||
folder={folder}
|
||||
level={0}
|
||||
selectedFolder={selectedFolder}
|
||||
expanded={expanded}
|
||||
onSelect={setSelectedFolder}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
|
|
|
|||
|
|
@ -35,9 +35,10 @@ interface UploadDialogProps {
|
|||
open: boolean;
|
||||
onClose: () => void;
|
||||
onComplete?: () => void;
|
||||
currentFolderId?: string | null;
|
||||
}
|
||||
|
||||
export default function UploadDialog({ open, onClose, onComplete }: UploadDialogProps) {
|
||||
export default function UploadDialog({ open, onClose, onComplete, currentFolderId }: UploadDialogProps) {
|
||||
const [files, setFiles] = useState<UploadFile[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
|
|
@ -75,6 +76,7 @@ export default function UploadDialog({ open, onClose, onComplete }: UploadDialog
|
|||
original_filename: file.name,
|
||||
content_type: file.type,
|
||||
size_bytes: file.size,
|
||||
folder_id: currentFolderId || undefined,
|
||||
});
|
||||
|
||||
updateFileProgress(index, 33, 'uploading', undefined, uploadData.asset_id);
|
||||
|
|
|
|||
|
|
@ -400,6 +400,7 @@ function LibraryPageContent() {
|
|||
open={uploadOpen}
|
||||
onClose={() => setUploadOpen(false)}
|
||||
onComplete={handleUploadComplete}
|
||||
currentFolderId={currentFolderId}
|
||||
/>
|
||||
|
||||
<ViewerModal
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export type AssetStatus = 'uploading' | 'ready' | 'failed';
|
|||
export interface Asset {
|
||||
id: string;
|
||||
user_id: string;
|
||||
folder_id?: string;
|
||||
type: AssetType;
|
||||
status: AssetStatus;
|
||||
original_filename: string;
|
||||
|
|
@ -42,6 +43,7 @@ export interface CreateUploadRequest {
|
|||
original_filename: string;
|
||||
content_type: string;
|
||||
size_bytes: number;
|
||||
folder_id?: string;
|
||||
}
|
||||
|
||||
export interface CreateUploadResponse {
|
||||
|
|
|
|||
Loading…
Reference in New Issue