Files
roboco/panel/src/components/tasks/task-detail/task-description.tsx
T
8f01446243 chore(panel): one disclosure primitive, DialogFooter everywhere, three dialog widths (Wave C-2) (#695)
* feat(panel): promote project settings to a full page

The edit-project dialog carried ~30 fields across 7 concerns in one
flat scroll with a per-tab width swap — outgrown. Project settings now
live at /projects/[id]/settings as a card-per-concern grid (the
settings page's own pattern) with per-card save and Conventions as a
page-level tab at natural width; the list Edit action routes there,
and a slim quick-edit dialog (name/cell/active) replaces the
kitchen-sink.

* chore(panel): one disclosure primitive, DialogFooter everywhere, three dialog widths

collapsible-section moves to ui/ as the single sectioned-disclosure
primitive (task dialogs' raw Collapsible and create-project's ad-hoc
showAdvanced converge onto it); every hand-rolled dialog footer becomes
DialogFooter; dialog widths collapse from ten ad-hoc classes to three
named sizes, with deliberate outliers annotated. No behavioral change.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-24 22:02:17 +02:00

212 lines
6.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { Task } from "@/types";
import { useUpdateTask } from "@/hooks/use-tasks";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Markdown } from "@/components/ui/markdown";
import { HelpTip } from "@/components/ui/help-tip";
import { CollapsibleSection } from "@/components/ui/collapsible-section";
import { Edit3, Eye, Check, X, ShieldAlert } from "lucide-react";
import { toast } from "sonner";
interface TaskDescriptionProps {
task: Task;
}
export function TaskDescription({ task }: TaskDescriptionProps) {
const updateTask = useUpdateTask();
const [isEditing, setIsEditing] = useState(false);
const [localEditValue, setLocalEditValue] = useState("");
const [editMode, setEditMode] = useState<"write" | "preview">("write");
const [sectionOpen, setSectionOpen] = useState(true);
// Display prop value when not editing, local value when editing
const editValue = isEditing ? localEditValue : task.description;
const setEditValue = (value: string) => setLocalEditValue(value);
// Start editing - copy current prop value to local state
const startEditing = () => {
setLocalEditValue(task.description);
setIsEditing(true);
};
const handleCheckboxChange = async (newDescription: string) => {
try {
await updateTask.mutateAsync({
taskId: task.id,
updates: { description: newDescription },
});
} catch {
toast.error("Failed to update task");
}
};
const handleSave = async () => {
if (editValue === task.description) {
setIsEditing(false);
return;
}
try {
await updateTask.mutateAsync({
taskId: task.id,
updates: { description: editValue },
});
setIsEditing(false);
setEditMode("write");
} catch {
toast.error("Failed to update description");
}
};
const handleCancel = () => {
setEditValue(task.description);
setIsEditing(false);
setEditMode("write");
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
handleCancel();
}
// Save with Cmd/Ctrl + Enter
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
handleSave();
}
};
return (
<>
<CollapsibleSection
title="Description"
open={isEditing || sectionOpen}
onOpenChange={setSectionOpen}
actions={
isEditing ? (
<>
<Tabs
value={editMode}
onValueChange={(v) => setEditMode(v as "write" | "preview")}
>
<TabsList className="h-8">
<TabsTrigger value="write" className="text-xs px-2 h-6">
<Edit3 className="h-3 w-3 mr-1" />
Write
</TabsTrigger>
<TabsTrigger value="preview" className="text-xs px-2 h-6">
<Eye className="h-3 w-3 mr-1" />
Preview
</TabsTrigger>
</TabsList>
</Tabs>
<HelpTip label="Discard changes without saving">
<Button
size="sm"
variant="ghost"
onClick={handleCancel}
disabled={updateTask.isPending}
aria-label="Cancel edit"
>
<X className="h-4 w-4" />
</Button>
</HelpTip>
<Button
size="sm"
onClick={handleSave}
disabled={updateTask.isPending}
>
<Check className="h-4 w-4 mr-1" />
Save
</Button>
</>
) : (
<Button size="sm" variant="ghost" onClick={startEditing}>
<Edit3 className="h-4 w-4 mr-1" />
Edit
</Button>
)
}
>
{isEditing ? (
<div className="space-y-2">
{editMode === "write" ? (
<Textarea
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Add a description..."
className="min-h-[200px] font-mono text-sm"
disabled={updateTask.isPending}
autoFocus
/>
) : (
<div className="min-h-[200px] p-3 border rounded-md bg-muted/30">
{editValue ? (
<Markdown>{editValue}</Markdown>
) : (
<p className="text-muted-foreground text-sm italic">
Nothing to preview
</p>
)}
</div>
)}
<p className="text-xs text-muted-foreground">
Markdown supported. Press Ctrl/Cmd + Enter to save, Escape to
cancel.
</p>
</div>
) : task.description ? (
<div
className="cursor-pointer hover:bg-muted/30 rounded-md p-2 -m-2 transition-colors"
onClick={startEditing}
title="Click to edit"
>
<Markdown
onCheckboxChange={handleCheckboxChange}
disabled={updateTask.isPending}
>
{task.description}
</Markdown>
</div>
) : (
<p
className="text-muted-foreground italic cursor-pointer hover:bg-muted/30 rounded-md p-2 -m-2 transition-colors"
onClick={startEditing}
title="Click to add description"
>
No description provided. Click to add one.
</p>
)}
</CollapsibleSection>
{task.constraints ? (
<CollapsibleSection
// Boilerplate identical on every task in the project — always
// starts collapsed.
defaultOpen={false}
title={
<>
<HelpTip label="Read-only architectural rules derived from the project's conventions — auto-attached, not editable here">
<ShieldAlert className="h-4 w-4 text-amber-600 dark:text-amber-400" />
</HelpTip>
<span className="text-amber-800 dark:text-amber-300">
Constraints
</span>
</>
}
className="border-amber-300 bg-amber-50/60 dark:border-amber-800 dark:bg-amber-950/20"
>
<p className="text-xs text-muted-foreground mb-3">
Architectural standard derived from the project conventions
read-only. Applies to every task in this project.
</p>
<Markdown>{task.constraints}</Markdown>
</CollapsibleSection>
) : null}
</>
);
}