Build the attachments sidebar section
The card's complete file inventory: compact QuickLook-thumbnail rows over Card.attachments — no reference tracking, subfolders tolerated and unsurfaced — with a quiet header add affordance and the drop hint empty state. The whole window is the file-drop surface, Edit mode included (the editor's drag types were already filtered; now tested), sharing the board's folder-refusal semantics literally: FinderDrop moved verbatim into its own file so both windows run the same partition and loss row. Dragged text still lands at the caret and is inert elsewhere — the window delegate accepts file payloads only. Rows open on double-click or Return, drag out their file URL, and Remove is a bracketed write through FileManager.trashItem — the system Trash, never a hard delete, returning the in-Trash URL so the promise is testable; the attachment listing is the guard, so traversal and subfolder names refuse in one line. Keyboard-native per 05: the section is one Tab stop, arrows walk rows by name, Space toggles the shared QuickLook panel, Backspace removes. File > Add Attachment (shift-cmd-A) comes alive through the same import path. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -2067,6 +2067,39 @@ public final class BoardStore {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Removing an attachment
|
||||
|
||||
/// Moves one of a card's attachments to the **system** Trash — the card window attachment row's
|
||||
/// Remove, its ⌫ twin, and nothing else (05-card-window.md ▸ Attachments).
|
||||
///
|
||||
/// **An ordinary bracketed write, which is the whole point of it being here** rather than a
|
||||
/// `FileManager` call in the view: it mutates the card's folder, so the churn has to round back
|
||||
/// as one *app-mediated* reload (the echo the watcher would otherwise read as a foreign edit),
|
||||
/// it has to refuse under the read-only lock like every other mutation (`performWrite`'s gate),
|
||||
/// and its failures have to reach the banner strip like every other write's. On git boards it
|
||||
/// is also one commit, for free, for the same reason.
|
||||
///
|
||||
/// The guards are `importAttachments`' exactly, and its inverse in every way: **liveness is
|
||||
/// ancestor-walked** (`liveItem`), so a card under a tombstoned lane is as gone as a deleted one
|
||||
/// and its attachments are not removable from a window that is dismissing itself in the same
|
||||
/// breath; a lane id is refused because attachments belong to cards. Which *file* may go is
|
||||
/// `BoardWriter.removeAttachment`'s listing check, and a name that is no longer there is a
|
||||
/// silent no-op rather than a failure — the reload is the authority on what the card has.
|
||||
public func removeAttachment(named name: String, fromCard cardID: ItemID) {
|
||||
guard !name.isEmpty,
|
||||
let item = Self.liveItem(cardID, in: snapshot),
|
||||
let card = item.cardID
|
||||
else { return }
|
||||
|
||||
let folder = rootURL
|
||||
.appendingPathComponent(item.laneID.rawValue, isDirectory: true)
|
||||
.appendingPathComponent(card.rawValue, isDirectory: true)
|
||||
|
||||
try? performWrite { () throws(BoardWriteError) -> Void in
|
||||
_ = try BoardWriter.removeAttachment(named: name, fromCard: folder)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The loose-file carve-out
|
||||
|
||||
/// Moves every loose file the last applied snapshot found beside a card's `index.md` into that
|
||||
|
||||
Reference in New Issue
Block a user