task(F1-frontend): library export/import buttons in block library tree
This commit is contained in:
@@ -9,6 +9,8 @@ import {
|
|||||||
createGlobalBlock,
|
createGlobalBlock,
|
||||||
renameGlobalBlock,
|
renameGlobalBlock,
|
||||||
deleteGlobalBlock,
|
deleteGlobalBlock,
|
||||||
|
exportLibrary,
|
||||||
|
importLibrary,
|
||||||
} from '../services/api';
|
} from '../services/api';
|
||||||
|
|
||||||
interface BlockLibraryTreeProps {
|
interface BlockLibraryTreeProps {
|
||||||
@@ -38,6 +40,7 @@ const BlockLibraryTree: React.FC<BlockLibraryTreeProps> = ({ token, onBlockDragS
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const wcadInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [dragOverFolderId, setDragOverFolderId] = useState<string | null>(null);
|
const [dragOverFolderId, setDragOverFolderId] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
@@ -59,6 +62,39 @@ const BlockLibraryTree: React.FC<BlockLibraryTreeProps> = ({ token, onBlockDragS
|
|||||||
loadData();
|
loadData();
|
||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
|
// ─── Task F1: Library-Paket Export/Import ────────────────
|
||||||
|
|
||||||
|
const handleLibraryExport = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const pkg = await exportLibrary(token);
|
||||||
|
const blob = new Blob([JSON.stringify(pkg, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `web-cad-library-${new Date().toISOString().slice(0, 10)}.wcadlib`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Library-Export fehlgeschlagen: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const handleWcadFile = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const pkg = JSON.parse(text);
|
||||||
|
const result = await importLibrary(token, pkg);
|
||||||
|
alert(`Bibliothek importiert: ${result.imported_blocks} Blöcke, ${result.imported_folders} Ordner`);
|
||||||
|
await loadData();
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Library-Import fehlgeschlagen: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
} finally {
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
}, [token, loadData]);
|
||||||
|
|
||||||
// Build tree structure from flat lists
|
// Build tree structure from flat lists
|
||||||
const buildTree = useCallback((): FolderNode[] => {
|
const buildTree = useCallback((): FolderNode[] => {
|
||||||
const folderMap = new Map<string, FolderNode>();
|
const folderMap = new Map<string, FolderNode>();
|
||||||
@@ -366,6 +402,20 @@ const BlockLibraryTree: React.FC<BlockLibraryTreeProps> = ({ token, onBlockDragS
|
|||||||
<div className="global-lib-tree" onContextMenu={(e) => handleContextMenu(e, 'root', null, null)}>
|
<div className="global-lib-tree" onContextMenu={(e) => handleContextMenu(e, 'root', null, null)}>
|
||||||
<div className="global-lib-header">
|
<div className="global-lib-header">
|
||||||
<span className="global-lib-title">🌐 Globale Bibliothek</span>
|
<span className="global-lib-title">🌐 Globale Bibliothek</span>
|
||||||
|
<button
|
||||||
|
className="global-lib-add-btn"
|
||||||
|
title="Bibliothek als .wcadlib exportieren"
|
||||||
|
onClick={() => void handleLibraryExport()}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 17 17 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="global-lib-add-btn"
|
||||||
|
title=".wcadlib-Bibliothek importieren"
|
||||||
|
onClick={() => wcadInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="global-lib-add-btn"
|
className="global-lib-add-btn"
|
||||||
title="SVG in globale Bibliothek importieren"
|
title="SVG in globale Bibliothek importieren"
|
||||||
@@ -374,6 +424,7 @@ const BlockLibraryTree: React.FC<BlockLibraryTreeProps> = ({ token, onBlockDragS
|
|||||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<input ref={fileInputRef} type="file" accept=".svg,image/svg+xml" style={{ display: 'none' }} onChange={handleSvgImport} />
|
<input ref={fileInputRef} type="file" accept=".svg,image/svg+xml" style={{ display: 'none' }} onChange={handleSvgImport} />
|
||||||
|
<input ref={wcadInputRef} type="file" accept=".wcadlib,application/json" style={{ display: 'none' }} onChange={handleWcadFile} />
|
||||||
</div>
|
</div>
|
||||||
<div className="tree tree-global" role="tree" aria-label="Globale Block-Bibliothek">
|
<div className="tree tree-global" role="tree" aria-label="Globale Block-Bibliothek">
|
||||||
{tree.map(node => renderFolderNode(node, 0))}
|
{tree.map(node => renderFolderNode(node, 0))}
|
||||||
|
|||||||
@@ -812,3 +812,33 @@ export async function deleteGuest(token: string, id: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`Failed to delete guest: ${res.status}`);
|
if (!res.ok) throw new Error(`Failed to delete guest: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Library Paket Export/Import (Task F1) ─────────────
|
||||||
|
export interface WcadlibPackage {
|
||||||
|
format: 'wcadlib';
|
||||||
|
version: number;
|
||||||
|
exported_at?: string;
|
||||||
|
folders: Array<{ id: string; name: string; parent_id: string | null }>;
|
||||||
|
blocks: Array<{ name: string; folder_id: string | null; block_data: string; svg_data: string | null }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportLibrary(token: string): Promise<unknown> {
|
||||||
|
const res = await fetch(`${API_BASE}/api/global-blocks/export`, {
|
||||||
|
headers: authHeaders(token),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Failed to export library: ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importLibrary(token: string, pkg: { format: string; version?: number; folders?: Array<{ id: string; name: string; parent_id: string | null }>; blocks: Array<{ name: string; folder_id: string | null; block_data: string; svg_data: string | null }> }): Promise<{ imported_blocks: number; imported_folders: number }> {
|
||||||
|
const res = await fetch(`${API_BASE}/api/global-blocks/import`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: authHeaders(token),
|
||||||
|
body: JSON.stringify(pkg),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text();
|
||||||
|
throw new Error(`Failed to import library: ${res.status} ${err}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user