85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
|
|
/**
|
|||
|
|
* Task F4 – UI für LibraryProviderExtensions.
|
|||
|
|
*
|
|||
|
|
* Lädt alle übergebenen Provider (builtin/user/global) und rendert je Ordner
|
|||
|
|
* eine Sektion mit Thumbnail-Blöcken. Jeder Block ist als DragSource an den
|
|||
|
|
* bestehenden 'text/global-block-data'-Kanal angebunden (DragDrop → Canvas).
|
|||
|
|
*/
|
|||
|
|
import React, { useEffect, useState } from 'react';
|
|||
|
|
import type { LibraryProviderExtension, LibBlock } from '../plugins/types';
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
providers: LibraryProviderExtension[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface Section { key: string; label: string; blocks: LibBlock[]; }
|
|||
|
|
|
|||
|
|
const LibraryProviderPanel: React.FC<Props> = ({ providers }) => {
|
|||
|
|
const [sections, setSections] = useState<Section[]>([]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
let cancelled = false;
|
|||
|
|
(async () => {
|
|||
|
|
const out: Section[] = [];
|
|||
|
|
for (const provider of providers) {
|
|||
|
|
try {
|
|||
|
|
const folders = await provider.listFolders();
|
|||
|
|
for (const folder of folders) {
|
|||
|
|
try {
|
|||
|
|
const blocks = await provider.listBlocks(folder.id);
|
|||
|
|
if (blocks.length > 0) {
|
|||
|
|
out.push({ key: `${provider.id}/${folder.id}`, label: folder.label, blocks });
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
// Ordner bei Fehler überspringen
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
// Provider bei Fehler überspringen
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (!cancelled) setSections(out);
|
|||
|
|
})();
|
|||
|
|
return () => {
|
|||
|
|
cancelled = true;
|
|||
|
|
};
|
|||
|
|
}, [providers]);
|
|||
|
|
|
|||
|
|
const handleDragStart = (e: React.DragEvent, block: LibBlock) => {
|
|||
|
|
const payload = typeof block.payload === 'string' ? block.payload : JSON.stringify(block.payload ?? []);
|
|||
|
|
e.dataTransfer.setData('text/global-block-data', payload);
|
|||
|
|
e.dataTransfer.effectAllowed = 'copy';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (sections.length === 0) return null;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="lib-provider-panel">
|
|||
|
|
{sections.map((section) => (
|
|||
|
|
<div className="lprov-section" key={section.key}>
|
|||
|
|
<div className="lprov-title">{section.label}</div>
|
|||
|
|
<div className="lprov-grid">
|
|||
|
|
{section.blocks.map((block) => (
|
|||
|
|
<button
|
|||
|
|
key={block.id}
|
|||
|
|
type="button"
|
|||
|
|
className="lprov-block"
|
|||
|
|
aria-label={block.name}
|
|||
|
|
title={block.name}
|
|||
|
|
draggable
|
|||
|
|
onDragStart={(e) => handleDragStart(e, block)}
|
|||
|
|
>
|
|||
|
|
{block.thumbnail
|
|||
|
|
? <span className="tree-thumb" dangerouslySetInnerHTML={{ __html: block.thumbnail }} />
|
|||
|
|
: <span className="lib-grid-placeholder">📦</span>}
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default LibraryProviderPanel;
|