Skip to content

Commit 8330885

Browse files
authored
Merge pull request #61 from nannany/copilot/add-copy-checklist-to-clipboard
feat: add markdown checkbox clipboard export for tasks
2 parents 3574db8 + 4c7bfe0 commit 8330885

File tree

3 files changed

+163
-8
lines changed

3 files changed

+163
-8
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, it, expect } from "vitest";
2+
import { formatTasksAsMarkdown } from "./formatTasksAsMarkdown";
3+
import { Task } from "@/types/task";
4+
5+
describe("formatTasksAsMarkdown", () => {
6+
const mockTask1: Task = {
7+
id: "1",
8+
user_id: "user1",
9+
title: "未完了タスク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: "完了タスク",
24+
description: "",
25+
estimated_minute: 20,
26+
task_order: 2,
27+
start_time: "2024-01-01T10:00:00Z",
28+
end_time: "2024-01-01T10:30:00Z",
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: "未完了タスク2",
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+
it("should format tasks as markdown checkboxes", () => {
49+
const result = formatTasksAsMarkdown([mockTask1, mockTask2, mockTask3]);
50+
const expected = `- [ ] 未完了タスク1
51+
- [x] 完了タスク
52+
- [ ] 未完了タスク2`;
53+
expect(result).toBe(expected);
54+
});
55+
56+
it("should return empty string for empty task list", () => {
57+
const result = formatTasksAsMarkdown([]);
58+
expect(result).toBe("");
59+
});
60+
61+
it("should mark tasks with end_time as completed", () => {
62+
const result = formatTasksAsMarkdown([mockTask2]);
63+
expect(result).toBe("- [x] 完了タスク");
64+
});
65+
66+
it("should mark tasks without end_time as incomplete", () => {
67+
const result = formatTasksAsMarkdown([mockTask1]);
68+
expect(result).toBe("- [ ] 未完了タスク1");
69+
});
70+
71+
it("should handle single task", () => {
72+
const result = formatTasksAsMarkdown([mockTask1]);
73+
expect(result).toBe("- [ ] 未完了タスク1");
74+
});
75+
76+
it("should handle tasks with special characters in title", () => {
77+
const specialTask: Task = {
78+
...mockTask1,
79+
title: "タスク [重要] & 必須",
80+
};
81+
const result = formatTasksAsMarkdown([specialTask]);
82+
expect(result).toBe("- [ ] タスク [重要] & 必須");
83+
});
84+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Task } from "@/types/task";
2+
3+
/**
4+
* Format tasks as Markdown checkboxes
5+
* Completed tasks (with end_time) are marked as [x], others as [ ]
6+
*/
7+
export function formatTasksAsMarkdown(tasks: Task[]): string {
8+
if (tasks.length === 0) {
9+
return "";
10+
}
11+
12+
return tasks
13+
.map((task) => {
14+
const isCompleted = task.end_time !== null;
15+
const checkbox = isCompleted ? "[x]" : "[ ]";
16+
return `- ${checkbox} ${task.title}`;
17+
})
18+
.join("\n");
19+
}

ui/src/pages/TaskList/TaskList.tsx

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import React, {
99
} from "react";
1010
import { Button } from "@/components/ui/button";
1111
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
12-
import { Calendar } from "lucide-react";
12+
import { Calendar, Clipboard } from "lucide-react";
1313
import { createClient } from "@/lib/supabase/client";
1414
import { useSupabaseUser } from "@/lib/supabase/hooks/useSupabaseUser";
1515
import { Input } from "@/components/ui/input";
@@ -44,6 +44,7 @@ import { useTaskActions } from "@/hooks/useTaskActions";
4444
import { CurrentTaskFooter } from "@/components/CurrentTaskFooter";
4545
import { TaskContextType } from "@/contexts/TaskContext";
4646
import { TaskProvider } from "@/contexts/TaskProvider";
47+
import { formatTasksAsMarkdown } from "@/lib/formatTasksAsMarkdown";
4748

4849
const supabase = createClient();
4950

@@ -293,6 +294,50 @@ const TaskList = () => {
293294
});
294295
};
295296

297+
// クリップボードにマークダウン形式でコピー
298+
const handleCopyToClipboard = async () => {
299+
if (tasks.length === 0) {
300+
toast({
301+
title: "コピー失敗",
302+
description: "コピーするタスクがありません",
303+
variant: "destructive",
304+
});
305+
return;
306+
}
307+
308+
const markdown = formatTasksAsMarkdown(tasks);
309+
310+
try {
311+
// navigator.clipboardが利用可能かチェック
312+
if (navigator.clipboard && navigator.clipboard.writeText) {
313+
await navigator.clipboard.writeText(markdown);
314+
} else {
315+
// フォールバック: 古いブラウザ向け
316+
const textArea = document.createElement("textarea");
317+
textArea.value = markdown;
318+
textArea.style.position = "fixed";
319+
textArea.style.left = "-999999px";
320+
textArea.style.top = "-999999px";
321+
document.body.appendChild(textArea);
322+
textArea.focus();
323+
textArea.select();
324+
document.execCommand("copy");
325+
textArea.remove();
326+
}
327+
toast({
328+
title: "コピー完了",
329+
description: "タスクをマークダウン形式でコピーしました",
330+
});
331+
} catch (error) {
332+
console.error("Failed to copy to clipboard:", error);
333+
toast({
334+
title: "コピー失敗",
335+
description: "クリップボードへのコピーに失敗しました",
336+
variant: "destructive",
337+
});
338+
}
339+
};
340+
296341
// クイックタスク追加
297342
const handleAddTask = async () => {
298343
if (!newTaskTitle.trim()) {
@@ -499,14 +544,21 @@ const TaskList = () => {
499544
<TaskProvider value={taskContextValue}>
500545
<div className="space-y-6 pb-20">
501546
<div className="flex items-center justify-between">
502-
<div>
547+
<div className="flex items-center gap-2">
548+
<Button
549+
onClick={handleCopyToClipboard}
550+
variant="outline"
551+
size="sm"
552+
className="flex items-center gap-1"
553+
aria-label="タスクをマークダウン形式でクリップボードにコピー"
554+
title="タスク一覧をマークダウンチェックボックス形式でコピーします"
555+
>
556+
<Clipboard className="h-4 w-4" />
557+
マークダウンでコピー
558+
</Button>
503559
{totalEstimatedMinutes > 0 && (
504-
<p className="text-sm text-muted-foreground mt-1">
505-
{totalEstimatedMinutes > 0 && (
506-
<span className="ml-2">
507-
完了予定: {calculateEndTime(totalEstimatedMinutes)}
508-
</span>
509-
)}
560+
<p className="text-sm text-muted-foreground">
561+
完了予定: {calculateEndTime(totalEstimatedMinutes)}
510562
</p>
511563
)}
512564
</div>

0 commit comments

Comments
 (0)