feat: layer-tree selection sync + group folders in tree + ribbonbar readability
This commit is contained in:
@@ -1088,6 +1088,12 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
|
||||
// Layer handlers
|
||||
const handleSelectLayer = useCallback((id: string) => setActiveLayerId(id), []);
|
||||
|
||||
const handleSelectElement = useCallback((id: string) => {
|
||||
setSelectedElementIds([id]);
|
||||
const el = elements.find(e => e.id === id);
|
||||
setSelectedElement(el ?? null);
|
||||
}, [elements]);
|
||||
const handleAddLayer = useCallback(() => {
|
||||
setLayers((prev) => {
|
||||
const newId = `layer-${Date.now()}`;
|
||||
@@ -1521,6 +1527,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
blocks={blocks}
|
||||
onBlockDrop={handleBlockDrop}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
externalSelectionIds={selectedElementIds}
|
||||
selectedTemplate={selectedTemplate}
|
||||
bgConfig={bgConfig}
|
||||
remoteCursors={collab.cursors}
|
||||
@@ -1563,6 +1570,9 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
onReorder={handleReorderLayer}
|
||||
onAddSubLayer={handleAddSubLayer}
|
||||
onUpdateElement={handleUpdateElement}
|
||||
selectedElementIds={selectedElementIds}
|
||||
groups={groups}
|
||||
onSelectElement={handleSelectElement}
|
||||
token={token}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
cursorPos, viewMode, onViewChange, gridEnabled, orthoEnabled, snapEnabled,
|
||||
polarEnabled,
|
||||
activeTool, elements, layers, activeLayerId, onElementCreated, onElementsDeleted, onElementsModified, onCursorMoved, onToolStateChanged,
|
||||
onToggleGrid, onToggleOrtho, onToggleSnap, onZoomIn, onZoomOut, onZoomFit, onTextEdit, onCommandTrigger, blocks, onBlockDrop, onSelectionChange, selectedTemplate, bgConfig, remoteCursors, zoomCommand,
|
||||
onToggleGrid, onToggleOrtho, onToggleSnap, onZoomIn, onZoomOut, onZoomFit, onTextEdit, onCommandTrigger, blocks, onBlockDrop, onSelectionChange, externalSelectionIds, selectedTemplate, bgConfig, remoteCursors, zoomCommand,
|
||||
groups,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
@@ -25,6 +25,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
const interactionRef = useRef<InteractionEngine | null>(null);
|
||||
const spatialIndexRef = useRef<SpatialIndex | null>(null);
|
||||
const layerManagerRef = useRef<LayerManager | null>(null);
|
||||
const selectionEngineRef = useRef<SelectionEngine | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
@@ -45,6 +46,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
interactionRef.current = interaction;
|
||||
spatialIndexRef.current = spatialIndex;
|
||||
layerManagerRef.current = layerManager;
|
||||
selectionEngineRef.current = selectionEngine;
|
||||
|
||||
interaction.setCallbacks({
|
||||
onElementCreated,
|
||||
@@ -66,6 +68,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
interactionRef.current = null;
|
||||
spatialIndexRef.current = null;
|
||||
layerManagerRef.current = null;
|
||||
selectionEngineRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -169,6 +172,28 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [groups]);
|
||||
|
||||
// Sync external selection (from tree panel) to SelectionEngine
|
||||
const prevExternalSelectionRef = useRef<string[]>([]);
|
||||
useEffect(() => {
|
||||
const selectionEngine = selectionEngineRef.current;
|
||||
const renderEngine = renderEngineRef.current;
|
||||
if (!selectionEngine || !renderEngine) return;
|
||||
if (!externalSelectionIds || externalSelectionIds.length === 0) return;
|
||||
// Avoid feedback loop: skip if selection is already identical
|
||||
const currentIds = Array.from(selectionEngine.getSelectedIds()).sort();
|
||||
const incomingIds = [...externalSelectionIds].sort();
|
||||
const same = currentIds.length === incomingIds.length && currentIds.every((id, i) => id === incomingIds[i]);
|
||||
if (same) return;
|
||||
// Avoid re-processing the same external selection we just applied
|
||||
const prevIds = prevExternalSelectionRef.current;
|
||||
const sameAsPrev = prevIds.length === incomingIds.length && prevIds.every((id, i) => id === incomingIds[i]);
|
||||
if (sameAsPrev) return;
|
||||
prevExternalSelectionRef.current = incomingIds;
|
||||
selectionEngine.selectByIds(externalSelectionIds, false);
|
||||
renderEngine.render();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [externalSelectionIds]);
|
||||
|
||||
// Sync active layer to LayerManager
|
||||
useEffect(() => {
|
||||
const layerManager = layerManagerRef.current;
|
||||
|
||||
@@ -32,6 +32,17 @@ const layerIcon = (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const groupIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
<rect x="7" y="11" width="4" height="4" rx="0.5" />
|
||||
<rect x="13" y="11" width="4" height="4" rx="0.5" />
|
||||
<line x1="9" y1="15" x2="9" y2="17" />
|
||||
<line x1="15" y1="15" x2="15" y2="17" />
|
||||
<line x1="9" y1="17" x2="15" y2="17" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const elementIcons: Record<string, React.ReactNode> = {
|
||||
line: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="4" y1="20" x2="20" y2="4"/></svg>,
|
||||
rect: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="4" y="4" width="16" height="16"/></svg>,
|
||||
@@ -58,7 +69,10 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
||||
layers,
|
||||
elements = [],
|
||||
activeLayerId,
|
||||
selectedElementIds = [],
|
||||
groups = [],
|
||||
onSelectLayer,
|
||||
onSelectElement,
|
||||
onAddLayer,
|
||||
onToggleLayer,
|
||||
onDeleteLayer,
|
||||
@@ -76,22 +90,71 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((l) => {
|
||||
const layerElements = elements.filter((e) => e.layerId === l.id);
|
||||
const elementNodes: TreeNode[] = layerElements.map((e) => ({
|
||||
|
||||
// Build group nodes for this layer
|
||||
const layerGroups = groups.filter((g) =>
|
||||
g.elementIds.some((eid) => layerElements.some((e) => e.id === eid))
|
||||
);
|
||||
const groupNodes: TreeNode[] = layerGroups.map((g) => {
|
||||
const groupElementIds = g.elementIds.filter((eid) =>
|
||||
layerElements.some((e) => e.id === eid)
|
||||
);
|
||||
const groupElementsList = groupElementIds
|
||||
.map((eid) => elements.find((e) => e.id === eid))
|
||||
.filter((e): e is CADElement => e !== undefined);
|
||||
|
||||
const groupChildNodes: TreeNode[] = groupElementsList.map((e) => ({
|
||||
id: e.id,
|
||||
name: elementTypeLabels[e.type] || e.type,
|
||||
icon: elementIcons[e.type] || defaultIcon,
|
||||
expanded: false,
|
||||
active: false,
|
||||
active: selectedElementIds.includes(e.id),
|
||||
children: [],
|
||||
}));
|
||||
|
||||
// A group is "active" if any of its elements are selected
|
||||
const groupActive = groupElementIds.some((eid) => selectedElementIds.includes(eid));
|
||||
|
||||
return {
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
icon: groupIcon,
|
||||
expanded: false,
|
||||
active: groupActive,
|
||||
children: groupChildNodes,
|
||||
count: groupElementIds.length > 0 ? groupElementIds.length : undefined,
|
||||
} as TreeNode;
|
||||
});
|
||||
|
||||
// Build element nodes — only elements NOT in any group go directly under layer
|
||||
const groupedElementIds = new Set<string>();
|
||||
for (const g of layerGroups) {
|
||||
for (const eid of g.elementIds) {
|
||||
if (layerElements.some((e) => e.id === eid)) {
|
||||
groupedElementIds.add(eid);
|
||||
}
|
||||
}
|
||||
}
|
||||
const ungroupedElements = layerElements.filter((e) => !groupedElementIds.has(e.id));
|
||||
const elementNodes: TreeNode[] = ungroupedElements.map((e) => ({
|
||||
id: e.id,
|
||||
name: elementTypeLabels[e.type] || e.type,
|
||||
icon: elementIcons[e.type] || defaultIcon,
|
||||
expanded: false,
|
||||
active: selectedElementIds.includes(e.id),
|
||||
children: [],
|
||||
}));
|
||||
|
||||
const subLayerNodes = buildTree(l.id);
|
||||
|
||||
// Groups appear above elements, sublayers appear first
|
||||
return {
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
icon: layerIcon,
|
||||
expanded: false,
|
||||
active: l.id === activeLayerId,
|
||||
children: [...subLayerNodes, ...elementNodes],
|
||||
children: [...subLayerNodes, ...groupNodes, ...elementNodes],
|
||||
count: layerElements.length > 0 ? layerElements.length : undefined,
|
||||
} as TreeNode;
|
||||
});
|
||||
@@ -99,6 +162,24 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
||||
|
||||
const tree = buildTree(null);
|
||||
|
||||
// Determine which IDs are layer IDs vs element IDs vs group IDs for dispatch
|
||||
const layerIds = new Set(layers.map((l) => l.id));
|
||||
const groupIds = new Set(groups.map((g) => g.id));
|
||||
|
||||
const handleSelect = (id: string) => {
|
||||
if (layerIds.has(id)) {
|
||||
onSelectLayer(id);
|
||||
} else if (groupIds.has(id)) {
|
||||
const group = groups.find((g) => g.id === id);
|
||||
if (group && group.elementIds.length > 0) {
|
||||
onSelectElement?.(group.elementIds[0]);
|
||||
}
|
||||
} else {
|
||||
// It's an element ID
|
||||
onSelectElement?.(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSubLayer = (parentId: string) => {
|
||||
if (onAddSubLayer) {
|
||||
onAddSubLayer(parentId);
|
||||
@@ -131,7 +212,8 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
||||
<TreeView
|
||||
nodes={tree}
|
||||
selectedId={activeLayerId}
|
||||
onSelect={onSelectLayer}
|
||||
selectedIds={selectedElementIds}
|
||||
onSelect={handleSelect}
|
||||
onReorder={onReorder}
|
||||
draggable={true}
|
||||
renderIcon={(node) => node.icon}
|
||||
|
||||
@@ -24,6 +24,9 @@ const RightSidebar: React.FC<RightSidebarProps> = ({
|
||||
onRenameBlock, onDuplicateBlock, onDeleteBlock, onSvgImport, onSaveGroupAsBlock,
|
||||
onBlockCategoryChange, onBlockSearch, onDragBlock,
|
||||
kiMessages, kiSuggestions, onKISend, onKISuggestionClick, kiLoading, onUpdateElement,
|
||||
selectedElementIds,
|
||||
groups,
|
||||
onSelectElement,
|
||||
onCollapse,
|
||||
collapsed,
|
||||
onToggleCollapse,
|
||||
@@ -79,7 +82,7 @@ const RightSidebar: React.FC<RightSidebarProps> = ({
|
||||
}} />}
|
||||
</div>
|
||||
<div className={`rightbar-panel${activePanel === 'layer' ? ' active' : ''}`} data-panel-content="layer">
|
||||
{activePanel === 'layer' && <LayerPanel layers={layers} elements={elements} onElementsDeleted={onElementsDeleted} onToggleElementVisible={onToggleElementVisible} activeLayerId={activeLayerId} onSelectLayer={onSelectLayer ?? (() => {})} onAddLayer={onAddLayer ?? (() => {})} onToggleLayer={onToggleLayer ?? (() => {})} onDeleteLayer={onDeleteLayer} onRenameLayer={onRenameLayer} onDuplicateLayer={onDuplicateLayer} onToggleLock={onToggleLock} onReorder={onReorder} onAddSubLayer={onAddSubLayer} />}
|
||||
{activePanel === 'layer' && <LayerPanel layers={layers} elements={elements} onElementsDeleted={onElementsDeleted} onToggleElementVisible={onToggleElementVisible} activeLayerId={activeLayerId} onSelectLayer={onSelectLayer ?? (() => {})} onSelectElement={onSelectElement} selectedElementIds={selectedElementIds} groups={groups} onAddLayer={onAddLayer ?? (() => {})} onToggleLayer={onToggleLayer ?? (() => {})} onDeleteLayer={onDeleteLayer} onRenameLayer={onRenameLayer} onDuplicateLayer={onDuplicateLayer} onToggleLock={onToggleLock} onReorder={onReorder} onAddSubLayer={onAddSubLayer} />}
|
||||
</div>
|
||||
<div className={`rightbar-panel${activePanel === 'library' ? ' active' : ''}`} data-panel-content="library">
|
||||
{activePanel === 'library' && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { TreeNode } from '../types/ui.types';
|
||||
interface TreeViewProps {
|
||||
nodes: TreeNode[];
|
||||
selectedId?: string | null;
|
||||
selectedIds?: string[];
|
||||
onSelect: (id: string) => void;
|
||||
onToggle?: (id: string) => void;
|
||||
onReorder?: (draggedId: string, targetId: string, position: 'before' | 'after' | 'inside') => void;
|
||||
@@ -16,6 +17,7 @@ interface TreeViewProps {
|
||||
const TreeView: React.FC<TreeViewProps> = ({
|
||||
nodes,
|
||||
selectedId,
|
||||
selectedIds,
|
||||
onSelect,
|
||||
onToggle,
|
||||
onReorder,
|
||||
@@ -87,7 +89,7 @@ const TreeView: React.FC<TreeViewProps> = ({
|
||||
const renderNode = (node: TreeNode, level: number): React.ReactNode => {
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const isExpanded = expandedSet.has(node.id) || node.expanded;
|
||||
const isSelected = selectedId === node.id;
|
||||
const isSelected = selectedId === node.id || (selectedIds !== undefined && selectedIds.includes(node.id));
|
||||
const isDragOver = dragOverId === node.id;
|
||||
const isDragging = draggedId === node.id;
|
||||
|
||||
|
||||
@@ -253,7 +253,7 @@ a:hover { text-decoration: underline; }
|
||||
.project-name:hover { background: var(--color-surface-2); }
|
||||
.project-name .caret { color: var(--color-text-faint); }
|
||||
.saved-badge {
|
||||
font-size: var(--fs-xs);
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--color-text-muted);
|
||||
padding: 2px 8px;
|
||||
background: var(--color-surface-2);
|
||||
@@ -355,7 +355,7 @@ a:hover { text-decoration: underline; }
|
||||
gap: 6px;
|
||||
padding: 0 12px;
|
||||
height: 100%;
|
||||
font-size: var(--fs-sm);
|
||||
font-size: var(--fs-md);
|
||||
color: var(--color-text-muted);
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
@@ -385,8 +385,8 @@ a:hover { text-decoration: underline; }
|
||||
}
|
||||
.ribbon-group-btns { display: flex; align-items: center; gap: 2px; flex: 1; }
|
||||
.ribbon-group-label {
|
||||
font-size: 10px;
|
||||
color: var(--color-text-faint);
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
@@ -414,7 +414,7 @@ a:hover { text-decoration: underline; }
|
||||
padding: 4px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text);
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all var(--t-fast);
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -132,6 +132,7 @@ export interface CanvasAreaProps {
|
||||
blocks?: BlockDefinition[];
|
||||
onBlockDrop?: (blockId: string, x: number, y: number) => void;
|
||||
onSelectionChange?: (selectedIds: string[]) => void;
|
||||
externalSelectionIds?: string[];
|
||||
selectedTemplate?: string | null;
|
||||
bgConfig?: BackgroundConfig | null;
|
||||
remoteCursors?: UserCursor[];
|
||||
@@ -174,6 +175,9 @@ export interface RightSidebarProps {
|
||||
onKISuggestionClick?: (suggestion: KISuggestion) => void;
|
||||
kiLoading?: boolean;
|
||||
onUpdateElement?: (el: CADElement) => void;
|
||||
selectedElementIds?: string[];
|
||||
groups?: Array<{ id: string; name: string; elementIds: string[]; parentGroupId: string | null }>;
|
||||
onSelectElement?: (id: string) => void;
|
||||
token?: string;
|
||||
className?: string;
|
||||
onCollapse?: () => void;
|
||||
@@ -192,7 +196,10 @@ export interface LayerPanelProps {
|
||||
layers: CADLayer[];
|
||||
elements?: CADElement[];
|
||||
activeLayerId?: string;
|
||||
selectedElementIds?: string[];
|
||||
groups?: Array<{ id: string; name: string; elementIds: string[]; parentGroupId: string | null }>;
|
||||
onSelectLayer: (id: string) => void;
|
||||
onSelectElement?: (id: string) => void;
|
||||
onAddLayer: () => void;
|
||||
onToggleLayer: (id: string) => void;
|
||||
onDeleteLayer?: (id: string) => void;
|
||||
|
||||
@@ -238,6 +238,112 @@ describe('LayerPanel', () => {
|
||||
fireEvent.click(toggleBtn);
|
||||
expect(onToggleLayer).toHaveBeenCalled();
|
||||
});
|
||||
// Helper: expand a tree node by clicking its toggle button
|
||||
function expandLayer(label: string) {
|
||||
const layerNode = screen.getByText(label).closest('.tree-node');
|
||||
expect(layerNode).toBeTruthy();
|
||||
const toggle = layerNode?.querySelector('.tree-toggle');
|
||||
expect(toggle).toBeTruthy();
|
||||
fireEvent.click(toggle!);
|
||||
}
|
||||
|
||||
it('should highlight selected element in tree when selectedElementIds is provided', () => {
|
||||
const layers = [makeLayer({ id: 'layer-1', name: 'Layer 1' })];
|
||||
const elements = [
|
||||
makeElement({ id: 'elem-1', type: 'rect', layerId: 'layer-1' }),
|
||||
makeElement({ id: 'elem-2', type: 'circle', layerId: 'layer-1' }),
|
||||
];
|
||||
const { container } = render(
|
||||
<LayerPanel
|
||||
layers={layers}
|
||||
elements={elements}
|
||||
selectedElementIds={['elem-2']}
|
||||
activeLayerId="layer-1"
|
||||
onSelectLayer={() => {}}
|
||||
onAddLayer={() => {}}
|
||||
onToggleLayer={() => {}}
|
||||
/>,
|
||||
);
|
||||
expandLayer('Layer 1');
|
||||
// The element node for elem-2 should have the 'active' class
|
||||
const treeNodes = container.querySelectorAll('.tree-node');
|
||||
const elemNode = Array.from(treeNodes).find((n) => n.textContent?.includes('Kreis'));
|
||||
expect(elemNode).toBeTruthy();
|
||||
expect(elemNode?.classList.contains('active')).toBe(true);
|
||||
});
|
||||
|
||||
it('should call onSelectElement when clicking an element in the tree', () => {
|
||||
const layers = [makeLayer({ id: 'layer-1', name: 'Layer 1' })];
|
||||
const elements = [
|
||||
makeElement({ id: 'elem-1', type: 'rect', layerId: 'layer-1' }),
|
||||
];
|
||||
const onSelectElement = vi.fn();
|
||||
render(
|
||||
<LayerPanel
|
||||
layers={layers}
|
||||
elements={elements}
|
||||
activeLayerId="layer-1"
|
||||
onSelectLayer={() => {}}
|
||||
onSelectElement={onSelectElement}
|
||||
onAddLayer={() => {}}
|
||||
onToggleLayer={() => {}}
|
||||
/>,
|
||||
);
|
||||
expandLayer('Layer 1');
|
||||
fireEvent.click(screen.getByText('Rechteck'));
|
||||
expect(onSelectElement).toHaveBeenCalledWith('elem-1');
|
||||
});
|
||||
|
||||
it('should render groups as tree nodes with their elements as children', () => {
|
||||
const layers = [makeLayer({ id: 'layer-1', name: 'Layer 1' })];
|
||||
const elements = [
|
||||
makeElement({ id: 'elem-1', type: 'rect', layerId: 'layer-1' }),
|
||||
makeElement({ id: 'elem-2', type: 'circle', layerId: 'layer-1' }),
|
||||
];
|
||||
const groups = [
|
||||
{ id: 'group-1', name: 'Gruppe A', elementIds: ['elem-1', 'elem-2'], parentGroupId: null },
|
||||
];
|
||||
render(
|
||||
<LayerPanel
|
||||
layers={layers}
|
||||
elements={elements}
|
||||
groups={groups}
|
||||
activeLayerId="layer-1"
|
||||
onSelectLayer={() => {}}
|
||||
onAddLayer={() => {}}
|
||||
onToggleLayer={() => {}}
|
||||
/>,
|
||||
);
|
||||
expandLayer('Layer 1');
|
||||
expect(screen.getByText('Gruppe A')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should select all group elements when clicking a group node', () => {
|
||||
const layers = [makeLayer({ id: 'layer-1', name: 'Layer 1' })];
|
||||
const elements = [
|
||||
makeElement({ id: 'elem-1', type: 'rect', layerId: 'layer-1' }),
|
||||
makeElement({ id: 'elem-2', type: 'circle', layerId: 'layer-1' }),
|
||||
];
|
||||
const groups = [
|
||||
{ id: 'group-1', name: 'Gruppe A', elementIds: ['elem-1', 'elem-2'], parentGroupId: null },
|
||||
];
|
||||
const onSelectElement = vi.fn();
|
||||
render(
|
||||
<LayerPanel
|
||||
layers={layers}
|
||||
elements={elements}
|
||||
groups={groups}
|
||||
activeLayerId="layer-1"
|
||||
onSelectLayer={() => {}}
|
||||
onSelectElement={onSelectElement}
|
||||
onAddLayer={() => {}}
|
||||
onToggleLayer={() => {}}
|
||||
/>,
|
||||
);
|
||||
expandLayer('Layer 1');
|
||||
fireEvent.click(screen.getByText('Gruppe A'));
|
||||
expect(onSelectElement).toHaveBeenCalledWith('elem-1');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── PropertiesPanel ─────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user