Skip to content

Commit de53fe9

Browse files
diegorvclaude
andcommitted
fix(kanban): disable card drag-reorder while a filter is active
Context: In board view, item drag-drop computes the drop index by scanning the rendered [data-item-id] cards (handleItemDrop), then passes that index to moveItem, which splices into the lane's full, stored items array. Dragging was already disabled for non-manual sort modes via disableDrag={!isManualSort}, because a sorted view reorders the cards relative to storage. Problem: A text filter (filterItems) also makes the rendered cards a subset of the lane's items, but drag stayed enabled in manual sort mode. With a filter active, the drop index (a position among the visible/filtered cards) was spliced into the unfiltered array, so the dragged card landed at the wrong position. applyChange then serialized and persisted the corrupted order to the .kanban file. The cross-lane drop path was affected identically. Solution: - Add a pure `canReorderItems(sortMode, filterQuery)` helper: reorder is valid only when sort is manual AND no filter is active (rendered order == stored order). - Wire it in KanbanView as `disableDrag={!canReorder}`. KanbanCard already maps disableDrag to draggable=false and drops the dragstart handler, so no drag begins and handleItemDrop's `if (!dragItemId) return` guard short-circuits. Behavior: - Manual sort + no filter: drag-reorder works (unchanged). - Manual sort + active filter: cards are no longer draggable (no silent misplacement / file corruption). - Non-manual sort: drag stays disabled (unchanged). Files: - src/lib/plugins/kanban/kanban.logic.ts:688-700 — `canReorderItems` helper. - src/lib/plugins/kanban/KanbanView.svelte:43,162-165,525 — import, `canReorder` derived, `disableDrag={!canReorder}`. - src/tests/lib/plugins/kanban/kanban.logic.test.ts — canReorderItems cases (manual/no-filter true; filtered false; sorted false). - tasks/todo/bug-hunt-fixes.md — H6 marked done. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2e0cd92 commit de53fe9

4 files changed

Lines changed: 42 additions & 2 deletions

File tree

src/lib/plugins/kanban/KanbanView.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
setSortMode,
4141
filterItems,
4242
setViewMode,
43+
canReorderItems,
4344
} from './kanban.logic';
4445
import type { KanbanBoard, KanbanSortMode, KanbanViewMode } from './kanban.types';
4546
import { kanbanStore } from './kanban.store.svelte';
@@ -159,6 +160,10 @@
159160
160161
let currentSortMode = $derived(board.settings.sortMode ?? 'manual');
161162
let isManualSort = $derived(currentSortMode === 'manual');
163+
// Manual card drag-reorder is only valid when the rendered order matches the
164+
// stored order: manual sort AND no active filter. Otherwise a drop index among
165+
// the filtered/sorted cards would misplace cards in the full lane list.
166+
let canReorder = $derived(canReorderItems(currentSortMode, kanbanStore.filterQuery));
162167
163168
function handleSetSortMode(mode: KanbanSortMode) {
164169
applyChange(setSortMode(board, mode));
@@ -519,7 +524,7 @@
519524
atLimit={isLaneAtLimit(board, lane.title)}
520525
autoComplete={isLaneAutoComplete(board, lane.title)}
521526
tagColors={board.settings.tagColors ?? {}}
522-
disableDrag={!isManualSort}
527+
disableDrag={!canReorder}
523528
{dragItemId}
524529
isDragOver={dragOverLaneId === lane.id}
525530
{dropIndicatorIndex}

src/lib/plugins/kanban/kanban.logic.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,20 @@ export function filterItems(items: KanbanItem[], query: string): KanbanItem[] {
685685
return items.filter((item) => item.text.toLowerCase().includes(q));
686686
}
687687

688+
/**
689+
* Whether manual drag-reordering of cards is valid for the current view.
690+
*
691+
* Drop positions are computed from the rendered (filtered + sorted) cards, but
692+
* moveItem splices into the lane's full, stored item list. That mapping is only
693+
* 1:1 when the rendered order equals the stored order: manual sort mode AND no
694+
* active filter. When the board is sorted or filtered, the rendered cards are a
695+
* reordered/subset view, so a drop index among them would misplace and persist
696+
* cards. Callers disable item dragging when this returns false.
697+
*/
698+
export function canReorderItems(sortMode: KanbanSortMode, filterQuery: string): boolean {
699+
return sortMode === 'manual' && filterQuery.trim() === '';
700+
}
701+
688702
// ── View mode ───────────────────────────────────────────────────────
689703

690704
/** Sets the board view mode */

src/tests/lib/plugins/kanban/kanban.logic.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
setSortMode,
4747
filterItems,
4848
setViewMode,
49+
canReorderItems,
4950
} from '$lib/plugins/kanban/kanban.logic';
5051
import type { KanbanBoard, KanbanSettings } from '$lib/plugins/kanban/kanban.types';
5152

@@ -1519,3 +1520,23 @@ describe('setViewMode', () => {
15191520
expect(parsed.settings.viewMode).toBe('list');
15201521
});
15211522
});
1523+
1524+
describe('canReorderItems', () => {
1525+
it('allows manual reorder when sort is manual and no filter is active', () => {
1526+
expect(canReorderItems('manual', '')).toBe(true);
1527+
expect(canReorderItems('manual', ' ')).toBe(true);
1528+
});
1529+
1530+
it('blocks reorder when a filter is active (visible order != stored order)', () => {
1531+
// Drop positions are computed among the filtered DOM cards; mapping them
1532+
// into the lane's full item array misplaces and persists cards.
1533+
expect(canReorderItems('manual', 'x')).toBe(false);
1534+
expect(canReorderItems('manual', ' fix ')).toBe(false);
1535+
});
1536+
1537+
it('blocks reorder when the board is sorted (non-manual)', () => {
1538+
expect(canReorderItems('date-asc', '')).toBe(false);
1539+
expect(canReorderItems('text-desc', '')).toBe(false);
1540+
expect(canReorderItems('checked', 'x')).toBe(false);
1541+
});
1542+
});

tasks/todo/bug-hunt-fixes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This batch fixes the **7 HIGH** bugs (one commit each). Medium + Low are backlog
1212
- [x] H3: collection parser — method/field chaining on a function-call result broken (`now().format()`, `today().date()`). `src/lib/features/collection/expression/parser.ts:130-143,206-212`. Fix: structural `methodCall` node holding receiver ASTNode, not flattened dotted string.
1313
- [x] H4: collection filter — single-row `not` group collapses to bare positive expression, inverting filter + persisting inverted YAML. `src/lib/features/collection/toolbar/filter.logic.ts:146-148`. Fix: shortcut only when `conjunction !== 'not'`.
1414
- [x] H5: properties panel crashes (Svelte `each_key_duplicate`) when frontmatter has alias + canonical twin (`color` + `_color`). `src/lib/features/properties/PropertiesView.svelte:92-96,221`. Fix: `dedupeCanonicalKeys` on parse path (or key=index).
15-
- [ ] H6: kanban drag-drop misplaces cards (DOM filtered index spliced into full array) when a filter is active -> corrupts `.kanban`. `src/lib/plugins/kanban/KanbanView.svelte:372-393`. Fix: disable drag while filtered, or map filtered->absolute index.
15+
- [x] H6: kanban drag-drop misplaces cards (DOM filtered index spliced into full array) when a filter is active -> corrupts `.kanban`. `src/lib/plugins/kanban/KanbanView.svelte:372-393`. Fix: disable drag while filtered, or map filtered->absolute index.
1616
- [ ] H7: Table of Contents empty for CRLF files (`\r` breaks HEADING_RE). `src/lib/plugins/table-of-contents/toc.logic.ts:63-93` (root: `wikilink/navigation.logic.ts:2`). Fix: split `/\r?\n/` or strip `\r`; also fixes wikilink `#heading` jump on CRLF docs.
1717

1818
## Backlog — MEDIUM (22)

0 commit comments

Comments
 (0)