Realign code with the 2026-07-30 findings-resolution rulings
Delete Immediately is removed entirely (rulingae1dd96, Redesign card d40bfac1): the delete vocabulary is purely staged — board → trash, trash → permanent (confirmed on no-git boards), Empty Trash for bulk. Gone: File ▸ Delete Immediately (⌥⌘⌫) and its validation, both ⌥-alternate context rows (card + the permanently-disabled lane row), the VO custom action, BoardStore.deleteImmediately, TrashModel.canDeleteImmediately, TrashConfirmations' .purge action (zero surviving callers — trash-side Delete always used .deleteTrashCards), and the pinning tests. purgePrompt drops its now-single-purpose container parameter (.trash is the only surviving caller). BoardWriter.purgeItem survives — create-undo rollback still needs it — with its comment rewritten. README's trash paragraph drops the ⌥⌘⌫ sentence. The other 2026-07-30 rulings (2ec2c95registry freshness stamp,c741b02unified-log-as-coerce-consumer) required no code changes — already conformant. Both schemes 1844 tests / 318 suites green. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -5,10 +5,11 @@ import SwiftUI
|
||||
|
||||
/// The board window's purge alert, as a piece of window-local state (03-board-ui.md § Trash).
|
||||
///
|
||||
/// **It exists because a menu item cannot present anything.** Delete Immediately and Empty Trash…
|
||||
/// live in the menu bar, the trash row's context menu carries a twin of the first, and all three must
|
||||
/// raise *the same* alert on *the window in front* — so the request travels through the focus system
|
||||
/// exactly as `BoardInfoPresentation` does, and the alert itself is hosted once by `BoardView`.
|
||||
/// **It exists because a menu item cannot present anything.** File ▸ Delete (landing on a trash
|
||||
/// selection) and Empty Trash… live in the menu bar, the trash row's context menu carries a twin of
|
||||
/// the first, and all three must raise *the same* alert on *the window in front* — so the request
|
||||
/// travels through the focus system exactly as `BoardInfoPresentation` does, and the alert itself is
|
||||
/// hosted once by `BoardView`.
|
||||
///
|
||||
/// `@State` in `BoardWindowHost`, therefore one per window and dying with it: a half-answered
|
||||
/// confirmation is not something to carry across a window's life.
|
||||
@@ -30,18 +31,11 @@ final class TrashConfirmations {
|
||||
let prompt: TrashModel.PurgePrompt
|
||||
let action: Action
|
||||
|
||||
/// What the confirmation is standing in front of. Three cases, because the three commands
|
||||
/// have genuinely different scopes and two different writes: the trash's own staged Delete,
|
||||
/// Delete Immediately (which skips the trash from either container), and Empty Trash (which
|
||||
/// names the whole container and re-derives its targets at the moment it runs).
|
||||
/// What the confirmation is standing in front of. Two cases, because permanence is only
|
||||
/// reachable inside the trash now: the trash's own staged Delete, and Empty Trash (which names
|
||||
/// the whole container and re-derives its targets at the moment it runs).
|
||||
enum Action: Equatable {
|
||||
case deleteTrashCards(Set<ItemID>)
|
||||
/// **The container travels with the ids**, because a purge can be aimed at either side and
|
||||
/// the two entry points below disagree about which: the menu-bar command means the
|
||||
/// selection's container, a context menu means `.board` whatever is selected. Re-reading
|
||||
/// the selection when the alert is answered would resolve a board card against `.trash` and
|
||||
/// purge nothing — a confirmed destructive command becoming a silent no-op.
|
||||
case purge(Set<ItemID>, ItemContainer)
|
||||
case emptyTrash
|
||||
}
|
||||
}
|
||||
@@ -52,7 +46,7 @@ final class TrashConfirmations {
|
||||
/// A board selection goes straight through: moving a card into the trash and deleting a lane are
|
||||
/// both recoverable (the trash itself, and native undo — 03-board-ui.md § Trash), so neither
|
||||
/// stands an alert. A **trash** selection is the permanent one, and it "confirms exactly where
|
||||
/// the loss is real": `purgeIsUnrecoverable` decides, exactly as it does for Delete Immediately.
|
||||
/// the loss is real": `purgeIsUnrecoverable` decides.
|
||||
///
|
||||
/// The staging itself lives on the store (`BoardStore.deleteSelection`), so this is the alert and
|
||||
/// nothing else — the two can never disagree about which write a ⌘⌫ performs.
|
||||
@@ -74,7 +68,7 @@ final class TrashConfirmations {
|
||||
///
|
||||
/// Same alert, same rule: it "confirms exactly where the loss is real", so
|
||||
/// `purgeIsUnrecoverable` decides — and where it does not, the purge runs straight through, which
|
||||
/// is the same shrug Delete Immediately gives on a board that keeps history.
|
||||
/// is what a board that keeps history does for every permanent delete (delete-never-forgets).
|
||||
func requestTrashDelete(of ids: Set<ItemID>, in store: BoardStore) {
|
||||
guard store.purgeIsUnrecoverable else {
|
||||
store.deleteTrashCards(ids)
|
||||
@@ -82,70 +76,12 @@ final class TrashConfirmations {
|
||||
}
|
||||
guard let prompt = TrashModel.purgePrompt(
|
||||
for: ids,
|
||||
in: .trash,
|
||||
snapshot: store.snapshot,
|
||||
unrecoverable: true
|
||||
) else { return }
|
||||
pending = Pending(prompt: prompt, action: .deleteTrashCards(ids))
|
||||
}
|
||||
|
||||
/// Raises Delete Immediately's alert — **or purges outright** where the loss is not real.
|
||||
///
|
||||
/// The mode check is the one thing that decides between the two, and it lives on the store as a
|
||||
/// named predicate (`BoardStore.purgeIsUnrecoverable`) so the git milestone changes one
|
||||
/// expression rather than three call sites.
|
||||
///
|
||||
/// Its one caller is File ▸ Delete Immediately, which passes the selection's own ids — which is
|
||||
/// what makes reading `store.selection.container` correct here and wrong for a context menu
|
||||
/// (`requestTrashDelete` above exists for exactly that difference).
|
||||
func requestPurge(of ids: Set<ItemID>, in store: BoardStore) {
|
||||
let container = store.selection.container
|
||||
guard store.purgeIsUnrecoverable else {
|
||||
store.deleteImmediately(ids, in: container)
|
||||
return
|
||||
}
|
||||
guard let prompt = TrashModel.purgePrompt(
|
||||
for: ids,
|
||||
in: container,
|
||||
snapshot: store.snapshot,
|
||||
unrecoverable: true
|
||||
) else { return }
|
||||
pending = Pending(prompt: prompt, action: .purge(ids, container))
|
||||
}
|
||||
|
||||
/// **Delete's ⌥-alternate, aimed at an explicit set** — the board-side card and lane
|
||||
/// context-menu rows' Delete Immediately (11-command-nexus.md ▸ Context menus' Card and Lane
|
||||
/// rows: "Delete — with Delete Immediately as its ⌥-alternate … Finder's pattern: hold ⌥ and
|
||||
/// Delete becomes Delete Immediately").
|
||||
///
|
||||
/// A third entry point beside `requestPurge` and `requestTrashDelete`, for `requestTrashDelete`'s
|
||||
/// own reason mirrored onto the other container: `requestPurge(of:in:)` reads
|
||||
/// `store.selection.container` for the prompt, which is correct for its one caller (File ▸
|
||||
/// Delete Immediately, whose ids *are* the selection) and wrong for a context menu, which names
|
||||
/// its target by where the ⌥-held click landed — right-clicking a card or lane while a *trash*
|
||||
/// selection stands must still purge the clicked item.
|
||||
///
|
||||
/// The container is always `.board`: this alternate exists only on the board-side rows —
|
||||
/// "the trash needs no alternate: its Delete is already permanent" (11-command-nexus.md's Lane
|
||||
/// row).
|
||||
///
|
||||
/// **The container is supplied end to end**, prompt and write alike: `store.deleteImmediately`
|
||||
/// takes it as a parameter rather than reading the selection, so a confirmed purge aimed at a board
|
||||
/// item cannot silently find nothing because a trash selection happened to be standing.
|
||||
func requestBoardPurge(of ids: Set<ItemID>, in store: BoardStore) {
|
||||
guard store.purgeIsUnrecoverable else {
|
||||
store.deleteImmediately(ids, in: .board)
|
||||
return
|
||||
}
|
||||
guard let prompt = TrashModel.purgePrompt(
|
||||
for: ids,
|
||||
in: .board,
|
||||
snapshot: store.snapshot,
|
||||
unrecoverable: true
|
||||
) else { return }
|
||||
pending = Pending(prompt: prompt, action: .purge(ids, .board))
|
||||
}
|
||||
|
||||
/// Raises Empty Trash…'s alert. **Always** — it guards bulk scope rather than per-item
|
||||
/// recoverability, so no board skips it.
|
||||
func requestEmptyTrash(in store: BoardStore) {
|
||||
@@ -163,7 +99,6 @@ final class TrashConfirmations {
|
||||
self.pending = nil
|
||||
switch pending.action {
|
||||
case let .deleteTrashCards(ids): store.deleteTrashCards(ids)
|
||||
case let .purge(ids, container): store.deleteImmediately(ids, in: container)
|
||||
case .emptyTrash: store.emptyTrash()
|
||||
}
|
||||
}
|
||||
@@ -186,7 +121,7 @@ extension FocusedValues {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - File ▸ Delete / Delete Immediately / Empty Trash…
|
||||
// MARK: - File ▸ Delete / Empty Trash…
|
||||
|
||||
/// The File menu's trash rows (11-command-nexus.md).
|
||||
///
|
||||
@@ -210,13 +145,6 @@ struct TrashCommands: View {
|
||||
.keyboardShortcut(.delete, modifiers: .command)
|
||||
.disabled(!canDelete || confirmations == nil)
|
||||
|
||||
Button("Delete Immediately") {
|
||||
guard let store, let confirmations else { return }
|
||||
confirmations.requestPurge(of: store.selection.ids, in: store)
|
||||
}
|
||||
.keyboardShortcut(.delete, modifiers: [.option, .command])
|
||||
.disabled(!canDeleteImmediately || confirmations == nil)
|
||||
|
||||
Button("Empty Trash…") {
|
||||
guard let store, let confirmations else { return }
|
||||
confirmations.requestEmptyTrash(in: store)
|
||||
@@ -232,13 +160,6 @@ struct TrashCommands: View {
|
||||
return TrashModel.canDelete(selection: store.selection, in: store.snapshot)
|
||||
}
|
||||
|
||||
/// A **card** selection, in either container — "skips the trash from anywhere"
|
||||
/// (11-command-nexus.md).
|
||||
private var canDeleteImmediately: Bool {
|
||||
guard let store, store.acceptsBoardMutations else { return false }
|
||||
return TrashModel.canDeleteImmediately(selection: store.selection, in: store.snapshot)
|
||||
}
|
||||
|
||||
private var canEmptyTrash: Bool {
|
||||
store?.canEmptyTrash == true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user