Add assigned column to task list with name labels, fix user_name null

- TaskRow: Replace tiny avatar-only display with proper assigned column
  showing avatar + name (single assignee) or avatar + "N people" (multi).
  Hidden on mobile, right-aligned, 96px width matching other columns.
- Load options: Chain selectinload(ProjectTaskAssignment.user) so the
  user relationship is available for serialization.
- TaskAssignmentResponse: Add model_validator to resolve user_name from
  eagerly loaded user relationship (same pattern as TaskCommentResponse).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-17 04:14:16 +08:00
parent f42175b3fe
commit 990c660fbf
3 changed files with 36 additions and 11 deletions

View File

@ -44,8 +44,8 @@ def _project_load_options():
selectinload(Project.tasks).selectinload(ProjectTask.comments).selectinload(TaskComment.user), selectinload(Project.tasks).selectinload(ProjectTask.comments).selectinload(TaskComment.user),
selectinload(Project.tasks).selectinload(ProjectTask.subtasks).selectinload(ProjectTask.comments).selectinload(TaskComment.user), selectinload(Project.tasks).selectinload(ProjectTask.subtasks).selectinload(ProjectTask.comments).selectinload(TaskComment.user),
selectinload(Project.tasks).selectinload(ProjectTask.subtasks).selectinload(ProjectTask.subtasks), selectinload(Project.tasks).selectinload(ProjectTask.subtasks).selectinload(ProjectTask.subtasks),
selectinload(Project.tasks).selectinload(ProjectTask.assignments), selectinload(Project.tasks).selectinload(ProjectTask.assignments).selectinload(ProjectTaskAssignment.user),
selectinload(Project.tasks).selectinload(ProjectTask.subtasks).selectinload(ProjectTask.assignments), selectinload(Project.tasks).selectinload(ProjectTask.subtasks).selectinload(ProjectTask.assignments).selectinload(ProjectTaskAssignment.user),
selectinload(Project.members), selectinload(Project.members),
] ]
@ -56,8 +56,8 @@ def _task_load_options():
selectinload(ProjectTask.comments).selectinload(TaskComment.user), selectinload(ProjectTask.comments).selectinload(TaskComment.user),
selectinload(ProjectTask.subtasks).selectinload(ProjectTask.comments), selectinload(ProjectTask.subtasks).selectinload(ProjectTask.comments),
selectinload(ProjectTask.subtasks).selectinload(ProjectTask.subtasks), selectinload(ProjectTask.subtasks).selectinload(ProjectTask.subtasks),
selectinload(ProjectTask.assignments), selectinload(ProjectTask.assignments).selectinload(ProjectTaskAssignment.user),
selectinload(ProjectTask.subtasks).selectinload(ProjectTask.assignments), selectinload(ProjectTask.subtasks).selectinload(ProjectTask.assignments).selectinload(ProjectTaskAssignment.user),
] ]

View File

@ -1,4 +1,4 @@
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field, model_validator
from datetime import datetime from datetime import datetime
@ -17,3 +17,19 @@ class TaskAssignmentResponse(BaseModel):
created_at: datetime created_at: datetime
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@model_validator(mode="before")
@classmethod
def resolve_user_name(cls, data): # type: ignore[override]
"""Populate user_name from eagerly loaded user relationship."""
if hasattr(data, "user") and data.user is not None:
if not getattr(data, "user_name", None):
data = dict(
id=data.id,
task_id=data.task_id,
user_id=data.user_id,
assigned_by=data.assigned_by,
user_name=data.user.username,
created_at=data.created_at,
)
return data

View File

@ -135,12 +135,21 @@ export default function TaskRow({
{hasSubtasks ? `${completedSubtasks}/${task.subtasks.length}` : '—'} {hasSubtasks ? `${completedSubtasks}/${task.subtasks.length}` : '—'}
</span> </span>
{/* Assignee avatars */} {/* Assigned column */}
{task.assignments && task.assignments.length > 0 && ( <span className="hidden sm:flex items-center gap-1.5 shrink-0 w-24 justify-end">
<span className="hidden sm:flex shrink-0"> {task.assignments && task.assignments.length > 0 ? (
<AssigneeAvatars assignments={task.assignments} max={3} /> <>
</span> <AssigneeAvatars assignments={task.assignments} max={2} />
)} <span className="text-[11px] text-muted-foreground truncate max-w-[60px]">
{task.assignments.length === 1
? (task.assignments[0].user_name ?? 'Unknown')
: `${task.assignments.length} people`}
</span>
</>
) : (
<span className="text-[11px] text-transparent"></span>
)}
</span>
{/* Mobile-only: compact priority dot + overdue indicator */} {/* Mobile-only: compact priority dot + overdue indicator */}
<div className="flex items-center gap-1.5 sm:hidden shrink-0"> <div className="flex items-center gap-1.5 sm:hidden shrink-0">