Files
backnotprop__plannotator/packages/editor/sourceDocumentClient.ts
Leonardo Reis 6407ef5d97 feat(annotate): manual refresh of rendered HTML from disk (#1232)
Local rendered-HTML annotate sessions get a Refresh action beside Hide tools: the document is re-fetched through /api/doc, the sandboxed viewer remounts, annotations are re-anchored and the ones that no longer match are reported while their comments are kept, and stale diff and share state is reset. Maintainer additions on top of the contributor's work: share-link invalidation no longer keys on the resolver's identity, /api/plan and /api/share-html serve a local root HTML file from its current bytes on both runtimes so a reload does not revert the page under the annotations, the Refresh button keeps keyboard focus via aria-disabled, and the tests were hardened. Verified end to end in a real browser.

Thanks @leoreisdias.

AI-assisted (Claude) under maintainer direction.
2026-08-26 14:53:28 -07:00

88 lines
3.0 KiB
TypeScript

import type { SourceSaveCapability } from '@plannotator/shared/source-save';
type EnabledSourceSaveCapability = Extract<SourceSaveCapability, { enabled: true }>;
export type SourceSaveProbeResult =
| { status: 'ok'; sourceSave: EnabledSourceSaveCapability }
| { status: 'missing' }
| { status: 'unavailable' };
interface SourceDocumentResponse {
markdown?: string;
rawHtml?: string;
filepath?: string;
sourceSave?: SourceSaveCapability;
renderAs?: 'markdown' | 'html';
}
type SourceDocumentFetchResult =
| { status: 'ok'; data: SourceDocumentResponse }
| { status: 'missing' }
| { status: 'unavailable' };
export interface SourceDocumentSnapshot {
markdown: string;
sourceSave: EnabledSourceSaveCapability;
}
export type SourceDocumentSnapshotResult =
| { status: 'ok'; snapshot: SourceDocumentSnapshot }
| { status: 'missing' }
| { status: 'unavailable' };
export interface HtmlDocumentSnapshot {
rawHtml: string;
filepath: string;
}
export type HtmlDocumentSnapshotResult =
| { status: 'ok'; snapshot: HtmlDocumentSnapshot }
| { status: 'missing' }
| { status: 'unavailable' };
async function fetchSourceDocument(path: string): Promise<SourceDocumentFetchResult> {
try {
const res = await fetch(`/api/doc?path=${encodeURIComponent(path)}`);
if (res.status === 404) return { status: 'missing' };
if (!res.ok) return { status: 'unavailable' };
return { status: 'ok', data: await res.json() as SourceDocumentResponse };
} catch {
return { status: 'unavailable' };
}
}
export async function probeSourceSave(path: string): Promise<SourceSaveProbeResult> {
const result = await fetchSourceDocument(path);
if (result.status !== 'ok') return { status: result.status };
const { sourceSave } = result.data;
if (sourceSave?.enabled) return { status: 'ok', sourceSave };
if (sourceSave?.enabled === false && sourceSave.reason === 'missing-file') {
return { status: 'missing' };
}
return { status: 'unavailable' };
}
export async function fetchSourceDocumentSnapshot(path: string): Promise<SourceDocumentSnapshotResult> {
const result = await fetchSourceDocument(path);
if (result.status !== 'ok') return { status: result.status };
const { markdown, renderAs, sourceSave } = result.data;
if (sourceSave?.enabled === false && sourceSave.reason === 'missing-file') {
return { status: 'missing' };
}
if (renderAs === 'html' || typeof markdown !== 'string' || !sourceSave?.enabled) return { status: 'unavailable' };
return { status: 'ok', snapshot: { markdown, sourceSave } };
}
export async function fetchHtmlDocumentSnapshot(path: string): Promise<HtmlDocumentSnapshotResult> {
const result = await fetchSourceDocument(path);
if (result.status !== 'ok') return { status: result.status };
const { filepath, rawHtml, renderAs } = result.data;
if (renderAs !== 'html' || typeof rawHtml !== 'string' || typeof filepath !== 'string') {
return { status: 'unavailable' };
}
return { status: 'ok', snapshot: { rawHtml, filepath } };
}