Merge branch 'hotfix/v0.2.10' into develop

This commit is contained in:
Ke Sun
2026-04-10 10:16:39 +08:00
26 changed files with 421 additions and 274 deletions

View File

@@ -149,8 +149,8 @@ const Editor: FC<LexicalEditorProps> =({
<HistoryPlugin />
<CommandPlugin />
<AutocompletePlugin options={options} />
<CharacterCountPlugin setCount={setCount} onChange={onChange} />
<InitialValuePlugin value={value} options={options} />
<CharacterCountPlugin setCount={setCount} />
<InitialValuePlugin value={value} options={options} onChange={onChange} />
<BlurPlugin />
</div>
</LexicalComposer>

View File

@@ -1,41 +1,22 @@
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';
import { $getRoot, $isParagraphNode } from 'lexical';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $isVariableNode } from '../nodes/VariableNode';
const CharacterCountPlugin = ({ setCount, onChange }: { setCount: (count: number) => void; onChange?: (value: string) => void }) => {
const CharacterCountPlugin = ({ setCount }: { setCount: (count: number) => void }) => {
const [editor] = useLexicalComposerContext();
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
useEffect(() => {
return editor.registerUpdateListener(({ editorState, tags }) => {
if (tags.has('programmatic')) return;
editorState.read(() => {
const root = $getRoot();
let serializedContent = '';
// Traverse all nodes and serialize properly
const paragraphs: string[] = [];
root.getChildren().forEach(child => {
if ($isParagraphNode(child)) {
let paragraphContent = '';
child.getChildren().forEach(node => {
if ($isVariableNode(node)) {
paragraphContent += node.getTextContent();
} else {
paragraphContent += node.getTextContent();
}
});
paragraphs.push(paragraphContent);
paragraphs.push(child.getChildren().map(n => n.getTextContent()).join(''));
}
});
serializedContent = paragraphs.join('\n');
setCount(serializedContent.length);
onChangeRef.current?.(serializedContent);
setCount(paragraphs.join('\n').length);
});
});
}, [editor, setCount]);

View File

@@ -50,7 +50,7 @@ const CommandPlugin = () => {
// Create and insert the variable node
const tagNode = $createVariableNode(payload.data);
const spaceNode = $createTextNode(' ');
const spaceNode = $createTextNode('');
anchorNode.insertAfter(tagNode);
tagNode.insertAfter(spaceNode);

View File

@@ -6,7 +6,7 @@
*/
import { useEffect, useRef } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical';
import { $getRoot, $createParagraphNode, $createTextNode, $isParagraphNode } from 'lexical';
import { $createVariableNode } from '../nodes/VariableNode';
import { type Suggestion } from '../plugin/AutocompletePlugin'
@@ -14,24 +14,34 @@ import { type Suggestion } from '../plugin/AutocompletePlugin'
interface InitialValuePluginProps {
value: string;
options?: Suggestion[];
onChange?: (value: string) => void;
}
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [] }) => {
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [], onChange }) => {
const [editor] = useLexicalComposerContext();
const prevValueRef = useRef<string>('');
const isUserInputRef = useRef(false);
const optionsRef = useRef(options);
optionsRef.current = options;
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
useEffect(() => {
return editor.registerUpdateListener(({ editorState, tags }) => {
if (tags.has('programmatic')) return;
editorState.read(() => {
const root = $getRoot();
const textContent = root.getTextContent();
if (textContent !== prevValueRef.current) {
const paragraphs: string[] = [];
root.getChildren().forEach(child => {
if ($isParagraphNode(child)) {
paragraphs.push(child.getChildren().map(n => n.getTextContent()).join(''));
}
});
const text = paragraphs.join('\n');
if (text !== prevValueRef.current) {
isUserInputRef.current = true;
prevValueRef.current = textContent;
prevValueRef.current = text;
onChangeRef.current?.(text);
}
});
});

View File

@@ -49,23 +49,24 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
const incomingEdges = graph.getIncomingEdges(node);
const outgoingEdges = graph.getOutgoingEdges(node);
incomingEdges?.forEach(edge => {
graph.addEdge({
const addedEdges: any[] = [];
incomingEdges?.forEach((edge: any) => {
addedEdges.push(graph.addEdge({
source: { cell: edge.getSourceCellId(), port: edge.getSourcePortId() },
target: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'left')?.id || 'left' },
...edgeAttrs
});
}));
});
outgoingEdges?.forEach(edge => {
outgoingEdges?.forEach((edge: any) => {
const targetCell = graph.getCellById(edge.getTargetCellId()) as any;
const targetPortId = targetCell?.getPorts?.()?.find((port: any) => port.group === 'left')?.id || edge.getTargetPortId();
graph.addEdge({
addedEdges.push(graph.addEdge({
source: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'right')?.id || 'right' },
target: { cell: edge.getTargetCellId(), port: targetPortId },
...edgeAttrs
});
}));
});
// Remove all add-node type nodes
@@ -75,6 +76,15 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
}
});
setTimeout(() => {
addedEdges.forEach(e => {
const src = graph.getCellById(e.getSourceCellId());
const tgt = graph.getCellById(e.getTargetCellId());
if (src?.isNode()) src.toFront();
if (tgt?.isNode()) tgt.toFront();
});
}, 50);
// Automatically adjust loop node size
const loopNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
if (loopNode) {

View File

@@ -59,6 +59,9 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
target: { cell: addNode.id, port: targetPort },
...edgeAttrs,
});
cycleStartNode.toFront()
addNode.toFront()
}
}
@@ -117,6 +120,12 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
...edgeAttrs
}
graph.addEdge(edgeConfig)
setTimeout(() => {
cycleStartNode.toFront()
addNode.toFront()
}, 0)
}
return (

View File

@@ -34,9 +34,12 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
};
window.addEventListener('port:click', handlePortClick as EventListener);
const handleBlankClick = () => handlePopoverClose();
window.addEventListener('blank:click', handleBlankClick);
return () => {
window.removeEventListener('port:click', handlePortClick as EventListener);
window.removeEventListener('blank:click', handleBlankClick);
};
}, []);
@@ -188,38 +191,39 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
setTimeout(() => {
const newPorts = newNode.getPorts();
const addedEdges: any[] = [];
if (edgeInsertion) {
// Edge insertion: create source→new and new→target edges
const { targetCell, targetPort: origTargetPort } = edgeInsertion;
const newLeftPort = newPorts.find((p: any) => p.group === 'left')?.id || 'left';
const newRightPort = newPorts.find((p: any) => p.group === 'right')?.id || 'right';
graph.addEdge({
addedEdges.push(graph.addEdge({
source: { cell: sourceNode.id, port: sourcePort },
target: { cell: newNode.id, port: newLeftPort },
...edgeAttrs
});
graph.addEdge({
}));
addedEdges.push(graph.addEdge({
source: { cell: newNode.id, port: newRightPort },
target: { cell: targetCell.id, port: origTargetPort },
...edgeAttrs
});
}));
setEdgeInsertion(null);
} else if (sourcePortGroup === 'left') {
// Connect from left port to new node's right side
const targetPort = newPorts.find((port: any) => port.group === 'right')?.id || 'right';
graph.addEdge({
addedEdges.push(graph.addEdge({
source: { cell: newNode.id, port: targetPort },
target: { cell: sourceNode.id, port: sourcePort },
...edgeAttrs
});
}));
} else {
// Connect from right port to new node's left side
const targetPort = newPorts.find((port: any) => port.group === 'left')?.id || 'left';
graph.addEdge({
addedEdges.push(graph.addEdge({
source: { cell: sourceNode.id, port: sourcePort },
target: { cell: newNode.id, port: targetPort },
...edgeAttrs
});
}));
}
// Adjust loop node size when child node is added via port within loop node
@@ -266,6 +270,44 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
});
}
}
const isCycleContainer = (type: string) => type === 'loop' || type === 'iteration';
const newNodeType = selectedNodeType.type;
// Helper: bring all child nodes and their edges of a cycle container to front
const bringCycleChildrenToFront = (cycleContainerId: string) => {
graph.getEdges().forEach((e: any) => {
const src = graph.getCellById(e.getSourceCellId());
const tgt = graph.getCellById(e.getTargetCellId());
if (src?.getData()?.cycle === cycleContainerId || tgt?.getData()?.cycle === cycleContainerId) e.toFront();
});
graph.getNodes().forEach((n: any) => {
if (n.getData()?.cycle === cycleContainerId) n.toFront();
});
};
if (isCycleContainer(sourceNodeType)) {
console.log('isCycleContainer(sourceNodeType)')
// Case 4: source is a loop/iteration node — bring new node to front, then its children
newNode.toFront();
sourceNode.toFront();
bringCycleChildrenToFront(sourceNodeData.id);
} else if (isCycleContainer(newNodeType)) {
console.log('isCycleContainer(newNodeType)')
// Case 3: adding a loop/iteration node from a normal node — bring new node to front, then its children
newNode.toFront();
sourceNode.toFront()
bringCycleChildrenToFront(id);
} else {
// Case 2: normal node → normal node
addedEdges.forEach(e => {
const src = graph.getCellById(e.getSourceCellId());
const tgt = graph.getCellById(e.getTargetCellId());
if (src?.isNode()) src.toFront();
if (tgt?.isNode()) tgt.toFront();
});
}
}, 50);
// Clean up temporary element

View File

@@ -61,6 +61,20 @@ const CaseList: FC<CaseListProps> = ({
const { t } = useTranslation();
const form = Form.useFormInstance();
const bringLoopChildrenToFront = (cell: any) => {
const type = cell?.getData()?.type;
if ((type !== 'loop' && type !== 'iteration') || !graphRef?.current) return;
const cycleId = cell.getData().id;
graphRef.current.getEdges().forEach((edge: any) => {
const src = graphRef.current?.getCellById(edge.getSourceCellId());
const tgt = graphRef.current?.getCellById(edge.getTargetCellId());
if (src?.getData()?.cycle === cycleId || tgt?.getData()?.cycle === cycleId) edge.toFront();
});
graphRef.current.getNodes().forEach((n: any) => {
if (n.getData()?.cycle === cycleId) n.toFront();
});
};
// Recalculate node height and port Y positions without rebuilding ports
const updateNodeLayout = (cases: any[]) => {
if (!selectedNode || !graphRef?.current) return;
@@ -139,6 +153,10 @@ const CaseList: FC<CaseListProps> = ({
...edgeAttrs,
});
}
sourceCell.toFront()
selectedNode.toFront()
bringLoopChildrenToFront(sourceCell)
bringLoopChildrenToFront(selectedNode)
graphRef.current?.removeCell(edge);
return;
}
@@ -183,6 +201,10 @@ const CaseList: FC<CaseListProps> = ({
target: { cell: targetCellId, port: targetPortId },
...edgeAttrs
});
selectedNode.toFront()
bringLoopChildrenToFront(selectedNode)
targetCell.toFront()
bringLoopChildrenToFront(targetCell)
}
}

View File

@@ -25,6 +25,20 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
const form = Form.useFormInstance();
const formValues = Form.useWatch([parentName], form);
const bringLoopChildrenToFront = (cell: any) => {
const type = cell?.getData()?.type;
if ((type !== 'loop' && type !== 'iteration') || !graphRef?.current) return;
const cycleId = cell.getData().id;
graphRef.current.getEdges().forEach((edge: any) => {
const src = graphRef.current?.getCellById(edge.getSourceCellId());
const tgt = graphRef.current?.getCellById(edge.getTargetCellId());
if (src?.getData()?.cycle === cycleId || tgt?.getData()?.cycle === cycleId) edge.toFront();
});
graphRef.current.getNodes().forEach((n: any) => {
if (n.getData()?.cycle === cycleId) n.toFront();
});
};
// Update node ports based on category count changes (add/remove categories)
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
if (!selectedNode || !graphRef?.current) return;
@@ -88,6 +102,10 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
target: { cell: selectedNode.id, port: targetPortId },
...edgeAttrs
});
sourceCell.toFront()
bringLoopChildrenToFront(sourceCell)
selectedNode.toFront()
bringLoopChildrenToFront(selectedNode)
}
return;
}
@@ -119,6 +137,10 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
target: { cell: targetCellId, port: targetPortId },
...edgeAttrs
});
selectedNode.toFront()
bringLoopChildrenToFront(selectedNode)
targetCell.toFront()
bringLoopChildrenToFront(targetCell)
}
}
});

View File

@@ -13,7 +13,7 @@ interface KnowledgeConfigModalProps {
refresh: (values: KnowledgeConfigForm, type: 'knowledgeConfig') => void;
}
const retrieveTypes: RetrieveType[] = ['participle', 'semantic', 'hybrid',
// 'graph'
'graph'
]
const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfigModalProps>(({