Skip to content

Commit 3574db8

Browse files
authored
Merge pull request #59 from nannany/copilot/fix-order-issue-on-start
startボタン押下時にタスクを自動再ソート
2 parents 6d42575 + 83c8214 commit 3574db8

File tree

2 files changed

+277
-2
lines changed

2 files changed

+277
-2
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import { describe, it, expect } from "vitest";
2+
import { taskReducer } from "./taskReducer";
3+
import { Task } from "@/types/task";
4+
5+
describe("taskReducer", () => {
6+
const mockTask1: Task = {
7+
id: "1",
8+
user_id: "user1",
9+
title: "Task 1",
10+
description: "",
11+
estimated_minute: 30,
12+
task_order: 1,
13+
start_time: null,
14+
end_time: null,
15+
category_id: null,
16+
created_at: "2024-01-01T00:00:00Z",
17+
task_date: "2024-01-01",
18+
};
19+
20+
const mockTask2: Task = {
21+
id: "2",
22+
user_id: "user1",
23+
title: "Task 2",
24+
description: "",
25+
estimated_minute: 20,
26+
task_order: 2,
27+
start_time: null,
28+
end_time: null,
29+
category_id: null,
30+
created_at: "2024-01-01T00:00:00Z",
31+
task_date: "2024-01-01",
32+
};
33+
34+
const mockTask3: Task = {
35+
id: "3",
36+
user_id: "user1",
37+
title: "Task 3",
38+
description: "",
39+
estimated_minute: 15,
40+
task_order: 3,
41+
start_time: null,
42+
end_time: null,
43+
category_id: null,
44+
created_at: "2024-01-01T00:00:00Z",
45+
task_date: "2024-01-01",
46+
};
47+
48+
describe("UPDATE_TASK with start_time", () => {
49+
it("should re-sort tasks when a task's start_time is updated", () => {
50+
const initialState = [mockTask1, mockTask2, mockTask3];
51+
52+
// Task2のstart_timeを設定
53+
const action = {
54+
type: "UPDATE_TASK" as const,
55+
payload: {
56+
id: "2",
57+
start_time: "2024-01-01T10:00:00Z",
58+
},
59+
};
60+
61+
const newState = taskReducer(initialState, action);
62+
63+
// Task2がstart_timeを持つため、最後に移動するはず
64+
expect(newState[0].id).toBe("1");
65+
expect(newState[1].id).toBe("3");
66+
expect(newState[2].id).toBe("2");
67+
expect(newState[2].start_time).toBe("2024-01-01T10:00:00Z");
68+
});
69+
70+
it("should re-sort tasks when start_time is set to null", () => {
71+
const task1WithStart = {
72+
...mockTask1,
73+
start_time: "2024-01-01T10:00:00Z",
74+
};
75+
const initialState = [mockTask2, mockTask3, task1WithStart];
76+
77+
// Task1のstart_timeをnullに設定
78+
const action = {
79+
type: "UPDATE_TASK" as const,
80+
payload: {
81+
id: "1",
82+
start_time: null,
83+
},
84+
};
85+
86+
const newState = taskReducer(initialState, action);
87+
88+
// Task1がstart_time: nullになるため、task_orderに基づいて先頭に移動
89+
expect(newState[0].id).toBe("1");
90+
expect(newState[1].id).toBe("2");
91+
expect(newState[2].id).toBe("3");
92+
expect(newState[0].start_time).toBeNull();
93+
});
94+
95+
it("should sort tasks by start_time when multiple tasks have start_time", () => {
96+
const task1WithStart = {
97+
...mockTask1,
98+
start_time: "2024-01-01T12:00:00Z",
99+
};
100+
const task2WithStart = {
101+
...mockTask2,
102+
start_time: "2024-01-01T10:00:00Z",
103+
};
104+
const initialState = [task1WithStart, task2WithStart, mockTask3];
105+
106+
// Task3のstart_timeを設定(最も早い時刻)
107+
const action = {
108+
type: "UPDATE_TASK" as const,
109+
payload: {
110+
id: "3",
111+
start_time: "2024-01-01T09:00:00Z",
112+
},
113+
};
114+
115+
const newState = taskReducer(initialState, action);
116+
117+
// start_timeが早い順にソート: Task3(9:00) -> Task2(10:00) -> Task1(12:00)
118+
expect(newState[0].id).toBe("3");
119+
expect(newState[1].id).toBe("2");
120+
expect(newState[2].id).toBe("1");
121+
});
122+
123+
it("should keep tasks without start_time at the beginning", () => {
124+
const task1WithStart = {
125+
...mockTask1,
126+
start_time: "2024-01-01T10:00:00Z",
127+
};
128+
const initialState = [mockTask2, mockTask3, task1WithStart];
129+
130+
// Task2のstart_timeを設定
131+
const action = {
132+
type: "UPDATE_TASK" as const,
133+
payload: {
134+
id: "2",
135+
start_time: "2024-01-01T11:00:00Z",
136+
},
137+
};
138+
139+
const newState = taskReducer(initialState, action);
140+
141+
// Task3(start_time: null)が先頭、その後start_timeの早い順
142+
expect(newState[0].id).toBe("3");
143+
expect(newState[1].id).toBe("1");
144+
expect(newState[2].id).toBe("2");
145+
});
146+
147+
it("should sort by task_order when start_time is null", () => {
148+
const task1Order5 = { ...mockTask1, task_order: 5 };
149+
const task2Order3 = { ...mockTask2, task_order: 3 };
150+
const task3Order7 = { ...mockTask3, task_order: 7 };
151+
const initialState = [task1Order5, task2Order3, task3Order7];
152+
153+
// Task1のstart_timeを設定
154+
const action = {
155+
type: "UPDATE_TASK" as const,
156+
payload: {
157+
id: "1",
158+
start_time: "2024-01-01T10:00:00Z",
159+
},
160+
};
161+
162+
const newState = taskReducer(initialState, action);
163+
164+
// start_timeがnullのものはtask_orderでソート: Task2(3) -> Task3(7)、その後Task1
165+
expect(newState[0].id).toBe("2");
166+
expect(newState[1].id).toBe("3");
167+
expect(newState[2].id).toBe("1");
168+
});
169+
});
170+
171+
describe("UPDATE_TASK without start_time", () => {
172+
it("should not re-sort tasks when start_time is not updated", () => {
173+
const initialState = [mockTask1, mockTask2, mockTask3];
174+
175+
const action = {
176+
type: "UPDATE_TASK" as const,
177+
payload: {
178+
id: "2",
179+
title: "Updated Task 2",
180+
},
181+
};
182+
183+
const newState = taskReducer(initialState, action);
184+
185+
// 順序は変わらないはず
186+
expect(newState[0].id).toBe("1");
187+
expect(newState[1].id).toBe("2");
188+
expect(newState[2].id).toBe("3");
189+
expect(newState[1].title).toBe("Updated Task 2");
190+
});
191+
});
192+
193+
describe("Other actions", () => {
194+
it("should handle SET_TASKS", () => {
195+
const newState = taskReducer([], {
196+
type: "SET_TASKS",
197+
payload: [mockTask1, mockTask2],
198+
});
199+
200+
expect(newState).toHaveLength(2);
201+
expect(newState[0].id).toBe("1");
202+
expect(newState[1].id).toBe("2");
203+
});
204+
205+
it("should handle ADD_TASK", () => {
206+
const initialState = [mockTask1];
207+
const newState = taskReducer(initialState, {
208+
type: "ADD_TASK",
209+
payload: mockTask2,
210+
});
211+
212+
expect(newState).toHaveLength(2);
213+
expect(newState[0].id).toBe("2"); // 新しいタスクは先頭に追加
214+
expect(newState[1].id).toBe("1");
215+
});
216+
217+
it("should handle DELETE_TASK", () => {
218+
const initialState = [mockTask1, mockTask2, mockTask3];
219+
const newState = taskReducer(initialState, {
220+
type: "DELETE_TASK",
221+
payload: "2",
222+
});
223+
224+
expect(newState).toHaveLength(2);
225+
expect(newState.find((t) => t.id === "2")).toBeUndefined();
226+
});
227+
228+
it("should handle REORDER_TASKS", () => {
229+
const initialState = [mockTask1, mockTask2, mockTask3];
230+
const reorderedTasks = [mockTask3, mockTask1, mockTask2];
231+
232+
const newState = taskReducer(initialState, {
233+
type: "REORDER_TASKS",
234+
payload: reorderedTasks,
235+
});
236+
237+
expect(newState[0].id).toBe("3");
238+
expect(newState[1].id).toBe("1");
239+
expect(newState[2].id).toBe("2");
240+
});
241+
});
242+
});

ui/src/reducers/taskReducer.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,49 @@
11
import { Task, TaskAction } from "@/types/task";
22

3+
// タスクをstart_time、task_orderの順でソート(データベースのクエリと同じロジック)
4+
const sortTasks = (tasks: Task[]): Task[] => {
5+
return [...tasks].sort((a, b) => {
6+
// start_time で比較(nullsFirst: nullが先頭)
7+
if (a.start_time === null && b.start_time === null) {
8+
// 両方nullの場合、task_orderで比較
9+
if (a.task_order === null && b.task_order === null) return 0;
10+
if (a.task_order === null) return -1;
11+
if (b.task_order === null) return 1;
12+
return a.task_order - b.task_order;
13+
}
14+
if (a.start_time === null) return -1;
15+
if (b.start_time === null) return 1;
16+
17+
// start_timeで比較
18+
const timeCompare =
19+
new Date(a.start_time).getTime() - new Date(b.start_time).getTime();
20+
if (timeCompare !== 0) return timeCompare;
21+
22+
// start_timeが同じ場合、task_orderで比較
23+
if (a.task_order === null && b.task_order === null) return 0;
24+
if (a.task_order === null) return -1;
25+
if (b.task_order === null) return 1;
26+
return a.task_order - b.task_order;
27+
});
28+
};
29+
330
// Task Reducer
431
export const taskReducer = (state: Task[], action: TaskAction): Task[] => {
532
switch (action.type) {
633
case "SET_TASKS":
734
return action.payload;
835
case "ADD_TASK":
936
return [action.payload, ...state]; // Adds new task to the beginning
10-
case "UPDATE_TASK":
11-
return state.map((task) =>
37+
case "UPDATE_TASK": {
38+
const updatedTasks = state.map((task) =>
1239
task.id === action.payload.id ? { ...task, ...action.payload } : task,
1340
);
41+
// start_timeが更新された場合、タスクを再ソート
42+
if ("start_time" in action.payload) {
43+
return sortTasks(updatedTasks);
44+
}
45+
return updatedTasks;
46+
}
1447
case "DELETE_TASK":
1548
return state.filter((task) => task.id !== action.payload);
1649
case "REORDER_TASKS":

0 commit comments

Comments
 (0)