mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* 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>
212 lines
6.8 KiB
TypeScript
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}
|
|
</>
|
|
);
|
|
}
|