feat: enhance BlockLibrary with create, delete, search and category features
This commit is contained in:
@@ -1,32 +1,112 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
|
||||||
const BlockLibrary = ({ canvasRef }) => {
|
const BlockLibrary = ({ canvasRef, yjsDoc }) => {
|
||||||
const [blocks, setBlocks] = useState([]);
|
const [blocks, setBlocks] = useState([]);
|
||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
const [activeCategory, setActiveCategory] = useState('Alle');
|
const [activeCategory, setActiveCategory] = useState('Alle');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedElements, setSelectedElements] = useState([]);
|
||||||
|
const [newBlockName, setNewBlockName] = useState('');
|
||||||
|
const [newBlockCategory, setNewBlockCategory] = useState('');
|
||||||
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// Load blocks on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get('/blocks')
|
loadBlocks();
|
||||||
.then(res => {
|
|
||||||
const data = res.data || [];
|
|
||||||
setBlocks(data);
|
|
||||||
const cats = ['Alle', ...new Set(data.map(b => b.category || 'Allgemein'))];
|
|
||||||
setCategories(cats);
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch(() => setLoading(false));
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredBlocks = activeCategory === 'Alle'
|
const loadBlocks = async () => {
|
||||||
? blocks
|
try {
|
||||||
: blocks.filter(b => (b.category || 'Allgemein') === activeCategory);
|
const res = await api.get('/blocks');
|
||||||
|
const data = res.data || [];
|
||||||
|
setBlocks(data);
|
||||||
|
|
||||||
|
// Extract unique categories
|
||||||
|
const cats = ['Alle', ...new Set(data.map(b => b.category || 'Allgemein'))];
|
||||||
|
setCategories(cats);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load blocks:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter blocks by category and search query
|
||||||
|
const filteredBlocks = blocks.filter(block => {
|
||||||
|
const matchesCategory = activeCategory === 'Alle' || (block.category || 'Allgemein') === activeCategory;
|
||||||
|
const matchesSearch = block.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
(block.category || '').toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
return matchesCategory && matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle creating a new block from selected elements
|
||||||
|
const handleCreateBlock = async () => {
|
||||||
|
if (!newBlockName.trim() || selectedElements.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post('/blocks', {
|
||||||
|
name: newBlockName,
|
||||||
|
category: newBlockCategory || 'Allgemein',
|
||||||
|
elements: selectedElements
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to Yjs document
|
||||||
|
if (yjsDoc) {
|
||||||
|
const blockId = response.data.id;
|
||||||
|
yjsDoc.blocks.set(blockId, response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh blocks list
|
||||||
|
loadBlocks();
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setNewBlockName('');
|
||||||
|
setNewBlockCategory('');
|
||||||
|
setShowCreateDialog(false);
|
||||||
|
setSelectedElements([]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create block:', error);
|
||||||
|
alert('Fehler beim Erstellen des Blocks');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle deleting a block
|
||||||
|
const handleDeleteBlock = async (blockId, blockName) => {
|
||||||
|
if (!window.confirm(`Sind Sie sicher, dass Sie den Block "${blockName}" löschen möchten?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/blocks/${blockId}`);
|
||||||
|
|
||||||
|
// Remove from Yjs document
|
||||||
|
if (yjsDoc) {
|
||||||
|
yjsDoc.blocks.delete(blockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh blocks list
|
||||||
|
loadBlocks();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete block:', error);
|
||||||
|
alert('Fehler beim Löschen des Blocks');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle inserting a block instance on canvas
|
||||||
|
const handleInsertBlock = (block) => {
|
||||||
|
if (canvasRef.current) {
|
||||||
|
canvasRef.current.addSvgObject(block.svg_data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle drag start for block insertion
|
||||||
const handleDragStart = (e, block) => {
|
const handleDragStart = (e, block) => {
|
||||||
e.dataTransfer.setData('text/plain', block.svg_data);
|
e.dataTransfer.setData('text/plain', block.svg_data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle drop on canvas
|
||||||
const handleDrop = (e) => {
|
const handleDrop = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const svgData = e.dataTransfer.getData('text/plain');
|
const svgData = e.dataTransfer.getData('text/plain');
|
||||||
@@ -47,6 +127,23 @@ const BlockLibrary = ({ canvasRef }) => {
|
|||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
>
|
>
|
||||||
|
{/* Search bar */}
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Blöcke suchen..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: 12,
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: 4
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Category filter */}
|
{/* Category filter */}
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 8 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 8 }}>
|
||||||
{categories.map(cat => (
|
{categories.map(cat => (
|
||||||
@@ -67,6 +164,26 @@ const BlockLibrary = ({ canvasRef }) => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Create block button */}
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateDialog(true)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '6px',
|
||||||
|
fontSize: 12,
|
||||||
|
background: '#52c41a',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Neuer Block
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Block grid */}
|
{/* Block grid */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
|
||||||
{filteredBlocks.map(block => (
|
{filteredBlocks.map(block => (
|
||||||
@@ -74,6 +191,7 @@ const BlockLibrary = ({ canvasRef }) => {
|
|||||||
key={block.id}
|
key={block.id}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => handleDragStart(e, block)}
|
onDragStart={(e) => handleDragStart(e, block)}
|
||||||
|
onClick={() => handleInsertBlock(block)}
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
@@ -81,6 +199,7 @@ const BlockLibrary = ({ canvasRef }) => {
|
|||||||
background: 'white',
|
background: 'white',
|
||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
title={block.name}
|
title={block.name}
|
||||||
>
|
>
|
||||||
@@ -91,11 +210,128 @@ const BlockLibrary = ({ canvasRef }) => {
|
|||||||
<div style={{ fontSize: 10, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
<div style={{ fontSize: 10, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
{block.name}
|
{block.name}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteBlock(block.id, block.name);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 2,
|
||||||
|
right: 2,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
fontSize: 10,
|
||||||
|
background: '#ff4d4f',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '50%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredBlocks.length === 0 && (
|
{filteredBlocks.length === 0 && (
|
||||||
<p style={{ fontSize: 12, color: '#999' }}>Keine Blöcke in dieser Kategorie.</p>
|
<p style={{ fontSize: 12, color: '#999' }}>Keine Blöcke gefunden.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Create Block Dialog */}
|
||||||
|
{showCreateDialog && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
background: 'rgba(0,0,0,0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 1000,
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'white',
|
||||||
|
padding: 20,
|
||||||
|
borderRadius: 8,
|
||||||
|
width: 300,
|
||||||
|
}}>
|
||||||
|
<h3 style={{ marginTop: 0 }}>Neuen Block erstellen</h3>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: 4, fontSize: 12 }}>Name:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newBlockName}
|
||||||
|
onChange={(e) => setNewBlockName(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '6px',
|
||||||
|
fontSize: 12,
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: 4, fontSize: 12 }}>Kategorie:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newBlockCategory}
|
||||||
|
onChange={(e) => setNewBlockCategory(e.target.value)}
|
||||||
|
placeholder="Allgemein"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '6px',
|
||||||
|
fontSize: 12,
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<button
|
||||||
|
onClick={handleCreateBlock}
|
||||||
|
disabled={!newBlockName.trim()}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '8px',
|
||||||
|
fontSize: 12,
|
||||||
|
background: '#1890ff',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Erstellen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowCreateDialog(false);
|
||||||
|
setNewBlockName('');
|
||||||
|
setNewBlockCategory('');
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '8px',
|
||||||
|
fontSize: 12,
|
||||||
|
background: '#f0f0f0',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user