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}>
|
<div className={styles.container}>
|
||||||
{/* Left panel: Workspace (Chats / Dateien / Quellen) */}
|
{/* Left panel: Workspace (Chats / Dateien / Quellen) */}
|
||||||
{leftPanelOpen && (<>
|
{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}>
|
<div className={styles.rightTabBar}>
|
||||||
{(['ai', 'chats', 'files', 'sources'] as const).map((tab) => (
|
{(['ai', 'chats', 'files', 'sources'] as const).map((tab) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -996,7 +999,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{configurableSelected && selectedNode && (
|
{configurableSelected && selectedNode && (
|
||||||
<div className={styles.nodeConfigPanelWrap}>
|
<div className={styles.nodeConfigPanelWrap} data-suppress-flow-node-hotkeys="">
|
||||||
<Automation2DataFlowProvider
|
<Automation2DataFlowProvider
|
||||||
node={selectedNode}
|
node={selectedNode}
|
||||||
nodes={canvasNodes}
|
nodes={canvasNodes}
|
||||||
|
|
@ -1029,7 +1032,10 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
|
|
||||||
{/* Right panel: Nodes + Tracing tabs */}
|
{/* Right panel: Nodes + Tracing tabs */}
|
||||||
<div className={styles.resizeDivider} onMouseDown={(e) => _startResize('right', e)} />
|
<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}>
|
<div className={styles.rightTabBar}>
|
||||||
<button
|
<button
|
||||||
className={`${styles.rightTab} ${rightTab === 'nodes' ? styles.rightTabActive : ''}`}
|
className={`${styles.rightTab} ${rightTab === 'nodes' ? styles.rightTabActive : ''}`}
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
||||||
!!canvasEdit && canvasEdit.selectedNodeCount === 1 && !canvasEdit.connectionSelected;
|
!!canvasEdit && canvasEdit.selectedNodeCount === 1 && !canvasEdit.connectionSelected;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.canvasHeader}>
|
<div className={styles.canvasHeader} data-suppress-flow-node-hotkeys="">
|
||||||
<div
|
<div
|
||||||
className={styles.canvasHeaderToolbar}
|
className={styles.canvasHeaderToolbar}
|
||||||
role="toolbar"
|
role="toolbar"
|
||||||
|
|
@ -518,16 +518,6 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
||||||
>
|
>
|
||||||
<HiOutlineDocumentDuplicate size={18} strokeWidth={2} aria-hidden />
|
<HiOutlineDocumentDuplicate size={18} strokeWidth={2} aria-hidden />
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.canvasHeaderGhostIconBtn}
|
className={styles.canvasHeaderGhostIconBtn}
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,28 @@ const NODE_HEIGHT = 72;
|
||||||
export const FLOW_CANVAS_MIN_ZOOM = 0.25;
|
export const FLOW_CANVAS_MIN_ZOOM = 0.25;
|
||||||
export const FLOW_CANVAS_MAX_ZOOM = 4;
|
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 {
|
export interface FlowCanvasViewportEditState {
|
||||||
zoom: number;
|
zoom: number;
|
||||||
selectedNodeCount: number;
|
selectedNodeCount: number;
|
||||||
|
|
@ -818,6 +840,9 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
||||||
onHistoryCheckpointRef.current?.();
|
onHistoryCheckpointRef.current?.();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const nodesRef = useRef(nodes);
|
||||||
|
nodesRef.current = nodes;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onViewportEditState?.({
|
onViewportEditState?.({
|
||||||
zoom,
|
zoom,
|
||||||
|
|
@ -925,11 +950,10 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
const newId = `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
const newId = `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
||||||
const clone: CanvasNode = {
|
const clone: CanvasNode = {
|
||||||
...node,
|
...deepCloneCanvasNode(node),
|
||||||
id: newId,
|
id: newId,
|
||||||
x: node.x + 40,
|
x: node.x + 40,
|
||||||
y: node.y + 40,
|
y: node.y + 40,
|
||||||
parameters: node.parameters ? { ...node.parameters } : {},
|
|
||||||
};
|
};
|
||||||
onNodesChange([...nodes, clone]);
|
onNodesChange([...nodes, clone]);
|
||||||
setSelectedNodeIds(new Set([newId]));
|
setSelectedNodeIds(new Set([newId]));
|
||||||
|
|
@ -1266,6 +1290,10 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
||||||
startClientY: e.clientY,
|
startClientY: e.clientY,
|
||||||
nodesInitial,
|
nodesInitial,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queueMicrotask(() => {
|
||||||
|
containerRef.current?.focus({ preventScroll: true });
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[nodes, selectedNodeIds]
|
[nodes, selectedNodeIds]
|
||||||
);
|
);
|
||||||
|
|
@ -1532,8 +1560,35 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement | null;
|
||||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
|
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') {
|
if (e.key === 'Escape') {
|
||||||
setConnectingFrom(null);
|
setConnectingFrom(null);
|
||||||
setDragPos(null);
|
setDragPos(null);
|
||||||
|
|
@ -1557,13 +1612,16 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', onKeyDown);
|
window.addEventListener('keydown', onKeyDown, true);
|
||||||
return () => window.removeEventListener('keydown', onKeyDown);
|
return () => window.removeEventListener('keydown', onKeyDown, true);
|
||||||
}, [
|
}, [
|
||||||
handleDeleteNode,
|
handleDeleteNode,
|
||||||
handleDeleteConnection,
|
handleDeleteConnection,
|
||||||
handleDeleteSelectedStickyNote,
|
handleDeleteSelectedStickyNote,
|
||||||
|
emitHistoryCheckpoint,
|
||||||
|
onNodesChange,
|
||||||
selectedNodeIds.size,
|
selectedNodeIds.size,
|
||||||
|
selectedNodeId,
|
||||||
selectedConnectionId,
|
selectedConnectionId,
|
||||||
selectedStickyId,
|
selectedStickyId,
|
||||||
]);
|
]);
|
||||||
|
|
@ -1977,6 +2035,7 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={styles.canvasNodeInput}
|
className={styles.canvasNodeInput}
|
||||||
|
data-suppress-flow-node-hotkeys=""
|
||||||
value={node.title ?? displayTitle}
|
value={node.title ?? displayTitle}
|
||||||
autoFocus
|
autoFocus
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
|
@ -2084,6 +2143,7 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
||||||
<textarea
|
<textarea
|
||||||
ref={stickyTextareaRef}
|
ref={stickyTextareaRef}
|
||||||
className={styles.canvasStickyNoteTextarea}
|
className={styles.canvasStickyNoteTextarea}
|
||||||
|
data-suppress-flow-node-hotkeys=""
|
||||||
value={sn.text}
|
value={sn.text}
|
||||||
placeholder={t('Kommentar eingeben …')}
|
placeholder={t('Kommentar eingeben …')}
|
||||||
spellCheck
|
spellCheck
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue