@@ -1910,8 +1948,55 @@
Launch campaign
// Attach click listeners
canvas.querySelectorAll('.flow-node[data-index]').forEach(node => {
+ node.addEventListener('dragstart', (event) => {
+ draggedStepIndex = parseInt(node.dataset.index);
+ node.classList.add('dragging');
+ event.dataTransfer.effectAllowed = 'move';
+ event.dataTransfer.setData('text/plain', node.dataset.index);
+ });
+
+ node.addEventListener('dragend', () => {
+ draggedStepIndex = null;
+ canvas.querySelectorAll('.flow-node').forEach(item => item.classList.remove('dragging', 'drop-target'));
+ });
+
+ node.addEventListener('dragover', (event) => {
+ if (draggedStepIndex === null) return;
+ event.preventDefault();
+ node.classList.add('drop-target');
+ event.dataTransfer.dropEffect = 'move';
+ });
+
+ node.addEventListener('dragleave', () => {
+ node.classList.remove('drop-target');
+ });
+
+ node.addEventListener('drop', async (event) => {
+ event.preventDefault();
+ node.classList.remove('drop-target');
+
+ const fromIndex = draggedStepIndex;
+ const toIndex = parseInt(node.dataset.index);
+ if (fromIndex === null || Number.isNaN(fromIndex) || Number.isNaN(toIndex)) return;
+ if (fromIndex === toIndex) return;
+
+ reorderSteps(fromIndex, toIndex);
+ draggedStepIndex = null;
+ renderCanvas();
+ renderEditor();
+
+ if (campaignId) {
+ try {
+ await saveCampaign();
+ } catch (error) {
+ console.warn('Unable to persist reordered steps automatically.', error);
+ }
+ }
+ });
+
node.addEventListener('click', (e) => {
if (e.target.closest('[data-delete]')) return;
+ if (draggedStepIndex !== null) return;
selectedNodeIndex = parseInt(node.dataset.index);
renderCanvas();
renderEditor();
@@ -2760,4 +2845,3 @@ Launch campaign