feat(jobs): complete workspace draft parity
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
BlockerFunction, useBeforeUnload, useBlocker, useLocation, useNavigate, useParams, useSearchParams,
|
||||
} from "react-router-dom";
|
||||
|
||||
import {
|
||||
Alert, Box, Button, Chip, Divider, IconButton, List, ListItemButton, ListItemText, Paper,
|
||||
@@ -24,10 +26,11 @@ import {
|
||||
ApplicationAnalysis, ApplicationMatch, ApplicationTimeline,
|
||||
} from "../components/ApplicationIntelligence";
|
||||
import {
|
||||
ApplicationCoverLetterSection, ApplicationCvSection,
|
||||
ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection,
|
||||
} from "../components/ApplicationAssets";
|
||||
import { ApplicationInterviewPrep } from "../components/InterviewPrep";
|
||||
import EditJobDialog from "../components/EditJobDialog";
|
||||
import { useConfirm } from "../confirm";
|
||||
import {
|
||||
WORKSPACE_SECTIONS, WorkspaceOverview, WorkspaceSectionKey, applicationWorkspaceApi, workspaceSection,
|
||||
} from "../applicationWorkspace";
|
||||
@@ -60,12 +63,48 @@ export function ApplicationWorkspace({
|
||||
const jobId = jobIdOverride ?? Number(id);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { confirm } = useConfirm();
|
||||
const [params, setParams] = useSearchParams();
|
||||
const section = sectionOverride ?? workspaceSection(params.get("section"));
|
||||
|
||||
const [overview, setOverview] = useState<WorkspaceOverview | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [coverLetterDirty, setCoverLetterDirty] = useState(false);
|
||||
const [packageDraftsDirty, setPackageDraftsDirty] = useState(false);
|
||||
const hasUnsavedChanges = coverLetterDirty || packageDraftsDirty;
|
||||
|
||||
const shouldBlock = useCallback<BlockerFunction>(
|
||||
({ currentLocation, nextLocation }) => hasUnsavedChanges && (
|
||||
currentLocation.pathname !== nextLocation.pathname || currentLocation.search !== nextLocation.search
|
||||
),
|
||||
[hasUnsavedChanges],
|
||||
);
|
||||
const blocker = useBlocker(shouldBlock);
|
||||
|
||||
useBeforeUnload(useCallback((event) => {
|
||||
if (!hasUnsavedChanges) return;
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
}, [hasUnsavedChanges]));
|
||||
|
||||
useEffect(() => {
|
||||
if (blocker.state !== "blocked") return;
|
||||
const blockedNavigation = blocker;
|
||||
let active = true;
|
||||
void confirm({
|
||||
title: "Unsaved application changes",
|
||||
message: "Leaving this section will discard changes that have not been saved.",
|
||||
confirmLabel: "Discard and leave",
|
||||
cancelLabel: "Keep editing",
|
||||
destructive: true,
|
||||
}).then((approved) => {
|
||||
if (!active) return;
|
||||
if (approved) blockedNavigation.proceed();
|
||||
else blockedNavigation.reset();
|
||||
});
|
||||
return () => { active = false; };
|
||||
}, [blocker, confirm]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!Number.isInteger(jobId) || jobId <= 0) {
|
||||
@@ -90,8 +129,13 @@ export function ApplicationWorkspace({
|
||||
else setParams({ section: next }, { replace: true, state: location.state });
|
||||
};
|
||||
const close = onClose ?? (() => {
|
||||
const from = (location.state as { from?: unknown } | null)?.from;
|
||||
navigate(typeof from === "string" && from.startsWith("/") && !from.startsWith("//") ? from : "/jobs", { replace: true });
|
||||
const state = location.state as { from?: unknown; focusJobId?: unknown } | null;
|
||||
const from = state?.from;
|
||||
const focusJobId = typeof state?.focusJobId === "number" ? state.focusJobId : undefined;
|
||||
navigate(
|
||||
typeof from === "string" && from.startsWith("/") && !from.startsWith("//") ? from : "/jobs",
|
||||
{ replace: true, state: focusJobId ? { focusJobId } : undefined },
|
||||
);
|
||||
});
|
||||
|
||||
if (error) {
|
||||
@@ -171,7 +215,14 @@ export function ApplicationWorkspace({
|
||||
{section === "cv" && jobId > 0 && <ApplicationCvSection jobId={jobId} />}
|
||||
{section === "cover-letter" && jobId > 0 && (
|
||||
<>
|
||||
<ApplicationCoverLetterSection jobId={jobId} />
|
||||
<ApplicationCoverLetterSection jobId={jobId} onDirtyChange={setCoverLetterDirty} />
|
||||
<ApplicationPackageDraftsSection
|
||||
jobId={jobId}
|
||||
initialApplicationAnswer={overview?.applicationAnswerDraft ?? ""}
|
||||
initialRecruiterMessage={overview?.recruiterMessageDraft ?? ""}
|
||||
onSaved={load}
|
||||
onDirtyChange={setPackageDraftsDirty}
|
||||
/>
|
||||
{/* Generation stays an explicit user action, below the editor the user owns. */}
|
||||
<Paper sx={{ p: 2, borderRadius: 3 }}><AiWorkspacePanel jobId={jobId} /></Paper>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user