feat: assignee picker in task detail panel
- Quick-assign buttons for Hammer/Donovan/David - Assignee tracked in save/cancel/dirty detection - Shows custom assignee names if set by API
This commit is contained in:
@@ -258,6 +258,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
|
||||
const [draftSource, setDraftSource] = useState(task.source);
|
||||
const [draftProjectId, setDraftProjectId] = useState(task.projectId || "");
|
||||
const [draftDueDate, setDraftDueDate] = useState(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "");
|
||||
const [draftAssigneeName, setDraftAssigneeName] = useState(task.assigneeName || "");
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [newSubtaskTitle, setNewSubtaskTitle] = useState("");
|
||||
const [addingSubtask, setAddingSubtask] = useState(false);
|
||||
@@ -284,7 +285,8 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
|
||||
setDraftSource(task.source);
|
||||
setDraftProjectId(task.projectId || "");
|
||||
setDraftDueDate(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "");
|
||||
}, [task.id, task.title, task.description, task.priority, task.source, task.projectId, task.dueDate]);
|
||||
setDraftAssigneeName(task.assigneeName || "");
|
||||
}, [task.id, task.title, task.description, task.priority, task.source, task.projectId, task.dueDate, task.assigneeName]);
|
||||
|
||||
// Detect if any field has been modified
|
||||
const currentDueDate = task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "";
|
||||
@@ -294,7 +296,8 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
|
||||
draftPriority !== task.priority ||
|
||||
draftSource !== task.source ||
|
||||
draftProjectId !== (task.projectId || "") ||
|
||||
draftDueDate !== currentDueDate;
|
||||
draftDueDate !== currentDueDate ||
|
||||
draftAssigneeName !== (task.assigneeName || "");
|
||||
|
||||
const handleCancel = () => {
|
||||
setDraftTitle(task.title);
|
||||
@@ -303,6 +306,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
|
||||
setDraftSource(task.source);
|
||||
setDraftProjectId(task.projectId || "");
|
||||
setDraftDueDate(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "");
|
||||
setDraftAssigneeName(task.assigneeName || "");
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -316,6 +320,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
|
||||
if (draftSource !== task.source) updates.source = draftSource;
|
||||
if (draftProjectId !== (task.projectId || "")) updates.projectId = draftProjectId || null;
|
||||
if (draftDueDate !== currentDueDate) updates.dueDate = draftDueDate ? new Date(draftDueDate).toISOString() : null;
|
||||
if (draftAssigneeName !== (task.assigneeName || "")) updates.assigneeName = draftAssigneeName || null;
|
||||
await updateTask(task.id, updates, token);
|
||||
onTaskUpdated();
|
||||
toast("Changes saved", "success");
|
||||
@@ -502,6 +507,45 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Assignee */}
|
||||
<div className="px-4 sm:px-6 py-4 border-b border-gray-100">
|
||||
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Assignee</h3>
|
||||
{hasToken ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{["Hammer", "Donovan", "David"].map((name) => (
|
||||
<button
|
||||
key={name}
|
||||
onClick={() => setDraftAssigneeName(draftAssigneeName === name ? "" : name)}
|
||||
className={`text-xs px-2.5 py-1.5 rounded-lg font-medium transition border ${
|
||||
draftAssigneeName === name
|
||||
? "bg-emerald-500 text-white border-emerald-500"
|
||||
: "bg-white text-gray-600 border-gray-200 hover:border-gray-300 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
👤 {name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{draftAssigneeName && !["Hammer", "Donovan", "David"].includes(draftAssigneeName) && (
|
||||
<span className="text-xs px-2 py-1 bg-emerald-100 text-emerald-700 rounded-full font-medium">
|
||||
👤 {draftAssigneeName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-gray-700">
|
||||
{task.assigneeName ? (
|
||||
<span className="text-xs px-2 py-1 bg-emerald-100 text-emerald-700 rounded-full font-medium">
|
||||
👤 {task.assigneeName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400 italic">Unassigned</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Due Date */}
|
||||
<div className="px-4 sm:px-6 py-4 border-b border-gray-100">
|
||||
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Due Date</h3>
|
||||
|
||||
Reference in New Issue
Block a user