feat: ctrl c shortcut und pfeil zeichnen
This commit is contained in:
parent
e3c93dc220
commit
590178b8f2
3 changed files with 76 additions and 20 deletions
|
|
@ -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 : ''}`}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue