Add File Manager and enhanced Queue features

File Manager:
- Add full File Manager page for browsing local library files
- Implement folder hierarchy with create/rename/delete operations
- Add file uploads with drag-and-drop support
- Enable adding sliced files directly to print queue
- Add linked folders with external path mounting

Queue Enhancements:
- Add plate selection for multi-plate 3MF files
- Add print options (bed levelling, flow calibration, vibration
  calibration, layer inspection, timelapse, use AMS)
- Fix print_count increment to only occur on actual prints

Testing:
- Add backend integration tests for library API (15 tests)
- Add backend integration tests for queue print options (8 tests)
- Add frontend tests for EditQueueItemModal (10 tests)
- Suppress console noise in frontend test suite

Closes #94
This commit is contained in:
maziggy
2026-01-19 13:29:37 +01:00
committed by MartinNYHC
parent b2831d09a2
commit d48abe0f6a
36 changed files with 13111 additions and 8793 deletions
+227
View File
@@ -590,6 +590,9 @@ export interface AppSettings {
ha_enabled: boolean;
ha_url: string;
ha_token: string;
// File Manager / Library settings
library_archive_mode: 'always' | 'never' | 'ask';
library_disk_warning_gb: number;
}
export type AppSettingsUpdate = Partial<AppSettings>;
@@ -847,6 +850,14 @@ export interface PrintQueueItem {
auto_off_after: boolean;
manual_start: boolean; // Requires manual trigger to start (staged)
ams_mapping: number[] | null; // AMS slot mapping for multi-color prints
plate_id: number | null; // Plate ID for multi-plate 3MF files
// Print options
bed_levelling: boolean;
flow_cali: boolean;
vibration_cali: boolean;
layer_inspect: boolean;
timelapse: boolean;
use_ams: boolean;
status: 'pending' | 'printing' | 'completed' | 'failed' | 'skipped' | 'cancelled';
started_at: string | null;
completed_at: string | null;
@@ -866,6 +877,14 @@ export interface PrintQueueItemCreate {
auto_off_after?: boolean;
manual_start?: boolean; // Requires manual trigger to start (staged)
ams_mapping?: number[] | null; // AMS slot mapping for multi-color prints
plate_id?: number | null; // Plate ID for multi-plate 3MF files
// Print options
bed_levelling?: boolean;
flow_cali?: boolean;
vibration_cali?: boolean;
layer_inspect?: boolean;
timelapse?: boolean;
use_ams?: boolean;
}
export interface PrintQueueItemUpdate {
@@ -876,6 +895,14 @@ export interface PrintQueueItemUpdate {
auto_off_after?: boolean;
manual_start?: boolean;
ams_mapping?: number[];
plate_id?: number | null; // Plate ID for multi-plate 3MF files
// Print options
bed_levelling?: boolean;
flow_cali?: boolean;
vibration_cali?: boolean;
layer_inspect?: boolean;
timelapse?: boolean;
use_ams?: boolean;
}
// MQTT Logging types
@@ -2443,6 +2470,75 @@ export const api = {
// System Info
getSystemInfo: () => request<SystemInfo>('/system/info'),
// Library (File Manager)
getLibraryFolders: () => request<LibraryFolderTree[]>('/library/folders'),
createLibraryFolder: (data: LibraryFolderCreate) =>
request<LibraryFolder>('/library/folders', {
method: 'POST',
body: JSON.stringify(data),
}),
updateLibraryFolder: (id: number, data: LibraryFolderUpdate) =>
request<LibraryFolder>(`/library/folders/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
deleteLibraryFolder: (id: number) =>
request<{ status: string; message: string }>(`/library/folders/${id}`, { method: 'DELETE' }),
getLibraryFoldersByProject: (projectId: number) =>
request<LibraryFolder[]>(`/library/folders/by-project/${projectId}`),
getLibraryFoldersByArchive: (archiveId: number) =>
request<LibraryFolder[]>(`/library/folders/by-archive/${archiveId}`),
getLibraryFiles: (folderId?: number | null, includeRoot = true) => {
const params = new URLSearchParams();
if (folderId !== undefined && folderId !== null) {
params.set('folder_id', String(folderId));
}
params.set('include_root', String(includeRoot));
return request<LibraryFileListItem[]>(`/library/files?${params}`);
},
getLibraryFile: (id: number) => request<LibraryFile>(`/library/files/${id}`),
uploadLibraryFile: async (file: File, folderId?: number | null): Promise<LibraryFileUploadResponse> => {
const formData = new FormData();
formData.append('file', file);
const params = folderId ? `?folder_id=${folderId}` : '';
const response = await fetch(`${API_BASE}/library/files${params}`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
return response.json();
},
updateLibraryFile: (id: number, data: LibraryFileUpdate) =>
request<LibraryFile>(`/library/files/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
deleteLibraryFile: (id: number) =>
request<{ status: string; message: string }>(`/library/files/${id}`, { method: 'DELETE' }),
getLibraryFileDownloadUrl: (id: number) => `${API_BASE}/library/files/${id}/download`,
getLibraryFileThumbnailUrl: (id: number) => `${API_BASE}/library/files/${id}/thumbnail`,
getLibraryFileGcodeUrl: (id: number) => `${API_BASE}/library/files/${id}/gcode`,
moveLibraryFiles: (fileIds: number[], folderId: number | null) =>
request<{ status: string; moved: number }>('/library/files/move', {
method: 'POST',
body: JSON.stringify({ file_ids: fileIds, folder_id: folderId }),
}),
bulkDeleteLibrary: (fileIds: number[], folderIds: number[]) =>
request<{ deleted_files: number; deleted_folders: number }>('/library/bulk-delete', {
method: 'POST',
body: JSON.stringify({ file_ids: fileIds, folder_ids: folderIds }),
}),
getLibraryStats: () => request<LibraryStats>('/library/stats'),
addLibraryFilesToQueue: (fileIds: number[]) =>
request<AddToQueueResponse>('/library/files/add-to-queue', {
method: 'POST',
body: JSON.stringify({ file_ids: fileIds }),
}),
};
// AMS History types
@@ -2536,6 +2632,137 @@ export interface SystemInfo {
};
}
// Library (File Manager) types
export interface LibraryFolderTree {
id: number;
name: string;
parent_id: number | null;
project_id: number | null;
archive_id: number | null;
project_name: string | null;
archive_name: string | null;
file_count: number;
children: LibraryFolderTree[];
}
export interface LibraryFolder {
id: number;
name: string;
parent_id: number | null;
project_id: number | null;
archive_id: number | null;
project_name: string | null;
archive_name: string | null;
file_count: number;
created_at: string;
updated_at: string;
}
export interface LibraryFolderCreate {
name: string;
parent_id?: number | null;
project_id?: number | null;
archive_id?: number | null;
}
export interface LibraryFolderUpdate {
name?: string;
parent_id?: number | null;
project_id?: number | null; // 0 to unlink
archive_id?: number | null; // 0 to unlink
}
export interface LibraryFileDuplicate {
id: number;
filename: string;
folder_id: number | null;
folder_name: string | null;
created_at: string;
}
export interface LibraryFile {
id: number;
folder_id: number | null;
folder_name: string | null;
project_id: number | null;
project_name: string | null;
filename: string;
file_path: string;
file_type: string;
file_size: number;
file_hash: string | null;
thumbnail_path: string | null;
metadata: Record<string, unknown> | null;
print_count: number;
last_printed_at: string | null;
notes: string | null;
duplicates: LibraryFileDuplicate[] | null;
duplicate_count: number;
created_at: string;
updated_at: string;
}
export interface LibraryFileListItem {
id: number;
folder_id: number | null;
filename: string;
file_type: string;
file_size: number;
thumbnail_path: string | null;
print_count: number;
duplicate_count: number;
created_at: string;
print_name: string | null;
print_time_seconds: number | null;
filament_used_grams: number | null;
}
export interface LibraryFileUpdate {
folder_id?: number | null;
project_id?: number | null;
notes?: string | null;
}
export interface LibraryFileUploadResponse {
id: number;
filename: string;
file_type: string;
file_size: number;
thumbnail_path: string | null;
duplicate_of: number | null;
metadata: Record<string, unknown> | null;
}
export interface LibraryStats {
total_files: number;
total_folders: number;
total_size_bytes: number;
files_by_type: Record<string, number>;
total_prints: number;
disk_free_bytes: number;
disk_total_bytes: number;
disk_used_bytes: number;
}
// Library Queue types
export interface AddToQueueResult {
file_id: number;
filename: string;
queue_item_id: number;
archive_id: number;
}
export interface AddToQueueError {
file_id: number;
filename: string;
error: string;
}
export interface AddToQueueResponse {
added: AddToQueueResult[];
errors: AddToQueueError[];
}
// Discovery types
export interface DiscoveredPrinter {
serial: string;