Fix migration/backup partial-failure residue, theme selector fallback, dialog promise orphaning, print table multi-block cells
This commit is contained in:
parent
ed3f3cd84b
commit
a3f366ca60
5 changed files with 47 additions and 4 deletions
|
|
@ -48,7 +48,18 @@ export function ensureRecoveryBackup(bookPath: string): string | null {
|
||||||
const dest = path.join(getRecoveryRoot(), `${bookName}-${stamp}`);
|
const dest = path.join(getRecoveryRoot(), `${bookName}-${stamp}`);
|
||||||
|
|
||||||
fs.mkdirSync(getRecoveryRoot(), { recursive: true });
|
fs.mkdirSync(getRecoveryRoot(), { recursive: true });
|
||||||
fs.cpSync(key, dest, { recursive: true });
|
try {
|
||||||
|
fs.cpSync(key, dest, { recursive: true });
|
||||||
|
} catch (err) {
|
||||||
|
// A half-copied directory is worse than none — it looks like a good
|
||||||
|
// backup. Remove the partial copy, then surface the failure.
|
||||||
|
try {
|
||||||
|
fs.rmSync(dest, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// best-effort cleanup only
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
sessionBackedUp.add(key);
|
sessionBackedUp.add(key);
|
||||||
return dest;
|
return dest;
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,19 @@ export function migrateChapterToNative(
|
||||||
entry.file = newFile;
|
entry.file = newFile;
|
||||||
meta.docFormat = "folio-doc";
|
meta.docFormat = "folio-doc";
|
||||||
meta.docSchemaVersion = CURRENT_DOC_SCHEMA_VERSION;
|
meta.docSchemaVersion = CURRENT_DOC_SCHEMA_VERSION;
|
||||||
saveMeta(bookPath, meta);
|
try {
|
||||||
|
saveMeta(bookPath, meta);
|
||||||
|
} catch (err) {
|
||||||
|
// Meta write failed: roll the orphan chapter JSON back so the book
|
||||||
|
// stays consistent with folio.json (still pointing at the .md). A
|
||||||
|
// later retry simply rewrites it.
|
||||||
|
try {
|
||||||
|
fs.rmSync(path.join(bookPath, newFile), { force: true });
|
||||||
|
} catch {
|
||||||
|
// best-effort cleanup only
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
return { status: "migrated", id, file: newFile };
|
return { status: "migrated", id, file: newFile };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ let currentMode: Mode = "prompt";
|
||||||
let resolver: ((value: string | null) => void) | null = null;
|
let resolver: ((value: string | null) => void) | null = null;
|
||||||
|
|
||||||
function open(mode: Mode, title: string, body: string, value: string): Promise<string | null> {
|
function open(mode: Mode, title: string, body: string, value: string): Promise<string | null> {
|
||||||
|
// If a previous dialog was somehow still open (menu accelerators can open
|
||||||
|
// one while another is visible), settle its promise as cancelled so the
|
||||||
|
// awaiting caller isn't stranded forever.
|
||||||
|
close(null);
|
||||||
currentMode = mode;
|
currentMode = mode;
|
||||||
titleEl.textContent = title;
|
titleEl.textContent = title;
|
||||||
msgEl.textContent = body;
|
msgEl.textContent = body;
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,18 @@ function tableToHtml(node: FolioNode): string {
|
||||||
const cells = (row.content ?? [])
|
const cells = (row.content ?? [])
|
||||||
.map((cell) => {
|
.map((cell) => {
|
||||||
const tag = cell.type === "tableHeader" ? "th" : "td";
|
const tag = cell.type === "tableHeader" ? "th" : "td";
|
||||||
return `<${tag}>${inlineToHtml((cell.content?.[0]?.content ?? []) as FolioNode[])}</${tag}>`;
|
const blocks = cell.content ?? [];
|
||||||
|
// A cell can hold multiple blocks (Tiptap allows multi-paragraph
|
||||||
|
// cells); render them all instead of silently dropping every block
|
||||||
|
// after the first. The common single-paragraph cell keeps its old
|
||||||
|
// bare-inline shape so existing table styling is untouched.
|
||||||
|
let inner: string;
|
||||||
|
if (blocks.length === 1 && blocks[0].type === "paragraph") {
|
||||||
|
inner = inlineToHtml(blocks[0].content);
|
||||||
|
} else {
|
||||||
|
inner = blocks.map(blockToHtml).join("");
|
||||||
|
}
|
||||||
|
return `<${tag}>${inner}</${tag}>`;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
return `<tr>${cells}</tr>`;
|
return `<tr>${cells}</tr>`;
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,14 @@ export function selector(): HTMLSelectElement {
|
||||||
// theme, which replaced it as Folio's default light appearance.
|
// theme, which replaced it as Folio's default light appearance.
|
||||||
const LIGHT = "light";
|
const LIGHT = "light";
|
||||||
|
|
||||||
|
// Themes with a real entry in themes.css + the selector. Anything else that
|
||||||
|
// might linger in saved settings (a removed theme, a typo) falls back to
|
||||||
|
// Light so the <select> never desyncs from its <option> list.
|
||||||
|
const KNOWN = new Set(["light", "dark-mode", "neon", "meridian"]);
|
||||||
|
|
||||||
function normalizeTheme(id: unknown): string {
|
function normalizeTheme(id: unknown): string {
|
||||||
if (typeof id !== "string" || !id || id === "default") return LIGHT;
|
if (typeof id !== "string" || !id || id === "default") return LIGHT;
|
||||||
return id;
|
return KNOWN.has(id) ? id : LIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyTheme(id: string): void {
|
export function applyTheme(id: string): void {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue