feat(jobs): complete workspace draft parity
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user