A board leaves as one file and comes back as one — headings are lanes, rows are cards, position is the order

File ▸ Export ▸ writes the frontmost board as Obsidian Kanban Markdown, a
plain Markdown outline, or RFC 4180 CSV; File ▸ Import Board… reads any of
the three back into a fresh board, format detected rather than asked. Every
format encodes order as document position, so an export writes no ranks and
an import mints them in parse order on the ordinary create path. Lossy
exports post a warning-tone loss row naming the comments and attachments the
destination cannot carry. Convert-once: nothing watches, nothing merges.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 08:38:06 -04:00
parent 242d013d8c
commit b0c134a896
15 changed files with 2851 additions and 1 deletions
+29 -1
View File
@@ -3018,6 +3018,27 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
/// never happened. `title` is the board's display name.
case shareBoard(title: String?)
/// File Export the board written out as one foreign document (15-import-export.md;
/// `BoardExporter.write`). Its own case beside the three copy-the-whole-board commands above, on
/// their standing reasoning: a fourth command produces a fourth kind of artifact, and a banner
/// saying the app could not "share" or "duplicate" a board nobody asked to share or duplicate would
/// name a gesture that never happened.
///
/// **It is the one operation in this vocabulary that writes outside a board**, which is also why it
/// says nothing about the board's own files: the export failed, the board is exactly as it was.
/// `title` is the board's display name.
case exportBoard(title: String?)
/// File Import Board a foreign document read, and the fresh board built from it
/// (15-import-export.md; `BoardImporter`). It covers the *read* half only: once the parse succeeds,
/// the tree is built by the ordinary create path and its failures speak as `.createBoard`,
/// `.createLane` and the rest, which is the honest account of what went wrong.
///
/// **It carries a filename rather than a title**, on `.importAttachment`'s precedent and for its
/// reason: the thing the user picked is a file, they are looking at its name in the panel they just
/// dismissed, and the board it was going to become does not exist yet to have a title.
case importBoard(fileName: String)
case importAttachment(filename: String)
case listAttachments
@@ -3231,7 +3252,10 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
// at all, and one with no `schema` is the file the walk just refused.
// `.setBoardBackground` joins them on `.mintBoardIndex`'s reasoning: it carries no title
// slot, and the board it writes to is the one the user is looking at.
case .createBoard, .createLane, .createCard, .importAttachment, .listAttachments,
// `.importBoard` joins them on `.importAttachment`'s reasoning: it carries a filename, which is
// the name the user is looking at and the only one its banner should say and there is no
// document to enrich from anyway, since the failure it describes is the read of one.
case .createBoard, .createLane, .createCard, .importAttachment, .importBoard, .listAttachments,
.removeAttachment, .renumberChildren, .relocateLooseFile, .agentGuide, .seedGitignore,
.mintBoardIndex, .stampSchema, .setBoardBackground,
.displaceClaimedName, .repairDuplicateID, .saveCommentDraft, .postComment,
@@ -3255,6 +3279,7 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
case .duplicateBoard: .duplicateBoard(title: title)
case .saveAsTemplate: .saveAsTemplate(title: title)
case .shareBoard: .shareBoard(title: title)
case .exportBoard: .exportBoard(title: title)
case .toggleTask: .toggleTask(title: title)
case .editBody: .editBody(title: title)
case .rawSource: .rawSource(title: title)
@@ -3299,6 +3324,7 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
// plain container reason its board-level twin does.
case .createBoard, .createLane, .createCard, .move, .copy, .delete, .purge, .migrateTombstone,
.style, .resize, .collapse, .expand, .rename, .duplicateBoard, .saveAsTemplate, .shareBoard, .paste,
.exportBoard, .importBoard,
.importAttachment,
.listAttachments, .removeAttachment, .relocateLooseFile, .agentGuide, .seedGitignore,
.mintBoardIndex, .stampSchema, .setBoardBackground,
@@ -3336,6 +3362,8 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
case let .duplicateBoard(title): Self.phrase("duplicate board", title)
case let .saveAsTemplate(title): Self.phrase("save as template", title)
case let .shareBoard(title): Self.phrase("share board", title)
case let .exportBoard(title): Self.phrase("export board", title)
case let .importBoard(fileName): "import board from '\(fileName)'"
case let .importAttachment(filename): "import attachment '\(filename)'"
case .listAttachments: "list attachments"
case let .removeAttachment(filename): "move attachment '\(filename)' to the Trash"