Spaces:
Sleeping
Sleeping
Update BlockEditor.tsx
Browse files
client/src/components/BlockEditor/BlockEditor.tsx
CHANGED
|
@@ -585,27 +585,6 @@ export default function BlockEditor() {
|
|
| 585 |
const remoteUpdateRef = useRef(false);
|
| 586 |
const isDraggingRef = useRef(false);
|
| 587 |
|
| 588 |
-
// Refs to avoid stale closures in Blockly change listeners.
|
| 589 |
-
// The socket emit functions change when projectId changes, but the
|
| 590 |
-
// BlockEditor's main useEffect only re-runs on [framework, activeFileId].
|
| 591 |
-
// By calling through refs, listeners always invoke the latest version.
|
| 592 |
-
const emitBlockStateSyncRef = useRef(emitBlockStateSync);
|
| 593 |
-
const emitBlockDragStartRef = useRef(emitBlockDragStart);
|
| 594 |
-
const emitBlockDragMoveRef = useRef(emitBlockDragMove);
|
| 595 |
-
const emitBlockDragEndRef = useRef(emitBlockDragEnd);
|
| 596 |
-
const emitBlockConnectRef = useRef(emitBlockConnect);
|
| 597 |
-
const emitBlockChangeRef = useRef(emitBlockChange);
|
| 598 |
-
const emitBlockCreateEventRef = useRef(emitBlockCreateEvent);
|
| 599 |
-
const emitPointerMoveRef = useRef(emitPointerMove);
|
| 600 |
-
emitBlockStateSyncRef.current = emitBlockStateSync;
|
| 601 |
-
emitBlockDragStartRef.current = emitBlockDragStart;
|
| 602 |
-
emitBlockDragMoveRef.current = emitBlockDragMove;
|
| 603 |
-
emitBlockDragEndRef.current = emitBlockDragEnd;
|
| 604 |
-
emitBlockConnectRef.current = emitBlockConnect;
|
| 605 |
-
emitBlockChangeRef.current = emitBlockChange;
|
| 606 |
-
emitBlockCreateEventRef.current = emitBlockCreateEvent;
|
| 607 |
-
emitPointerMoveRef.current = emitPointerMove;
|
| 608 |
-
|
| 609 |
// Modal state for editing variable/function blocks
|
| 610 |
const [editingModal, setEditingModal] = useState<EditModalData | null>(null);
|
| 611 |
const [editFormValues, setEditFormValues] = useState<Record<string, string>>({});
|
|
@@ -747,7 +726,7 @@ export default function BlockEditor() {
|
|
| 747 |
x: (relX - ws.scrollX) / ws.scale,
|
| 748 |
y: (relY - ws.scrollY) / ws.scale,
|
| 749 |
};
|
| 750 |
-
|
| 751 |
};
|
| 752 |
svg.addEventListener('mousemove', handleMouseMove);
|
| 753 |
// Store for cleanup
|
|
@@ -770,7 +749,7 @@ export default function BlockEditor() {
|
|
| 770 |
dragEndId++;
|
| 771 |
isDraggingRef.current = true;
|
| 772 |
draggedBlockId = e.blockId;
|
| 773 |
-
|
| 774 |
// Poll block position during drag (BLOCK_MOVE may not fire)
|
| 775 |
dragPoll = setInterval(() => {
|
| 776 |
if (remoteUpdateRef.current || !isDraggingRef.current) {
|
|
@@ -780,14 +759,14 @@ export default function BlockEditor() {
|
|
| 780 |
const b = ws.getBlockById(draggedBlockId!);
|
| 781 |
if (b) {
|
| 782 |
const pos = b.getRelativeToSurfaceXY();
|
| 783 |
-
|
| 784 |
}
|
| 785 |
}, 100);
|
| 786 |
} else if (e.isEnd) {
|
| 787 |
const endedBlockId = e.blockId;
|
| 788 |
draggedBlockId = null;
|
| 789 |
if (dragPoll) { clearInterval(dragPoll); dragPoll = null; }
|
| 790 |
-
|
| 791 |
// Defer isDraggingRef=false + saveBlocks until after Blockly
|
| 792 |
// applies the connection (which fires BLOCK_MOVE next)
|
| 793 |
const thisId = ++dragEndId;
|
|
@@ -803,12 +782,12 @@ export default function BlockEditor() {
|
|
| 803 |
// fires BLOCK_MOVE synchronously with newParentId/newInputName.
|
| 804 |
// isDraggingRef.current ensures we only emit for user-initiated drags.
|
| 805 |
if (e.type === Blockly.Events.BLOCK_MOVE && e.newParentId && isDraggingRef.current) {
|
| 806 |
-
|
| 807 |
}
|
| 808 |
// Broadcast field value changes (dropdowns, text inputs, etc.)
|
| 809 |
// Only emit for user-initiated changes, not during remote updates or workspace load.
|
| 810 |
if (e.type === Blockly.Events.BLOCK_CHANGE && e.element === 'field' && !remoteUpdateRef.current) {
|
| 811 |
-
|
| 812 |
}
|
| 813 |
// Broadcast block creation (toolbox drag-out, paste, duplicate) so collaborator
|
| 814 |
// screens can create the block immediately instead of waiting for XML sync.
|
|
@@ -818,7 +797,7 @@ export default function BlockEditor() {
|
|
| 818 |
try {
|
| 819 |
const dom = Blockly.Xml.blockToDom(block, { xy: true } as any);
|
| 820 |
const xml = Blockly.Xml.domToText(dom);
|
| 821 |
-
|
| 822 |
} catch {}
|
| 823 |
}
|
| 824 |
}
|
|
@@ -843,7 +822,7 @@ export default function BlockEditor() {
|
|
| 843 |
const currentFileId = useEditorStore.getState().activeFileId;
|
| 844 |
if (currentFileId) {
|
| 845 |
setBlocksXml(currentFileId, xml);
|
| 846 |
-
|
| 847 |
let finalCode = '';
|
| 848 |
try {
|
| 849 |
// Try manual code generation first (uses our compile functions directly)
|
|
@@ -934,11 +913,8 @@ export default function BlockEditor() {
|
|
| 934 |
remoteUpdateRef.current = true;
|
| 935 |
const ws = workspaceRef.current;
|
| 936 |
if (ws) {
|
| 937 |
-
// Snapshot and clear atomically so no items are lost
|
| 938 |
-
const queue = useCollaborationStore.getState().pendingBlockConnects;
|
| 939 |
-
useCollaborationStore.setState({ pendingBlockConnects: [] });
|
| 940 |
const unprocessed: Array<{ childId: string; parentId: string; inputName?: string }> = [];
|
| 941 |
-
for (const connect of
|
| 942 |
const child = ws.getBlockById(connect.childId);
|
| 943 |
const parent = ws.getBlockById(connect.parentId);
|
| 944 |
if (!child || !parent) {
|
|
@@ -972,8 +948,8 @@ export default function BlockEditor() {
|
|
| 972 |
}
|
| 973 |
}
|
| 974 |
}
|
| 975 |
-
//
|
| 976 |
-
if (unprocessed.length
|
| 977 |
useCollaborationStore.setState({ pendingBlockConnects: unprocessed });
|
| 978 |
}
|
| 979 |
ws.render();
|
|
@@ -987,10 +963,8 @@ export default function BlockEditor() {
|
|
| 987 |
remoteUpdateRef.current = true;
|
| 988 |
const ws = workspaceRef.current;
|
| 989 |
if (ws) {
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
useCollaborationStore.setState({ pendingBlockChanges: [] });
|
| 993 |
-
for (const change of queue) {
|
| 994 |
const block = ws.getBlockById(change.blockId);
|
| 995 |
if (!block) continue;
|
| 996 |
try {
|
|
@@ -1008,11 +982,10 @@ export default function BlockEditor() {
|
|
| 1008 |
remoteUpdateRef.current = true;
|
| 1009 |
const ws = workspaceRef.current;
|
| 1010 |
if (ws) {
|
| 1011 |
-
// Snapshot the queue and clear atomically so no items are lost
|
| 1012 |
-
const queue = useCollaborationStore.getState().pendingBlockCreates;
|
| 1013 |
-
useCollaborationStore.setState({ pendingBlockCreates: [] });
|
| 1014 |
const unprocessed: Array<{ blockId: string; xml: string }> = [];
|
| 1015 |
-
|
|
|
|
|
|
|
| 1016 |
if (ws.getBlockById(create.blockId)) continue;
|
| 1017 |
try {
|
| 1018 |
const dom = Blockly.utils.xml.textToDom(create.xml);
|
|
@@ -1021,7 +994,7 @@ export default function BlockEditor() {
|
|
| 1021 |
unprocessed.push(create);
|
| 1022 |
}
|
| 1023 |
}
|
| 1024 |
-
if (unprocessed.length
|
| 1025 |
useCollaborationStore.setState({ pendingBlockCreates: unprocessed });
|
| 1026 |
}
|
| 1027 |
}
|
|
|
|
| 585 |
const remoteUpdateRef = useRef(false);
|
| 586 |
const isDraggingRef = useRef(false);
|
| 587 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
// Modal state for editing variable/function blocks
|
| 589 |
const [editingModal, setEditingModal] = useState<EditModalData | null>(null);
|
| 590 |
const [editFormValues, setEditFormValues] = useState<Record<string, string>>({});
|
|
|
|
| 726 |
x: (relX - ws.scrollX) / ws.scale,
|
| 727 |
y: (relY - ws.scrollY) / ws.scale,
|
| 728 |
};
|
| 729 |
+
emitPointerMove(wsCoord.x, wsCoord.y);
|
| 730 |
};
|
| 731 |
svg.addEventListener('mousemove', handleMouseMove);
|
| 732 |
// Store for cleanup
|
|
|
|
| 749 |
dragEndId++;
|
| 750 |
isDraggingRef.current = true;
|
| 751 |
draggedBlockId = e.blockId;
|
| 752 |
+
emitBlockDragStart(e.blockId, xy.x, xy.y);
|
| 753 |
// Poll block position during drag (BLOCK_MOVE may not fire)
|
| 754 |
dragPoll = setInterval(() => {
|
| 755 |
if (remoteUpdateRef.current || !isDraggingRef.current) {
|
|
|
|
| 759 |
const b = ws.getBlockById(draggedBlockId!);
|
| 760 |
if (b) {
|
| 761 |
const pos = b.getRelativeToSurfaceXY();
|
| 762 |
+
emitBlockDragMove(draggedBlockId!, pos.x, pos.y);
|
| 763 |
}
|
| 764 |
}, 100);
|
| 765 |
} else if (e.isEnd) {
|
| 766 |
const endedBlockId = e.blockId;
|
| 767 |
draggedBlockId = null;
|
| 768 |
if (dragPoll) { clearInterval(dragPoll); dragPoll = null; }
|
| 769 |
+
emitBlockDragEnd(endedBlockId);
|
| 770 |
// Defer isDraggingRef=false + saveBlocks until after Blockly
|
| 771 |
// applies the connection (which fires BLOCK_MOVE next)
|
| 772 |
const thisId = ++dragEndId;
|
|
|
|
| 782 |
// fires BLOCK_MOVE synchronously with newParentId/newInputName.
|
| 783 |
// isDraggingRef.current ensures we only emit for user-initiated drags.
|
| 784 |
if (e.type === Blockly.Events.BLOCK_MOVE && e.newParentId && isDraggingRef.current) {
|
| 785 |
+
emitBlockConnect(e.blockId, e.newParentId, e.newInputName || undefined);
|
| 786 |
}
|
| 787 |
// Broadcast field value changes (dropdowns, text inputs, etc.)
|
| 788 |
// Only emit for user-initiated changes, not during remote updates or workspace load.
|
| 789 |
if (e.type === Blockly.Events.BLOCK_CHANGE && e.element === 'field' && !remoteUpdateRef.current) {
|
| 790 |
+
emitBlockChange(e.blockId, e.name, e.newValue, e.element);
|
| 791 |
}
|
| 792 |
// Broadcast block creation (toolbox drag-out, paste, duplicate) so collaborator
|
| 793 |
// screens can create the block immediately instead of waiting for XML sync.
|
|
|
|
| 797 |
try {
|
| 798 |
const dom = Blockly.Xml.blockToDom(block, { xy: true } as any);
|
| 799 |
const xml = Blockly.Xml.domToText(dom);
|
| 800 |
+
emitBlockCreateEvent(e.blockId, xml);
|
| 801 |
} catch {}
|
| 802 |
}
|
| 803 |
}
|
|
|
|
| 822 |
const currentFileId = useEditorStore.getState().activeFileId;
|
| 823 |
if (currentFileId) {
|
| 824 |
setBlocksXml(currentFileId, xml);
|
| 825 |
+
emitBlockStateSync(currentFileId, xml);
|
| 826 |
let finalCode = '';
|
| 827 |
try {
|
| 828 |
// Try manual code generation first (uses our compile functions directly)
|
|
|
|
| 913 |
remoteUpdateRef.current = true;
|
| 914 |
const ws = workspaceRef.current;
|
| 915 |
if (ws) {
|
|
|
|
|
|
|
|
|
|
| 916 |
const unprocessed: Array<{ childId: string; parentId: string; inputName?: string }> = [];
|
| 917 |
+
for (const connect of pendingBlockConnects) {
|
| 918 |
const child = ws.getBlockById(connect.childId);
|
| 919 |
const parent = ws.getBlockById(connect.parentId);
|
| 920 |
if (!child || !parent) {
|
|
|
|
| 948 |
}
|
| 949 |
}
|
| 950 |
}
|
| 951 |
+
// Only update store when items were actually consumed (blocks existed)
|
| 952 |
+
if (unprocessed.length < pendingBlockConnects.length) {
|
| 953 |
useCollaborationStore.setState({ pendingBlockConnects: unprocessed });
|
| 954 |
}
|
| 955 |
ws.render();
|
|
|
|
| 963 |
remoteUpdateRef.current = true;
|
| 964 |
const ws = workspaceRef.current;
|
| 965 |
if (ws) {
|
| 966 |
+
let change;
|
| 967 |
+
while ((change = useCollaborationStore.getState().shiftPendingBlockChange())) {
|
|
|
|
|
|
|
| 968 |
const block = ws.getBlockById(change.blockId);
|
| 969 |
if (!block) continue;
|
| 970 |
try {
|
|
|
|
| 982 |
remoteUpdateRef.current = true;
|
| 983 |
const ws = workspaceRef.current;
|
| 984 |
if (ws) {
|
|
|
|
|
|
|
|
|
|
| 985 |
const unprocessed: Array<{ blockId: string; xml: string }> = [];
|
| 986 |
+
let create;
|
| 987 |
+
while ((create = useCollaborationStore.getState().shiftPendingBlockCreate())) {
|
| 988 |
+
// Skip if block already exists (e.g. already created by XML sync)
|
| 989 |
if (ws.getBlockById(create.blockId)) continue;
|
| 990 |
try {
|
| 991 |
const dom = Blockly.utils.xml.textToDom(create.xml);
|
|
|
|
| 994 |
unprocessed.push(create);
|
| 995 |
}
|
| 996 |
}
|
| 997 |
+
if (unprocessed.length < pendingBlockCreates.length) {
|
| 998 |
useCollaborationStore.setState({ pendingBlockCreates: unprocessed });
|
| 999 |
}
|
| 1000 |
}
|