Skip to content

Commit ab600cb

Browse files
committed
Fix(workflows): a run on a remote vault carries each note's comments
On a vault served by znserver, the app plans a workflow run and sends the server the file changes to make; a move arrives there as a delete and a write. The server carries a note's comments on its own renames and moves, but a workflow move left them behind under the old name, detached from the note, the way the desktop applier did before 1cdb0e8: the changes name only notes, and a note's comments live in .zennotes, which a change may not name. The prepared run now also lists its moves, one per path op as it lands, suffix and all, in order. The matching change in ZenNotes/znserver carries each note's comments with its move, refuses a destination where an earlier note's comments still sit, and records what it moved in the desktop's ledger format so undo puts it back. The web bridge and the desktop's remote client pass the prepared run through unchanged, so nothing else moves here. A server from before the field ignores it and moves the Markdown alone; an older app sends no moves and gets what it always got.
1 parent 57acec4 commit ab600cb

2 files changed

Lines changed: 47 additions & 2 deletions

File tree

‎packages/shared-domain/src/workflows/prepare-run.test.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,36 @@ describe('prepareWorkflowRun', () => {
2727
changes: [
2828
{ path: 'inbox/A.md', before: '# A\n', after: null },
2929
{ path: 'archive/A.md', before: null, after: '# A\ndone\n' }
30-
]
30+
],
31+
moves: [{ from: 'inbox/A.md', to: 'archive/A.md' }]
3132
})
3233
})
3334

35+
it('lists the moves as they land, so the server can carry each note\'s comments', async () => {
36+
const files = new Map<string, string>([['inbox/A.md', '# A\n']])
37+
38+
const prepared = await prepareWorkflowRun(
39+
{
40+
workflowId: 'chain',
41+
ops: [
42+
{ kind: 'move', path: 'inbox/A.md', to: 'inbox/Work' },
43+
{ kind: 'rename', path: 'inbox/Work/A.md', to: 'Final' },
44+
{ kind: 'append', path: 'inbox/Work/Final.md', text: 'done' }
45+
]
46+
},
47+
{
48+
read: async (path) => files.get(path) ?? null,
49+
systemFolderDirs: {}
50+
}
51+
)
52+
53+
// In order, each from where the note really is: a text op adds no move.
54+
expect(prepared.moves).toEqual([
55+
{ from: 'inbox/A.md', to: 'inbox/Work/A.md' },
56+
{ from: 'inbox/Work/A.md', to: 'inbox/Work/Final.md' }
57+
])
58+
})
59+
3460
it('refuses a create that would replace an existing note', async () => {
3561
await expect(
3662
prepareWorkflowRun(
@@ -70,6 +96,8 @@ describe('prepareWorkflowRun', () => {
7096
{ path: 'inbox/A.md', before: '# A\n', after: null },
7197
{ path: 'archive/A 2.md', before: null, after: '# A\ndone\n' }
7298
])
99+
// The move names where the note landed, suffix and all.
100+
expect(prepared.moves).toEqual([{ from: 'inbox/A.md', to: 'archive/A 2.md' }])
73101
})
74102

75103
it('rejects malformed operations before reading or preparing files', async () => {

‎packages/shared-domain/src/workflows/prepare-run.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,26 @@ export interface WorkflowRunFileChange {
2121
after: string | null
2222
}
2323

24+
/** One path op as it will land: the note's path before and after. */
25+
export interface WorkflowRunMove {
26+
from: string
27+
to: string
28+
}
29+
2430
export interface PreparedWorkflowRun {
2531
workflowId: string
2632
ops: WorkflowOp[]
2733
applied: number
2834
irreversible: number
2935
changes: WorkflowRunFileChange[]
36+
/**
37+
* The path ops in the order they land. `changes` names only notes, and a
38+
* note's comments live in `.zennotes`, which a change may not name; the
39+
* server carries them along for each move and records them for undo. A
40+
* server from before this field ignores it and moves the Markdown alone, as
41+
* it always did.
42+
*/
43+
moves: WorkflowRunMove[]
3044
}
3145

3246
export interface WorkflowRunSource {
@@ -171,6 +185,7 @@ export async function prepareWorkflowRun(
171185
const live = new Map<string, string | null>()
172186
const journal = new Map<string, { path: string; before: string | null }>()
173187
const redirects = new Map<string, string>()
188+
const moves: WorkflowRunMove[] = []
174189

175190
const read = async (path: string): Promise<string | null> => {
176191
const normalized = normalizeRel(path)
@@ -240,6 +255,7 @@ export async function prepareWorkflowRun(
240255
await touch(destination)
241256
live.set(from, null)
242257
live.set(destination, body)
258+
moves.push({ from, to: destination })
243259
const promised = normalizeRel(promisedPath)
244260
if (destination !== promised) redirects.set(promised, destination)
245261
}
@@ -301,6 +317,7 @@ export async function prepareWorkflowRun(
301317
ops,
302318
applied,
303319
irreversible: ops.filter((op) => IRREVERSIBLE_OP_KINDS.has(op.kind)).length,
304-
changes
320+
changes,
321+
moves
305322
}
306323
}

0 commit comments

Comments
 (0)