# Fix: MAIN SESSION & SECTION-TAB Mouse Behavior (MOUSE.md) ## Mô tả Kiểm tra và sửa các lỗi trong MAIN SESSION và SECTION-TAB theo đặc tả [MOUSE.md](file:///home/locpham/SonicForgeStudio/md/MOUSE.md): | Hành động | Đặc tả MOUSE.md | |---|---| | Click item | select item, drag to move | | Ctrl+Click item | toggle selection (add/remove from group) | | Ctrl+Click empty | deselect all | | Ctrl+Click+Drag item/group | **copy** item/group to new track | | Ctrl+Click+Drag empty | marquee selection/deselection | --- ## Bugs đã tìm thấy ### Bug 1 – `handleSetPendingDrag`: luôn thêm `itemId` vào `ids`, bất kể đã toggle hay deselect **Vị trí**: [app.jsx L11834-11837](file:///home/locpham/SonicForgeStudio/app/static/js/app.jsx#L11834-L11837) ```js const handleSetPendingDrag = (trackId, itemType, itemId, clickOffset, e, preToggleSnapshot) => { var ids = preToggleSnapshot || new Set(selectedItemIds); ids.add(itemId); // ← luôn ADD itemId, bất kể đây là lần toggle-off pendingDragRef.current = { ... }; }; ``` - Khi user **Ctrl+Click** vào một item đang được selected → item đó bị deselect (line 1107). - Nhưng `preToggleSnapshot` (được build **trước** toggle tại line 1111/1178) luôn được `ids.add(itemId)`, nên pendingDrag vẫn include item đó trong nhóm copy. - **Kết quả**: Ctrl+Drag sau deselect vẫn copy item bị deselect → số lượng copies bị lỗi. **Fix**: Khi pendingDrag được set sau toggle, nếu item bị deselect (tức `preToggleSnapshot.has(itemId)`) thì **xóa** `itemId` khỏi ids thay vì add. Caller (line 1111) truyền `preToggleSnapshot` (snapshot TRƯỚC toggle), nên logic cần: ```js // Nếu item đang có trong snapshot (tức là toggle-off / deselect), xóa khỏi ids // Nếu item không có trong snapshot (tức là toggle-on / add), thêm vào ids if (preToggleSnapshot && preToggleSnapshot.has(itemId)) { ids.delete(itemId); } else { ids.add(itemId); } ``` --- ### Bug 2 – `handleSectionItemDragStart` với `isDuplicate=true`: `newOriginals` bị build bên trong callback async của `updateActiveTracks` **Vị trí**: [app.jsx L11888-11924](file:///home/locpham/SonicForgeStudio/app/static/js/app.jsx#L11888-L11924) ```js var newOriginals = {}; updateActiveTracks(function(prev) { return prev.map(function(t) { // ... build newOriginals ở đây (bên trong callback) newOriginals[newId] = { ... }; return { ...t, ... }; }); }); // Sử dụng newOriginals NGAY SAU updateActiveTracks (có thể chưa được fill) var newItemId = Object.keys(newOriginals)[0] || itemId; setDraggedSectionItem({ ..., itemId: newItemId, multiIds: newOriginals }); ``` Vì `updateActiveTracks` gọi setState (React batch), callback của nó được gọi đồng bộ (React's setState updater function chạy đồng bộ). Nhưng vì `newOriginals` được closure-captured bên trong callback, điều này **thực ra hoạt động đúng** trong React 18 với batching. Tuy nhiên: nếu **nhiều items có cùng loại** (section + midiItem + clip), vòng lặp qua `Object.keys(multiIds)` gọi `Date.now()` nhiều lần trong cùng một tick → có thể cho **cùng một newId** cho các items khác nhau, dẫn đến items bị merge. **Fix**: Thêm index counter vào ID tạo để đảm bảo unique: ```js var dupCounter = 0; var newId = 'sec_dup_' + Date.now() + '_' + (dupCounter++) + '_' + Math.random()... ``` --- ### Bug 3 – Section-only path trong `isDuplicate` (non-multi): không xử lý `clip` type **Vị trí**: [app.jsx L11925-11938](file:///home/locpham/SonicForgeStudio/app/static/js/app.jsx#L11925-L11938) Khi không có `multiIds` (chỉ 1 item đơn lẻ), code chỉ handle `section` và `midiItem`: ```js var items = itemType === 'section' ? (track.sections || []) : (track.midiItems || []); ``` Nếu `itemType === 'clip'`, `items` sẽ dùng `midiItems` (sai). Cần thêm case cho `clip`. --- ### Bug 4 – Ctrl+Click trên CLIP ở line 1172: điều kiện kiểm tra bỏ qua hitItem **Vị trí**: [app.jsx L1172](file:///home/locpham/SonicForgeStudio/app/static/js/app.jsx#L1172) ```js if (clickedClip && (e.altKey || e.ctrlKey)) { ... } ``` Đoạn này xử lý Ctrl+Click trên **audio clip**. Tuy nhiên ở trên (line 1101), block `if (hitItem && !e.altKey && !e.shiftKey)` đã `return` nếu hitItem (section/midiItem) được click với Ctrl. Nhưng nếu không có hitItem và không có clickedClip, flow đúng (sweep select). Không có lỗi ở đây. --- ### Bug 5 – `handleSetPendingDrag` cho Clip (line 1184): cùng vấn đề như Bug 1 Tương tự Bug 1, khi Ctrl+Click trên clip đang selected (sẽ bị deselect ở line 1180), nhưng `clipPreToggleSnapshot.has(clipCanonicalId)` là true và `ids.add(clipCanonicalId)` vẫn được thực thi trong `handleSetPendingDrag`. --- ## Proposed Changes ### [MODIFY] [app.jsx](file:///home/locpham/SonicForgeStudio/app/static/js/app.jsx) #### Fix Bug 1 & 5 – `handleSetPendingDrag` (L11834-11838) Sửa logic toggle để đúng: nếu item **có** trong preToggleSnapshot → nó vừa bị deselect → **xóa** khỏi ids. Nếu không có → vừa được add → **thêm** vào ids. #### Fix Bug 2 – ID uniqueness trong multi-copy loop Thêm counter index vào ID generation trong vòng lặp `Object.keys(multiIds).forEach`. #### Fix Bug 3 – Single-item copy: xử lý `clip` type Trong nhánh `else` (non-multiIds) của `isDuplicate`, thêm xử lý `clip` type. --- ## Verification Plan ### Manual Verification 1. Mở MAIN SESSION 2. Thêm vài sections/MIDI items trên nhiều tracks 3. **Test Ctrl+Click toggle**: Click vào item A → selected. Ctrl+Click A → deselected. Ctrl+Click B → B selected (không phải A+B). 4. **Test Ctrl+Drag single**: Ctrl+Click item → drag → chỉ copy 1 item, không thêm item nào bị deselect 5. **Test Ctrl+Drag group**: Ctrl+Click A, Ctrl+Click B (2 selected) → drag từ A → copy cả A và B, không thêm C hay D 6. **Test Ctrl+Click empty**: Click trống → deselect all 7. **Test Ctrl+Drag empty**: Drag trên empty → marquee select 8. Lặp lại trên SECTION-TAB (session_xxx)