allow editing jd
This commit is contained in:
@@ -3,13 +3,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { ArrowUpDown, Filter, LayoutGrid, Search, Sparkles, Table2, X } from "lucide-react";
|
import { ArrowUpDown, Edit2, Filter, LayoutGrid, Save, Search, Sparkles, Table2, Undo, X } from "lucide-react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import * as api from "../api";
|
||||||
import type { Job, JobStatus, JobSource } from "../../shared/types";
|
import type { Job, JobStatus, JobSource } from "../../shared/types";
|
||||||
import { JobCard } from "./JobCard";
|
import { JobCard } from "./JobCard";
|
||||||
import { JobTable, type JobSort } from "./JobTable";
|
import { JobTable, type JobSort } from "./JobTable";
|
||||||
@@ -205,6 +207,9 @@ export const JobList: React.FC<JobListProps> = ({
|
|||||||
const [batchAction, setBatchAction] = useState<null | "process" | "reject" | "apply">(null);
|
const [batchAction, setBatchAction] = useState<null | "process" | "reject" | "apply">(null);
|
||||||
const [highlightedJobId, setHighlightedJobId] = useState<string | null>(null);
|
const [highlightedJobId, setHighlightedJobId] = useState<string | null>(null);
|
||||||
const [isHighlightVisible, setIsHighlightVisible] = useState(false);
|
const [isHighlightVisible, setIsHighlightVisible] = useState(false);
|
||||||
|
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
||||||
|
const [editedDescription, setEditedDescription] = useState("");
|
||||||
|
const [isSavingDescription, setIsSavingDescription] = useState(false);
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(JOB_LIST_VIEW_STORAGE_KEY);
|
const raw = localStorage.getItem(JOB_LIST_VIEW_STORAGE_KEY);
|
||||||
@@ -314,6 +319,31 @@ export const JobList: React.FC<JobListProps> = ({
|
|||||||
return jd;
|
return jd;
|
||||||
}, [highlightedJob]);
|
}, [highlightedJob]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!highlightedJobId) {
|
||||||
|
setIsEditingDescription(false);
|
||||||
|
setEditedDescription("");
|
||||||
|
} else if (highlightedJob && !isEditingDescription) {
|
||||||
|
setEditedDescription(highlightedJob.jobDescription || "");
|
||||||
|
}
|
||||||
|
}, [highlightedJobId, highlightedJob, isEditingDescription]);
|
||||||
|
|
||||||
|
const handleSaveDescription = async () => {
|
||||||
|
if (!highlightedJobId) return;
|
||||||
|
try {
|
||||||
|
setIsSavingDescription(true);
|
||||||
|
await api.updateJob(highlightedJobId, { jobDescription: editedDescription });
|
||||||
|
toast.success("Job description updated");
|
||||||
|
setIsEditingDescription(false);
|
||||||
|
await onUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Failed to update description";
|
||||||
|
toast.error(message);
|
||||||
|
} finally {
|
||||||
|
setIsSavingDescription(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedJobIds((current) => {
|
setSelectedJobIds((current) => {
|
||||||
const visibleIds = new Set(activeTabJobs.map((job) => job.id));
|
const visibleIds = new Set(activeTabJobs.map((job) => job.id));
|
||||||
@@ -432,16 +462,65 @@ export const JobList: React.FC<JobListProps> = ({
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="space-y-1">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||||
<CardTitle className="text-base">Job description</CardTitle>
|
<div className="space-y-1">
|
||||||
<div className="text-xs text-muted-foreground">Press Esc or click outside to exit highlight.</div>
|
<CardTitle className="text-base">Job description</CardTitle>
|
||||||
|
{!isEditingDescription && (
|
||||||
|
<div className="text-xs text-muted-foreground">Press Esc or click outside to exit highlight.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isEditingDescription ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsEditingDescription(true)}
|
||||||
|
className="h-8 gap-1.5"
|
||||||
|
>
|
||||||
|
<Edit2 className="h-3.5 w-3.5" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditingDescription(false);
|
||||||
|
setEditedDescription(highlightedJob?.jobDescription || "");
|
||||||
|
}}
|
||||||
|
className="h-8 gap-1.5"
|
||||||
|
disabled={isSavingDescription}
|
||||||
|
>
|
||||||
|
<Undo className="h-3.5 w-3.5" />
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSaveDescription}
|
||||||
|
className="h-8 gap-1.5"
|
||||||
|
disabled={isSavingDescription}
|
||||||
|
>
|
||||||
|
<Save className="h-3.5 w-3.5" />
|
||||||
|
{isSavingDescription ? "Saving..." : "Save"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="max-h-[60vh] overflow-auto text-sm text-muted-foreground">
|
<CardContent className="max-h-[60vh] overflow-auto text-sm text-muted-foreground">
|
||||||
<div className="whitespace-pre-wrap leading-relaxed">
|
{isEditingDescription ? (
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
<Textarea
|
||||||
{highlightedJobDescription}
|
value={editedDescription}
|
||||||
</ReactMarkdown>
|
onChange={(e) => setEditedDescription(e.target.value)}
|
||||||
</div>
|
className="min-h-[40vh] font-mono leading-relaxed"
|
||||||
|
placeholder="Enter job description..."
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="whitespace-pre-wrap leading-relaxed">
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{highlightedJobDescription}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<
|
||||||
|
HTMLTextAreaElement,
|
||||||
|
React.ComponentProps<"textarea">
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -103,6 +103,7 @@ apiRouter.get('/jobs/:id', async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
const updateJobSchema = z.object({
|
const updateJobSchema = z.object({
|
||||||
status: z.enum(['discovered', 'processing', 'ready', 'applied', 'rejected', 'expired']).optional(),
|
status: z.enum(['discovered', 'processing', 'ready', 'applied', 'rejected', 'expired']).optional(),
|
||||||
|
jobDescription: z.string().optional(),
|
||||||
suitabilityScore: z.number().min(0).max(100).optional(),
|
suitabilityScore: z.number().min(0).max(100).optional(),
|
||||||
suitabilityReason: z.string().optional(),
|
suitabilityReason: z.string().optional(),
|
||||||
tailoredSummary: z.string().optional(),
|
tailoredSummary: z.string().optional(),
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ export interface CreateJobInput {
|
|||||||
|
|
||||||
export interface UpdateJobInput {
|
export interface UpdateJobInput {
|
||||||
status?: JobStatus;
|
status?: JobStatus;
|
||||||
|
jobDescription?: string;
|
||||||
suitabilityScore?: number;
|
suitabilityScore?: number;
|
||||||
suitabilityReason?: string;
|
suitabilityReason?: string;
|
||||||
tailoredSummary?: string;
|
tailoredSummary?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user