Merge branch 'copilot/update-file-upload-method' into copilot/fix-pull-request-1-failures

This commit is contained in:
copilot-swe-agent[bot]
2026-02-04 01:13:32 +00:00
7 changed files with 150 additions and 146 deletions
@@ -535,7 +535,7 @@ describe('FileManagerPage', () => {
});
});
describe('upload modal STL options', () => {
describe('upload modal with advanced 3MF support', () => {
it('opens upload modal', async () => {
const user = userEvent.setup();
render(<FileManagerPage />);
@@ -552,6 +552,37 @@ describe('FileManagerPage', () => {
});
});
it('shows 3MF extraction info when 3MF 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 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 />);
@@ -579,96 +610,7 @@ describe('FileManagerPage', () => {
// STL thumbnail option should appear
await waitFor(() => {
expect(screen.getByText('STL thumbnail generation')).toBeInTheDocument();
expect(screen.getByText('Generate thumbnails for STL files')).toBeInTheDocument();
});
});
it('STL thumbnail checkbox is checked by default', 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();
});
// Add an STL file
const stlFile = new File(['solid test'], 'model.stl', { type: 'application/sla' });
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
await user.upload(fileInput, stlFile);
await waitFor(() => {
expect(screen.getByText('Generate thumbnails for STL files')).toBeInTheDocument();
});
// Checkbox should be checked by default
const checkbox = screen.getByRole('checkbox', { name: /Generate thumbnails for STL files/i });
expect(checkbox).toBeChecked();
});
it('can toggle STL thumbnail checkbox', 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();
});
// Add an STL file
const stlFile = new File(['solid test'], 'model.stl', { type: 'application/sla' });
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
await user.upload(fileInput, stlFile);
await waitFor(() => {
expect(screen.getByText('Generate thumbnails for STL files')).toBeInTheDocument();
});
const checkbox = screen.getByRole('checkbox', { name: /Generate thumbnails for STL files/i });
expect(checkbox).toBeChecked();
// Toggle off
await user.click(checkbox);
expect(checkbox).not.toBeChecked();
// Toggle back on
await user.click(checkbox);
expect(checkbox).toBeChecked();
});
it('shows STL thumbnail option for ZIP files', 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 ZIP file
const zipFile = new File(['PK'], 'models.zip', { type: 'application/zip' });
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
await user.upload(fileInput, zipFile);
// STL thumbnail option should appear for ZIP files (may contain STLs)
await waitFor(() => {
expect(screen.getByText('STL thumbnail generation')).toBeInTheDocument();
expect(screen.getByText(/ZIP files may contain STL files/)).toBeInTheDocument();
expect(screen.getByText(/Thumbnails can be generated/i)).toBeInTheDocument();
});
});
});
+2
View File
@@ -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',
+62 -2
View File
@@ -429,7 +429,9 @@ interface UploadFile {
status: 'pending' | 'uploading' | 'success' | 'error';
error?: string;
isZip?: boolean;
is3mf?: boolean;
extractedCount?: number;
archiveId?: number;
}
function UploadModal({ folderId, onClose, onUploadComplete, t }: UploadModalProps) {
@@ -469,6 +471,7 @@ function UploadModal({ folderId, onClose, onUploadComplete, t }: UploadModalProp
file,
status: 'pending',
isZip: file.name.toLowerCase().endsWith('.zip'),
is3mf: file.name.toLowerCase().endsWith('.3mf'),
}));
setFiles((prev) => [...prev, ...uploadFiles]);
};
@@ -479,14 +482,56 @@ function UploadModal({ folderId, onClose, onUploadComplete, t }: UploadModalProp
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') continue;
if (files[i].status !== 'pending' || files[i].is3mf) continue;
setFiles((prev) =>
prev.map((f, idx) => (idx === i ? { ...f, status: 'uploading' } : f))
@@ -509,7 +554,7 @@ function UploadModal({ folderId, onClose, onUploadComplete, t }: UploadModalProp
)
);
} else {
// Regular file upload
// 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))
@@ -609,6 +654,21 @@ function UploadModal({ folderId, onClose, onUploadComplete, t }: UploadModalProp
</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">
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
View File
@@ -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-B5vyvTs3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DME4t7XG.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>