mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 15:35:39 +02:00
Implement hybrid upload: advanced 3MF extraction + basic upload for other files
Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
This commit is contained in:
co-authored by
cadtoolbox
parent
3c2631348a
commit
556493ff5b
@@ -159,11 +159,11 @@ describe('FileManagerPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows Upload 3MF button', async () => {
|
||||
it('shows Upload button', async () => {
|
||||
render(<FileManagerPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Upload 3MF')).toBeInTheDocument();
|
||||
expect(screen.getByText('Upload')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -454,7 +454,7 @@ describe('FileManagerPage', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No files yet')).toBeInTheDocument();
|
||||
expect(screen.getByText('Upload 3MF Files')).toBeInTheDocument();
|
||||
expect(screen.getByText('Upload Files')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -535,36 +535,82 @@ describe('FileManagerPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('upload modal 3MF support', () => {
|
||||
describe('upload modal with advanced 3MF support', () => {
|
||||
it('opens upload modal', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<FileManagerPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Upload 3MF')).toBeInTheDocument();
|
||||
expect(screen.getByText('Upload')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByText('Upload 3MF'));
|
||||
await user.click(screen.getByText('Upload'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Upload 3MF Files')).toBeInTheDocument();
|
||||
expect(screen.getByText('Upload Files')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Drag & drop/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows printer model extraction info', async () => {
|
||||
it('shows 3MF extraction info when 3MF file is added', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<FileManagerPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Upload 3MF')).toBeInTheDocument();
|
||||
expect(screen.getByText('Upload')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByText('Upload 3MF'));
|
||||
await user.click(screen.getByText('Upload'));
|
||||
|
||||
await waitFor(() => {
|
||||
// The shared UploadModal shows info about printer model extraction
|
||||
expect(screen.getByText(/printer model/i)).toBeInTheDocument();
|
||||
expect(screen.getByText('Upload Files')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Create a mock 3MF file
|
||||
const threemfFile = new File(['content'], 'model.gcode.3mf', { type: 'application/octet-stream' });
|
||||
|
||||
// Get the hidden file input
|
||||
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
expect(fileInput).toBeInTheDocument();
|
||||
|
||||
// Simulate file selection
|
||||
await user.upload(fileInput, threemfFile);
|
||||
|
||||
// 3MF extraction info should appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('3MF files detected')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Printer model.*will be automatically extracted/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows STL thumbnail option when STL file is added', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<FileManagerPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Upload')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByText('Upload'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Upload Files')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Create a mock STL file
|
||||
const stlFile = new File(['solid test'], 'model.stl', { type: 'application/sla' });
|
||||
|
||||
// Get the hidden file input
|
||||
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
expect(fileInput).toBeInTheDocument();
|
||||
|
||||
// Simulate file selection
|
||||
await user.upload(fileInput, stlFile);
|
||||
|
||||
// STL thumbnail option should appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('STL thumbnail generation')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Thumbnails can be generated/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1837,6 +1837,8 @@ export default {
|
||||
zipMayContainStl: 'ZIP files may contain STL files. Thumbnails can be generated during extraction.',
|
||||
thumbnailsCanBeGenerated: 'Thumbnails can be generated for STL files. Large models may take longer to process.',
|
||||
generateThumbnailsForStl: 'Generate thumbnails for STL files',
|
||||
threemfDetected: '3MF files detected',
|
||||
threemfExtractionInfo: 'Printer model, material, color, and print settings will be automatically extracted from 3MF files.',
|
||||
willBeExtracted: 'Will be extracted',
|
||||
filesExtracted: '{{count}} files extracted',
|
||||
uploadComplete: 'Upload complete: {{succeeded}} succeeded',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useState, useRef, useCallback, useMemo, useEffect, type DragEvent } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
AlertTriangle,
|
||||
Filter,
|
||||
X,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Link2,
|
||||
Unlink,
|
||||
Archive as ArchiveIcon,
|
||||
@@ -49,7 +51,6 @@ import type {
|
||||
import { Button } from '../components/Button';
|
||||
import { ConfirmModal } from '../components/ConfirmModal';
|
||||
import { PrintModal } from '../components/PrintModal';
|
||||
import { UploadModal } from '../components/UploadModal';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
@@ -415,6 +416,373 @@ function LinkFolderModal({ folder, onClose, onLink, isLoading, t }: LinkFolderMo
|
||||
);
|
||||
}
|
||||
|
||||
// Upload Modal with Drag & Drop
|
||||
interface UploadModalProps {
|
||||
folderId: number | null;
|
||||
onClose: () => void;
|
||||
onUploadComplete: () => void;
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
interface UploadFile {
|
||||
file: File;
|
||||
status: 'pending' | 'uploading' | 'success' | 'error';
|
||||
error?: string;
|
||||
isZip?: boolean;
|
||||
is3mf?: boolean;
|
||||
extractedCount?: number;
|
||||
archiveId?: number;
|
||||
}
|
||||
|
||||
function UploadModal({ folderId, onClose, onUploadComplete, t }: UploadModalProps) {
|
||||
const [files, setFiles] = useState<UploadFile[]>([]);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [preserveZipStructure, setPreserveZipStructure] = useState(true);
|
||||
const [createFolderFromZip, setCreateFolderFromZip] = useState(false);
|
||||
const [generateStlThumbnails, setGenerateStlThumbnails] = useState(true);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||
addFiles(droppedFiles);
|
||||
};
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
addFiles(Array.from(e.target.files));
|
||||
}
|
||||
};
|
||||
|
||||
const addFiles = (newFiles: File[]) => {
|
||||
const uploadFiles: UploadFile[] = newFiles.map((file) => ({
|
||||
file,
|
||||
status: 'pending',
|
||||
isZip: file.name.toLowerCase().endsWith('.zip'),
|
||||
is3mf: file.name.toLowerCase().endsWith('.3mf'),
|
||||
}));
|
||||
setFiles((prev) => [...prev, ...uploadFiles]);
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
setFiles((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const hasZipFiles = files.some((f) => f.isZip && f.status === 'pending');
|
||||
const hasStlFiles = files.some((f) => f.file.name.toLowerCase().endsWith('.stl') && f.status === 'pending');
|
||||
const has3mfFiles = files.some((f) => f.is3mf && f.status === 'pending');
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (files.length === 0) return;
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
// Handle .3mf files with bulk upload API (advanced extraction)
|
||||
const threemfFiles = files.filter((f) => f.is3mf && f.status === 'pending');
|
||||
if (threemfFiles.length > 0) {
|
||||
try {
|
||||
// Mark files as uploading
|
||||
setFiles((prev) =>
|
||||
prev.map((f) => (f.is3mf && f.status === 'pending' ? { ...f, status: 'uploading' } : f))
|
||||
);
|
||||
|
||||
// Use the archives bulk upload API for .3mf files (extracts printer model)
|
||||
const result = await api.uploadArchivesBulk(threemfFiles.map((f) => f.file));
|
||||
|
||||
// Update file statuses based on result
|
||||
setFiles((prev) =>
|
||||
prev.map((f) => {
|
||||
if (!f.is3mf || f.status !== 'uploading') return f;
|
||||
|
||||
const success = result.results.find((r) => r.filename === f.file.name);
|
||||
const error = result.errors.find((e) => e.filename === f.file.name);
|
||||
|
||||
if (success) {
|
||||
return { ...f, status: 'success', archiveId: success.id };
|
||||
}
|
||||
if (error) {
|
||||
return { ...f, status: 'error', error: error.error };
|
||||
}
|
||||
return f;
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
setFiles((prev) =>
|
||||
prev.map((f) =>
|
||||
f.is3mf && f.status === 'uploading'
|
||||
? { ...f, status: 'error', error: err instanceof Error ? err.message : 'Upload failed' }
|
||||
: f
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle other files (ZIP and regular files) with library upload
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (files[i].status !== 'pending' || files[i].is3mf) continue;
|
||||
|
||||
setFiles((prev) =>
|
||||
prev.map((f, idx) => (idx === i ? { ...f, status: 'uploading' } : f))
|
||||
);
|
||||
|
||||
try {
|
||||
if (files[i].isZip) {
|
||||
// Extract ZIP file
|
||||
const result = await api.extractZipFile(files[i].file, folderId, preserveZipStructure, createFolderFromZip, generateStlThumbnails);
|
||||
setFiles((prev) =>
|
||||
prev.map((f, idx) =>
|
||||
idx === i
|
||||
? {
|
||||
...f,
|
||||
status: result.errors.length > 0 && result.extracted === 0 ? 'error' : 'success',
|
||||
extractedCount: result.extracted,
|
||||
error: result.errors.length > 0 ? `${result.errors.length} files failed` : undefined,
|
||||
}
|
||||
: f
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Regular file upload (STL, etc.)
|
||||
await api.uploadLibraryFile(files[i].file, folderId, generateStlThumbnails);
|
||||
setFiles((prev) =>
|
||||
prev.map((f, idx) => (idx === i ? { ...f, status: 'success' } : f))
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setFiles((prev) =>
|
||||
prev.map((f, idx) =>
|
||||
idx === i
|
||||
? { ...f, status: 'error', error: err instanceof Error ? err.message : 'Upload failed' }
|
||||
: f
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setIsUploading(false);
|
||||
onUploadComplete();
|
||||
// Auto-close modal after upload completes
|
||||
onClose();
|
||||
};
|
||||
|
||||
const pendingCount = files.filter((f) => f.status === 'pending').length;
|
||||
const successCount = files.filter((f) => f.status === 'success').length;
|
||||
const errorCount = files.filter((f) => f.status === 'error').length;
|
||||
const allDone = files.length > 0 && pendingCount === 0 && !isUploading;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-bambu-dark-secondary rounded-lg w-full max-w-lg border border-bambu-dark-tertiary">
|
||||
<div className="p-4 border-b border-bambu-dark-tertiary flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">{t('fileManager.uploadFiles')}</h2>
|
||||
<button onClick={onClose} className="p-1 hover:bg-bambu-dark rounded">
|
||||
<X className="w-5 h-5 text-bambu-gray" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Drop Zone */}
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
|
||||
isDragging
|
||||
? 'border-bambu-green bg-bambu-green/10'
|
||||
: 'border-bambu-dark-tertiary hover:border-bambu-green/50'
|
||||
}`}
|
||||
>
|
||||
<Upload className={`w-10 h-10 mx-auto mb-3 ${isDragging ? 'text-bambu-green' : 'text-bambu-gray'}`} />
|
||||
<p className="text-white font-medium">
|
||||
{isDragging ? t('fileManager.dropFilesHere') : t('fileManager.dragDropFiles')}
|
||||
</p>
|
||||
<p className="text-sm text-bambu-gray mt-1">{t('fileManager.orClickToBrowse')}</p>
|
||||
<p className="text-xs text-bambu-gray/70 mt-2">{t('fileManager.allFileTypesSupported')}</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
|
||||
{/* ZIP Options */}
|
||||
{hasZipFiles && (
|
||||
<div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<ArchiveIcon className="w-5 h-5 text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-blue-300 font-medium">{t('fileManager.zipFilesDetected')}</p>
|
||||
<p className="text-xs text-blue-300/70 mt-1">
|
||||
{t('fileManager.zipExtractOptions')}
|
||||
</p>
|
||||
<label className="flex items-center gap-2 mt-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={preserveZipStructure}
|
||||
onChange={(e) => setPreserveZipStructure(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
|
||||
/>
|
||||
<span className="text-sm text-white">{t('fileManager.preserveZipStructure')}</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 mt-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={createFolderFromZip}
|
||||
onChange={(e) => setCreateFolderFromZip(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
|
||||
/>
|
||||
<span className="text-sm text-white">{t('fileManager.createFolderFromZip')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3MF File Info - Advanced Extraction */}
|
||||
{has3mfFiles && (
|
||||
<div className="p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<Printer className="w-5 h-5 text-purple-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-purple-300 font-medium">{t('fileManager.threemfDetected')}</p>
|
||||
<p className="text-xs text-purple-300/70 mt-1">
|
||||
{t('fileManager.threemfExtractionInfo')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* STL Thumbnail Options - show for STL files or ZIP files (which may contain STLs) */}
|
||||
{(hasStlFiles || hasZipFiles) && (
|
||||
<div className="p-3 bg-bambu-green/10 border border-bambu-green/30 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<Image className="w-5 h-5 text-bambu-green mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-bambu-green font-medium">{t('fileManager.stlThumbnailGeneration')}</p>
|
||||
<p className="text-xs text-bambu-green/70 mt-1">
|
||||
{hasZipFiles && !hasStlFiles
|
||||
? t('fileManager.zipMayContainStl')
|
||||
: t('fileManager.thumbnailsCanBeGenerated')}
|
||||
</p>
|
||||
<label className="flex items-center gap-2 mt-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={generateStlThumbnails}
|
||||
onChange={(e) => setGenerateStlThumbnails(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
|
||||
/>
|
||||
<span className="text-sm text-white">{t('fileManager.generateThumbnailsForStl')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File List */}
|
||||
{files.length > 0 && (
|
||||
<div className="max-h-48 overflow-y-auto space-y-2">
|
||||
{files.map((uploadFile, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-3 p-2 bg-bambu-dark rounded-lg"
|
||||
>
|
||||
{uploadFile.isZip ? (
|
||||
<ArchiveIcon className="w-4 h-4 text-blue-400 flex-shrink-0" />
|
||||
) : (
|
||||
<File className="w-4 h-4 text-bambu-gray flex-shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-white truncate">{uploadFile.file.name}</p>
|
||||
<p className="text-xs text-bambu-gray">
|
||||
{(uploadFile.file.size / 1024 / 1024).toFixed(2)} MB
|
||||
{uploadFile.isZip && uploadFile.status === 'pending' && (
|
||||
<span className="text-blue-400 ml-2">• {t('fileManager.willBeExtracted')}</span>
|
||||
)}
|
||||
{uploadFile.extractedCount !== undefined && (
|
||||
<span className="text-green-400 ml-2">• {t('fileManager.filesExtracted', { count: uploadFile.extractedCount })}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{uploadFile.status === 'pending' && (
|
||||
<button
|
||||
onClick={() => removeFile(index)}
|
||||
className="p-1 hover:bg-bambu-dark-tertiary rounded"
|
||||
>
|
||||
<X className="w-4 h-4 text-bambu-gray" />
|
||||
</button>
|
||||
)}
|
||||
{uploadFile.status === 'uploading' && (
|
||||
<Loader2 className="w-4 h-4 text-bambu-green animate-spin" />
|
||||
)}
|
||||
{uploadFile.status === 'success' && (
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
)}
|
||||
{uploadFile.status === 'error' && (
|
||||
<span title={uploadFile.error}>
|
||||
<XCircle className="w-4 h-4 text-red-500" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary */}
|
||||
{allDone && (
|
||||
<div className="p-3 bg-bambu-dark rounded-lg">
|
||||
<p className="text-sm text-white">
|
||||
{t('fileManager.uploadComplete', { succeeded: successCount })}
|
||||
{errorCount > 0 && <span className="text-red-400">, {t('fileManager.uploadFailed', { count: errorCount })}</span>}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-bambu-dark-tertiary flex justify-end gap-2">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{allDone ? t('common.close') : t('common.cancel')}
|
||||
</Button>
|
||||
{!allDone && (
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={pendingCount === 0 || isUploading}
|
||||
>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
{t('fileManager.uploading')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{t('common.upload')} {pendingCount > 0 ? `(${pendingCount})` : ''}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Folder Tree Item
|
||||
interface FolderTreeItemProps {
|
||||
folder: LibraryFolderTree;
|
||||
@@ -1248,7 +1616,7 @@ export function FileManagerPage() {
|
||||
title={!hasPermission('library:upload') ? t('fileManager.noPermissionUpload') : undefined}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Upload 3MF
|
||||
{t('common.upload')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1601,7 +1969,7 @@ export function FileManagerPage() {
|
||||
title={!hasPermission('library:upload') ? t('fileManager.noPermissionUpload') : undefined}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Upload 3MF Files
|
||||
{t('fileManager.uploadFiles')}
|
||||
</Button>
|
||||
</div>
|
||||
) : filteredAndSortedFiles.length === 0 ? (
|
||||
@@ -1832,11 +2200,10 @@ export function FileManagerPage() {
|
||||
|
||||
{showUploadModal && (
|
||||
<UploadModal
|
||||
onClose={() => {
|
||||
setShowUploadModal(false);
|
||||
// Also refresh library in case files are used in library context
|
||||
handleUploadComplete();
|
||||
}}
|
||||
folderId={selectedFolderId}
|
||||
onClose={() => setShowUploadModal(false)}
|
||||
onUploadComplete={handleUploadComplete}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -23,8 +23,8 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-DvkTQXdN.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-E0e2gAOg.css">
|
||||
<script type="module" crossorigin src="/assets/index-ChuiDDfR.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Mcnrrd7v.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user