feat(documents): pré-visualização em PDF do rascunho via Chromium

Novo endpoint POST /documents/templates/:id/draft/preview: renderiza o
conteúdo atual (ainda não salvo) do editor com placeholders {{variavel}}
substituídos por [variavel], via Chromium real, sem persistir nada
(sem generation, sem upload S3) — permite validar visualmente o
resultado antes do "Publicar", que é uma ação permanente.

Corrige de passagem um bug real: css_content do template nunca chegava
ao renderPdf na geração real de documento (createGeneration), sendo
descartado silenciosamente.

No frontend, a pré-visualização aparece embutida num iframe na própria
tela (não em nova aba): abrir em nova aba se mostrou não-confiável ao
vivo — a navegação tardia de uma aba pré-aberta pode ser ignorada pelo
navegador quando o gesto do usuário que a autorizou já expirou no tempo
que o Chromium leva pra renderizar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 23:59:02 -03:00
parent 8ab18ababc
commit 553c0d9533
7 changed files with 222 additions and 5 deletions

View File

@@ -208,6 +208,41 @@ export async function downloadGenerationPdf(id: string, accessToken: string, fil
URL.revokeObjectURL(url);
}
/**
* Renders the CURRENT (possibly unsaved) draft content to PDF via Chromium
* — nothing is persisted server-side. Same raw-fetch pattern as
* `downloadGenerationPdf` (binary response, still needs the bearer token).
* Every `{{variavel}}` shows as `[variavel]` in the result (no real
* client/reseller context at this stage).
*
* Returns an object URL for the caller to show inline (`<iframe>`), rather
* than opening a new tab: a Chromium render is slow enough (real
* server-side rendering, not instant) that by the time the blob is ready,
* the browser's "this navigation is tied to a user gesture" grace window
* has often already expired — `window.open()` (even pre-opened and
* navigated later) can then be silently ignored, leaving a blank tab.
* Confirmed live: a synchronous in-memory blob navigates the pre-opened
* tab fine, but the real (few-hundred-ms) round trip through this
* endpoint does not. Showing the PDF in the same page sidesteps the whole
* class of popup-timing issues.
*/
export async function previewDraft(
templateId: string,
input: { plainText: string; cssContent?: string },
accessToken: string,
): Promise<string> {
const response = await fetch(`${API_BASE_URL}/documents/templates/${templateId}/draft/preview`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}` },
body: JSON.stringify({ editorContent: { plainText: input.plainText }, cssContent: input.cssContent }),
});
if (!response.ok) {
throw new Error("Não foi possível gerar a pré-visualização.");
}
const blob = await response.blob();
return URL.createObjectURL(blob);
}
/** Extracts `{{variavel}}` references from a plain-text template body — used to auto-build the generation context form. */
export function extractTemplateVariables(plainText: string): string[] {
const found = new Set<string>();