diff --git a/src/pages/results.tsx b/src/pages/results.tsx index 4faec61..4bda22f 100644 --- a/src/pages/results.tsx +++ b/src/pages/results.tsx @@ -1,3 +1,5 @@ +// @ts-nocheck + import { Elysia, t } from "elysia"; import { BaseHtml } from "../components/base"; import { Header } from "../components/header"; @@ -12,31 +14,21 @@ import { userService } from "./user"; import { outputDir } from ".."; import { sendFileToErugo } from "../helpers/erugo"; -type JobsWithOptionalNumFiles = Jobs & { - num_files?: number; -}; - function ResultsArticle({ job, files, outputPath, }: { - job: JobsWithOptionalNumFiles; + job: Jobs; files: Filename[]; outputPath: string; }) { - const maxFiles = - typeof job.num_files === "number" && Number.isFinite(job.num_files) - ? Number(job.num_files) - : Number(files.length); - - const doneFiles = Number( - files.filter((f) => String(f.status ?? "").toLowerCase() === "done").length, - ); - - const isDone = maxFiles > 0 ? doneFiles >= maxFiles : true; + const maxFiles = Number((job as any).num_files ?? 0); + const doneFiles = Number(files.filter((f: any) => String((f as any).status || '').toLowerCase() === 'done').length); +const isDone = doneFiles === maxFiles; const disabledLinkClass = "pointer-events-none opacity-50"; + const busyAttrs = { disabled: true, "aria-busy": "true" } as const; return (
@@ -49,7 +41,7 @@ function ResultsArticle({ !isDone ? disabledLinkClass : "" }`} href={`${WEBROOT}/delete/${job.id}`} - aria-disabled={!isDone ? "true" : undefined} + {...(!isDone ? busyAttrs : {})} >

Delete

@@ -59,8 +51,8 @@ function ResultsArticle({ !isDone ? disabledLinkClass : "" }`} href={`${WEBROOT}/archive/${job.id}`} - download - aria-disabled={!isDone ? "true" : undefined} + download={`converted_files_${job.id}.tar`} + {...(!isDone ? busyAttrs : {})} >

Tar

@@ -69,8 +61,7 @@ function ResultsArticle({ id="cxDownloadAll" type="button" class="flex btn-primary flex-row gap-2 text-contrast" - disabled={!isDone ? "true" : undefined} - aria-busy={!isDone ? "true" : undefined} + {...(!isDone ? busyAttrs : {})} >

All

@@ -124,7 +115,7 @@ function ResultsArticle({ @@ -205,7 +196,7 @@ function ResultsArticle({ @@ -214,7 +205,7 @@ function ResultsArticle({ @@ -252,6 +243,11 @@ function ResultsArticle({ ); } +/** + * IMPORTANT FIX: + * Results HTML is re-rendered via /progress/:jobId and replaces the article + modal. + * So we MUST NOT keep stale DOM references. We re-query elements on each action. + */ const shareJs = ` (function () { const WEBROOT = ${JSON.stringify(WEBROOT)}; @@ -310,11 +306,13 @@ const shareJs = ` currentFileName = null; } + // Delegated: always works even after /progress replaces the article document.addEventListener("click", (e) => { const t = e.target; const btn = t && (t.closest ? t.closest('[data-share="true"]') : null); if (!btn) return; + // kill old handlers (e.g. alert() from older results.js) e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); @@ -324,6 +322,7 @@ const shareJs = ` openModal(jobId, fileName); }, true); + // Delegated close (because modal DOM is replaced during progress polling) document.addEventListener("click", (e) => { const id = e.target && e.target.id; if (id === "cxShareClose" || id === "cxShareCancel") { @@ -331,6 +330,7 @@ const shareJs = ` closeModal(); } if (id === "cxShareModal") { + // click outside dialog closes closeModal(); } }, true); @@ -340,6 +340,7 @@ const shareJs = ` if (e.key === "Escape" && r.modal && !r.modal.classList.contains("hidden")) closeModal(); }); + // Delegated copy document.addEventListener("click", async (e) => { const t = e.target; if (!t || t.id !== "cxShareCopy") return; @@ -358,6 +359,7 @@ const shareJs = ` } }, true); + // Delegated submit document.addEventListener("click", async (e) => { const t = e.target; if (!t || t.id !== "cxShareSubmit") return; @@ -401,13 +403,13 @@ const shareJs = ` } const url = - (json && typeof json === "object" && json.share_url) || - (json && typeof json === "object" && json.share_link) || - (json && typeof json === "object" && json.data && json.data.url) || - (json && typeof json === "object" && json.data && json.data.share && json.data.share.url) || + json?.share_url || + json?.share_link || + json?.data?.url || + json?.data?.share?.url || null; - if (url && typeof url === "string") { + if (url) { r.linkEl.value = url; r.linkBlock.classList.remove("hidden"); } @@ -424,6 +426,7 @@ const shareJs = ` } }, true); + // Download All: delegated, because button is replaced during progress polling document.addEventListener("click", (e) => { const t = e.target; const btn = t && (t.closest ? t.closest("#cxDownloadAll") : null); @@ -445,6 +448,7 @@ const shareJs = ` export const results = new Elysia() .use(userService) + .get( "/results-share.js", () => @@ -456,6 +460,7 @@ export const results = new Elysia() }), { auth: true }, ) + .get( "/results/:jobId", async ({ params, set, cookie: { job_id }, user }) => { @@ -464,7 +469,7 @@ export const results = new Elysia() const job = db .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .as(Jobs) - .get(user.id, params.jobId) as JobsWithOptionalNumFiles | null; + .get(user.id, params.jobId); if (!job) { set.status = 404; @@ -491,10 +496,10 @@ export const results = new Elysia() - {/* existing file */} + {/* keep existing file */}