文件
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

98 行
3.8 KiB
TypeScript

import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { ModelType } from '@joplin/lib/BaseModel';
import Note from '@joplin/lib/models/Note';
import ItemChange from '@joplin/lib/models/ItemChange';
import Logger from '@joplin/utils/Logger';
import { Mode } from '../../../plugins/GotoAnything';
import { GotoAnythingOptions, UiType } from './gotoAnything';
import { parseWhiteboard } from '@joplin/lib/services/whiteboard/parse';
import { serializeWhiteboard } from '@joplin/lib/services/whiteboard/serialize';
import { CanvasNode } from '@joplin/lib/services/whiteboard/jsoncanvas';
import generateId from '@joplin/lib/services/whiteboard/generateId';
const logger = Logger.create('addNoteToWhiteboard');
export const declaration: CommandDeclaration = {
name: 'addNoteToWhiteboard',
label: () => _('Add note to whiteboard...'),
};
// Adds a note (chosen via Goto Anything) as a card on the currently open
// whiteboard. The whiteboard is the currently selected note — the command is
// only enabled when that note contains a jsoncanvas fence.
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext) => {
const targetId = context.state.selectedNoteIds?.[0];
if (!targetId) return;
// Quick gate before opening the picker — if the active note isn't
// a whiteboard, bail out without disturbing the user.
const initial = await Note.load(targetId);
if (!initial) return;
if (!parseWhiteboard(initial.body || '').hasCanvas) {
logger.warn('Active note is not a whiteboard:', targetId);
return;
}
const options: GotoAnythingOptions = { mode: Mode.TitleOnly };
const result = await CommandService.instance().execute('gotoAnything', UiType.ControlledApi, options);
if (!result) return;
if (result.type !== ModelType.Note) {
logger.warn('Selected item is not a note:', result);
return;
}
// Reload the note after the picker resolves: between opening the
// picker and now, the whiteboard editor (or a sync) may have
// written a newer body, and we want to append onto the freshest
// persisted state — not the snapshot we read before the prompt.
const fresh = await Note.load(targetId);
if (!fresh) return;
const parsed = parseWhiteboard(fresh.body || '');
if (!parsed.hasCanvas) {
logger.warn('Active note is no longer a whiteboard:', targetId);
return;
}
// Place the new card near the centre of the existing layout, with a
// small offset for each subsequent add so cards don't stack exactly.
const xs = parsed.canvas.nodes.map(n => n.x + n.width / 2);
const ys = parsed.canvas.nodes.map(n => n.y + n.height / 2);
const cx = xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
const cy = ys.length ? ys.reduce((a, b) => a + b, 0) / ys.length : 0;
const offset = (parsed.canvas.nodes.length % 8) * 24;
const newNode: CanvasNode = {
id: generateId(),
type: 'file',
x: cx - 120 + offset,
y: cy - 80 + offset,
width: 240,
height: 160,
file: `:/${result.item.id}`,
};
const nextCanvas = {
...parsed.canvas,
nodes: [...parsed.canvas.nodes, newNode],
};
const newBody = serializeWhiteboard(fresh.body || '', nextCanvas);
// Pass user_updated_time so the save layer can detect concurrent
// edits (the whiteboard editor's own debounced save, or a sync
// write between Note.load above and Note.save here). Mirrors the
// linked-note write path in FileNode.tsx.
await Note.save(
{
id: targetId,
body: newBody,
...(fresh.user_updated_time ? { user_updated_time: fresh.user_updated_time } : {}),
},
{ changeSource: ItemChange.SOURCE_UNSPECIFIED },
);
},
enabledCondition: 'oneNoteSelected && activeNoteIsWhiteboard && !noteIsReadOnly',
};
};