joplin-plugin-gtd-calendar/src/Gtd/matrixMoveHandler.ts

132 lines
4.5 KiB
TypeScript

const YAML = require("yaml");
import extractGtdBlock from "./gtdBlock";
import resolveScopedFolderIds from "./folderScope";
import { moveMatrixItem } from "./matrixMutation";
import { isMatrixDestination } from "./moveProtocol";
import parseMatrixConfig from "./parseMatrixConfig";
import resolveNotebook from "./resolveNotebook";
import {
DataAdapter,
MoveCardResult,
MoveMatrixCardIntent,
MutationAdapter,
RawNote,
RawTag,
} from "./types";
export interface MatrixMoveHost {
id: string;
parentId: string;
}
class StaleMatrixMoveError extends Error {}
function result(
status: "stale" | "error",
viewInstanceId: string,
message: string
): MoveCardResult {
return { status, viewInstanceId, message };
}
function validId(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
function validateFreshEligibility(
note: RawNote,
tags: RawTag[],
host: MatrixMoveHost,
folderIds: Set<string>,
config: ReturnType<typeof parseMatrixConfig>
): void {
if (note.id === host.id) throw new StaleMatrixMoveError("The dashboard note cannot be moved.");
if (!folderIds.has(note.parent_id)) throw new StaleMatrixMoveError("The card is no longer in scope.");
if (!note.is_todo && config.notes === "none") {
throw new StaleMatrixMoveError("The card is no longer admitted by notes.");
}
const block = extractGtdBlock(note.body);
if (note.is_todo) {
if (config.todos === "none") throw new StaleMatrixMoveError("The card is no longer admitted by todos.");
if (config.todos === "gtd-only" && !block.found) {
throw new StaleMatrixMoveError("The to-do no longer contains a gtd block.");
}
if (note.todo_completed > 0) {
throw new StaleMatrixMoveError("The to-do is now complete and no longer belongs in the matrix.");
}
} else {
if (!block.found) throw new StaleMatrixMoveError("The note no longer contains a gtd block.");
const titles = tags.map((tag) => (tag.title || "").trim().toLowerCase());
if (titles.includes(config.doneTag)) {
throw new StaleMatrixMoveError("The note is now complete and no longer belongs in the matrix.");
}
}
}
export async function validateAndMoveMatrixCard(
intent: unknown,
host: MatrixMoveHost | null,
readAdapter: DataAdapter,
mutationAdapter: MutationAdapter,
logDiagnostic: (error: unknown) => void = () => undefined
): Promise<MoveCardResult> {
const candidate = intent as Partial<MoveMatrixCardIntent> | null;
const viewInstanceId = validId(candidate?.viewInstanceId)
? candidate.viewInstanceId
: "unknown";
const allowedKeys = new Set([
"type", "view", "hostNoteId", "cardId", "destination", "rawConfig", "viewInstanceId",
]);
if (
!candidate || Array.isArray(candidate) ||
Object.keys(candidate).some((key) => !allowedKeys.has(key)) ||
candidate.type !== "moveMatrixCard" || candidate.view !== "matrix" ||
!validId(candidate.hostNoteId) || !validId(candidate.cardId) ||
!validId(candidate.viewInstanceId) || typeof candidate.rawConfig !== "string" ||
!isMatrixDestination(candidate.destination)
) {
return result("error", viewInstanceId, "Invalid matrix move request.");
}
if (!host || candidate.hostNoteId !== host.id) {
return result("stale", viewInstanceId, "The rendered dashboard is no longer current.");
}
let parsed: unknown;
try {
parsed = YAML.parse(candidate.rawConfig || "") || {};
} catch (_parseError) {
return result("stale", viewInstanceId, "The matrix configuration is no longer valid.");
}
const config = parseMatrixConfig(parsed);
if (!config.editable || config.mode !== "eisenhower") {
return result("error", viewInstanceId, "This matrix is read-only.");
}
try {
const folders = await readAdapter.getFolders();
let scanFolderId = host.parentId;
if (!config.scopeAll && config.notebook) {
scanFolderId = resolveNotebook(folders, config.notebook, host.parentId).folderId;
}
const folderIds = new Set(
resolveScopedFolderIds(folders, scanFolderId, config.scopeDepth, config.scopeAll)
);
const outcome = await moveMatrixItem(
mutationAdapter,
candidate.cardId,
candidate.destination,
config,
(note, tags) => validateFreshEligibility(note, tags, host, folderIds, config)
);
return { status: "success", viewInstanceId, changed: outcome.changed };
} catch (moveError) {
if (moveError instanceof StaleMatrixMoveError ||
(moveError instanceof Error && moveError.message === "The card no longer exists.")) {
return result("stale", viewInstanceId, (moveError as Error).message);
}
logDiagnostic(moveError);
return result("error", viewInstanceId, "Could not apply the move. Refresh and try again.");
}
}