Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f2ea0b6
Initial plan
Copilot Dec 30, 2025
d1d7223
Implement task to event conversion and migrate tasks to dnd-kit
Copilot Dec 30, 2025
e1f9339
Pass dateInView to useEventDNDActions for correct event timing
Copilot Dec 30, 2025
2bc3ae7
Fix DraggableTask tests after migrating to dnd-kit
Copilot Dec 30, 2025
1f89668
Address code review feedback: improve code clarity and documentation
Copilot Dec 30, 2025
4b49359
Address code review feedback: use Schema_Event_Regular type, restore …
Copilot Dec 30, 2025
81ebc37
Use Schema_Event instead of Schema_Event_Regular for return type
Copilot Dec 30, 2025
dc5a0b8
Infer dateInView from URL context and create task mock utility
Copilot Dec 30, 2025
abaed15
Address PR review: move mock utility, fix Task naming, add allday sup…
Copilot Dec 30, 2025
68a730a
Add error handling for task-to-event conversion and move mock utility…
Copilot Dec 30, 2025
a23883e
Fix code review issues: import Task type, fix race condition, prevent…
Copilot Dec 30, 2025
12a1430
Merge remote-tracking branch 'origin/main' into copilot/add-dnd-task-…
Copilot Dec 31, 2025
c89b828
Refactor error handling: remove ref/useEffect, extract logic to teste…
Copilot Jan 2, 2026
5af5d6e
Remove doc comment and replace TODO with explanation of current approach
Copilot Jan 2, 2026
ee85d21
Address code review: restore task.test.util.ts, simplify conditionals…
Copilot Jan 2, 2026
35691f3
Fix missing getSnappedMinutes import in useEventDNDActions
Copilot Jan 2, 2026
2be21b3
Merge remote-tracking branch 'origin/main' into copilot/add-dnd-task-…
Copilot Jan 2, 2026
dbec324
Rename handleTaskToEventConversion to task.util and merge latest from…
Copilot Jan 2, 2026
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
18 changes: 18 additions & 0 deletions packages/core/src/__tests__/helpers/task.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { faker } from "@faker-js/faker";
import { Task } from "@web/common/types/task.types";

/**
* Creates a mock task with optional overrides
* @param overrides - Partial task properties to override defaults
* @returns A complete Task object
*/
export function createMockTask(overrides: Partial<Task> = {}): Task {
return {
id: faker.string.uuid(),
title: faker.lorem.sentence({ min: 3, max: 5 }),
status: "todo",
order: faker.number.int({ min: 0, max: 100 }),
createdAt: faker.date.recent().toISOString(),
...overrides,
};
}
1 change: 1 addition & 0 deletions packages/web/src/common/constants/web.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const ID_GRID_EVENTS_ALLDAY = "allDayEvents";
export const ID_GRID_EVENTS_TIMED = "timedEvents";
export const ID_SOMEDAY_WEEK_COLUMN = "somedayWeekColumn";
export const ID_GRID_MAIN = "mainGrid";
export const ID_DROPPABLE_TASKS = "task-list";
export const ID_REMINDER_INPUT = "reminderInput";
export const ID_MAIN = "mainSection";
export const ID_DATEPICKER_SIDEBAR = "sidebarDatePicker";
Expand Down
64 changes: 62 additions & 2 deletions packages/web/src/common/hooks/useEventDNDActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback } from "react";
import { Active, DragEndEvent, Over, useDndMonitor } from "@dnd-kit/core";
import { Categories_Event } from "@core/types/event.types";
import dayjs from "@core/util/date/dayjs";
import { getUserId } from "@web/auth/auth.util";
import {
ID_GRID_ALLDAY_ROW,
ID_GRID_MAIN,
Expand All @@ -12,12 +13,18 @@ import {
setFloatingReferenceAtCursor,
} from "@web/common/hooks/useOpenAtCursor";
import { useUpdateEvent } from "@web/common/hooks/useUpdateEvent";
import { Task } from "@web/common/types/task.types";
import { Schema_GridEvent } from "@web/common/types/web.event.types";
import { reorderGrid } from "@web/common/utils/dom/grid-organization.util";
import { getCalendarEventElementFromGrid } from "@web/common/utils/event/event.util";
import { selectEventById } from "@web/ducks/events/selectors/event.selectors";
import { createEventSlice } from "@web/ducks/events/slices/event.slice";
import { pendingEventsSlice } from "@web/ducks/events/slices/pending.slice";
import { store } from "@web/store";
import { useAppDispatch } from "@web/store/store.hooks";
import { useDateInView } from "@web/views/Day/hooks/navigation/useDateInView";
import { getSnappedMinutes } from "@web/views/Day/util/agenda/agenda.util";
import { handleTaskToEventConversion } from "@web/views/Day/util/task/task.util";

const shouldSaveImmediately = (_id: string) => {
const storeEvent = selectEventById(store.getState(), _id);
Expand All @@ -36,6 +43,47 @@ const setReference = (_id: string) => {

export function useEventDNDActions() {
const updateEvent = useUpdateEvent();
const dispatch = useAppDispatch();
const dateInView = useDateInView();

const convertTaskToEventOnAgenda = useCallback(
async (
task: Task,
active: Active,
over: Over,
deleteTask: () => void,
isAllDay: boolean = false,
) => {
const userId = await getUserId();
if (!userId) return;

const event = handleTaskToEventConversion(
task,
active,
over,
dateInView,
userId,
isAllDay,
);

if (!event) return;

// Add event to pending events and create optimistically
dispatch(pendingEventsSlice.actions.add(event._id!));
dispatch(createEventSlice.actions.request(event));

// Note: Task deletion happens immediately for simplicity. The event saga
// will remove the optimistic event if creation fails, but the task cannot
// be restored because tasks are not in Redux (only local state + localStorage).
// Proper error handling would require: (1) moving tasks to Redux, (2) storing
// task-to-event mappings, and (3) dispatching deleteTask actions from the saga.
deleteTask();

reorderGrid();
setReference(event._id!);
},
[dispatch, dateInView],
);

const moveTimedAroundMainGridDayView = useCallback(
(event: Schema_GridEvent, active: Active, over: Over) => {
Expand Down Expand Up @@ -126,25 +174,37 @@ export function useEventDNDActions() {
(e: DragEndEvent) => {
const { active, over } = e;
const { data } = active;
const { view, type, event } = data.current ?? {};
const { view, type, event, task, deleteTask } = data.current ?? {};

if (!over?.id || !event) return;
if (!over?.id) return;

const switchCase = `${view}-${type}-to-${over.id}`;

switch (switchCase) {
case `day-task-to-${ID_GRID_MAIN}`:
if (!task || !deleteTask) break;
convertTaskToEventOnAgenda(task, active, over, deleteTask, false);
break;
case `day-task-to-${ID_GRID_ALLDAY_ROW}`:
if (!task || !deleteTask) break;
convertTaskToEventOnAgenda(task, active, over, deleteTask, true);
break;
case `day-${Categories_Event.ALLDAY}-to-${ID_GRID_MAIN}`:
if (!event) break;
moveAllDayToMainGridDayView(event, active, over);
break;
case `day-${Categories_Event.TIMED}-to-${ID_GRID_MAIN}`:
if (!event) break;
moveTimedAroundMainGridDayView(event, active, over);
break;
case `day-${Categories_Event.TIMED}-to-${ID_GRID_ALLDAY_ROW}`:
if (!event) break;
moveTimedToAllDayGridDayView(event);
break;
}
},
[
convertTaskToEventOnAgenda,
moveAllDayToMainGridDayView,
moveTimedAroundMainGridDayView,
moveTimedToAllDayGridDayView,
Expand Down
7 changes: 6 additions & 1 deletion packages/web/src/components/DND/Draggable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ import {
import { CSS } from "@dnd-kit/utilities";
import { useMergeRefs } from "@floating-ui/react";
import { Categories_Event } from "@core/types/event.types";
import { Task } from "@web/common/types/task.types";
import { Schema_GridEvent } from "@web/common/types/web.event.types";

export type DraggableDataType = Categories_Event | "task";

export interface DraggableDNDData {
type: Categories_Event;
type: DraggableDataType;
event: Schema_GridEvent | null;
task: Task | null;
view: "day" | "week" | "now";
deleteTask: (() => void) | null;
}

export interface DNDChildProps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ export const DraggableAllDayAgendaEvent = memo(
data: {
event,
type: Categories_Event.ALLDAY,
task: null,
view: "day",
deleteTask: null,
},
disabled: isDisabled,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ export const DraggableTimedAgendaEvent = memo(
data: {
event: event,
type: Categories_Event.TIMED,
task: null,
view: "day",
deleteTask: null,
},
disabled: isDisabled,
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { act } from "react";
import { DragDropContext, Droppable } from "@hello-pangea/dnd";
import { fireEvent, screen } from "@testing-library/react";
import { render } from "@web/__tests__/__mocks__/mock.render";
import { Task } from "@web/common/types/task.types";
Expand Down Expand Up @@ -52,22 +51,7 @@ const renderDraggableTask = (
tasksProps = defaultTasksProps,
) => {
return act(() =>
render(
<DragDropContext onDragEnd={jest.fn()}>
<Droppable droppableId="test-droppable">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
<DraggableTask
task={task}
index={index}
tasksProps={tasksProps}
/>
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>,
),
render(<DraggableTask task={task} index={index} tasksProps={tasksProps} />),
);
};

Expand Down
Loading