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:
@@ -5,32 +5,16 @@ import SwiftUI
|
||||
/// Holds only `boardRoot`, never a `BoardSummary`/`BoardModel` value: the session and its
|
||||
/// snapshot are pulled from the environment's index store fresh on every body evaluation, so a
|
||||
/// 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. 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.
|
||||
/// the session's reload lands. Long-pressing a row drags it to a new position, reordering the
|
||||
/// board's own manual arrangement.
|
||||
struct BoardScreen: View {
|
||||
let boardRoot: URL
|
||||
|
||||
@Environment(BoardIndexStore.self) private var index
|
||||
@State private var isPresentingNewLane = false
|
||||
|
||||
/// Persisted as its raw value, not the enum itself — same reasoning as `BoardsTabView`'s
|
||||
/// `sortOrderRaw`.
|
||||
@AppStorage("laneListSortOrder") 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 }
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
.navigationTitle(title)
|
||||
@@ -41,9 +25,6 @@ struct BoardScreen: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
sortToolbarItem
|
||||
}
|
||||
.task { session.open() }
|
||||
.onDisappear {
|
||||
// Stops the materializing/downloading retry timer. In practice a no-op here: a
|
||||
@@ -71,30 +52,6 @@ struct BoardScreen: View {
|
||||
session.snapshot?.title.value ?? boardRoot.deletingPathExtension().lastPathComponent
|
||||
}
|
||||
|
||||
/// The row's display title, "Untitled Lane" fallback included — fed to `ItemSortOrder.sorted`
|
||||
/// so `.name` sorts on exactly what the row shows.
|
||||
private func displayTitle(of lane: Lane) -> String {
|
||||
guard let title = lane.title.value, !title.isEmpty else { return "Untitled Lane" }
|
||||
return title
|
||||
}
|
||||
|
||||
/// Shown only once there is something to sort — an empty or not-yet-loaded lane list has no
|
||||
/// rows for the segments to reorder.
|
||||
@ToolbarContentBuilder
|
||||
private var sortToolbarItem: some ToolbarContent {
|
||||
if case .ready = session.phase, let snapshot = session.snapshot, !snapshot.lanes.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 {
|
||||
switch session.phase {
|
||||
@@ -145,24 +102,20 @@ struct BoardScreen: View {
|
||||
description: Text("Add a lane to start organizing cards.")
|
||||
)
|
||||
} else {
|
||||
// The trusted order is `.manual`'s base — `BoardModel.lanes` is already in display
|
||||
// order (the loader's `Ranks.sortedForDisplay`) — and `.name`/`.recent` layer a
|
||||
// display-only re-sort on top, never touching that base or what's on disk.
|
||||
ForEach(sortOrder.wrappedValue.sorted(snapshot.lanes, title: displayTitle(of:), modified: { $0.modified.value })) { lane in
|
||||
// `snapshot.lanes` is already in display order (the loader's `Ranks.sortedForDisplay`),
|
||||
// so it's trusted here rather than re-sorted.
|
||||
ForEach(snapshot.lanes) { lane in
|
||||
NavigationLink(value: BoardRoute.cards(boardRoot: boardRoot, laneID: lane.id)) {
|
||||
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)
|
||||
.onMove(perform: { moveRows(from: $0, to: $1) })
|
||||
}
|
||||
}
|
||||
.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.
|
||||
/// Commits a lane drag's release.
|
||||
///
|
||||
/// `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
|
||||
|
||||
Reference in New Issue
Block a user