Materialize the trash — store, undo, and the container universe
Phase 2 swaps every consumer: Liveness and its ancestor walk are gone, replaced by ItemContainer — a UUID set plus the container side it lives on, presence the whole test, one selection boundary instead of the old liveness law. Deletion stages by place: board cards move to the trash at a store-minted head rank, trash-side delete is permanent behind its confirmation, Delete Immediately skips the trash from anywhere, lane delete captures the subtree and removes the folder. Restore has no method at all — moveCards resolves members in either container, so drag-out and cut-paste are the ordinary moves 13 calls them, registering ordinary Move steps. The delete inverse moves the card back to its captured lane and rank; redo replays the captured trash rank, a value the gesture actually wrote; lane undo recreates the subtree byte-faithfully in session. Purges register nothing — where 13's trash section contradicts its own Rules on that, Rules wins, filed for ruling. Staleness collapsed to present-or-absent: a container is a path, so a foreign restore fails the delete step's expectation structurally. Legacy tombstones migrate on the loose-file tail hook, cards oldest-first so minting above top reproduces the retired newest-first column, lanes returning live, one folded loss row naming both directions. Put Back, restoreByDrag, receiveRestoredCards, TrashEntry, and the kind machinery are deleted; the trash column renders the container correctly with its full face rework left to phase 3. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -15,11 +15,11 @@ import Testing
|
||||
@Suite("DragPayload")
|
||||
struct DragPayloadTests {
|
||||
|
||||
private static func payload(kind: DragKind = .cards, side: Liveness = .live) -> DragPayload {
|
||||
private static func payload(kind: DragKind = .cards, container: ItemContainer = .board) -> DragPayload {
|
||||
DragPayload(
|
||||
boardRoot: URL(fileURLWithPath: "/Boards/Work.kanban", isDirectory: true),
|
||||
kind: kind,
|
||||
side: side,
|
||||
container: container,
|
||||
items: [
|
||||
DragPayload.Item(id: "aaa", folder: "/Boards/Work.kanban/lane/aaa", title: "First"),
|
||||
DragPayload.Item(id: "bbb", folder: "/Boards/Work.kanban/lane/bbb", title: nil)
|
||||
@@ -30,8 +30,8 @@ struct DragPayloadTests {
|
||||
@Test("A payload round-trips through its JSON representation unchanged")
|
||||
func roundTrip() throws {
|
||||
for kind in [DragKind.cards, .lanes] {
|
||||
for side in [Liveness.live, .trashed] {
|
||||
let original = Self.payload(kind: kind, side: side)
|
||||
for container in [ItemContainer.board, .trash] {
|
||||
let original = Self.payload(kind: kind, container: container)
|
||||
let data = try #require(original.encoded())
|
||||
#expect(DragPayload(data: data) == original)
|
||||
}
|
||||
@@ -62,10 +62,10 @@ struct DragPayloadTests {
|
||||
#expect(Self.payload().plainText == "First\nUntitled")
|
||||
}
|
||||
|
||||
@Test("The side survives the round trip, because it is what makes a trash drag a trash drag")
|
||||
func sideSurvives() throws {
|
||||
let data = try #require(Self.payload(side: .trashed).encoded())
|
||||
#expect(DragPayload(data: data)?.side.liveness == .trashed)
|
||||
@Test("The container survives the round trip, because it is what makes a trash drag a trash drag")
|
||||
func containerSurvives() throws {
|
||||
let data = try #require(Self.payload(container: .trash).encoded())
|
||||
#expect(DragPayload(data: data)?.container == .trash)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,25 +95,25 @@ struct DragLocalityTests {
|
||||
/// The Finder volume model: within a board a drag rearranges, between boards it transfers.
|
||||
@Test("Locality picks the default — within is a move, across is a copy")
|
||||
func theDefault() {
|
||||
#expect(DragLocality.operation(kind: .cards, side: .live, isWithinBoard: true, modifiers: none) == .move)
|
||||
#expect(DragLocality.operation(kind: .cards, side: .live, isWithinBoard: false, modifiers: none) == .copy)
|
||||
#expect(DragLocality.operation(kind: .lanes, side: .live, isWithinBoard: false, modifiers: none) == .copy)
|
||||
#expect(DragLocality.operation(kind: .cards, container: .board, isWithinBoard: true, modifiers: none) == .move)
|
||||
#expect(DragLocality.operation(kind: .cards, container: .board, isWithinBoard: false, modifiers: none) == .copy)
|
||||
#expect(DragLocality.operation(kind: .lanes, container: .board, isWithinBoard: false, modifiers: none) == .copy)
|
||||
}
|
||||
|
||||
@Test("⌥ forces copy and ⌘ forces move, each a no-op where it is already the default")
|
||||
func modifiersOverride() {
|
||||
#expect(DragLocality.operation(kind: .cards, side: .live, isWithinBoard: true, modifiers: option) == .copy)
|
||||
#expect(DragLocality.operation(kind: .cards, side: .live, isWithinBoard: false, modifiers: command) == .move)
|
||||
#expect(DragLocality.operation(kind: .cards, container: .board, isWithinBoard: true, modifiers: option) == .copy)
|
||||
#expect(DragLocality.operation(kind: .cards, container: .board, isWithinBoard: false, modifiers: command) == .move)
|
||||
// The no-ops.
|
||||
#expect(DragLocality.operation(kind: .cards, side: .live, isWithinBoard: true, modifiers: command) == .move)
|
||||
#expect(DragLocality.operation(kind: .cards, side: .live, isWithinBoard: false, modifiers: option) == .copy)
|
||||
#expect(DragLocality.operation(kind: .cards, container: .board, isWithinBoard: true, modifiers: command) == .move)
|
||||
#expect(DragLocality.operation(kind: .cards, container: .board, isWithinBoard: false, modifiers: option) == .copy)
|
||||
}
|
||||
|
||||
@Test("⌘ wins over ⌥ when both are held")
|
||||
func commandWinsOverOption() {
|
||||
// Finder's own reduction, and the same precedence `ClickModifier.current` applies to clicks.
|
||||
#expect(DragLocality.operation(
|
||||
kind: .cards, side: .live, isWithinBoard: false, modifiers: [.option, .command]) == .move)
|
||||
kind: .cards, container: .board, isWithinBoard: false, modifiers: [.option, .command]) == .move)
|
||||
}
|
||||
|
||||
/// The first carve-out: "Lane drags never copy *within their board*. ⌥ is simply ignored there:
|
||||
@@ -122,13 +122,13 @@ struct DragLocalityTests {
|
||||
func laneDragsNeverCopyWithinTheirBoard() {
|
||||
for modifiers in [none, option, command, [.option, .command] as NSEvent.ModifierFlags] {
|
||||
#expect(
|
||||
DragLocality.operation(kind: .lanes, side: .live, isWithinBoard: true, modifiers: modifiers) == .move,
|
||||
DragLocality.operation(kind: .lanes, container: .board, isWithinBoard: true, modifiers: modifiers) == .move,
|
||||
"a within-board lane drag is a reorder whatever is held"
|
||||
)
|
||||
}
|
||||
// Across boards the lane obeys the ordinary grammar again.
|
||||
#expect(DragLocality.operation(kind: .lanes, side: .live, isWithinBoard: false, modifiers: option) == .copy)
|
||||
#expect(DragLocality.operation(kind: .lanes, side: .live, isWithinBoard: false, modifiers: command) == .move)
|
||||
#expect(DragLocality.operation(kind: .lanes, container: .board, isWithinBoard: false, modifiers: option) == .copy)
|
||||
#expect(DragLocality.operation(kind: .lanes, container: .board, isWithinBoard: false, modifiers: command) == .move)
|
||||
}
|
||||
|
||||
/// The second: a trash row's drag is copy-out grammar (04-interactions.md ▸ The trash). Within its
|
||||
@@ -137,11 +137,11 @@ struct DragLocalityTests {
|
||||
/// live copy either way.
|
||||
@Test("A trash row drags as a restore at home and as a copy-out abroad")
|
||||
func trashDragDefaults() {
|
||||
#expect(DragLocality.operation(kind: .cards, side: .trashed, isWithinBoard: true, modifiers: none) == .move)
|
||||
#expect(DragLocality.operation(kind: .cards, side: .trashed, isWithinBoard: false, modifiers: none) == .copy)
|
||||
#expect(DragLocality.operation(kind: .cards, container: .trash, isWithinBoard: true, modifiers: none) == .move)
|
||||
#expect(DragLocality.operation(kind: .cards, container: .trash, isWithinBoard: false, modifiers: none) == .copy)
|
||||
#expect(DragLocality.operation(
|
||||
kind: .cards, side: .trashed, isWithinBoard: false, modifiers: command) == .move)
|
||||
#expect(DragLocality.operation(kind: .cards, side: .trashed, isWithinBoard: true, modifiers: option) == .copy)
|
||||
kind: .cards, container: .trash, isWithinBoard: false, modifiers: command) == .move)
|
||||
#expect(DragLocality.operation(kind: .cards, container: .trash, isWithinBoard: true, modifiers: option) == .copy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ struct TrashDropTests {
|
||||
/// The accepted session, with one clause at a time knocked out by the cases.
|
||||
private func accepts(
|
||||
kind: DragKind? = .cards,
|
||||
side: Liveness = .live,
|
||||
container: ItemContainer = .board,
|
||||
isWithinBoard: Bool = true,
|
||||
operation: TransferOperation = .move,
|
||||
isTrashShown: Bool = true,
|
||||
@@ -166,7 +166,7 @@ struct TrashDropTests {
|
||||
) -> Bool {
|
||||
TrashDrop.accepts(
|
||||
kind: kind,
|
||||
side: side,
|
||||
container: container,
|
||||
isWithinBoard: isWithinBoard,
|
||||
operation: operation,
|
||||
isTrashShown: isTrashShown,
|
||||
@@ -196,12 +196,12 @@ struct TrashDropTests {
|
||||
#expect(!accepts(kind: nil))
|
||||
}
|
||||
|
||||
/// A trash row's drag is restore/copy-out grammar; dropped back where it came from it writes
|
||||
/// nothing, so it never proposes.
|
||||
@Test("A trash row dropped back on the trash is refused")
|
||||
func theTrashedSideIsRefused() {
|
||||
#expect(!accepts(side: .trashed))
|
||||
#expect(!accepts(side: .trashed, isWithinBoard: false))
|
||||
/// A trash card's drag is the restore; dropped back where it came from it writes nothing, so it
|
||||
/// never proposes.
|
||||
@Test("A trash card dropped back on the trash is refused")
|
||||
func theTrashContainerIsRefused() {
|
||||
#expect(!accepts(container: .trash))
|
||||
#expect(!accepts(container: .trash, isWithinBoard: false))
|
||||
}
|
||||
|
||||
/// "No move or paste ever targets the trash": a foreign card delivered into this board's trash
|
||||
@@ -340,7 +340,7 @@ struct DropSettleTests {
|
||||
.appendingPathComponent($0.id.rawValue, isDirectory: true)
|
||||
},
|
||||
heights: members.map { _ in 44 },
|
||||
side: .live,
|
||||
container: .board,
|
||||
source: store
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user