Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions backend/app/api/routers/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ async def commit_drafts(
session: Session = Depends(get_db),
) -> TasksCreateResponse:
tasks = task_crud.commit_drafts(task_ids, user_id, session)
session.commit()
return TasksCreateResponse(
task_ids=[t.id for t in tasks if t.id],
created_count=len(tasks),
Expand All @@ -184,10 +183,20 @@ async def update_task(
) -> TaskRead:
user_timezone: str = get_user_timezone(user_id, session)
updated_task = task_crud.update_task(task_id, task_update, user_id, session)
session.commit()
return TaskRead.from_model(updated_task, user_timezone)


@router.post("/deschedule", status_code=status.HTTP_200_OK)
async def deschedule_tasks(
task_ids: TasksDelete = Body(...),
user_id: int = Depends(get_current_user_id),
session: Session = Depends(get_db),
) -> dict[str, Any]:
"""Deschedule tasks by removing scheduled_at timestamp and deleting schedule items."""
task_crud.deschedule_tasks(task_ids.task_ids, user_id, session)
return {"descheduled_count": len(task_ids.task_ids)}


@router.get("/jobs/{job_id}", status_code=status.HTTP_200_OK)
async def get_job_status(
job_id: str,
Expand Down
2 changes: 1 addition & 1 deletion backend/app/crud/schedule_item_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def create_schedule_items_from_blocks(
schedule_items.append(schedule_item)
session.add(schedule_item)

session.commit()
session.flush()
for item in schedule_items:
session.refresh(item)

Expand Down
40 changes: 40 additions & 0 deletions backend/app/crud/task_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,43 @@ def commit_drafts(draft_ids: list[int], user_id: int, session: Session) -> list[
session.add(draft)
session.flush()
return list(drafts)


def deschedule_tasks(task_ids: list[int], user_id: int, session: Session) -> None:
"""
Deschedule tasks by setting scheduled_at to None and deleting their schedule items.

Args:
task_ids: List of task IDs to deschedule
user_id: User ID to ensure ownership
session: Database session
"""
if not task_ids:
return

from app.models.schedule_item import ScheduleItem

tasks = session.exec(
select(Task)
.where(Task.id.in_(task_ids)) # type: ignore[union-attr]
.where(Task.user_id == user_id)
).all()

if not tasks:
return

schedule_items = session.exec(
select(ScheduleItem)
.where(ScheduleItem.task_id.in_(task_ids)) # type: ignore[union-attr]
.where(ScheduleItem.user_id == user_id)
.where(ScheduleItem.source == "task")
).all()

for schedule_item in schedule_items:
session.delete(schedule_item)

for task in tasks:
task.scheduled_at = None
session.add(task)

session.flush()
2 changes: 1 addition & 1 deletion frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function RootLayout({
<body>
<AuthProvider>
{children}
<Toaster position="top-center" />
<Toaster position="bottom-right" />
</AuthProvider>
</body>
</html>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/tasks/CompletedTasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Link from "next/link";
import { useCallback } from "react";
import { useSchedule } from "@/context/schedule-context";
import { useTaskList } from "@/hooks/useTaskLists";
import { Trash } from "lucide-react";

export default function CompletedTasks() {
const { tasks: completedTasks, fetchTasks } = useTaskList("/tasks/completed");
Expand Down Expand Up @@ -42,6 +43,7 @@ export default function CompletedTasks() {
label: "Delete Selected Tasks",
onClick: deleteSelectedTasks,
variant: "destructive",
icon: <Trash className="h-4 w-4" />,
},
]}
/>
Expand Down
32 changes: 27 additions & 5 deletions frontend/src/components/tasks/ScheduledTasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import Link from "next/link";
import { useSchedule } from "@/context/schedule-context";
import { useTaskList } from "@/hooks/useTaskLists";
import { toast } from "sonner";
import { CalendarX, Trash } from "lucide-react";

export default function ScheduledTasks() {
const { tasks: scheduledTasks, fetchTasks } = useTaskList("/tasks/scheduled");
const { deleteTasks, refreshScheduleItems } = useSchedule();
const { deleteTasks, descheduleTasks, refreshScheduleItems } = useSchedule();

const markAsCompleted = useCallback(
async (selectedIndices: Set<number>) => {
Expand Down Expand Up @@ -45,6 +46,21 @@ export default function ScheduledTasks() {
[scheduledTasks, fetchTasks, refreshScheduleItems]
);

const descheduleSelectedTasks = useCallback(
async (selectedIndices: Set<number>) => {
const selectedTasks = scheduledTasks.filter((task, index) =>
selectedIndices.has(index)
);
const taskIds = selectedTasks.map((task) => task.id);
const success = await descheduleTasks(taskIds);
if (success) {
await fetchTasks();
refreshScheduleItems();
}
},
[scheduledTasks, fetchTasks, descheduleTasks, refreshScheduleItems]
);

const deleteSelectedTasks = useCallback(
async (selectedIndices: Set<number>) => {
const selectedTasks = scheduledTasks.filter((task, index) =>
Expand Down Expand Up @@ -74,14 +90,20 @@ export default function ScheduledTasks() {
</>
}
actionButtons={[
{
label: "Mark as Completed",
onClick: markAsCompleted,
},
{
label: "Deschedule",
onClick: descheduleSelectedTasks,
icon: <CalendarX className="h-4 w-4" />,
},
{
label: "Delete Selected Tasks",
onClick: deleteSelectedTasks,
variant: "destructive",
},
{
label: "Mark as Completed",
onClick: markAsCompleted,
icon: <Trash className="h-4 w-4" />,
},
]}
/>
Expand Down
20 changes: 11 additions & 9 deletions frontend/src/components/tasks/TaskDrafts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
import { DraftEditDialog } from "./DraftEditDialog";
import { apiRequest, ApiError } from "@/lib/chrono-client";
import { toast } from "sonner";
import { Pencil } from "lucide-react";
import { Pencil, Trash } from "lucide-react";
import TaskList from "./TaskList";
import { useTaskList } from "@/hooks/useTaskLists";

Expand All @@ -17,16 +17,16 @@ const commitResponseSchema = z.object({
export default function TaskDrafts() {
const { tasks: drafts, fetchTasks } = useTaskList("/tasks/drafts");

async function commitDrafts() {
async function commitDrafts(selectedIndices: Set<number>) {
const selectedDrafts = drafts.filter((draft, index) =>
selectedIndices.has(index)
);
if (drafts.length === 0) {
toast.error("No drafts to commit");
return;
}

const draftIds = drafts
.filter((draft) => draft.committed_at === null)
.map((draft) => draft.id);

const draftIds = selectedDrafts.map((draft) => draft.id);
if (draftIds.length === 0) {
toast.info("All drafts are already committed");
return;
Expand Down Expand Up @@ -90,13 +90,14 @@ export default function TaskDrafts() {
emptyStateDescription="Task drafts will appear here after ingestion"
actionButtons={[
{
label: "Commit All Drafts",
label: "Commit Selected Drafts",
onClick: commitDrafts,
},
{
label: "Delete Selected Tasks",
label: "Delete Selected Drafts",
onClick: deleteSelectedTasks,
variant: "destructive",
icon: <Trash className="h-4 w-4" />,
},
]}
renderEditDialog={(draft, index) => (
Expand All @@ -119,8 +120,9 @@ export default function TaskDrafts() {
isSingleEdit={false}
onUpdate={fetchTasks}
trigger={
<Button variant="ghost" size="icon" className="h-8 w-8">
<Button variant="outline" size="icon" className="flex-1">
<Pencil className="h-4 w-4" />
Bulk Edit
</Button>
}
/>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/tasks/TaskList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface TaskListProps {
| "ghost"
| "link";
className?: string;
icon?: React.ReactNode;
}>;
renderEditDialog?: (task: TaskBase, index: number) => React.ReactNode;
renderBulkEditDialog?: (selectedIndices: Set<number>) => React.ReactNode;
Expand Down Expand Up @@ -132,6 +133,7 @@ export default function TaskList({
className={button.className || "flex-1"}
onClick={() => handleButtonClick(button.onClick)}
>
{button.icon}
{button.label}
</Button>
))}
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/components/tasks/UnscheduledTasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Link from "next/link";
import { useCallback } from "react";
import { useSchedule } from "@/context/schedule-context";
import { useTaskList } from "@/hooks/useTaskLists";
import { CalendarCog, Trash } from "lucide-react";

export default function UnscheduledTasks() {
const { tasks: unscheduledTasks, fetchTasks } =
Expand Down Expand Up @@ -54,14 +55,16 @@ export default function UnscheduledTasks() {
</>
}
actionButtons={[
{
label: "Schedule Selected Tasks",
onClick: handleScheduleTasks,
icon: <CalendarCog className="h-4 w-4" />,
},
{
label: "Delete Selected Tasks",
onClick: deleteSelectedTasks,
variant: "destructive",
},
{
label: "Schedule Selected Tasks",
onClick: handleScheduleTasks,
icon: <Trash className="h-4 w-4" />,
},
]}
/>
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/context/schedule-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { toast } from "sonner";

interface ScheduleContextType {
deleteTasks: (taskIds: number[]) => Promise<boolean>;
descheduleTasks: (taskIds: number[]) => Promise<boolean>;
refreshScheduleItems: () => void;
refreshTrigger: number;
scheduleTasks: (taskIds: number[]) => Promise<ScheduleResponse | null>;
Expand Down Expand Up @@ -88,10 +89,33 @@ export function ScheduleProvider({ children }: { children: React.ReactNode }) {
}
}

async function descheduleTasks(taskIds: number[]): Promise<boolean> {
if (taskIds.length === 0) {
return true;
}

try {
await apiRequest("/tasks/deschedule", z.object({}), {
method: "POST",
body: JSON.stringify({
task_ids: taskIds,
}),
});
refreshScheduleItems();
const taskWord = taskIds.length === 1 ? "task" : "tasks";
toast.success(`Successfully descheduled ${taskIds.length} ${taskWord}`);
return true;
} catch {
toast.error("Failed to deschedule tasks. Please try again.");
return false;
}
}

return (
<ScheduleContext.Provider
value={{
deleteTasks,
descheduleTasks,
scheduleTasks,
refreshScheduleItems,
refreshTrigger,
Expand Down