diff --git a/backend/Dockerfile b/backend/Dockerfile
index 8236346..294f689 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -10,3 +10,4 @@ COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
EXPOSE 8000
+
diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc
index c8f154a..b68de2f 100644
Binary files a/backend/__pycache__/main.cpython-313.pyc and b/backend/__pycache__/main.cpython-313.pyc differ
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index a00bf2d..3e81629 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -12,6 +12,7 @@
"react": "^19.1.0",
"react-confirm-alert": "^3.0.6",
"react-dom": "^19.1.0",
+ "react-quill-new": "^3.4.6",
"react-toastify": "^11.0.5",
"reactflow": "^11.11.4"
},
@@ -1988,12 +1989,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="
+ },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -2247,6 +2258,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+ },
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead."
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -2361,6 +2388,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parchment": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
+ "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A=="
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -2465,6 +2497,33 @@
"node": ">=6"
}
},
+ "node_modules/quill": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
+ "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
+ "dependencies": {
+ "eventemitter3": "^5.0.1",
+ "lodash-es": "^4.17.21",
+ "parchment": "^3.0.0",
+ "quill-delta": "^5.1.0"
+ },
+ "engines": {
+ "npm": ">=8.2.3"
+ }
+ },
+ "node_modules/quill-delta": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
+ "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
+ "dependencies": {
+ "fast-diff": "^1.3.0",
+ "lodash.clonedeep": "^4.5.0",
+ "lodash.isequal": "^4.5.0"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
@@ -2498,6 +2557,20 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/react-quill-new": {
+ "version": "3.4.6",
+ "resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.4.6.tgz",
+ "integrity": "sha512-S2kEAwoKRo+xUIAEpb94fwiPe2QU3FmwIfQ+7Lkchf+izPa2nRu1mr4i4QxyVYg8TjHDryDUiOEYZuFEV45QFA==",
+ "dependencies": {
+ "lodash-es": "^4.17.21",
+ "quill": "~2.0.2"
+ },
+ "peerDependencies": {
+ "quill-delta": "^5.1.0",
+ "react": "^16 || ^17 || ^18 || ^19",
+ "react-dom": "^16 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-toastify": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index f6c6816..f72054b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,6 +14,7 @@
"react": "^19.1.0",
"react-confirm-alert": "^3.0.6",
"react-dom": "^19.1.0",
+ "react-quill-new": "^3.4.6",
"react-toastify": "^11.0.5",
"reactflow": "^11.11.4"
},
diff --git a/frontend/src/components/Diagram.jsx b/frontend/src/components/Diagram.jsx
index 2e28434..425d0bc 100644
--- a/frontend/src/components/Diagram.jsx
+++ b/frontend/src/components/Diagram.jsx
@@ -16,14 +16,18 @@ import {
listDiagrams,
deleteDiagramByName,
} from '../services/api';
+import ReactQuill from 'react-quill-new';
+import 'react-quill-new/dist/quill.snow.css';
import CustomNode from './CustomNode';
+import RouterNode from './RouterNode';
+import TextNode from './TextNode';
import IconSelector from './IconSelector';
import { toast } from 'react-toastify';
-import RouterNode from './RouterNode';
const nodeTypes = {
custom: CustomNode,
router: RouterNode,
+ text: TextNode,
};
function Diagram() {
@@ -37,6 +41,8 @@ function Diagram() {
const [selectedIcon, setSelectedIcon] = useState('');
const [diagramName, setDiagramName] = useState(null);
const [diagramList, setDiagramList] = useState([]);
+ const [editingTextNode, setEditingTextNode] = useState(null);
+ const [tempHtml, setTempHtml] = useState('');
const isIframe = window.self !== window.top;
useEffect(() => {
@@ -68,7 +74,45 @@ function Diagram() {
}
try {
const data = await fetchDiagramByName(diagramName);
- setNodes(data.nodes || []);
+ const enhancedNodes = (data.nodes || []).map((n) => {
+ if (n.type === 'text') {
+ return {
+ ...n,
+ draggable: true, // ✅ ADD THIS HERE
+ data: {
+ ...(n.data || {}),
+ editing: false,
+ onChange: (val) =>
+ setNodes((nds) =>
+ nds.map((node) =>
+ node.id === n.id ? { ...node, data: { ...node.data, value: val } } : node
+ )
+ ),
+ onSave: () =>
+ setNodes((nds) =>
+ nds.map((node) =>
+ node.id === n.id
+ ? {
+ ...node,
+ draggable: true, // ✅ RE-ENABLE ON SAVE
+ data: { ...node.data, editing: false },
+ }
+ : node
+ )
+ ),
+ },
+ };
+ }
+
+ return {
+ ...n,
+ draggable: true // ✅ optionally apply for all node types too
+ };
+ });
+
+
+ setNodes(enhancedNodes);
+
const sanitizedEdges = (data.edges || []).map(({ id, source, target, animated }) => ({
id, source, target, animated: !!animated
}));
@@ -142,17 +186,55 @@ function Diagram() {
const onNodeClick = (_, node) => setSelectedNode(node);
const handleNodeDoubleClick = (_, node) => {
- const newLabel = prompt('Enter new name:', node.data.label);
- if (newLabel !== null) {
+ if (node.type === 'text') {
setNodes((nds) =>
nds.map((n) =>
- n.id === node.id ? { ...n, data: { ...n.data, label: newLabel } } : n
+ n.id === node.id
+ ? {
+ ...n,
+ draggable: false, // <-- disable dragging
+ data: {
+ ...n.data,
+ editing: true,
+ onChange: (val) =>
+ setNodes((prev) =>
+ prev.map((x) =>
+ x.id === n.id ? { ...x, data: { ...x.data, value: val } } : x
+ )
+ ),
+ onSave: () =>
+ setNodes((prev) =>
+ prev.map((x) =>
+ x.id === n.id
+ ? {
+ ...x,
+ draggable: true, // <-- re-enable dragging on save
+ data: { ...x.data, editing: false },
+ }
+ : x
+ )
+ ),
+ },
+ }
+ : n
)
);
- toast.info(`✏️ Node renamed to "${newLabel}"`);
+ } else {
+ const newLabel = prompt('Enter new name:', node.data.label);
+ if (newLabel !== null) {
+ setNodes((nds) =>
+ nds.map((n) =>
+ n.id === node.id ? { ...n, data: { ...n.data, label: newLabel } } : n
+ )
+ );
+ toast.info(`✏️ Node renamed to "${newLabel}"`);
+ }
}
};
+
+
+
const handleDeleteDiagram = async () => {
if (!diagramName || diagramName === 'default') {
toast.warn("❌ Cannot delete 'default' diagram or nothing selected.");
@@ -258,6 +340,39 @@ function Diagram() {
)}
+
+