Rows learn to be carried — long-press reorder for lanes and cards, manual mode only
An onMove on both lists, live only while the sort picker says Manual: the drag mints a midpoint rank between its landing neighbours through BoardWriter.moveItem's same-parent reorder path, and when a gap is exhausted the whole container renumbers to a fresh 1024 ladder in one perform bracket. Neither path stamps modified, so a reorder never disturbs the Recent sort. UI suite 4/4 in the simulator. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -7,7 +7,9 @@ import SwiftUI
|
||||
/// write from anywhere in the stack (a lane created, a card moved) reaches this screen the moment
|
||||
/// the session's reload lands. A segmented control in the toolbar lets the list be sorted by
|
||||
/// name or by most recent change, on top of the board's own manual arrangement, remembered across
|
||||
/// launches.
|
||||
/// launches. Long-pressing a row drags it to a new position while `.manual` is selected — under
|
||||
/// `.name`/`.recent` the gesture is inert, since dragging a re-sorted list would reorder lanes the
|
||||
/// user is not looking at in rank order.
|
||||
struct BoardScreen: View {
|
||||
let boardRoot: URL
|
||||
|
||||
@@ -151,10 +153,68 @@ struct BoardScreen: View {
|
||||
LaneSummaryRow(lane: lane)
|
||||
}
|
||||
}
|
||||
// `nil` under `.name`/`.recent` disables the drag outright — `onMove` accepts an
|
||||
// optional closure, so the gate lives here rather than in a branch of the `ForEach`.
|
||||
.onMove(perform: sortOrder.wrappedValue == .manual ? { moveRows(from: $0, to: $1) } : nil)
|
||||
}
|
||||
}
|
||||
.refreshable { session.reload() }
|
||||
}
|
||||
|
||||
/// Commits a lane drag's release, gated to `.manual` by the `onMove` call site above — reordering
|
||||
/// a `.name`/`.recent` re-sort would rewrite ranks for rows the user isn't seeing in rank order.
|
||||
///
|
||||
/// `manualOrder` is `snapshot.lanes` with the drag already applied locally (`Array.move`), so the
|
||||
/// dragged lane's post-drag index is both the row SwiftUI just drew and the position
|
||||
/// `Ranks.insertionRank` mints a rank against — the same index convention `BoardStore.moveLane`
|
||||
/// uses on the Mac. The happy path asks for the midpoint between the two neighbours the lane
|
||||
/// lands between; on the rare board where that gap is already exhausted
|
||||
/// (01-storage-format.md § Ordering), there is no `HealScheduler` to reach for — that engine
|
||||
/// lives in `Kanban/LiveStore`, which this target does not compile — so the fallback renumbers
|
||||
/// every lane to a fresh whole-multiple-of-1024 ladder in `manualOrder`'s own order, one
|
||||
/// `BoardWriter.moveItem` reorder call per lane inside the same `session.perform` bracket. Both
|
||||
/// paths are same-parent reorders, so neither stamps `modified` (the reorders-don't-stamp rule).
|
||||
private func moveRows(from source: IndexSet, to destination: Int) {
|
||||
guard let sourceIndex = source.first,
|
||||
let snapshot = session.snapshot,
|
||||
snapshot.lanes.indices.contains(sourceIndex)
|
||||
else { return }
|
||||
|
||||
var manualOrder = snapshot.lanes
|
||||
let moved = manualOrder[sourceIndex]
|
||||
manualOrder.move(fromOffsets: IndexSet(integer: sourceIndex), toOffset: destination)
|
||||
guard let landingIndex = manualOrder.firstIndex(where: { $0.id == moved.id }) else { return }
|
||||
|
||||
let siblingOrders = manualOrder.filter { $0.id != moved.id }.map(\.order)
|
||||
// A `let` copy — the mutating `Array.move` above needs `manualOrder` as a `var`, but a
|
||||
// `@Sendable` closure cannot capture a mutable var, and the fallback loop below needs the
|
||||
// finished (post-move) arrangement, not the pre-move one.
|
||||
let finalOrder = manualOrder
|
||||
|
||||
Task {
|
||||
await session.perform { (root: URL) throws(BoardWriteError) -> MoveResult in
|
||||
if let rank = Ranks.insertionRank(amongVisible: siblingOrders, at: landingIndex) {
|
||||
return try BoardWriter.moveItem(
|
||||
at: root.appendingPathComponent(moved.id.rawValue, isDirectory: true),
|
||||
toParent: root,
|
||||
sourceBoardRoot: root,
|
||||
destinationBoardRoot: root,
|
||||
order: rank
|
||||
)
|
||||
}
|
||||
for (lane, freshRank) in zip(finalOrder, Ranks.renumbered(count: finalOrder.count)) {
|
||||
_ = try BoardWriter.moveItem(
|
||||
at: root.appendingPathComponent(lane.id.rawValue, isDirectory: true),
|
||||
toParent: root,
|
||||
sourceBoardRoot: root,
|
||||
destinationBoardRoot: root,
|
||||
order: freshRank
|
||||
)
|
||||
}
|
||||
return MoveResult(id: moved.id, reminted: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One lane row: its icon when it has one, title, and its card count.
|
||||
|
||||
Reference in New Issue
Block a user