Fix 5 testing bugs: priority badge width, recurring event errors, nested subtask UI, comment timestamps, close button
- Widen priority badge from w-10 to w-14 to fit "medium" text, add "none" case - Guard against null end_datetime in event update validation - Exclude current event from this_and_future DELETE to prevent 404 - Use Python-side datetime.now for comment timestamps (avoids UTC offset) - Hide "Add subtask" button when viewing a subtask (prevents nested nesting) - Add X close button to TaskDetailPanel header on desktop Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6e50089201
commit
bfe97fd749
@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy import Text, Integer, ForeignKey, func
|
from sqlalchemy import Text, Integer, ForeignKey
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship as sa_relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship as sa_relationship
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
@ -12,7 +12,7 @@ class TaskComment(Base):
|
|||||||
Integer, ForeignKey("project_tasks.id", ondelete="CASCADE"), nullable=False, index=True
|
Integer, ForeignKey("project_tasks.id", ondelete="CASCADE"), nullable=False, index=True
|
||||||
)
|
)
|
||||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(default=datetime.now)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
task: Mapped["ProjectTask"] = sa_relationship(back_populates="comments")
|
task: Mapped["ProjectTask"] = sa_relationship(back_populates="comments")
|
||||||
|
|||||||
@ -287,7 +287,7 @@ async def update_event(
|
|||||||
|
|
||||||
start = update_data.get("start_datetime", event.start_datetime)
|
start = update_data.get("start_datetime", event.start_datetime)
|
||||||
end_dt = update_data.get("end_datetime", event.end_datetime)
|
end_dt = update_data.get("end_datetime", event.end_datetime)
|
||||||
if end_dt < start:
|
if end_dt is not None and end_dt < start:
|
||||||
raise HTTPException(status_code=400, detail="End datetime must be after start datetime")
|
raise HTTPException(status_code=400, detail="End datetime must be after start datetime")
|
||||||
|
|
||||||
if scope == "this":
|
if scope == "this":
|
||||||
@ -316,6 +316,7 @@ async def update_event(
|
|||||||
delete(CalendarEvent).where(
|
delete(CalendarEvent).where(
|
||||||
CalendarEvent.parent_event_id == parent_id,
|
CalendarEvent.parent_event_id == parent_id,
|
||||||
CalendarEvent.original_start >= this_original_start,
|
CalendarEvent.original_start >= this_original_start,
|
||||||
|
CalendarEvent.id != event_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -57,10 +57,11 @@ export default function UpcomingWidget({ items, days = 7 }: UpcomingWidgetProps)
|
|||||||
{config.label}
|
{config.label}
|
||||||
</span>
|
</span>
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
'text-[9px] font-semibold px-1.5 py-0.5 rounded shrink-0 w-10 text-center',
|
'text-[9px] font-semibold px-1.5 py-0.5 rounded shrink-0 w-14 text-center',
|
||||||
item.priority === 'high' ? 'bg-red-500/10 text-red-400' :
|
item.priority === 'high' ? 'bg-red-500/10 text-red-400' :
|
||||||
item.priority === 'medium' ? 'bg-yellow-500/10 text-yellow-400' :
|
item.priority === 'medium' ? 'bg-yellow-500/10 text-yellow-400' :
|
||||||
item.priority === 'low' ? 'bg-green-500/10 text-green-400' :
|
item.priority === 'low' ? 'bg-green-500/10 text-green-400' :
|
||||||
|
item.priority === 'none' ? 'bg-gray-500/10 text-gray-400' :
|
||||||
'invisible'
|
'invisible'
|
||||||
)}>
|
)}>
|
||||||
{item.priority || ''}
|
{item.priority || ''}
|
||||||
|
|||||||
@ -575,6 +575,7 @@ export default function ProjectDetail() {
|
|||||||
onEdit={(task) => openTaskForm(task, null)}
|
onEdit={(task) => openTaskForm(task, null)}
|
||||||
onDelete={handleDeleteTask}
|
onDelete={handleDeleteTask}
|
||||||
onAddSubtask={(parentId) => openTaskForm(null, parentId)}
|
onAddSubtask={(parentId) => openTaskForm(null, parentId)}
|
||||||
|
onClose={() => setSelectedTaskId(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -603,6 +604,7 @@ export default function ProjectDetail() {
|
|||||||
onEdit={(task) => openTaskForm(task, null)}
|
onEdit={(task) => openTaskForm(task, null)}
|
||||||
onDelete={handleDeleteTask}
|
onDelete={handleDeleteTask}
|
||||||
onAddSubtask={(parentId) => openTaskForm(null, parentId)}
|
onAddSubtask={(parentId) => openTaskForm(null, parentId)}
|
||||||
|
onClose={() => setSelectedTaskId(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { toast } from 'sonner';
|
|||||||
import { format, formatDistanceToNow, parseISO } from 'date-fns';
|
import { format, formatDistanceToNow, parseISO } from 'date-fns';
|
||||||
import {
|
import {
|
||||||
Pencil, Trash2, Plus, MessageSquare, ClipboardList,
|
Pencil, Trash2, Plus, MessageSquare, ClipboardList,
|
||||||
Calendar, User, Flag, Activity, Send,
|
Calendar, User, Flag, Activity, Send, X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import api, { getErrorMessage } from '@/lib/api';
|
import api, { getErrorMessage } from '@/lib/api';
|
||||||
import type { ProjectTask, TaskComment, Person } from '@/types';
|
import type { ProjectTask, TaskComment, Person } from '@/types';
|
||||||
@ -37,6 +37,7 @@ interface TaskDetailPanelProps {
|
|||||||
onEdit: (task: ProjectTask) => void;
|
onEdit: (task: ProjectTask) => void;
|
||||||
onDelete: (taskId: number) => void;
|
onDelete: (taskId: number) => void;
|
||||||
onAddSubtask: (parentId: number) => void;
|
onAddSubtask: (parentId: number) => void;
|
||||||
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TaskDetailPanel({
|
export default function TaskDetailPanel({
|
||||||
@ -45,6 +46,7 @@ export default function TaskDetailPanel({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onAddSubtask,
|
onAddSubtask,
|
||||||
|
onClose,
|
||||||
}: TaskDetailPanelProps) {
|
}: TaskDetailPanelProps) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [commentText, setCommentText] = useState('');
|
const [commentText, setCommentText] = useState('');
|
||||||
@ -145,6 +147,17 @@ export default function TaskDetailPanel({
|
|||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
{onClose && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7"
|
||||||
|
onClick={onClose}
|
||||||
|
title="Close panel"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -222,6 +235,7 @@ export default function TaskDetailPanel({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
|
{!task.parent_task_id && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -231,6 +245,7 @@ export default function TaskDetailPanel({
|
|||||||
<Plus className="h-3 w-3 mr-1" />
|
<Plus className="h-3 w-3 mr-1" />
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{task.subtasks.length > 0 ? (
|
{task.subtasks.length > 0 ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user