@mindtnv/todoist-cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/api/activity.ts +8 -0
- package/src/api/client.ts +214 -0
- package/src/api/comments.ts +18 -0
- package/src/api/completed.ts +15 -0
- package/src/api/labels.ts +18 -0
- package/src/api/projects.ts +22 -0
- package/src/api/sections.ts +20 -0
- package/src/api/stats.ts +38 -0
- package/src/api/tasks.ts +34 -0
- package/src/api/types.ts +202 -0
- package/src/cli/auth.ts +40 -0
- package/src/cli/commands/task/add.ts +328 -0
- package/src/cli/commands/task/complete.ts +62 -0
- package/src/cli/commands/task/delete.ts +62 -0
- package/src/cli/commands/task/helpers.ts +289 -0
- package/src/cli/commands/task/index.ts +27 -0
- package/src/cli/commands/task/list.ts +151 -0
- package/src/cli/commands/task/move.ts +49 -0
- package/src/cli/commands/task/reopen.ts +43 -0
- package/src/cli/commands/task/show.ts +115 -0
- package/src/cli/commands/task/update.ts +122 -0
- package/src/cli/comment.ts +83 -0
- package/src/cli/completed.ts +87 -0
- package/src/cli/completion.ts +360 -0
- package/src/cli/filter.ts +115 -0
- package/src/cli/index.ts +638 -0
- package/src/cli/label.ts +120 -0
- package/src/cli/log.ts +57 -0
- package/src/cli/matrix.ts +100 -0
- package/src/cli/plugin-loader.ts +38 -0
- package/src/cli/plugin.ts +289 -0
- package/src/cli/project.ts +172 -0
- package/src/cli/review.ts +116 -0
- package/src/cli/section.ts +98 -0
- package/src/cli/stats.ts +62 -0
- package/src/cli/template.ts +89 -0
- package/src/config/index.ts +229 -0
- package/src/plugins/api-proxy.ts +70 -0
- package/src/plugins/extension-registry.ts +53 -0
- package/src/plugins/hook-registry.ts +36 -0
- package/src/plugins/loader.ts +200 -0
- package/src/plugins/marketplace-types.ts +55 -0
- package/src/plugins/marketplace.ts +576 -0
- package/src/plugins/palette-registry.ts +21 -0
- package/src/plugins/storage.ts +101 -0
- package/src/plugins/types.ts +226 -0
- package/src/plugins/view-registry.ts +19 -0
- package/src/ui/App.tsx +234 -0
- package/src/ui/components/Breadcrumb.tsx +18 -0
- package/src/ui/components/CommandPalette.tsx +237 -0
- package/src/ui/components/ConfirmDialog.tsx +28 -0
- package/src/ui/components/EditTaskModal.tsx +484 -0
- package/src/ui/components/HelpOverlay.tsx +195 -0
- package/src/ui/components/InputPrompt.tsx +109 -0
- package/src/ui/components/LabelPicker.tsx +110 -0
- package/src/ui/components/ModalManager.tsx +275 -0
- package/src/ui/components/ProjectPicker.tsx +95 -0
- package/src/ui/components/Sidebar.tsx +282 -0
- package/src/ui/components/SortMenu.tsx +77 -0
- package/src/ui/components/StatusBar.tsx +67 -0
- package/src/ui/components/TaskList.tsx +258 -0
- package/src/ui/components/TaskRow.tsx +105 -0
- package/src/ui/hooks/useKeyboardHandler.ts +291 -0
- package/src/ui/hooks/useStatusMessage.ts +32 -0
- package/src/ui/hooks/useTaskOperations.ts +558 -0
- package/src/ui/hooks/useUndoSystem.ts +218 -0
- package/src/ui/views/ActivityView.tsx +213 -0
- package/src/ui/views/CompletedView.tsx +337 -0
- package/src/ui/views/StatsView.tsx +178 -0
- package/src/ui/views/TaskDetailView.tsx +438 -0
- package/src/ui/views/TasksView.tsx +851 -0
- package/src/utils/colors.ts +27 -0
- package/src/utils/date-format.ts +54 -0
- package/src/utils/errors.ts +159 -0
- package/src/utils/exit.ts +11 -0
- package/src/utils/format.ts +46 -0
- package/src/utils/open-url.ts +9 -0
- package/src/utils/output.ts +29 -0
- package/src/utils/quick-add.ts +202 -0
- package/src/utils/resolve.ts +359 -0
- package/src/utils/sorting.ts +27 -0
- package/src/utils/validation.ts +88 -0
- package/dist/index.js +0 -11355
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
+
import type { Task, Comment, Project, Label, UpdateTaskParams } from "../../api/types.ts";
|
|
4
|
+
import { getComments, createComment } from "../../api/comments.ts";
|
|
5
|
+
import { closeTask, deleteTask, getTasks, updateTask } from "../../api/tasks.ts";
|
|
6
|
+
import { openUrl } from "../../utils/open-url.ts";
|
|
7
|
+
import { formatDeadlineLong, isDeadlineOverdue, formatCreatedAt } from "../../utils/date-format.ts";
|
|
8
|
+
import { ConfirmDialog } from "../components/ConfirmDialog.tsx";
|
|
9
|
+
import { InputPrompt } from "../components/InputPrompt.tsx";
|
|
10
|
+
import { EditTaskModal } from "../components/EditTaskModal.tsx";
|
|
11
|
+
import { ProjectPicker } from "../components/ProjectPicker.tsx";
|
|
12
|
+
import { LabelPicker } from "../components/LabelPicker.tsx";
|
|
13
|
+
import type { DetailSectionDefinition, PluginContext, HookRegistry } from "../../plugins/types.ts";
|
|
14
|
+
|
|
15
|
+
interface TaskDetailViewProps {
|
|
16
|
+
task: Task;
|
|
17
|
+
allTasks?: Task[];
|
|
18
|
+
projects: Project[];
|
|
19
|
+
labels: Label[];
|
|
20
|
+
onBack: () => void;
|
|
21
|
+
onTaskChanged: (message?: string) => void;
|
|
22
|
+
pluginSections?: DetailSectionDefinition[];
|
|
23
|
+
pluginSectionContextMap?: Map<string, PluginContext>;
|
|
24
|
+
pluginHooks?: HookRegistry | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const priorityLabels: Record<number, { label: string; color: string }> = {
|
|
28
|
+
4: { label: "P4 (Urgent)", color: "red" },
|
|
29
|
+
3: { label: "P3 (High)", color: "yellow" },
|
|
30
|
+
2: { label: "P2 (Medium)", color: "blue" },
|
|
31
|
+
1: { label: "P1 (Normal)", color: "white" },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function TaskDetailView({ task, allTasks, projects, labels, onBack, onTaskChanged, pluginSections, pluginSectionContextMap, pluginHooks }: TaskDetailViewProps) {
|
|
35
|
+
const [comments, setComments] = useState<Comment[]>([]);
|
|
36
|
+
const [subtasks, setSubtasks] = useState<Task[]>([]);
|
|
37
|
+
const [loadingComments, setLoadingComments] = useState(true);
|
|
38
|
+
const [statusMessage, setStatusMessage] = useState("");
|
|
39
|
+
const [confirmAction, setConfirmAction] = useState<"none" | "delete">("none");
|
|
40
|
+
const [modal, setModal] = useState<"none" | "comment" | "due" | "deadline" | "move" | "label" | "editFull">("none");
|
|
41
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
42
|
+
const { stdout } = useStdout();
|
|
43
|
+
const viewportHeight = stdout?.rows ? Math.max(5, stdout.rows - 6) : 30;
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let cancelled = false;
|
|
47
|
+
setLoadingComments(true);
|
|
48
|
+
getComments(task.id)
|
|
49
|
+
.then((c) => {
|
|
50
|
+
if (!cancelled) {
|
|
51
|
+
setComments(c);
|
|
52
|
+
setLoadingComments(false);
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
.catch(() => {
|
|
56
|
+
// Comment loading failed — hide spinner, but don't show error (non-critical data)
|
|
57
|
+
if (!cancelled) {
|
|
58
|
+
setLoadingComments(false);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return () => {
|
|
62
|
+
cancelled = true;
|
|
63
|
+
};
|
|
64
|
+
}, [task.id]);
|
|
65
|
+
|
|
66
|
+
// Load subtasks
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (allTasks) {
|
|
69
|
+
setSubtasks(allTasks.filter((t) => t.parent_id === task.id));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Fallback: fetch all tasks and filter
|
|
73
|
+
let cancelled = false;
|
|
74
|
+
getTasks()
|
|
75
|
+
.then((all) => {
|
|
76
|
+
if (!cancelled) {
|
|
77
|
+
setSubtasks(all.filter((t) => t.parent_id === task.id));
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
.catch(() => {
|
|
81
|
+
// Subtask loading failed — non-critical, silently ignore
|
|
82
|
+
});
|
|
83
|
+
return () => {
|
|
84
|
+
cancelled = true;
|
|
85
|
+
};
|
|
86
|
+
}, [task.id, allTasks]);
|
|
87
|
+
|
|
88
|
+
const handleComplete = useCallback(async () => {
|
|
89
|
+
try {
|
|
90
|
+
setStatusMessage("Completing task...");
|
|
91
|
+
await closeTask(task.id);
|
|
92
|
+
try { await pluginHooks?.emit("task.completed", { task }); } catch { /* hook error is non-critical */ }
|
|
93
|
+
onTaskChanged("Task completed!");
|
|
94
|
+
} catch {
|
|
95
|
+
setStatusMessage("Failed to complete task");
|
|
96
|
+
}
|
|
97
|
+
}, [task, onTaskChanged, pluginHooks]);
|
|
98
|
+
|
|
99
|
+
const handleDeleteConfirm = useCallback(async () => {
|
|
100
|
+
setConfirmAction("none");
|
|
101
|
+
try {
|
|
102
|
+
setStatusMessage("Deleting task...");
|
|
103
|
+
await deleteTask(task.id);
|
|
104
|
+
try { await pluginHooks?.emit("task.deleted", { task }); } catch { /* hook error is non-critical */ }
|
|
105
|
+
onTaskChanged("Task deleted!");
|
|
106
|
+
} catch {
|
|
107
|
+
setStatusMessage("Failed to delete task");
|
|
108
|
+
}
|
|
109
|
+
}, [task, onTaskChanged, pluginHooks]);
|
|
110
|
+
|
|
111
|
+
const handleAddComment = useCallback(
|
|
112
|
+
async (content: string) => {
|
|
113
|
+
setModal("none");
|
|
114
|
+
try {
|
|
115
|
+
setStatusMessage("Adding comment...");
|
|
116
|
+
await createComment({ task_id: task.id, content });
|
|
117
|
+
const updated = await getComments(task.id);
|
|
118
|
+
setComments(updated);
|
|
119
|
+
setStatusMessage("Comment added!");
|
|
120
|
+
} catch {
|
|
121
|
+
setStatusMessage("Failed to add comment");
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
[task.id],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const handleOpenInBrowser = useCallback(() => {
|
|
128
|
+
try {
|
|
129
|
+
openUrl(task.url);
|
|
130
|
+
setStatusMessage("Opened in browser");
|
|
131
|
+
} catch {
|
|
132
|
+
setStatusMessage("Failed to open in browser");
|
|
133
|
+
}
|
|
134
|
+
}, [task.url]);
|
|
135
|
+
|
|
136
|
+
const handleSetPriority = useCallback(async (priority: 1 | 2 | 3 | 4) => {
|
|
137
|
+
try {
|
|
138
|
+
setStatusMessage("Setting priority...");
|
|
139
|
+
try { await pluginHooks?.emit("task.updating", { task, changes: { priority } }); } catch { /* hook error is non-critical */ }
|
|
140
|
+
await updateTask(task.id, { priority });
|
|
141
|
+
try { await pluginHooks?.emit("task.updated", { task: { ...task, priority }, changes: { priority } }); } catch { /* hook error is non-critical */ }
|
|
142
|
+
onTaskChanged(`Priority set to p${priority}`);
|
|
143
|
+
} catch {
|
|
144
|
+
setStatusMessage("Failed to set priority");
|
|
145
|
+
}
|
|
146
|
+
}, [task, onTaskChanged, pluginHooks]);
|
|
147
|
+
|
|
148
|
+
const handleSetDueDate = useCallback(async (dueString: string) => {
|
|
149
|
+
setModal("none");
|
|
150
|
+
try {
|
|
151
|
+
const isRemove = dueString.toLowerCase() === "none" || dueString.toLowerCase() === "clear";
|
|
152
|
+
const changes: UpdateTaskParams = { due_string: isRemove ? "no date" : dueString };
|
|
153
|
+
try { await pluginHooks?.emit("task.updating", { task, changes }); } catch { /* hook error is non-critical */ }
|
|
154
|
+
await updateTask(task.id, changes);
|
|
155
|
+
try { await pluginHooks?.emit("task.updated", { task, changes }); } catch { /* hook error is non-critical */ }
|
|
156
|
+
onTaskChanged(isRemove ? "Due date removed" : `Due set to "${dueString}"`);
|
|
157
|
+
} catch {
|
|
158
|
+
setStatusMessage("Failed to set due date");
|
|
159
|
+
}
|
|
160
|
+
}, [task, onTaskChanged, pluginHooks]);
|
|
161
|
+
|
|
162
|
+
const handleSetDeadline = useCallback(async (value: string) => {
|
|
163
|
+
setModal("none");
|
|
164
|
+
const isRemove = value.toLowerCase() === "none" || value.toLowerCase() === "clear" || value === "";
|
|
165
|
+
if (!isRemove && !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
166
|
+
setStatusMessage("Invalid date format. Use YYYY-MM-DD.");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const changes: UpdateTaskParams = { deadline_date: isRemove ? null : value };
|
|
171
|
+
try { await pluginHooks?.emit("task.updating", { task, changes }); } catch { /* hook error is non-critical */ }
|
|
172
|
+
await updateTask(task.id, changes);
|
|
173
|
+
try { await pluginHooks?.emit("task.updated", { task, changes }); } catch { /* hook error is non-critical */ }
|
|
174
|
+
onTaskChanged(isRemove ? "Deadline removed" : `Deadline set to ${value}`);
|
|
175
|
+
} catch {
|
|
176
|
+
setStatusMessage("Failed to set deadline");
|
|
177
|
+
}
|
|
178
|
+
}, [task, onTaskChanged, pluginHooks]);
|
|
179
|
+
|
|
180
|
+
const handleMoveToProject = useCallback(async (projectId: string) => {
|
|
181
|
+
setModal("none");
|
|
182
|
+
try {
|
|
183
|
+
const projectName = projects.find((p) => p.id === projectId)?.name ?? "project";
|
|
184
|
+
const changes: UpdateTaskParams = { project_id: projectId };
|
|
185
|
+
try { await pluginHooks?.emit("task.updating", { task, changes }); } catch { /* hook error is non-critical */ }
|
|
186
|
+
await updateTask(task.id, changes);
|
|
187
|
+
try { await pluginHooks?.emit("task.updated", { task: { ...task, project_id: projectId }, changes }); } catch { /* hook error is non-critical */ }
|
|
188
|
+
onTaskChanged(`Moved to ${projectName}`);
|
|
189
|
+
} catch {
|
|
190
|
+
setStatusMessage("Failed to move task");
|
|
191
|
+
}
|
|
192
|
+
}, [task, projects, onTaskChanged, pluginHooks]);
|
|
193
|
+
|
|
194
|
+
const handleLabelsSave = useCallback(async (newLabels: string[]) => {
|
|
195
|
+
setModal("none");
|
|
196
|
+
try {
|
|
197
|
+
const changes: UpdateTaskParams = { labels: newLabels };
|
|
198
|
+
try { await pluginHooks?.emit("task.updating", { task, changes }); } catch { /* hook error is non-critical */ }
|
|
199
|
+
await updateTask(task.id, changes);
|
|
200
|
+
try { await pluginHooks?.emit("task.updated", { task: { ...task, labels: newLabels }, changes }); } catch { /* hook error is non-critical */ }
|
|
201
|
+
onTaskChanged("Labels updated");
|
|
202
|
+
} catch {
|
|
203
|
+
setStatusMessage("Failed to update labels");
|
|
204
|
+
}
|
|
205
|
+
}, [task, onTaskChanged, pluginHooks]);
|
|
206
|
+
|
|
207
|
+
const handleEditFull = useCallback(async (params: UpdateTaskParams & { project_id?: string }) => {
|
|
208
|
+
setModal("none");
|
|
209
|
+
try {
|
|
210
|
+
try { await pluginHooks?.emit("task.updating", { task, changes: params }); } catch { /* hook error is non-critical */ }
|
|
211
|
+
await updateTask(task.id, params);
|
|
212
|
+
try { await pluginHooks?.emit("task.updated", { task, changes: params }); } catch { /* hook error is non-critical */ }
|
|
213
|
+
onTaskChanged("Task updated");
|
|
214
|
+
} catch {
|
|
215
|
+
setStatusMessage("Failed to update task");
|
|
216
|
+
}
|
|
217
|
+
}, [task, onTaskChanged, pluginHooks]);
|
|
218
|
+
|
|
219
|
+
useInput((input, key) => {
|
|
220
|
+
if (confirmAction !== "none") return;
|
|
221
|
+
if (modal !== "none") return;
|
|
222
|
+
|
|
223
|
+
if (input === "e") {
|
|
224
|
+
setModal("editFull");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (input === "t") {
|
|
228
|
+
setModal("due");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (input === "D") {
|
|
232
|
+
setModal("deadline");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (input === "m") {
|
|
236
|
+
setModal("move");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (input === "l") {
|
|
240
|
+
setModal("label");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (input === "1" || input === "2" || input === "3" || input === "4") {
|
|
244
|
+
handleSetPriority(Number(input) as 1 | 2 | 3 | 4);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (key.escape || key.backspace || key.delete) {
|
|
249
|
+
onBack();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// Scrolling — estimate content lines to cap scroll
|
|
253
|
+
const contentLines = 6 + (task.description ? 2 : 0) + subtasks.length + comments.length * 2 + 4;
|
|
254
|
+
const maxScroll = Math.max(0, contentLines - viewportHeight);
|
|
255
|
+
if (input === "j" || key.downArrow) {
|
|
256
|
+
setScrollOffset((s) => Math.min(s + 1, maxScroll));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (input === "k" || key.upArrow) {
|
|
260
|
+
setScrollOffset((s) => Math.max(0, s - 1));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (input === "c") {
|
|
264
|
+
handleComplete();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (input === "d") {
|
|
268
|
+
setConfirmAction("delete");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (input === "n") {
|
|
272
|
+
setModal("comment");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (input === "o") {
|
|
276
|
+
handleOpenInBrowser();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const project = projects.find((p) => p.id === task.project_id);
|
|
282
|
+
const prio = priorityLabels[task.priority] ?? { label: "Unknown", color: "white" };
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<Box flexDirection="column" width="100%" height="100%">
|
|
286
|
+
<Box flexDirection="column" flexGrow={1} borderStyle="single" borderColor="cyan" paddingX={2} paddingY={1} height={viewportHeight} overflow="hidden">
|
|
287
|
+
<Box marginBottom={1}>
|
|
288
|
+
<Text bold color="cyan">Task Detail</Text>
|
|
289
|
+
{scrollOffset > 0 && <Text color="gray"> (scroll: {scrollOffset})</Text>}
|
|
290
|
+
</Box>
|
|
291
|
+
|
|
292
|
+
<Box flexDirection="column" marginTop={-scrollOffset}>
|
|
293
|
+
<Box marginBottom={1}>
|
|
294
|
+
<Text bold>{task.content}</Text>
|
|
295
|
+
</Box>
|
|
296
|
+
|
|
297
|
+
{task.description ? (
|
|
298
|
+
<Box marginBottom={1} flexDirection="column">
|
|
299
|
+
<Text color="gray">Description:</Text>
|
|
300
|
+
<Text>{task.description}</Text>
|
|
301
|
+
</Box>
|
|
302
|
+
) : null}
|
|
303
|
+
|
|
304
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
305
|
+
<Box>
|
|
306
|
+
<Box width={14}><Text color="gray">Priority:</Text></Box>
|
|
307
|
+
<Text color={prio.color}>{prio.label}</Text>
|
|
308
|
+
</Box>
|
|
309
|
+
<Box>
|
|
310
|
+
<Box width={14}><Text color="gray">Project:</Text></Box>
|
|
311
|
+
<Text color="cyan">{project?.name ?? "Unknown"}</Text>
|
|
312
|
+
</Box>
|
|
313
|
+
<Box>
|
|
314
|
+
<Box width={14}><Text color="gray">Due:</Text></Box>
|
|
315
|
+
<Text>
|
|
316
|
+
{task.due ? `${task.due.string} (${task.due.date})` : "No due date"}
|
|
317
|
+
{task.due?.is_recurring ? <Text color="cyan"> \u21BB recurring</Text> : null}
|
|
318
|
+
</Text>
|
|
319
|
+
</Box>
|
|
320
|
+
{task.deadline && (() => {
|
|
321
|
+
const formatted = formatDeadlineLong(task.deadline!.date);
|
|
322
|
+
const overdue = isDeadlineOverdue(task.deadline!.date);
|
|
323
|
+
return (
|
|
324
|
+
<Box>
|
|
325
|
+
<Box width={14}><Text color="gray">Deadline:</Text></Box>
|
|
326
|
+
<Text color="red" bold={overdue}>
|
|
327
|
+
{formatted}{overdue ? " (OVERDUE!)" : ""}
|
|
328
|
+
</Text>
|
|
329
|
+
</Box>
|
|
330
|
+
);
|
|
331
|
+
})()}
|
|
332
|
+
<Box>
|
|
333
|
+
<Box width={14}><Text color="gray">Labels:</Text></Box>
|
|
334
|
+
<Text color="magenta">{task.labels.length > 0 ? task.labels.map((l) => `@${l}`).join(" ") : "None"}</Text>
|
|
335
|
+
</Box>
|
|
336
|
+
<Box>
|
|
337
|
+
<Box width={14}><Text color="gray">Created:</Text></Box>
|
|
338
|
+
<Text>{formatCreatedAt(task.created_at)}</Text>
|
|
339
|
+
</Box>
|
|
340
|
+
</Box>
|
|
341
|
+
|
|
342
|
+
{subtasks.length > 0 && (
|
|
343
|
+
<Box flexDirection="column" marginTop={1}>
|
|
344
|
+
<Text bold color="green">Subtasks ({subtasks.length})</Text>
|
|
345
|
+
{subtasks.map((sub) => {
|
|
346
|
+
const subCheckbox = sub.is_completed ? "\u2611" : "\u2610";
|
|
347
|
+
const subPrioColor = priorityLabels[sub.priority]?.color ?? "white";
|
|
348
|
+
return (
|
|
349
|
+
<Box key={sub.id}>
|
|
350
|
+
<Text>
|
|
351
|
+
<Text color="gray"> {"\u2514"} </Text>
|
|
352
|
+
<Text color={subPrioColor}>{subCheckbox}</Text>
|
|
353
|
+
<Text> {sub.content}</Text>
|
|
354
|
+
{sub.due ? <Text color="cyan">{` [${sub.due.date}]`}</Text> : null}
|
|
355
|
+
</Text>
|
|
356
|
+
</Box>
|
|
357
|
+
);
|
|
358
|
+
})}
|
|
359
|
+
</Box>
|
|
360
|
+
)}
|
|
361
|
+
|
|
362
|
+
<Box flexDirection="column" marginTop={1}>
|
|
363
|
+
<Text bold color="yellow">Comments {loadingComments ? "(loading...)" : `(${comments.length})`}</Text>
|
|
364
|
+
{comments.length === 0 && !loadingComments ? (
|
|
365
|
+
<Text color="gray">No comments</Text>
|
|
366
|
+
) : null}
|
|
367
|
+
{comments.map((comment) => (
|
|
368
|
+
<Box key={comment.id} marginTop={1} flexDirection="column">
|
|
369
|
+
<Text color="gray" dimColor>{comment.posted_at}</Text>
|
|
370
|
+
<Text>{comment.content}</Text>
|
|
371
|
+
</Box>
|
|
372
|
+
))}
|
|
373
|
+
</Box>
|
|
374
|
+
{pluginSections?.map(section => {
|
|
375
|
+
const Component = section.component;
|
|
376
|
+
const sectionCtx = pluginSectionContextMap?.get(section.id);
|
|
377
|
+
if (!sectionCtx) return null;
|
|
378
|
+
return (
|
|
379
|
+
<Box key={section.id} flexDirection="column" marginTop={1}>
|
|
380
|
+
<Text bold color="cyan">{section.label}</Text>
|
|
381
|
+
<Component task={task} ctx={sectionCtx} />
|
|
382
|
+
</Box>
|
|
383
|
+
);
|
|
384
|
+
})}
|
|
385
|
+
</Box>
|
|
386
|
+
</Box>
|
|
387
|
+
|
|
388
|
+
{modal === "comment" && (
|
|
389
|
+
<InputPrompt
|
|
390
|
+
prompt="New comment"
|
|
391
|
+
onSubmit={handleAddComment}
|
|
392
|
+
onCancel={() => setModal("none")}
|
|
393
|
+
/>
|
|
394
|
+
)}
|
|
395
|
+
|
|
396
|
+
{modal === "due" && (
|
|
397
|
+
<InputPrompt prompt="Due date" onSubmit={handleSetDueDate} onCancel={() => setModal("none")} />
|
|
398
|
+
)}
|
|
399
|
+
{modal === "deadline" && (
|
|
400
|
+
<InputPrompt prompt="Deadline (YYYY-MM-DD)" onSubmit={handleSetDeadline} onCancel={() => setModal("none")} />
|
|
401
|
+
)}
|
|
402
|
+
{modal === "move" && (
|
|
403
|
+
<ProjectPicker projects={projects} onSelect={handleMoveToProject} onCancel={() => setModal("none")} />
|
|
404
|
+
)}
|
|
405
|
+
{modal === "label" && (
|
|
406
|
+
<LabelPicker labels={labels} currentLabels={task.labels} onSave={handleLabelsSave} onCancel={() => setModal("none")} />
|
|
407
|
+
)}
|
|
408
|
+
{modal === "editFull" && (
|
|
409
|
+
<EditTaskModal task={task} projects={projects} labels={labels} onSave={handleEditFull} onCancel={() => setModal("none")} />
|
|
410
|
+
)}
|
|
411
|
+
|
|
412
|
+
{confirmAction === "delete" && (
|
|
413
|
+
<ConfirmDialog
|
|
414
|
+
message={`Delete "${task.content}"?`}
|
|
415
|
+
onConfirm={handleDeleteConfirm}
|
|
416
|
+
onCancel={() => setConfirmAction("none")}
|
|
417
|
+
/>
|
|
418
|
+
)}
|
|
419
|
+
|
|
420
|
+
<Box borderStyle="single" borderColor="gray" paddingX={1} justifyContent="space-between">
|
|
421
|
+
<Text>
|
|
422
|
+
<Text color="blue">[e]</Text><Text>dit </Text>
|
|
423
|
+
<Text color="yellow">[c]</Text><Text>omplete </Text>
|
|
424
|
+
<Text color="red">[d]</Text><Text>elete </Text>
|
|
425
|
+
<Text color="cyan">[1-4]</Text><Text>prio </Text>
|
|
426
|
+
<Text color="green">[t]</Text><Text>due </Text>
|
|
427
|
+
<Text color="magenta">[D]</Text><Text>eadline </Text>
|
|
428
|
+
<Text color="blue">[m]</Text><Text>ove </Text>
|
|
429
|
+
<Text color="magenta">[l]</Text><Text>abel </Text>
|
|
430
|
+
<Text color="green">[n]</Text><Text>ew comment </Text>
|
|
431
|
+
<Text color="cyan">[o]</Text><Text>pen </Text>
|
|
432
|
+
<Text color="gray">[Esc]</Text><Text> back</Text>
|
|
433
|
+
</Text>
|
|
434
|
+
{statusMessage ? <Text color="yellow">{statusMessage}</Text> : null}
|
|
435
|
+
</Box>
|
|
436
|
+
</Box>
|
|
437
|
+
);
|
|
438
|
+
}
|