The lane and card lists give up their pickers — rank order is the only order below the boards

The sort control stays on the board list alone; lanes and cards return
to the arrangement the ranks state, long-press reorder now ungated
because the rows always show the order a drag would rewrite.
ItemSortOrder leaves with the pickers that needed it.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-08-08 13:41:09 -04:00
parent 2d919b8131
commit 28abaac2d9
4 changed files with 16 additions and 169 deletions
+7 -55
View File
@@ -5,11 +5,8 @@ import SwiftUI
/// Holds `boardRoot` and `laneID`, never a `Lane` value: the lane is re-read from
/// `session.snapshot` on every body evaluation, so a write this screen makes or one relayed
/// from elsewhere through the metadata query reaches the list the moment the session's reload
/// lands, and a lane deleted on another device is noticed rather than shown stale. A segmented
/// control in the toolbar lets the list be sorted by name or by most recent change, on top
/// of the lane's own manual arrangement, remembered across launches. Long-pressing a card drags it
/// to a new position while `.manual` is selected, for the same reason the lane list's drag is gated
/// `.name`/`.recent` are not the rank order a reorder would rewrite.
/// lands, and a lane deleted on another device is noticed rather than shown stale. Long-pressing
/// a card drags it to a new position, reordering the lane's own manual arrangement.
struct LaneScreen: View {
let boardRoot: URL
let laneID: ItemID
@@ -20,21 +17,8 @@ struct LaneScreen: View {
@State private var isPresentingNewCard = false
@State private var cardPendingMove: ItemID?
/// Persisted as its raw value, not the enum itself same reasoning as `BoardsTabView`'s
/// `sortOrderRaw`.
@AppStorage("cardListSortOrder") private var sortOrderRaw = ItemSortOrder.recent.rawValue
private var session: BoardSession { index.session(forBoardAt: boardRoot) }
/// The stored raw value as the enum, defaulting to `.recent` on anything the store didn't
/// write itself an unset key or a stale raw value from a build that no longer has this case.
private var sortOrder: Binding<ItemSortOrder> {
Binding(
get: { ItemSortOrder(rawValue: sortOrderRaw) ?? .recent },
set: { sortOrderRaw = $0.rawValue }
)
}
private var lane: Lane? {
session.snapshot?.lanes.first { $0.id == laneID }
}
@@ -49,9 +33,6 @@ struct LaneScreen: View {
}
}
}
.toolbar {
sortToolbarItem
}
.task { session.open() }
.titlePromptAlert("New Card", isPresented: $isPresentingNewCard, placeholder: "Card Title") { title in
createCard(titled: title.isEmpty ? nil : title)
@@ -86,30 +67,6 @@ struct LaneScreen: View {
return title
}
/// The row's display title, "Untitled Card" fallback included fed to `ItemSortOrder.sorted`
/// so `.name` sorts on exactly what the row shows.
private func displayTitle(of card: Card) -> String {
guard let title = card.title.value, !title.isEmpty else { return "Untitled Card" }
return title
}
/// Shown only once there is something to sort an empty or not-yet-loaded card list has no
/// rows for the segments to reorder.
@ToolbarContentBuilder
private var sortToolbarItem: some ToolbarContent {
if let lane, !lane.cards.isEmpty {
ToolbarItem(placement: .principal) {
Picker("Sort", selection: sortOrder) {
Label("Manual", systemImage: "hand.draw").tag(ItemSortOrder.manual)
Label("Name", systemImage: "textformat.abc").tag(ItemSortOrder.name)
Label("Recent", systemImage: "clock").tag(ItemSortOrder.recent)
}
.pickerStyle(.segmented)
.frame(maxWidth: 260)
}
}
}
@ViewBuilder
private var content: some View {
if let lane {
@@ -128,11 +85,9 @@ struct LaneScreen: View {
description: Text("Add a card to this lane.")
)
} else {
// The trusted order is `.manual`'s base `Lane.cards` is already in display
// order, exactly as the lane list trusts `BoardModel.lanes` and `.name`/
// `.recent` layer a display-only re-sort on top, never touching that base or
// what's on disk.
ForEach(sortOrder.wrappedValue.sorted(lane.cards, title: displayTitle(of:), modified: { $0.modified.value })) { card in
// `lane.cards` is already in display order, exactly as the lane list trusts
// `BoardModel.lanes` so it's trusted here rather than re-sorted.
ForEach(lane.cards) { card in
NavigationLink(value: BoardRoute.card(boardRoot: boardRoot, laneID: laneID, cardID: card.id)) {
CardSummaryRow(card: card)
}
@@ -154,9 +109,7 @@ struct LaneScreen: View {
}
}
}
// `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)
.onMove(perform: { moveRows(from: $0, to: $1) })
}
}
.refreshable { session.reload() }
@@ -216,8 +169,7 @@ struct LaneScreen: View {
}
}
/// Commits a card 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.
/// Commits a card drag's release.
///
/// `manualOrder` is `lane.cards` with the drag already applied locally (`Array.move`), so the
/// dragged card's post-drag index is both the row SwiftUI just drew and the position