feat: ctrl c shortcut und pfeil zeichnen

This commit is contained in:
Ida 2026-05-13 16:24:38 +02:00
parent e3c93dc220
commit 590178b8f2
3 changed files with 76 additions and 20 deletions

View file

@ -854,7 +854,10 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
<div className={styles.container}>
{/* Left panel: Workspace (Chats / Dateien / Quellen) */}
{leftPanelOpen && (<>
<div style={{ width: leftPanelWidth, flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', background: 'var(--bg-primary, #fff)' }}>
<div
data-suppress-flow-node-hotkeys=""
style={{ width: leftPanelWidth, flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', background: 'var(--bg-primary, #fff)' }}
>
<div className={styles.rightTabBar}>
{(['ai', 'chats', 'files', 'sources'] as const).map((tab) => (
<button
@ -996,7 +999,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
/>
</div>
{configurableSelected && selectedNode && (
<div className={styles.nodeConfigPanelWrap}>
<div className={styles.nodeConfigPanelWrap} data-suppress-flow-node-hotkeys="">
<Automation2DataFlowProvider
node={selectedNode}
nodes={canvasNodes}
@ -1029,7 +1032,10 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
{/* Right panel: Nodes + Tracing tabs */}
<div className={styles.resizeDivider} onMouseDown={(e) => _startResize('right', e)} />
<div style={{ width: sidebarWidth, flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', background: 'var(--bg-secondary, #f8f9fa)' }}>
<div
data-suppress-flow-node-hotkeys=""
style={{ width: sidebarWidth, flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', background: 'var(--bg-secondary, #f8f9fa)' }}
>
<div className={styles.rightTabBar}>
<button
className={`${styles.rightTab} ${rightTab === 'nodes' ? styles.rightTabActive : ''}`}

View file

@ -218,7 +218,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
!!canvasEdit && canvasEdit.selectedNodeCount === 1 && !canvasEdit.connectionSelected;
return (
<div className={styles.canvasHeader}>
<div className={styles.canvasHeader} data-suppress-flow-node-hotkeys="">
<div
className={styles.canvasHeaderToolbar}
role="toolbar"
@ -518,16 +518,6 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
>
<HiOutlineDocumentDuplicate size={18} strokeWidth={2} aria-hidden />
</button>
<button
type="button"
className={styles.canvasHeaderGhostIconBtn}
aria-pressed={canvasEdit.connectionToolActive}
onClick={canvasEdit.onToggleConnectionTool}
title={t('Verbindungen zeichnen')}
aria-label={t('Verbindungen zeichnen')}
>
<HiOutlineArrowLongRight size={18} strokeWidth={2} aria-hidden />
</button>
<button
type="button"
className={styles.canvasHeaderGhostIconBtn}

View file

@ -119,6 +119,28 @@ const NODE_HEIGHT = 72;
export const FLOW_CANVAS_MIN_ZOOM = 0.25;
export const FLOW_CANVAS_MAX_ZOOM = 4;
function deepCloneCanvasNode(node: CanvasNode): CanvasNode {
return {
...node,
parameters: node.parameters ? { ...node.parameters } : {},
inputPorts: node.inputPorts?.map((p) => ({ ...p })),
outputPorts: node.outputPorts?.map((p) => ({ ...p })),
};
}
/** Konfig-/Sidebar-/Header blenden Knoten-Duplizieren per Strg+C aus (normales Kopieren). */
const FLOW_HOTKEY_SHIELD_SELECTOR = '[data-suppress-flow-node-hotkeys]';
function isDuplicateNodeHotkeyShielded(el: HTMLElement): boolean {
return el.closest(FLOW_HOTKEY_SHIELD_SELECTOR) != null;
}
function isKeyboardTypingTarget(el: HTMLElement): boolean {
if (el.isContentEditable) return true;
const t = el.tagName;
return t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT';
}
export interface FlowCanvasViewportEditState {
zoom: number;
selectedNodeCount: number;
@ -818,6 +840,9 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
onHistoryCheckpointRef.current?.();
}, []);
const nodesRef = useRef(nodes);
nodesRef.current = nodes;
useEffect(() => {
onViewportEditState?.({
zoom,
@ -925,11 +950,10 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
if (!node) return;
const newId = `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
const clone: CanvasNode = {
...node,
...deepCloneCanvasNode(node),
id: newId,
x: node.x + 40,
y: node.y + 40,
parameters: node.parameters ? { ...node.parameters } : {},
};
onNodesChange([...nodes, clone]);
setSelectedNodeIds(new Set([newId]));
@ -1266,6 +1290,10 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
startClientY: e.clientY,
nodesInitial,
});
queueMicrotask(() => {
containerRef.current?.focus({ preventScroll: true });
});
},
[nodes, selectedNodeIds]
);
@ -1532,8 +1560,35 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
React.useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
const target = e.target as HTMLElement | null;
if (!target) return;
const mod = e.ctrlKey || e.metaKey;
if (mod && e.code === 'KeyC') {
if (selectedConnectionId || selectedStickyId || !selectedNodeId) return;
if (isDuplicateNodeHotkeyShielded(target)) return;
const node = nodesRef.current.find((n) => n.id === selectedNodeId);
if (!node) return;
e.preventDefault();
e.stopPropagation();
const newId = `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
const clone: CanvasNode = {
...deepCloneCanvasNode(node),
id: newId,
x: node.x + 40,
y: node.y + 40,
};
onNodesChange([...nodesRef.current, clone]);
setSelectedConnectionId(null);
setSelectedNodeIds(new Set([newId]));
setSelectedStickyId(null);
setEditingNodeId(null);
setEditingField(null);
emitHistoryCheckpoint();
return;
}
if (isKeyboardTypingTarget(target)) return;
if (e.key === 'Escape') {
setConnectingFrom(null);
setDragPos(null);
@ -1557,13 +1612,16 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
}
}
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
window.addEventListener('keydown', onKeyDown, true);
return () => window.removeEventListener('keydown', onKeyDown, true);
}, [
handleDeleteNode,
handleDeleteConnection,
handleDeleteSelectedStickyNote,
emitHistoryCheckpoint,
onNodesChange,
selectedNodeIds.size,
selectedNodeId,
selectedConnectionId,
selectedStickyId,
]);
@ -1977,6 +2035,7 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
<input
type="text"
className={styles.canvasNodeInput}
data-suppress-flow-node-hotkeys=""
value={node.title ?? displayTitle}
autoFocus
onClick={(e) => e.stopPropagation()}
@ -2084,6 +2143,7 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
<textarea
ref={stickyTextareaRef}
className={styles.canvasStickyNoteTextarea}
data-suppress-flow-node-hotkeys=""
value={sn.text}
placeholder={t('Kommentar eingeben …')}
spellCheck