feat(tasks): pre-fill task status when creating from column header and route new task button

This commit is contained in:
Chneemann 2026-08-13 16:59:25 +02:00
parent dd2a8a5637
commit 3e7d943fe0
No known key found for this signature in database
7 changed files with 49 additions and 18 deletions

View file

@ -86,6 +86,7 @@ export default function Column(props: ColumnProps) {
}`} }`}
> >
<ColumnHeader <ColumnHeader
id={props.id}
title={props.title} title={props.title}
color={props.color} color={props.color}
count={props.count} count={props.count}

View file

@ -1,21 +1,25 @@
/** /**
* @file dashboard/column/ColumnHeader.tsx * @file dashboard/column/ColumnHeader.tsx
* @description Component rendering the column header with title, color indicator, item count, and add button. * @description Component rendering the column header with title, color indicator, item count, and add button with pre-filled status.
*/ */
"use client"; "use client";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { useRouter } from "next/navigation";
import { TaskStatus } from "@/types/task";
/** /**
* Properties for the ColumnHeader component. * Properties for the ColumnHeader component.
* *
* @interface ColumnHeaderProps * @interface ColumnHeaderProps
* @property {TaskStatus} id - The unique identifier/status of the column.
* @property {string} title - The title of the column. * @property {string} title - The title of the column.
* @property {string} [color] - Tailwind CSS color class for the status indicator dot. * @property {string} [color] - Tailwind CSS color class for the status indicator dot.
* @property {number} count - The number of tasks currently inside this column. * @property {number} count - The number of tasks currently inside this column.
*/ */
interface ColumnHeaderProps { interface ColumnHeaderProps {
id: TaskStatus;
title: string; title: string;
color?: string; color?: string;
count: number; count: number;
@ -23,16 +27,19 @@ interface ColumnHeaderProps {
/** /**
* Renders the header section of a column, displaying a color-coded status indicator, * Renders the header section of a column, displaying a color-coded status indicator,
* the column name, the total task count badge, and a button to add new tasks. * the column name, the total task count badge, and a button to add new tasks pre-filled with the column status.
* *
* @param {ColumnHeaderProps} props - The component props. * @param {ColumnHeaderProps} props - The component props.
* @returns {JSX.Element} The rendered column header component. * @returns {JSX.Element} The rendered column header component.
*/ */
export default function ColumnHeader({ export default function ColumnHeader({
id,
title, title,
color = "bg-primary", color = "bg-primary",
count, count,
}: ColumnHeaderProps) { }: ColumnHeaderProps) {
const router = useRouter();
return ( return (
<div className="flex items-center justify-between mb-4 pb-2 border-b border-border/40"> <div className="flex items-center justify-between mb-4 pb-2 border-b border-border/40">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
@ -42,8 +49,12 @@ export default function ColumnHeader({
{count} {count}
</span> </span>
</div> </div>
<button className="text-background hover:text-foreground p-1.5 rounded-lg bg-primary hover:bg-primary-hover transition-all duration-200 cursor-pointer shadow-sm active:scale-95"> <button
<Plus size={14} /> onClick={() => router.push(`/tasks?task=new&status=${id}`)}
className="text-black hover:text-foreground p-1 rounded-lg border border-black hover:border-foreground bg-primary hover:bg-primary-hover transition-all duration-200 cursor-pointer shadow-sm active:scale-90"
aria-label={`Add task to ${title}`}
>
<Plus size={16} />
</button> </button>
</div> </div>
); );

View file

@ -1,11 +1,12 @@
/** /**
* @file dasboard/header/NewTaskButton.tsx * @file dashboard/header/NewTaskButton.tsx
* @description Client component rendering an interactive trigger button for creating new tasks. * @description Client component rendering an interactive trigger button for creating new tasks.
*/ */
"use client"; "use client";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { useRouter } from "next/navigation";
/** /**
* Renders a stylized button component with an icon and active state animations to initiate task creation. * Renders a stylized button component with an icon and active state animations to initiate task creation.
@ -13,10 +14,15 @@ import { Plus } from "lucide-react";
* @returns {JSX.Element} The rendered new task button component. * @returns {JSX.Element} The rendered new task button component.
*/ */
export default function NewTaskButton() { export default function NewTaskButton() {
const router = useRouter();
return ( return (
<button className="inline-flex items-center gap-2 px-3.5 py-2.5 rounded-xl font-medium text-sm text-black hover:text-foreground bg-primary hover:bg-primary-hover active:scale-95 transition-colors duration-200 cursor-pointer"> <button
<Plus size={16} /> onClick={() => router.push("/tasks?task=new")}
New Task className="inline-flex items-center justify-center gap-2 px-3.5 py-2.5 rounded-xl font-medium text-sm text-black hover:text-foreground bg-primary hover:bg-primary-hover border border-black hover:border-foreground active:scale-95 transition-colors duration-200 cursor-pointer"
>
<Plus size={18} />
<span>New Task</span>
</button> </button>
); );
} }

View file

@ -34,11 +34,11 @@ export default function TrashLink() {
return ( return (
<Link <Link
href="/trash" href="/trash"
className="relative group p-2 rounded-xl border border-border hover:border-primary transition-all duration-200" className="relative group p-2 rounded-xl border border-border hover:border-primary transition-all duration-200 active:scale-95 cursor-pointer"
title="View Trash" title="View Trash"
> >
<Trash2 <Trash2
size={16} size={18}
className="text-foreground-muted group-hover:text-primary transition-colors" className="text-foreground-muted group-hover:text-primary transition-colors"
/> />
{count > 0 && ( {count > 0 && (

View file

@ -7,7 +7,7 @@
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { DbTask } from "@/types/task"; import { DbTask, TaskStatus } from "@/types/task";
import { useTaskForm } from "./useTaskForm"; import { useTaskForm } from "./useTaskForm";
import TaskHeader from "./task/TaskHeader"; import TaskHeader from "./task/TaskHeader";
import TaskBasicInfo from "./task/TaskBasicInfo"; import TaskBasicInfo from "./task/TaskBasicInfo";
@ -22,12 +22,15 @@ import TaskDateTime from "./task/TaskDateTime";
* @property {{ id: string; email: string }[]} users - The list of available users who can be assigned to the task. * @property {{ id: string; email: string }[]} users - The list of available users who can be assigned to the task.
* @property {DbTask & { assignees?: { id: string }[] }} [initialData] - Optional initial task data for editing an existing task. * @property {DbTask & { assignees?: { id: string }[] }} [initialData] - Optional initial task data for editing an existing task.
* @property {string} [mode] - Optional mode indicator (e.g., create or edit). * @property {string} [mode] - Optional mode indicator (e.g., create or edit).
* @property {string | null} [serverError] - Optional server-side error message.
* @property {TaskStatus} [defaultStatus] - Optional default status for new tasks.
*/ */
interface TaskFormProps { interface TaskFormProps {
users: { id: string; email: string }[]; users: { id: string; email: string }[];
initialData?: DbTask & { assignees?: { id: string }[] }; initialData?: DbTask & { assignees?: { id: string }[] };
mode?: string; mode?: string;
serverError?: string | null; serverError?: string | null;
defaultStatus?: TaskStatus;
} }
/** /**
@ -37,7 +40,12 @@ interface TaskFormProps {
* @param {TaskFormProps} props - The component props. * @param {TaskFormProps} props - The component props.
* @returns {JSX.Element} The rendered task form component. * @returns {JSX.Element} The rendered task form component.
*/ */
export default function TaskForm({ users, initialData, mode }: TaskFormProps) { export default function TaskForm({
users,
initialData,
mode,
defaultStatus,
}: TaskFormProps) {
const { const {
form, form,
error, error,
@ -46,7 +54,7 @@ export default function TaskForm({ users, initialData, mode }: TaskFormProps) {
updateField, updateField,
handleAssigneeToggle, handleAssigneeToggle,
handleSubmit, handleSubmit,
} = useTaskForm(initialData, mode); } = useTaskForm(initialData, mode, defaultStatus);
return ( return (
<form onSubmit={handleSubmit} className="space-y-6 mx-auto"> <form onSubmit={handleSubmit} className="space-y-6 mx-auto">

View file

@ -10,17 +10,19 @@ import { redirect } from "next/navigation";
import TaskForm from "./TaskForm"; import TaskForm from "./TaskForm";
import { not, eq } from "drizzle-orm"; import { not, eq } from "drizzle-orm";
import { TaskService } from "@/services/task.service"; import { TaskService } from "@/services/task.service";
import { TaskStatus } from "@/types/task";
/** /**
* Properties for the TaskPage component. * Properties for the TaskPage component.
* *
* @interface TaskPageProps * @interface TaskPageProps
* @property {Promise<{ task?: string; id?: string; }>} searchParams - A promise resolving to the search parameters containing mode and task ID. * @property {Promise<{ task?: string; id?: string; status?: string; }>} searchParams - A promise resolving to the search parameters containing mode, task ID, and status.
*/ */
interface TaskPageProps { interface TaskPageProps {
searchParams: Promise<{ searchParams: Promise<{
task?: string; task?: string;
id?: string; id?: string;
status?: string;
}>; }>;
} }
@ -63,6 +65,7 @@ export default async function TaskPage({ searchParams }: TaskPageProps) {
users={users} users={users}
initialData={initialData} initialData={initialData}
mode={params.task} mode={params.task}
defaultStatus={params.status as TaskStatus}
/> />
</div> </div>
); );

View file

@ -11,13 +11,15 @@ import { TaskPriority, TaskStatus, DbTask } from "@/types/task";
* Custom React hook that encapsulates form state management, field updates, assignee toggling, * Custom React hook that encapsulates form state management, field updates, assignee toggling,
* validation rules, and network submission logic for both task creation and editing workflows. * validation rules, and network submission logic for both task creation and editing workflows.
* *
* @param {DbTask & { assignees?: { id: string }[] }} [initialData] - Optional initial task data for editing workflows. * @param {DbTask & { assignees?: { id: string }[] }} [initialData] - Initial task data loaded for editing workflows.
* @param {string} [mode] - Operation mode indicator (e.g., "edit"). * @param {string} [mode] - The current operating mode (e.g., "edit").
* @returns An object containing form state values, error states, and event handler functions. * @param {TaskStatus} [defaultStatus] - The fallback status applied when creating new tasks.
* @returns {Object} An object containing form state values, error states, and event handling functions.
*/ */
export function useTaskForm( export function useTaskForm(
initialData?: DbTask & { assignees?: { id: string }[] }, initialData?: DbTask & { assignees?: { id: string }[] },
mode?: string, mode?: string,
defaultStatus?: TaskStatus,
) { ) {
const router = useRouter(); const router = useRouter();
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
@ -32,7 +34,7 @@ export function useTaskForm(
title: initialData?.title ?? "", title: initialData?.title ?? "",
description: initialData?.description ?? "", description: initialData?.description ?? "",
priority: (initialData?.priority ?? "medium") as TaskPriority, priority: (initialData?.priority ?? "medium") as TaskPriority,
status: (initialData?.status ?? "todo") as TaskStatus, status: (initialData?.status ?? defaultStatus ?? "todo") as TaskStatus,
assignees: initialData?.assignees?.map((a) => a.id) ?? [], assignees: initialData?.assignees?.map((a) => a.id) ?? [],
dueDate: dueDateObj ? dueDateObj.toISOString().split("T")[0] : todayString, dueDate: dueDateObj ? dueDateObj.toISOString().split("T")[0] : todayString,
dueTime: dueDateObj ? dueDateObj.toTimeString().slice(0, 5) : "23:59", dueTime: dueDateObj ? dueDateObj.toTimeString().slice(0, 5) : "23:59",