From 4169c245c2778e3183d65c227662a7b056cea95b Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sun, 22 Feb 2026 11:58:19 +0800 Subject: [PATCH] Global enhancements: none priority, optional remind_at, required labels, textarea flex, remove color picker - Add "none" priority (grey) to task/todo schemas, types, and all priority color maps - Make remind_at optional on reminders (schema, model, migration 010) - Add required prop to Label component with red asterisk indicator - Add invalid:ring-red-500 to Input, Select, Textarea base classes - Mark mandatory fields with required labels across all forms - Replace fixed textarea rows with min-h + flex-1 for auto-expand - Remove color picker from ProjectForm - Align TaskRow metadata into fixed-width columns Co-Authored-By: Claude Opus 4.6 --- .../versions/010_make_remind_at_nullable.py | 23 +++++++++++ backend/app/models/reminder.py | 2 +- backend/app/schemas/project_task.py | 2 +- backend/app/schemas/reminder.py | 4 +- backend/app/schemas/todo.py | 2 +- .../src/components/calendar/EventForm.tsx | 6 +-- .../src/components/projects/KanbanBoard.tsx | 1 + .../src/components/projects/ProjectDetail.tsx | 2 +- .../src/components/projects/ProjectForm.tsx | 14 +------ .../components/projects/TaskDetailPanel.tsx | 1 + frontend/src/components/projects/TaskForm.tsx | 5 ++- frontend/src/components/projects/TaskRow.tsx | 38 +++++++++---------- .../src/components/reminders/ReminderForm.tsx | 5 +-- frontend/src/components/todos/TodoForm.tsx | 5 ++- frontend/src/components/ui/input.tsx | 2 +- frontend/src/components/ui/label.tsx | 11 ++++-- frontend/src/components/ui/select.tsx | 2 +- frontend/src/components/ui/textarea.tsx | 2 +- frontend/src/types/index.ts | 6 +-- 19 files changed, 76 insertions(+), 57 deletions(-) create mode 100644 backend/alembic/versions/010_make_remind_at_nullable.py diff --git a/backend/alembic/versions/010_make_remind_at_nullable.py b/backend/alembic/versions/010_make_remind_at_nullable.py new file mode 100644 index 0000000..e9de4f2 --- /dev/null +++ b/backend/alembic/versions/010_make_remind_at_nullable.py @@ -0,0 +1,23 @@ +"""make remind_at nullable + +Revision ID: 010 +Revises: 009 +Create Date: 2026-02-22 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "010" +down_revision = "009" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.alter_column("reminders", "remind_at", existing_type=sa.DateTime(), nullable=True) + + +def downgrade() -> None: + op.alter_column("reminders", "remind_at", existing_type=sa.DateTime(), nullable=False) diff --git a/backend/app/models/reminder.py b/backend/app/models/reminder.py index 24162ac..b1fbd71 100644 --- a/backend/app/models/reminder.py +++ b/backend/app/models/reminder.py @@ -11,7 +11,7 @@ class Reminder(Base): id: Mapped[int] = mapped_column(primary_key=True, index=True) title: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) - remind_at: Mapped[datetime] = mapped_column(nullable=False) + remind_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True) is_dismissed: Mapped[bool] = mapped_column(Boolean, default=False) recurrence_rule: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) diff --git a/backend/app/schemas/project_task.py b/backend/app/schemas/project_task.py index 9a407af..4d0cd32 100644 --- a/backend/app/schemas/project_task.py +++ b/backend/app/schemas/project_task.py @@ -4,7 +4,7 @@ from typing import Optional, List, Literal from app.schemas.task_comment import TaskCommentResponse TaskStatus = Literal["pending", "in_progress", "completed"] -TaskPriority = Literal["low", "medium", "high"] +TaskPriority = Literal["none", "low", "medium", "high"] class ProjectTaskCreate(BaseModel): diff --git a/backend/app/schemas/reminder.py b/backend/app/schemas/reminder.py index f34cfdd..b9be26d 100644 --- a/backend/app/schemas/reminder.py +++ b/backend/app/schemas/reminder.py @@ -6,7 +6,7 @@ from typing import Optional class ReminderCreate(BaseModel): title: str description: Optional[str] = None - remind_at: datetime + remind_at: Optional[datetime] = None is_active: bool = True recurrence_rule: Optional[str] = None @@ -24,7 +24,7 @@ class ReminderResponse(BaseModel): id: int title: str description: Optional[str] - remind_at: datetime + remind_at: Optional[datetime] is_active: bool is_dismissed: bool recurrence_rule: Optional[str] diff --git a/backend/app/schemas/todo.py b/backend/app/schemas/todo.py index 9748811..147eb87 100644 --- a/backend/app/schemas/todo.py +++ b/backend/app/schemas/todo.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, ConfigDict from datetime import datetime, date from typing import Optional, Literal -TodoPriority = Literal["low", "medium", "high"] +TodoPriority = Literal["none", "low", "medium", "high"] class TodoCreate(BaseModel): diff --git a/frontend/src/components/calendar/EventForm.tsx b/frontend/src/components/calendar/EventForm.tsx index ea99c8e..c12d9d4 100644 --- a/frontend/src/components/calendar/EventForm.tsx +++ b/frontend/src/components/calendar/EventForm.tsx @@ -214,7 +214,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
- + setFormData({ ...formData, description: e.target.value })} - rows={4} + className="min-h-[80px] flex-1" />
@@ -252,7 +252,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
- + = { + none: 'bg-gray-500/20 text-gray-400', low: 'bg-green-500/20 text-green-400', medium: 'bg-yellow-500/20 text-yellow-400', high: 'bg-red-500/20 text-red-400', diff --git a/frontend/src/components/projects/ProjectDetail.tsx b/frontend/src/components/projects/ProjectDetail.tsx index f89de7f..c13a3e9 100644 --- a/frontend/src/components/projects/ProjectDetail.tsx +++ b/frontend/src/components/projects/ProjectDetail.tsx @@ -53,7 +53,7 @@ const statusLabels: Record = { type SortMode = 'manual' | 'priority' | 'due_date'; type ViewMode = 'list' | 'kanban'; -const PRIORITY_ORDER: Record = { high: 0, medium: 1, low: 2 }; +const PRIORITY_ORDER: Record = { high: 0, medium: 1, low: 2, none: 3 }; function SortableTaskRow({ task, diff --git a/frontend/src/components/projects/ProjectForm.tsx b/frontend/src/components/projects/ProjectForm.tsx index 2d32fa3..0c51849 100644 --- a/frontend/src/components/projects/ProjectForm.tsx +++ b/frontend/src/components/projects/ProjectForm.tsx @@ -28,7 +28,6 @@ export default function ProjectForm({ project, onClose }: ProjectFormProps) { name: project?.name || '', description: project?.description || '', status: project?.status || 'not_started', - color: project?.color || '', due_date: project?.due_date ? project.due_date.slice(0, 10) : '', }); @@ -84,7 +83,7 @@ export default function ProjectForm({ project, onClose }: ProjectFormProps) {
- + setFormData({ ...formData, description: e.target.value })} - rows={4} + className="min-h-[80px] flex-1" />
@@ -128,15 +127,6 @@ export default function ProjectForm({ project, onClose }: ProjectFormProps) {
-
- - setFormData({ ...formData, color: e.target.value })} - /> -
diff --git a/frontend/src/components/projects/TaskDetailPanel.tsx b/frontend/src/components/projects/TaskDetailPanel.tsx index 17f8190..3fe9c62 100644 --- a/frontend/src/components/projects/TaskDetailPanel.tsx +++ b/frontend/src/components/projects/TaskDetailPanel.tsx @@ -26,6 +26,7 @@ const taskStatusLabels: Record = { }; const priorityColors: Record = { + none: 'bg-gray-500/20 text-gray-400', low: 'bg-green-500/20 text-green-400', medium: 'bg-yellow-500/20 text-yellow-400', high: 'bg-red-500/20 text-red-400', diff --git a/frontend/src/components/projects/TaskForm.tsx b/frontend/src/components/projects/TaskForm.tsx index 3a0ea02..1670f15 100644 --- a/frontend/src/components/projects/TaskForm.tsx +++ b/frontend/src/components/projects/TaskForm.tsx @@ -99,7 +99,7 @@ export default function TaskForm({ projectId, task, parentTaskId, onClose }: Tas
- + setFormData({ ...formData, description: e.target.value })} - rows={3} + className="min-h-[80px] flex-1" />
@@ -139,6 +139,7 @@ export default function TaskForm({ projectId, task, parentTaskId, onClose }: Tas value={formData.priority} onChange={(e) => setFormData({ ...formData, priority: e.target.value as ProjectTask['priority'] })} > + diff --git a/frontend/src/components/projects/TaskRow.tsx b/frontend/src/components/projects/TaskRow.tsx index e8f29b8..8f9887d 100644 --- a/frontend/src/components/projects/TaskRow.tsx +++ b/frontend/src/components/projects/TaskRow.tsx @@ -11,6 +11,7 @@ const taskStatusColors: Record = { }; const priorityColors: Record = { + none: 'bg-gray-500/20 text-gray-400', low: 'bg-green-500/20 text-green-400', medium: 'bg-yellow-500/20 text-yellow-400', high: 'bg-red-500/20 text-red-400', @@ -103,35 +104,32 @@ export default function TaskRow({ {task.title} - {/* Status badge */} - + {/* Metadata columns */} + {task.status.replace('_', ' ')} - {/* Priority pill */} {task.priority} - {/* Due date */} - {task.due_date && ( - - {format(parseISO(task.due_date), 'MMM d')} - - )} + + {task.due_date ? format(parseISO(task.due_date), 'MMM d') : '—'} + - {/* Subtask count */} - {hasSubtasks && ( - - {completedSubtasks}/{task.subtasks.length} - - )} + + {hasSubtasks ? `${completedSubtasks}/${task.subtasks.length}` : '—'} +
); } diff --git a/frontend/src/components/reminders/ReminderForm.tsx b/frontend/src/components/reminders/ReminderForm.tsx index 0edf7f4..a06b79d 100644 --- a/frontend/src/components/reminders/ReminderForm.tsx +++ b/frontend/src/components/reminders/ReminderForm.tsx @@ -68,7 +68,7 @@ export default function ReminderForm({ reminder, onClose }: ReminderFormProps) {
- + setFormData({ ...formData, description: e.target.value })} - rows={4} + className="min-h-[80px] flex-1" />
@@ -95,7 +95,6 @@ export default function ReminderForm({ reminder, onClose }: ReminderFormProps) { type="datetime-local" value={formData.remind_at} onChange={(e) => setFormData({ ...formData, remind_at: e.target.value })} - required />
diff --git a/frontend/src/components/todos/TodoForm.tsx b/frontend/src/components/todos/TodoForm.tsx index 166353f..5ef9318 100644 --- a/frontend/src/components/todos/TodoForm.tsx +++ b/frontend/src/components/todos/TodoForm.tsx @@ -70,7 +70,7 @@ export default function TodoForm({ todo, onClose }: TodoFormProps) {
- + setFormData({ ...formData, description: e.target.value })} - rows={4} + className="min-h-[80px] flex-1" />
@@ -97,6 +97,7 @@ export default function TodoForm({ todo, onClose }: TodoFormProps) { value={formData.priority} onChange={(e) => setFormData({ ...formData, priority: e.target.value as any })} > + diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx index 04db187..86ff8bc 100644 --- a/frontend/src/components/ui/input.tsx +++ b/frontend/src/components/ui/input.tsx @@ -9,7 +9,7 @@ const Input = React.forwardRef( {} +export interface LabelProps extends React.LabelHTMLAttributes { + required?: boolean; +} const Label = React.forwardRef( - ({ className, ...props }, ref) => ( + ({ className, required, children, ...props }, ref) => ( ) ); Label.displayName = 'Label'; diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx index 6f771df..4045ece 100644 --- a/frontend/src/components/ui/select.tsx +++ b/frontend/src/components/ui/select.tsx @@ -10,7 +10,7 @@ const Select = React.forwardRef(