fix(ui): preserve application drafts and recovery states

This commit is contained in:
cesnimda
2026-10-13 23:27:16 +02:00
parent 627955afa9
commit 4c3254c472
19 changed files with 5388 additions and 118 deletions
@@ -82,7 +82,9 @@ export function ApplicationWorkspace({
const secondaryId = useId();
const panelRef = useRef<HTMLDivElement>(null);
const [overview, setOverview] = useState<WorkspaceOverview | null>(null);
const [loadedOverview, setOverview] = useState<WorkspaceOverview | null>(null);
const overview = loadedOverview?.id === jobId ? loadedOverview : null;
const overviewRequest = useRef(0);
const [error, setError] = useState<ViewResourceError | null>(null);
const [loading, setLoading] = useState(true);
const [editOpen, setEditOpen] = useState(false);
@@ -123,6 +125,7 @@ export function ApplicationWorkspace({
}, [blocker, confirm, t]);
const load = useCallback(async () => {
const request = ++overviewRequest.current;
if (!Number.isInteger(jobId) || jobId <= 0) {
setOverview(null);
setError({ kind: "error", message: t("workspaceInvalidLink"), retryable: false });
@@ -132,17 +135,20 @@ export function ApplicationWorkspace({
try {
setError(null);
setLoading(true);
setOverview(await applicationWorkspaceApi.overview(jobId));
const next = await applicationWorkspaceApi.overview(jobId);
if (request === overviewRequest.current) setOverview(next);
} catch (err) {
if (request !== overviewRequest.current) return;
const status = (err as { response?: { status?: number } })?.response?.status;
setError({ kind: "error", message: getApiErrorMessage(err, t("workspaceLoadFailed")), retryable: !status || status >= 500, status });
} finally {
setLoading(false);
if (request === overviewRequest.current) setLoading(false);
}
}, [jobId, t]);
useEffect(() => {
load();
void load();
return () => { overviewRequest.current += 1; };
}, [load]);
useEffect(() => {
@@ -253,16 +259,17 @@ export function ApplicationWorkspace({
{subsections.map((sub) => <Tab key={sub.key} value={sub.key} id={`${secondaryId}-${sub.key}`} aria-controls={`${secondaryId}-panel`} label={sub.label} />)}
</Tabs>
<Box role="tabpanel" id={`${secondaryId}-panel`} aria-labelledby={`${secondaryId}-${subsection}`} tabIndex={0} sx={{ display: "grid", gap: 2, minWidth: 0 }}>
{section === "application" && subsection === "cv" && jobId > 0 && <ApplicationCvSection jobId={jobId} includePackages={false} />}
{section === "application" && subsection === "letter" && jobId > 0 && <ApplicationCoverLetterSection jobId={jobId} onDirtyChange={setCoverLetterDirty} />}
{section === "application" && subsection === "answers" && jobId > 0 && <ApplicationPackageDraftsSection
{section === "application" && subsection === "cv" && jobId > 0 && <ApplicationCvSection key={jobId} jobId={jobId} includePackages={false} />}
{section === "application" && subsection === "letter" && jobId > 0 && <ApplicationCoverLetterSection key={jobId} jobId={jobId} onDirtyChange={setCoverLetterDirty} />}
{section === "application" && subsection === "answers" && overview && jobId > 0 && <ApplicationPackageDraftsSection
key={jobId}
jobId={jobId}
initialApplicationAnswer={overview?.applicationAnswerDraft ?? ""}
initialRecruiterMessage={overview?.recruiterMessageDraft ?? ""}
onSaved={load}
onDirtyChange={setPackageDraftsDirty}
/>}
{section === "application" && subsection === "submission" && jobId > 0 && <SubmittedPackagesSection jobId={jobId} />}
{section === "application" && subsection === "submission" && jobId > 0 && <SubmittedPackagesSection key={jobId} jobId={jobId} />}
{section === "timeline" && subsection === "activity" && jobId > 0 && <ApplicationTimeline jobId={jobId} />}
{section === "timeline" && subsection === "correspondence" && jobId > 0 && <PageSection surface title={t("workspaceCommunication")} level="h3"><Correspondence jobId={jobId} jobContext={{ companyName: overview?.company, jobTitle: overview?.jobTitle }} /></PageSection>}
</Box>
@@ -272,7 +279,7 @@ export function ApplicationWorkspace({
<Button onClick={() => go("application", "cv")}>{t("workspaceCv")}</Button>
<Button onClick={() => go("application", "letter")}>{t("workspaceCoverLetter")}</Button>
</>}><Attachments jobId={jobId} /></PageSection>
<SubmittedPackagesSection jobId={jobId} allowCapture={false} />
<SubmittedPackagesSection key={jobId} jobId={jobId} allowCapture={false} />
</>}
{section === "tasks" && jobId > 0 && <>
<PageSection surface title={t("workspaceNotes")} actions={<Button startIcon={<EditOutlinedIcon />} onClick={() => setEditOpen(true)}>{t("workspaceEdit")}</Button>}>