From c770560a22187f884fae1861be21b7a3505ced45 Mon Sep 17 00:00:00 2001 From: Sudo Date: Sat, 3 Jan 2026 21:35:24 -0800 Subject: [PATCH 1/2] Add drag-and-drop between encoding channels with swap behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable dragging field pills from one encoding channel to another. When dropped on an occupied channel, fields swap positions. When dropped on an empty channel, field moves (source becomes empty). Changes: - Add DragPayload type to distinguish fieldList vs encodingShelf drags - Update FieldPill to send sourceType in drag payload - Make encoding shelf pills draggable with move/swap logic - Fix effectAllowed/dropEffect mismatch that silently blocked drops Includes debug console.logs (to be removed in follow-up commit). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../EncodingPanel/EncodingShelf.tsx | 69 +++++++++++++++++-- src/components/FieldList/FieldPill.tsx | 5 +- src/types/index.ts | 6 ++ 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/components/EncodingPanel/EncodingShelf.tsx b/src/components/EncodingPanel/EncodingShelf.tsx index c91ca5d..8714bcc 100644 --- a/src/components/EncodingPanel/EncodingShelf.tsx +++ b/src/components/EncodingPanel/EncodingShelf.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import type { EncodingChannel, DetectedField, FieldType } from '../../types'; +import type { EncodingChannel, FieldType, DragPayload } from '../../types'; import { useApp } from '../../context/AppContext'; const TYPE_COLORS: Record = { @@ -25,12 +25,16 @@ export function EncodingShelf({ channel, label }: EncodingShelfProps) { const { state, assignField, removeField } = useApp(); const [isOver, setIsOver] = useState(false); const [isHoveredRemove, setIsHoveredRemove] = useState(false); + const [isDragging, setIsDragging] = useState(false); const assignedField = state.encodings[channel]; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); - e.dataTransfer.dropEffect = 'copy'; + // Don't set dropEffect - let the browser use effectAllowed from the drag source + if (!isOver) { + console.log('[handleDragOver] dragging over channel:', channel); + } setIsOver(true); }; @@ -39,14 +43,61 @@ export function EncodingShelf({ channel, label }: EncodingShelfProps) { }; const handleDrop = (e: React.DragEvent) => { + console.log('[handleDrop] DROP EVENT FIRED on channel:', channel); e.preventDefault(); setIsOver(false); - const fieldData = e.dataTransfer.getData('application/json'); - if (fieldData) { - const field = JSON.parse(fieldData) as DetectedField; - assignField(channel, field); + const data = e.dataTransfer.getData('application/json'); + console.log('[handleDrop] raw data:', data); + if (!data) return; + + const payload = JSON.parse(data) as DragPayload; + console.log('[handleDrop] parsed payload:', payload); + console.log('[handleDrop] dropping on channel:', channel); + console.log('[handleDrop] current assignedField:', assignedField); + + // Edge case: dropping on same channel - no-op + if (payload.sourceType === 'encodingShelf' && payload.sourceChannel === channel) { + console.log('[handleDrop] same channel, skipping'); + return; + } + + // Move from another encoding shelf + if (payload.sourceType === 'encodingShelf' && payload.sourceChannel) { + const targetField = assignedField; + console.log('[handleDrop] move operation from', payload.sourceChannel, 'to', channel); + + if (targetField) { + // SWAP: move target field to source channel + console.log('[handleDrop] SWAP: assigning', targetField.name, 'to', payload.sourceChannel); + assignField(payload.sourceChannel, targetField); + } else { + // No field in target - just remove from source + console.log('[handleDrop] removing from source:', payload.sourceChannel); + removeField(payload.sourceChannel); + } } + + // Assign dragged field to this channel + console.log('[handleDrop] assigning', payload.field.name, 'to', channel); + assignField(channel, payload.field); + }; + + const handlePillDragStart = (e: React.DragEvent) => { + if (!assignedField) return; + const payload: DragPayload = { + field: assignedField, + sourceType: 'encodingShelf', + sourceChannel: channel + }; + console.log('[handlePillDragStart] starting drag with payload:', payload); + e.dataTransfer.setData('application/json', JSON.stringify(payload)); + e.dataTransfer.effectAllowed = 'move'; + setIsDragging(true); + }; + + const handlePillDragEnd = () => { + setIsDragging(false); }; const handleRemove = () => { @@ -115,6 +166,9 @@ export function EncodingShelf({ channel, label }: EncodingShelfProps) { > {assignedField ? (
diff --git a/src/components/FieldList/FieldPill.tsx b/src/components/FieldList/FieldPill.tsx index 0180c1c..68f65c1 100644 --- a/src/components/FieldList/FieldPill.tsx +++ b/src/components/FieldList/FieldPill.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import type { DetectedField, FieldType } from '../../types'; +import type { DetectedField, FieldType, DragPayload } from '../../types'; import { useApp } from '../../context/AppContext'; const TYPE_COLORS: Record = { @@ -27,7 +27,8 @@ export function FieldPill({ field, index }: FieldPillProps) { const [isDragging, setIsDragging] = useState(false); const handleDragStart = (e: React.DragEvent) => { - e.dataTransfer.setData('application/json', JSON.stringify(field)); + const payload: DragPayload = { field, sourceType: 'fieldList' }; + e.dataTransfer.setData('application/json', JSON.stringify(payload)); e.dataTransfer.effectAllowed = 'copy'; setIsDragging(true); }; diff --git a/src/types/index.ts b/src/types/index.ts index 1e61868..ad9345e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,6 +8,12 @@ export interface DetectedField { export type EncodingChannel = 'x' | 'y' | 'color' | 'size' | 'shape' | 'row' | 'column'; +export interface DragPayload { + field: DetectedField; + sourceType: 'fieldList' | 'encodingShelf'; + sourceChannel?: EncodingChannel; +} + export type EncodingState = { [K in EncodingChannel]?: DetectedField; }; From 0c6b41d2e9e82294d15e3f42919107c678934c08 Mon Sep 17 00:00:00 2001 From: Sudo Date: Sat, 3 Jan 2026 21:36:54 -0800 Subject: [PATCH 2/2] Remove debug console.logs from drag-drop handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../EncodingPanel/EncodingShelf.tsx | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/components/EncodingPanel/EncodingShelf.tsx b/src/components/EncodingPanel/EncodingShelf.tsx index 8714bcc..1ca946b 100644 --- a/src/components/EncodingPanel/EncodingShelf.tsx +++ b/src/components/EncodingPanel/EncodingShelf.tsx @@ -31,10 +31,6 @@ export function EncodingShelf({ channel, label }: EncodingShelfProps) { const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); - // Don't set dropEffect - let the browser use effectAllowed from the drag source - if (!isOver) { - console.log('[handleDragOver] dragging over channel:', channel); - } setIsOver(true); }; @@ -43,43 +39,32 @@ export function EncodingShelf({ channel, label }: EncodingShelfProps) { }; const handleDrop = (e: React.DragEvent) => { - console.log('[handleDrop] DROP EVENT FIRED on channel:', channel); e.preventDefault(); setIsOver(false); const data = e.dataTransfer.getData('application/json'); - console.log('[handleDrop] raw data:', data); if (!data) return; const payload = JSON.parse(data) as DragPayload; - console.log('[handleDrop] parsed payload:', payload); - console.log('[handleDrop] dropping on channel:', channel); - console.log('[handleDrop] current assignedField:', assignedField); - // Edge case: dropping on same channel - no-op + // Dropping on same channel - no-op if (payload.sourceType === 'encodingShelf' && payload.sourceChannel === channel) { - console.log('[handleDrop] same channel, skipping'); return; } // Move from another encoding shelf if (payload.sourceType === 'encodingShelf' && payload.sourceChannel) { const targetField = assignedField; - console.log('[handleDrop] move operation from', payload.sourceChannel, 'to', channel); if (targetField) { - // SWAP: move target field to source channel - console.log('[handleDrop] SWAP: assigning', targetField.name, 'to', payload.sourceChannel); + // Swap: move target field to source channel assignField(payload.sourceChannel, targetField); } else { // No field in target - just remove from source - console.log('[handleDrop] removing from source:', payload.sourceChannel); removeField(payload.sourceChannel); } } - // Assign dragged field to this channel - console.log('[handleDrop] assigning', payload.field.name, 'to', channel); assignField(channel, payload.field); }; @@ -90,7 +75,6 @@ export function EncodingShelf({ channel, label }: EncodingShelfProps) { sourceType: 'encodingShelf', sourceChannel: channel }; - console.log('[handlePillDragStart] starting drag with payload:', payload); e.dataTransfer.setData('application/json', JSON.stringify(payload)); e.dataTransfer.effectAllowed = 'move'; setIsDragging(true);