132 lines
4.5 KiB
TypeScript
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.");
|
|
}
|
|
}
|