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 [draftSource, setDraftSource] = useState(task.source);
|
||||||
const [draftProjectId, setDraftProjectId] = useState(task.projectId || "");
|
const [draftProjectId, setDraftProjectId] = useState(task.projectId || "");
|
||||||
const [draftDueDate, setDraftDueDate] = useState(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "");
|
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 [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [newSubtaskTitle, setNewSubtaskTitle] = useState("");
|
const [newSubtaskTitle, setNewSubtaskTitle] = useState("");
|
||||||
const [addingSubtask, setAddingSubtask] = useState(false);
|
const [addingSubtask, setAddingSubtask] = useState(false);
|
||||||
@@ -284,7 +285,8 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
|
|||||||
setDraftSource(task.source);
|
setDraftSource(task.source);
|
||||||
setDraftProjectId(task.projectId || "");
|
setDraftProjectId(task.projectId || "");
|
||||||
setDraftDueDate(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "");
|
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
|
// Detect if any field has been modified
|
||||||
const currentDueDate = task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "";
|
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 ||
|
draftPriority !== task.priority ||
|
||||||
draftSource !== task.source ||
|
draftSource !== task.source ||
|
||||||
draftProjectId !== (task.projectId || "") ||
|
draftProjectId !== (task.projectId || "") ||
|
||||||
draftDueDate !== currentDueDate;
|
draftDueDate !== currentDueDate ||
|
||||||
|
draftAssigneeName !== (task.assigneeName || "");
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setDraftTitle(task.title);
|
setDraftTitle(task.title);
|
||||||
@@ -303,6 +306,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
|
|||||||
setDraftSource(task.source);
|
setDraftSource(task.source);
|
||||||
setDraftProjectId(task.projectId || "");
|
setDraftProjectId(task.projectId || "");
|
||||||
setDraftDueDate(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "");
|
setDraftDueDate(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "");
|
||||||
|
setDraftAssigneeName(task.assigneeName || "");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -316,6 +320,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
|
|||||||
if (draftSource !== task.source) updates.source = draftSource;
|
if (draftSource !== task.source) updates.source = draftSource;
|
||||||
if (draftProjectId !== (task.projectId || "")) updates.projectId = draftProjectId || null;
|
if (draftProjectId !== (task.projectId || "")) updates.projectId = draftProjectId || null;
|
||||||
if (draftDueDate !== currentDueDate) updates.dueDate = draftDueDate ? new Date(draftDueDate).toISOString() : 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);
|
await updateTask(task.id, updates, token);
|
||||||
onTaskUpdated();
|
onTaskUpdated();
|
||||||
toast("Changes saved", "success");
|
toast("Changes saved", "success");
|
||||||
@@ -502,6 +507,45 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
|
|||||||
</div>
|
</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 */}
|
{/* Due Date */}
|
||||||
<div className="px-4 sm:px-6 py-4 border-b border-gray-100">
|
<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>
|
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Due Date</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user