feat(jobs): complete workspace draft parity
CI and Deploy / test (pull_request) Successful in 5m4s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 17:34:32 +02:00
parent 3b86ea2da0
commit deed948183
28 changed files with 676 additions and 131 deletions
@@ -243,7 +243,7 @@ I would welcome the chance to talk it through.
Kind regards,
[Your name]`;
export function ApplicationCoverLetterSection({ jobId }: { jobId: number }) {
export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId: number; onDirtyChange?: (dirty: boolean) => void }) {
const { data, error, loading, setData, setError } = useAsset<CoverLetter>(
() => applicationAssetsApi.coverLetter(jobId),
[jobId],
@@ -256,6 +256,11 @@ export function ApplicationCoverLetterSection({ jobId }: { jobId: number }) {
const text = draft ?? data?.text ?? "";
const dirty = draft !== null && draft !== (data?.text ?? "");
useEffect(() => {
onDirtyChange?.(dirty);
return () => onDirtyChange?.(false);
}, [dirty, onDirtyChange]);
const save = async (value: string, source = "manual") => {
setBusy(true);
try {
@@ -367,3 +372,97 @@ export function ApplicationCoverLetterSection({ jobId }: { jobId: number }) {
</Stack>
);
}
// ---------- Application answer and recruiter message ----------
type PackageDrafts = { applicationAnswer: string; recruiterMessage: string };
export function ApplicationPackageDraftsSection({
jobId,
initialApplicationAnswer,
initialRecruiterMessage,
onSaved,
onDirtyChange,
}: {
jobId: number;
initialApplicationAnswer: string;
initialRecruiterMessage: string;
onSaved?: () => void;
onDirtyChange?: (dirty: boolean) => void;
}) {
const initial = { applicationAnswer: initialApplicationAnswer, recruiterMessage: initialRecruiterMessage };
const [saved, setSaved] = useState<PackageDrafts>(initial);
const [draft, setDraft] = useState<PackageDrafts | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (draft === null) setSaved(initial);
// `draft` is deliberately excluded: a parent refresh must never overwrite in-progress edits.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [jobId, initialApplicationAnswer, initialRecruiterMessage]);
const value = draft ?? saved;
const dirty = draft !== null && (
draft.applicationAnswer !== saved.applicationAnswer ||
draft.recruiterMessage !== saved.recruiterMessage
);
useEffect(() => {
onDirtyChange?.(dirty);
return () => onDirtyChange?.(false);
}, [dirty, onDirtyChange]);
const update = (patch: Partial<PackageDrafts>) => setDraft({ ...value, ...patch });
const save = async () => {
setBusy(true);
try {
await applicationAssetsApi.saveApplicationDrafts(jobId, value.applicationAnswer, value.recruiterMessage);
setSaved(value);
setDraft(null);
setError(null);
onSaved?.();
} catch (err) {
setError(getApiErrorMessage(err, "Could not save the application drafts."));
} finally {
setBusy(false);
}
};
return (
<Shell
title="Application answers and recruiter message"
subtitle="Keep reusable application-form answers and a recruiter note with this application. Ordinary job notes stay separate."
loading={false}
error={error}
>
<Stack spacing={2}>
<RichTextField
minRows={6}
label="Application answer"
value={value.applicationAnswer}
disabled={busy}
onChange={(applicationAnswer) => update({ applicationAnswer })}
placeholder="Draft an answer for motivation, suitability, or another application-form question."
/>
<RichTextField
minRows={4}
label="Recruiter message"
value={value.recruiterMessage}
disabled={busy}
onChange={(recruiterMessage) => update({ recruiterMessage })}
placeholder="Draft a concise message to the recruiter or hiring manager."
/>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap alignItems="center">
<Button variant="contained" disabled={busy || !dirty} onClick={save}>
Save application drafts
</Button>
<Button disabled={busy || !dirty} onClick={() => setDraft(null)}>
Discard changes
</Button>
{dirty && <Chip size="small" color="warning" variant="outlined" label="Unsaved changes" />}
</Stack>
</Stack>
</Shell>
);
}