floq 1.6.0 → 1.8.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/dist/cli.js +17 -3
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +16 -0
- package/dist/commands/project.d.ts +6 -0
- package/dist/commands/project.js +95 -0
- package/dist/db/projectDelete.d.ts +20 -0
- package/dist/db/projectDelete.js +49 -0
- package/dist/i18n/en.d.ts +16 -0
- package/dist/i18n/en.js +11 -0
- package/dist/i18n/ja.js +11 -0
- package/dist/ui/App.js +78 -2
- package/dist/ui/components/GtdDQ.js +77 -2
- package/dist/ui/components/GtdMario.js +76 -2
- package/dist/ui/history/commands/DeleteProjectCommand.d.ts +31 -0
- package/dist/ui/history/commands/DeleteProjectCommand.js +133 -0
- package/dist/ui/history/commands/index.d.ts +1 -0
- package/dist/ui/history/commands/index.js +1 -0
- package/dist/ui/history/commands/registry.js +2 -0
- package/dist/ui/history/index.d.ts +1 -1
- package/dist/ui/history/index.js +1 -1
- package/package.json +7 -6
package/dist/cli.js
CHANGED
|
@@ -6,8 +6,8 @@ import { addTask } from './commands/add.js';
|
|
|
6
6
|
import { listTasks, listProjects } from './commands/list.js';
|
|
7
7
|
import { moveTask } from './commands/move.js';
|
|
8
8
|
import { markDone } from './commands/done.js';
|
|
9
|
-
import { addProject, listProjectsCommand, showProject, completeProject, } from './commands/project.js';
|
|
10
|
-
import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, enableTurso, clearTurso, syncCommand, resetDatabase, setSplashCommand, showSplash, showDateFormatCommand, setDateFormatCommand } from './commands/config.js';
|
|
9
|
+
import { addProject, listProjectsCommand, showProject, completeProject, deleteProjectCommand, } from './commands/project.js';
|
|
10
|
+
import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, enableTurso, clearTurso, showTursoQr, syncCommand, resetDatabase, setSplashCommand, showSplash, showDateFormatCommand, setDateFormatCommand } from './commands/config.js';
|
|
11
11
|
import { addComment, listComments } from './commands/comment.js';
|
|
12
12
|
import { listContexts, addContextCommand, removeContextCommand } from './commands/context.js';
|
|
13
13
|
import { showInsights } from './commands/insights.js';
|
|
@@ -96,6 +96,16 @@ projectCmd
|
|
|
96
96
|
.action(async (id) => {
|
|
97
97
|
await completeProject(id);
|
|
98
98
|
});
|
|
99
|
+
projectCmd
|
|
100
|
+
.command('delete <id>')
|
|
101
|
+
.alias('rm')
|
|
102
|
+
.description('Delete a project (prompts how to handle its tasks)')
|
|
103
|
+
.option('--with-tasks', 'Delete the project together with all its tasks')
|
|
104
|
+
.option('--keep-tasks', 'Delete the project but move its tasks to Inbox')
|
|
105
|
+
.option('-f, --force', 'Skip confirmation prompts')
|
|
106
|
+
.action(async (id, options) => {
|
|
107
|
+
await deleteProjectCommand(id, options);
|
|
108
|
+
});
|
|
99
109
|
// Config commands
|
|
100
110
|
const configCmd = program
|
|
101
111
|
.command('config')
|
|
@@ -153,8 +163,12 @@ configCmd
|
|
|
153
163
|
.option('--disable', 'Temporarily disable Turso sync (preserves config)')
|
|
154
164
|
.option('--enable', 'Re-enable Turso sync')
|
|
155
165
|
.option('--clear', 'Remove Turso configuration completely')
|
|
166
|
+
.option('--qr', 'Display Turso config as QR code')
|
|
156
167
|
.action(async (options) => {
|
|
157
|
-
if (options.
|
|
168
|
+
if (options.qr) {
|
|
169
|
+
await showTursoQr();
|
|
170
|
+
}
|
|
171
|
+
else if (options.clear) {
|
|
158
172
|
await clearTurso();
|
|
159
173
|
}
|
|
160
174
|
else if (options.disable) {
|
|
@@ -7,6 +7,7 @@ export declare function selectTheme(): Promise<void>;
|
|
|
7
7
|
export declare function showViewMode(): Promise<void>;
|
|
8
8
|
export declare function setViewModeCommand(mode: string): Promise<void>;
|
|
9
9
|
export declare function selectMode(): Promise<void>;
|
|
10
|
+
export declare function showTursoQr(): Promise<void>;
|
|
10
11
|
export declare function setTurso(url: string, token: string): Promise<void>;
|
|
11
12
|
export declare function disableTurso(): Promise<void>;
|
|
12
13
|
export declare function enableTurso(): Promise<void>;
|
package/dist/commands/config.js
CHANGED
|
@@ -127,6 +127,22 @@ export async function selectMode() {
|
|
|
127
127
|
}));
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
+
export async function showTursoQr() {
|
|
131
|
+
const turso = getTursoConfig();
|
|
132
|
+
if (!turso || !turso.url || !turso.authToken) {
|
|
133
|
+
console.error('Turso is not configured.');
|
|
134
|
+
console.error('Use "floq config turso --url <url> --token <token>" to configure first.');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const qrcode = await import('qrcode');
|
|
138
|
+
const data = JSON.stringify({ url: turso.url, authToken: turso.authToken });
|
|
139
|
+
const qr = await qrcode.default.toString(data, { type: 'terminal', small: true });
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log('Turso Configuration QR Code:');
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log(qr);
|
|
144
|
+
console.log('⚠️ This QR code contains your auth token. Do not share publicly.');
|
|
145
|
+
}
|
|
130
146
|
export async function setTurso(url, token) {
|
|
131
147
|
setTursoConfig({ url, authToken: token, enabled: true });
|
|
132
148
|
console.log('Turso sync enabled');
|
|
@@ -5,4 +5,10 @@ export declare function addProject(name: string, options: AddProjectOptions): Pr
|
|
|
5
5
|
export declare function listProjectsCommand(): Promise<void>;
|
|
6
6
|
export declare function showProject(projectId: string): Promise<void>;
|
|
7
7
|
export declare function completeProject(projectId: string): Promise<void>;
|
|
8
|
+
interface DeleteProjectOptions {
|
|
9
|
+
withTasks?: boolean;
|
|
10
|
+
keepTasks?: boolean;
|
|
11
|
+
force?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function deleteProjectCommand(projectId: string, options: DeleteProjectOptions): Promise<void>;
|
|
8
14
|
export {};
|
package/dist/commands/project.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
2
3
|
import { eq, like, and } from 'drizzle-orm';
|
|
3
4
|
import { getDb, schema } from '../db/index.js';
|
|
5
|
+
import { deleteProject } from '../db/projectDelete.js';
|
|
4
6
|
import { t, fmt } from '../i18n/index.js';
|
|
5
7
|
export async function addProject(name, options) {
|
|
6
8
|
const db = getDb();
|
|
@@ -150,3 +152,96 @@ export async function completeProject(projectId) {
|
|
|
150
152
|
.where(eq(schema.tasks.id, project.id));
|
|
151
153
|
console.log(fmt(i18n.commands.project.completed, { name: project.title }));
|
|
152
154
|
}
|
|
155
|
+
function ask(rl, question) {
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
rl.question(question, (answer) => {
|
|
158
|
+
resolve(answer.trim().toLowerCase());
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
export async function deleteProjectCommand(projectId, options) {
|
|
163
|
+
const db = getDb();
|
|
164
|
+
const i18n = t();
|
|
165
|
+
// Find project by ID prefix, then by exact name.
|
|
166
|
+
let projects = await db
|
|
167
|
+
.select()
|
|
168
|
+
.from(schema.tasks)
|
|
169
|
+
.where(and(eq(schema.tasks.isProject, true), like(schema.tasks.id, `${projectId}%`)));
|
|
170
|
+
if (projects.length === 0) {
|
|
171
|
+
projects = await db
|
|
172
|
+
.select()
|
|
173
|
+
.from(schema.tasks)
|
|
174
|
+
.where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.title, projectId)));
|
|
175
|
+
}
|
|
176
|
+
if (projects.length === 0) {
|
|
177
|
+
console.error(fmt(i18n.commands.project.notFound, { id: projectId }));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
if (projects.length > 1) {
|
|
181
|
+
console.error(fmt(i18n.commands.project.multipleMatch, { id: projectId }));
|
|
182
|
+
for (const p of projects) {
|
|
183
|
+
console.error(` [${p.id.slice(0, 8)}] ${p.title}`);
|
|
184
|
+
}
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const project = projects[0];
|
|
188
|
+
const children = await db
|
|
189
|
+
.select()
|
|
190
|
+
.from(schema.tasks)
|
|
191
|
+
.where(eq(schema.tasks.parentId, project.id));
|
|
192
|
+
const childCount = children.length;
|
|
193
|
+
// Decide how to handle child tasks. When neither flag is given and the
|
|
194
|
+
// project has tasks, the choice is resolved interactively below.
|
|
195
|
+
let mode = options.withTasks ? 'cascade' :
|
|
196
|
+
options.keepTasks ? 'keep' :
|
|
197
|
+
childCount === 0 ? 'cascade' :
|
|
198
|
+
null;
|
|
199
|
+
if (!options.force) {
|
|
200
|
+
// A single prompt keeps this reliable for piped/non-TTY input.
|
|
201
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
202
|
+
try {
|
|
203
|
+
if (mode !== null) {
|
|
204
|
+
const answer = await ask(rl, `${fmt(i18n.commands.project.deletePrompt, { name: project.title })} (y/N): `);
|
|
205
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
206
|
+
console.log(i18n.commands.project.deleteCancelled);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const answer = await ask(rl, `${fmt(i18n.commands.project.deleteChildrenPrompt, { name: project.title, count: childCount })}: `);
|
|
212
|
+
if (answer === 'a' || answer === 'all') {
|
|
213
|
+
mode = 'cascade';
|
|
214
|
+
}
|
|
215
|
+
else if (answer === 'k' || answer === 'keep') {
|
|
216
|
+
mode = 'keep';
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
console.log(i18n.commands.project.deleteCancelled);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
rl.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (mode === null) {
|
|
229
|
+
// Forced with no explicit choice: keep tasks to avoid silent data loss.
|
|
230
|
+
mode = 'keep';
|
|
231
|
+
}
|
|
232
|
+
await deleteProject(project, mode);
|
|
233
|
+
if (mode === 'cascade') {
|
|
234
|
+
if (childCount > 0) {
|
|
235
|
+
console.log(fmt(i18n.commands.project.deletedWithTasks, { name: project.title, count: childCount }));
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
console.log(fmt(i18n.commands.project.deleted, { name: project.title }));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
console.log(fmt(i18n.commands.project.deleted, { name: project.title }));
|
|
243
|
+
if (childCount > 0) {
|
|
244
|
+
console.log(fmt(i18n.commands.project.tasksMovedToInbox, { count: childCount }));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Task, Comment } from './schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* How to handle child tasks when deleting a project.
|
|
4
|
+
* - `cascade`: delete the project together with all its child tasks (and their comments)
|
|
5
|
+
* - `keep`: delete only the project, moving its child tasks back to Inbox (unlinked)
|
|
6
|
+
*/
|
|
7
|
+
export type ProjectDeleteMode = 'cascade' | 'keep';
|
|
8
|
+
export interface ProjectDeleteResult {
|
|
9
|
+
/** Child tasks that were deleted (only populated for `cascade`). */
|
|
10
|
+
deletedTasks: Task[];
|
|
11
|
+
/** Child tasks moved to Inbox, in their original state (only populated for `keep`). */
|
|
12
|
+
movedTasks: Task[];
|
|
13
|
+
/** Comments that were deleted (project's own comments, plus child comments in `cascade`). */
|
|
14
|
+
deletedComments: Comment[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Delete a project and handle its child tasks according to `mode`.
|
|
18
|
+
* Returns the prior state so the operation can be undone.
|
|
19
|
+
*/
|
|
20
|
+
export declare function deleteProject(project: Task, mode: ProjectDeleteMode): Promise<ProjectDeleteResult>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { getDb, schema } from './index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Delete a project and handle its child tasks according to `mode`.
|
|
5
|
+
* Returns the prior state so the operation can be undone.
|
|
6
|
+
*/
|
|
7
|
+
export async function deleteProject(project, mode) {
|
|
8
|
+
const db = getDb();
|
|
9
|
+
const children = await db
|
|
10
|
+
.select()
|
|
11
|
+
.from(schema.tasks)
|
|
12
|
+
.where(eq(schema.tasks.parentId, project.id));
|
|
13
|
+
const deletedTasks = [];
|
|
14
|
+
const movedTasks = [];
|
|
15
|
+
let deletedComments = [];
|
|
16
|
+
// Always remove the project's own comments.
|
|
17
|
+
const projectComments = await db
|
|
18
|
+
.select()
|
|
19
|
+
.from(schema.comments)
|
|
20
|
+
.where(eq(schema.comments.taskId, project.id));
|
|
21
|
+
deletedComments = deletedComments.concat(projectComments);
|
|
22
|
+
await db.delete(schema.comments).where(eq(schema.comments.taskId, project.id));
|
|
23
|
+
if (mode === 'cascade') {
|
|
24
|
+
for (const child of children) {
|
|
25
|
+
const childComments = await db
|
|
26
|
+
.select()
|
|
27
|
+
.from(schema.comments)
|
|
28
|
+
.where(eq(schema.comments.taskId, child.id));
|
|
29
|
+
deletedComments = deletedComments.concat(childComments);
|
|
30
|
+
await db.delete(schema.comments).where(eq(schema.comments.taskId, child.id));
|
|
31
|
+
await db.delete(schema.tasks).where(eq(schema.tasks.id, child.id));
|
|
32
|
+
deletedTasks.push(child);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// keep: unlink children and move them back to Inbox.
|
|
37
|
+
const now = new Date();
|
|
38
|
+
for (const child of children) {
|
|
39
|
+
movedTasks.push(child);
|
|
40
|
+
await db
|
|
41
|
+
.update(schema.tasks)
|
|
42
|
+
.set({ parentId: null, status: 'inbox', updatedAt: now })
|
|
43
|
+
.where(eq(schema.tasks.id, child.id));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Finally remove the project itself.
|
|
47
|
+
await db.delete(schema.tasks).where(eq(schema.tasks.id, project.id));
|
|
48
|
+
return { deletedTasks, movedTasks, deletedComments };
|
|
49
|
+
}
|
package/dist/i18n/en.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export declare const en: {
|
|
|
51
51
|
notFound: string;
|
|
52
52
|
multipleMatch: string;
|
|
53
53
|
completed: string;
|
|
54
|
+
deleted: string;
|
|
55
|
+
deletedWithTasks: string;
|
|
56
|
+
tasksMovedToInbox: string;
|
|
57
|
+
deletePrompt: string;
|
|
58
|
+
deleteChildrenPrompt: string;
|
|
59
|
+
deleteCancelled: string;
|
|
54
60
|
noProjects: string;
|
|
55
61
|
description: string;
|
|
56
62
|
statusLabel: string;
|
|
@@ -247,6 +253,11 @@ export declare const en: {
|
|
|
247
253
|
deleteConfirm: string;
|
|
248
254
|
deleted: string;
|
|
249
255
|
deleteCancelled: string;
|
|
256
|
+
deleteProjectConfirm: string;
|
|
257
|
+
deleteProjectChildren: string;
|
|
258
|
+
deletedProject: string;
|
|
259
|
+
deletedProjectWithTasks: string;
|
|
260
|
+
projectTasksToInbox: string;
|
|
250
261
|
undone: string;
|
|
251
262
|
redone: string;
|
|
252
263
|
nothingToUndo: string;
|
|
@@ -675,6 +686,11 @@ export type TuiTranslations = {
|
|
|
675
686
|
deleteConfirm: string;
|
|
676
687
|
deleted: string;
|
|
677
688
|
deleteCancelled: string;
|
|
689
|
+
deleteProjectConfirm?: string;
|
|
690
|
+
deleteProjectChildren?: string;
|
|
691
|
+
deletedProject?: string;
|
|
692
|
+
deletedProjectWithTasks?: string;
|
|
693
|
+
projectTasksToInbox?: string;
|
|
678
694
|
undone: string;
|
|
679
695
|
redone: string;
|
|
680
696
|
nothingToUndo: string;
|
package/dist/i18n/en.js
CHANGED
|
@@ -55,6 +55,12 @@ export const en = {
|
|
|
55
55
|
notFound: 'Project not found: {id}',
|
|
56
56
|
multipleMatch: 'Multiple projects match "{id}". Please be more specific.',
|
|
57
57
|
completed: 'Completed project: "{name}"',
|
|
58
|
+
deleted: 'Deleted project: "{name}"',
|
|
59
|
+
deletedWithTasks: 'Deleted project "{name}" and {count} task(s)',
|
|
60
|
+
tasksMovedToInbox: 'Moved {count} task(s) to Inbox',
|
|
61
|
+
deletePrompt: 'Delete project "{name}"?',
|
|
62
|
+
deleteChildrenPrompt: 'Project "{name}" has {count} task(s). a=delete all k=keep tasks (→Inbox) n=cancel',
|
|
63
|
+
deleteCancelled: 'Cancelled.',
|
|
58
64
|
noProjects: 'No projects',
|
|
59
65
|
description: 'Description: {description}',
|
|
60
66
|
statusLabel: 'Status: {status}',
|
|
@@ -260,6 +266,11 @@ export const en = {
|
|
|
260
266
|
deleteConfirm: 'Delete "{title}"? (y/n)',
|
|
261
267
|
deleted: 'Deleted: "{title}"',
|
|
262
268
|
deleteCancelled: 'Delete cancelled',
|
|
269
|
+
deleteProjectConfirm: 'Delete project "{title}"? (y/n)',
|
|
270
|
+
deleteProjectChildren: 'Project "{title}" has {count} task(s). t=delete all i=keep tasks (→Inbox) n=cancel',
|
|
271
|
+
deletedProject: 'Deleted project: "{title}"',
|
|
272
|
+
deletedProjectWithTasks: 'Deleted project "{title}" and {count} task(s)',
|
|
273
|
+
projectTasksToInbox: 'Deleted project "{title}", moved {count} task(s) to Inbox',
|
|
263
274
|
// Undo/Redo
|
|
264
275
|
undone: 'Undone: {action}',
|
|
265
276
|
redone: 'Redone: {action}',
|
package/dist/i18n/ja.js
CHANGED
|
@@ -55,6 +55,12 @@ export const ja = {
|
|
|
55
55
|
notFound: 'プロジェクトが見つかりません: {id}',
|
|
56
56
|
multipleMatch: '「{id}」に一致するプロジェクトが複数あります。より具体的に指定してください。',
|
|
57
57
|
completed: 'プロジェクトを完了しました: 「{name}」',
|
|
58
|
+
deleted: 'プロジェクトを削除しました: 「{name}」',
|
|
59
|
+
deletedWithTasks: 'プロジェクト「{name}」とタスク{count}件を削除しました',
|
|
60
|
+
tasksMovedToInbox: 'タスク{count}件をInboxへ移動しました',
|
|
61
|
+
deletePrompt: 'プロジェクト「{name}」を削除しますか?',
|
|
62
|
+
deleteChildrenPrompt: 'プロジェクト「{name}」にはタスクが{count}件あります。 a=すべて削除 k=タスクを残す(→Inbox) n=キャンセル',
|
|
63
|
+
deleteCancelled: 'キャンセルしました。',
|
|
58
64
|
noProjects: 'プロジェクトなし',
|
|
59
65
|
description: '説明: {description}',
|
|
60
66
|
statusLabel: 'ステータス: {status}',
|
|
@@ -260,6 +266,11 @@ export const ja = {
|
|
|
260
266
|
deleteConfirm: '「{title}」を削除しますか? (y/n)',
|
|
261
267
|
deleted: '削除しました: 「{title}」',
|
|
262
268
|
deleteCancelled: '削除をキャンセルしました',
|
|
269
|
+
deleteProjectConfirm: 'プロジェクト「{title}」を削除しますか? (y/n)',
|
|
270
|
+
deleteProjectChildren: 'プロジェクト「{title}」にはタスクが{count}件あります。 t=すべて削除 i=タスクを残す(→Inbox) n=キャンセル',
|
|
271
|
+
deletedProject: 'プロジェクトを削除しました: 「{title}」',
|
|
272
|
+
deletedProjectWithTasks: 'プロジェクト「{title}」とタスク{count}件を削除しました',
|
|
273
|
+
projectTasksToInbox: 'プロジェクト「{title}」を削除し、タスク{count}件をInboxへ移動しました',
|
|
263
274
|
// Undo/Redo
|
|
264
275
|
undone: '元に戻しました: {action}',
|
|
265
276
|
redone: 'やり直しました: {action}',
|
package/dist/ui/App.js
CHANGED
|
@@ -30,7 +30,7 @@ import { KanbanMario } from './components/KanbanMario.js';
|
|
|
30
30
|
import { GtdDQ } from './components/GtdDQ.js';
|
|
31
31
|
import { GtdMario } from './components/GtdMario.js';
|
|
32
32
|
import { VERSION } from '../version.js';
|
|
33
|
-
import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from './history/index.js';
|
|
33
|
+
import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from './history/index.js';
|
|
34
34
|
const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
|
|
35
35
|
export function App() {
|
|
36
36
|
const [themeName, setThemeNameState] = useState(getThemeName);
|
|
@@ -102,6 +102,8 @@ function AppContent({ onOpenSettings }) {
|
|
|
102
102
|
const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
|
|
103
103
|
const [taskToWaiting, setTaskToWaiting] = useState(null);
|
|
104
104
|
const [taskToDelete, setTaskToDelete] = useState(null);
|
|
105
|
+
const [projectToDelete, setProjectToDelete] = useState(null);
|
|
106
|
+
const [projectChildCount, setProjectChildCount] = useState(0);
|
|
105
107
|
const [projectProgress, setProjectProgress] = useState({});
|
|
106
108
|
// Search state
|
|
107
109
|
const [searchQuery, setSearchQuery] = useState('');
|
|
@@ -517,6 +519,19 @@ function AppContent({ onOpenSettings }) {
|
|
|
517
519
|
setMessage(fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }));
|
|
518
520
|
await loadTasks();
|
|
519
521
|
}, [i18n.tui.deleted, loadTasks, history]);
|
|
522
|
+
const deleteProjectAction = useCallback(async (project, mode, childCount) => {
|
|
523
|
+
const message = mode === 'cascade'
|
|
524
|
+
? (childCount > 0
|
|
525
|
+
? fmt(i18n.tui.deletedProjectWithTasks || 'Deleted project "{title}" and {count} task(s)', { title: project.title, count: childCount })
|
|
526
|
+
: fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }))
|
|
527
|
+
: (childCount > 0
|
|
528
|
+
? fmt(i18n.tui.projectTasksToInbox || 'Deleted project "{title}", moved {count} task(s) to Inbox', { title: project.title, count: childCount })
|
|
529
|
+
: fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }));
|
|
530
|
+
const command = new DeleteProjectCommand({ project, mode, description: message });
|
|
531
|
+
await history.execute(command);
|
|
532
|
+
setMessage(message);
|
|
533
|
+
await loadTasks();
|
|
534
|
+
}, [i18n.tui.deletedProject, i18n.tui.deletedProjectWithTasks, i18n.tui.projectTasksToInbox, loadTasks, history]);
|
|
520
535
|
const getTabLabel = (tab) => {
|
|
521
536
|
switch (tab) {
|
|
522
537
|
case 'inbox':
|
|
@@ -576,6 +591,51 @@ function AppContent({ onOpenSettings }) {
|
|
|
576
591
|
// Ignore other keys in confirm mode
|
|
577
592
|
return;
|
|
578
593
|
}
|
|
594
|
+
// Handle confirm-delete-project mode
|
|
595
|
+
if (mode === 'confirm-delete-project' && projectToDelete) {
|
|
596
|
+
const cancel = () => {
|
|
597
|
+
setMessage(i18n.tui.deleteCancelled || 'Delete cancelled');
|
|
598
|
+
setProjectToDelete(null);
|
|
599
|
+
setMode('normal');
|
|
600
|
+
};
|
|
601
|
+
const runDelete = (deleteMode) => {
|
|
602
|
+
const project = projectToDelete;
|
|
603
|
+
const count = projectChildCount;
|
|
604
|
+
deleteProjectAction(project, deleteMode, count).then(() => {
|
|
605
|
+
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
606
|
+
setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
setProjectToDelete(null);
|
|
610
|
+
setMode('normal');
|
|
611
|
+
};
|
|
612
|
+
if (projectChildCount > 0) {
|
|
613
|
+
if (input === 't' || input === 'T') {
|
|
614
|
+
runDelete('cascade');
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (input === 'i' || input === 'I') {
|
|
618
|
+
runDelete('keep');
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
622
|
+
cancel();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
if (input === 'y' || input === 'Y') {
|
|
628
|
+
runDelete('cascade');
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
632
|
+
cancel();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Ignore other keys in confirm mode
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
579
639
|
// Handle add mode
|
|
580
640
|
if (mode === 'add' || mode === 'add-to-project' || mode === 'add-comment' || mode === 'move-to-waiting') {
|
|
581
641
|
if (key.escape) {
|
|
@@ -1077,6 +1137,20 @@ function AppContent({ onOpenSettings }) {
|
|
|
1077
1137
|
});
|
|
1078
1138
|
return;
|
|
1079
1139
|
}
|
|
1140
|
+
// Delete project (D key on projects tab - with confirmation)
|
|
1141
|
+
if (input === 'D' && currentTasks.length > 0 && currentTab === 'projects') {
|
|
1142
|
+
const project = currentTasks[selectedTaskIndex];
|
|
1143
|
+
const db = getDb();
|
|
1144
|
+
db.select()
|
|
1145
|
+
.from(schema.tasks)
|
|
1146
|
+
.where(eq(schema.tasks.parentId, project.id))
|
|
1147
|
+
.then((children) => {
|
|
1148
|
+
setProjectChildCount(children.length);
|
|
1149
|
+
setProjectToDelete(project);
|
|
1150
|
+
setMode('confirm-delete-project');
|
|
1151
|
+
});
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1080
1154
|
// Delete task (D key - with confirmation)
|
|
1081
1155
|
if (input === 'D' && currentTasks.length > 0 && currentTab !== 'projects') {
|
|
1082
1156
|
const task = currentTasks[selectedTaskIndex];
|
|
@@ -1224,7 +1298,9 @@ function AppContent({ onOpenSettings }) {
|
|
|
1224
1298
|
const isActive = (ctx === 'clear' && !currentContext) ||
|
|
1225
1299
|
(ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
|
|
1226
1300
|
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
|
|
1227
|
-
}) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'set-effort' && (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.modal, borderColor: theme.colors.borderActive, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: i18n.tui.effort?.set || 'Set effort' }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.effort?.setHelp || 'j/k: select, Enter: confirm, Esc: cancel' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, option.label] }, option.label))) })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'confirm-delete' && taskToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })),
|
|
1301
|
+
}) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'set-effort' && (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.modal, borderColor: theme.colors.borderActive, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: i18n.tui.effort?.set || 'Set effort' }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.effort?.setHelp || 'j/k: select, Enter: confirm, Esc: cancel' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, option.label] }, option.label))) })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'confirm-delete' && taskToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })), mode === 'confirm-delete-project' && projectToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: projectChildCount > 0
|
|
1302
|
+
? fmt(i18n.tui.deleteProjectChildren || 'Project "{title}" has {count} task(s). t=delete all i=keep tasks (→Inbox) n=cancel', { title: projectToDelete.title, count: projectChildCount })
|
|
1303
|
+
: fmt(i18n.tui.deleteProjectConfirm || 'Delete project "{title}"? (y/n)', { title: projectToDelete.title }) }) })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [(mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
|
|
1228
1304
|
{ key: 'i', label: i18n.tui.keyBar.comment },
|
|
1229
1305
|
{ key: 'd', label: i18n.tui.keyBar.delete },
|
|
1230
1306
|
{ key: 'P', label: i18n.tui.keyBar.project },
|
|
@@ -8,7 +8,7 @@ import { getDb, schema } from '../../db/index.js';
|
|
|
8
8
|
import { t, fmt } from '../../i18n/index.js';
|
|
9
9
|
import { useTheme } from '../theme/index.js';
|
|
10
10
|
import { isTursoEnabled, getContexts, addContext, getLocale, getContextFilter, setContextFilter as saveContextFilter, getPomodoroFocusMode, setPomodoroFocusMode, getFocusFilter, setFocusFilter } from '../../config.js';
|
|
11
|
-
import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
|
|
11
|
+
import { useHistory, CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
|
|
12
12
|
import { SearchBar } from './SearchBar.js';
|
|
13
13
|
import { SearchResults } from './SearchResults.js';
|
|
14
14
|
import { HelpModal } from './HelpModal.js';
|
|
@@ -145,6 +145,8 @@ export function GtdDQ({ onOpenSettings }) {
|
|
|
145
145
|
const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
|
|
146
146
|
const [taskToWaiting, setTaskToWaiting] = useState(null);
|
|
147
147
|
const [taskToDelete, setTaskToDelete] = useState(null);
|
|
148
|
+
const [projectToDelete, setProjectToDelete] = useState(null);
|
|
149
|
+
const [projectChildCount, setProjectChildCount] = useState(0);
|
|
148
150
|
const [projectProgress, setProjectProgress] = useState({});
|
|
149
151
|
// Context filter state - load from config for persistence across sessions/terminals
|
|
150
152
|
const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
|
|
@@ -401,6 +403,19 @@ export function GtdDQ({ onOpenSettings }) {
|
|
|
401
403
|
setMessage(fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }));
|
|
402
404
|
await loadTasks();
|
|
403
405
|
}, [i18n.tui.deleted, loadTasks, history]);
|
|
406
|
+
const deleteProjectAction = useCallback(async (project, mode, childCount) => {
|
|
407
|
+
const message = mode === 'cascade'
|
|
408
|
+
? (childCount > 0
|
|
409
|
+
? fmt(i18n.tui.deletedProjectWithTasks || 'Deleted project "{title}" and {count} task(s)', { title: project.title, count: childCount })
|
|
410
|
+
: fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }))
|
|
411
|
+
: (childCount > 0
|
|
412
|
+
? fmt(i18n.tui.projectTasksToInbox || 'Deleted project "{title}", moved {count} task(s) to Inbox', { title: project.title, count: childCount })
|
|
413
|
+
: fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }));
|
|
414
|
+
const command = new DeleteProjectCommand({ project, mode, description: message });
|
|
415
|
+
await history.execute(command);
|
|
416
|
+
setMessage(message);
|
|
417
|
+
await loadTasks();
|
|
418
|
+
}, [i18n.tui.deletedProject, i18n.tui.deletedProjectWithTasks, i18n.tui.projectTasksToInbox, loadTasks, history]);
|
|
404
419
|
const linkTaskToProject = useCallback(async (task, project) => {
|
|
405
420
|
const command = new LinkTaskCommand({
|
|
406
421
|
taskId: task.id,
|
|
@@ -738,6 +753,50 @@ export function GtdDQ({ onOpenSettings }) {
|
|
|
738
753
|
}
|
|
739
754
|
return;
|
|
740
755
|
}
|
|
756
|
+
// Handle confirm-delete-project
|
|
757
|
+
if (mode === 'confirm-delete-project' && projectToDelete) {
|
|
758
|
+
const cancel = () => {
|
|
759
|
+
setMessage(i18n.tui.deleteCancelled || 'Delete cancelled');
|
|
760
|
+
setProjectToDelete(null);
|
|
761
|
+
setMode('normal');
|
|
762
|
+
};
|
|
763
|
+
const runDelete = (deleteMode) => {
|
|
764
|
+
const project = projectToDelete;
|
|
765
|
+
const count = projectChildCount;
|
|
766
|
+
deleteProjectAction(project, deleteMode, count).then(() => {
|
|
767
|
+
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
768
|
+
setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
setProjectToDelete(null);
|
|
772
|
+
setMode('normal');
|
|
773
|
+
};
|
|
774
|
+
if (projectChildCount > 0) {
|
|
775
|
+
if (input === 't' || input === 'T') {
|
|
776
|
+
runDelete('cascade');
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (input === 'i' || input === 'I') {
|
|
780
|
+
runDelete('keep');
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
784
|
+
cancel();
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
if (input === 'y' || input === 'Y') {
|
|
790
|
+
runDelete('cascade');
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
794
|
+
cancel();
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
741
800
|
// Handle context-filter mode
|
|
742
801
|
if (mode === 'context-filter') {
|
|
743
802
|
if (key.escape) {
|
|
@@ -1052,6 +1111,20 @@ export function GtdDQ({ onOpenSettings }) {
|
|
|
1052
1111
|
setMode('select-project');
|
|
1053
1112
|
return;
|
|
1054
1113
|
}
|
|
1114
|
+
// Delete project (on projects tab)
|
|
1115
|
+
if (input === 'D' && currentTab === 'projects') {
|
|
1116
|
+
const project = task;
|
|
1117
|
+
const db = getDb();
|
|
1118
|
+
db.select()
|
|
1119
|
+
.from(schema.tasks)
|
|
1120
|
+
.where(eq(schema.tasks.parentId, project.id))
|
|
1121
|
+
.then((children) => {
|
|
1122
|
+
setProjectChildCount(children.length);
|
|
1123
|
+
setProjectToDelete(project);
|
|
1124
|
+
setMode('confirm-delete-project');
|
|
1125
|
+
});
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1055
1128
|
// Delete
|
|
1056
1129
|
if (input === 'D' && currentTab !== 'projects') {
|
|
1057
1130
|
setTaskToDelete(task);
|
|
@@ -1179,7 +1252,9 @@ export function GtdDQ({ onOpenSettings }) {
|
|
|
1179
1252
|
}) })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
|
|
1180
1253
|
const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
|
|
1181
1254
|
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
|
|
1182
|
-
}) })] })) : mode === 'set-effort' && getCurrentTask() ? (_jsx(Box, { flexDirection: "column", children: _jsx(TitledBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: Math.min(40, terminalWidth - 4), minHeight: EFFORT_OPTIONS.length, isActive: true, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '▶ ' : ' ', option.label] }, option.label))) }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '▶ ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) :
|
|
1255
|
+
}) })] })) : mode === 'set-effort' && getCurrentTask() ? (_jsx(Box, { flexDirection: "column", children: _jsx(TitledBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: Math.min(40, terminalWidth - 4), minHeight: EFFORT_OPTIONS.length, isActive: true, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '▶ ' : ' ', option.label] }, option.label))) }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '▶ ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : mode === 'confirm-delete-project' && projectToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: projectChildCount > 0
|
|
1256
|
+
? fmt(i18n.tui.deleteProjectChildren || 'Project "{title}" has {count} task(s). t=delete all i=keep tasks (→Inbox) n=cancel', { title: projectToDelete.title, count: projectChildCount })
|
|
1257
|
+
: fmt(i18n.tui.deleteProjectConfirm || 'Delete project "{title}"? (y/n)', { title: projectToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(TitledBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(TitledBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
1183
1258
|
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
1184
1259
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.textMuted, children: [isSelected ? '▶ ' : ' ', "[", comment.createdAt.toLocaleString(), "]"] }), _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [' ', comment.content] })] }, comment.id));
|
|
1185
1260
|
})) }) })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(TitledBoxInline, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : 'GTD', width: leftPaneWidth, minHeight: 8, isActive: paneFocus === 'tabs' && mode !== 'project-detail', children: mode === 'project-detail' ? (_jsx(Text, { color: theme.colors.textMuted, children: "\u2190 Esc/b: back" })) : (TABS.map((tab, index) => {
|
|
@@ -9,7 +9,7 @@ import { t, fmt } from '../../i18n/index.js';
|
|
|
9
9
|
import { useTheme } from '../theme/index.js';
|
|
10
10
|
import { isTursoEnabled, getContexts, addContext, getContextFilter, setContextFilter as saveContextFilter, getPomodoroFocusMode, setPomodoroFocusMode, getFocusFilter, setFocusFilter } from '../../config.js';
|
|
11
11
|
import { VERSION } from '../../version.js';
|
|
12
|
-
import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
|
|
12
|
+
import { useHistory, CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
|
|
13
13
|
import { SearchBar } from './SearchBar.js';
|
|
14
14
|
import { SearchResults } from './SearchResults.js';
|
|
15
15
|
import { HelpModal } from './HelpModal.js';
|
|
@@ -87,6 +87,8 @@ export function GtdMario({ onOpenSettings }) {
|
|
|
87
87
|
const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
|
|
88
88
|
const [taskToWaiting, setTaskToWaiting] = useState(null);
|
|
89
89
|
const [taskToDelete, setTaskToDelete] = useState(null);
|
|
90
|
+
const [projectToDelete, setProjectToDelete] = useState(null);
|
|
91
|
+
const [projectChildCount, setProjectChildCount] = useState(0);
|
|
90
92
|
const [projectProgress, setProjectProgress] = useState({});
|
|
91
93
|
// Context filter state - load from config for persistence across sessions/terminals
|
|
92
94
|
const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
|
|
@@ -347,6 +349,19 @@ export function GtdMario({ onOpenSettings }) {
|
|
|
347
349
|
setMessage(fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }));
|
|
348
350
|
await loadTasks();
|
|
349
351
|
}, [i18n.tui.deleted, loadTasks, history]);
|
|
352
|
+
const deleteProjectAction = useCallback(async (project, mode, childCount) => {
|
|
353
|
+
const message = mode === 'cascade'
|
|
354
|
+
? (childCount > 0
|
|
355
|
+
? fmt(i18n.tui.deletedProjectWithTasks || 'Deleted project "{title}" and {count} task(s)', { title: project.title, count: childCount })
|
|
356
|
+
: fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }))
|
|
357
|
+
: (childCount > 0
|
|
358
|
+
? fmt(i18n.tui.projectTasksToInbox || 'Deleted project "{title}", moved {count} task(s) to Inbox', { title: project.title, count: childCount })
|
|
359
|
+
: fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }));
|
|
360
|
+
const command = new DeleteProjectCommand({ project, mode, description: message });
|
|
361
|
+
await history.execute(command);
|
|
362
|
+
setMessage(message);
|
|
363
|
+
await loadTasks();
|
|
364
|
+
}, [i18n.tui.deletedProject, i18n.tui.deletedProjectWithTasks, i18n.tui.projectTasksToInbox, loadTasks, history]);
|
|
350
365
|
const linkTaskToProject = useCallback(async (task, project) => {
|
|
351
366
|
const command = new LinkTaskCommand({
|
|
352
367
|
taskId: task.id,
|
|
@@ -653,6 +668,49 @@ export function GtdMario({ onOpenSettings }) {
|
|
|
653
668
|
}
|
|
654
669
|
return;
|
|
655
670
|
}
|
|
671
|
+
if (mode === 'confirm-delete-project' && projectToDelete) {
|
|
672
|
+
const cancel = () => {
|
|
673
|
+
setMessage(i18n.tui.deleteCancelled || 'Delete cancelled');
|
|
674
|
+
setProjectToDelete(null);
|
|
675
|
+
setMode('normal');
|
|
676
|
+
};
|
|
677
|
+
const runDelete = (deleteMode) => {
|
|
678
|
+
const project = projectToDelete;
|
|
679
|
+
const count = projectChildCount;
|
|
680
|
+
deleteProjectAction(project, deleteMode, count).then(() => {
|
|
681
|
+
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
682
|
+
setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
setProjectToDelete(null);
|
|
686
|
+
setMode('normal');
|
|
687
|
+
};
|
|
688
|
+
if (projectChildCount > 0) {
|
|
689
|
+
if (input === 't' || input === 'T') {
|
|
690
|
+
runDelete('cascade');
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (input === 'i' || input === 'I') {
|
|
694
|
+
runDelete('keep');
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
698
|
+
cancel();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
if (input === 'y' || input === 'Y') {
|
|
704
|
+
runDelete('cascade');
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
708
|
+
cancel();
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
656
714
|
if (mode === 'context-filter') {
|
|
657
715
|
if (key.escape) {
|
|
658
716
|
setContextSelectIndex(0);
|
|
@@ -950,6 +1008,20 @@ export function GtdMario({ onOpenSettings }) {
|
|
|
950
1008
|
setMode('select-project');
|
|
951
1009
|
return;
|
|
952
1010
|
}
|
|
1011
|
+
// Delete project (on projects tab)
|
|
1012
|
+
if (input === 'D' && currentTab === 'projects') {
|
|
1013
|
+
const project = task;
|
|
1014
|
+
const db = getDb();
|
|
1015
|
+
db.select()
|
|
1016
|
+
.from(schema.tasks)
|
|
1017
|
+
.where(eq(schema.tasks.parentId, project.id))
|
|
1018
|
+
.then((children) => {
|
|
1019
|
+
setProjectChildCount(children.length);
|
|
1020
|
+
setProjectToDelete(project);
|
|
1021
|
+
setMode('confirm-delete-project');
|
|
1022
|
+
});
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
953
1025
|
if (input === 'D' && currentTab !== 'projects') {
|
|
954
1026
|
setTaskToDelete(task);
|
|
955
1027
|
setMode('confirm-delete');
|
|
@@ -1073,7 +1145,9 @@ export function GtdMario({ onOpenSettings }) {
|
|
|
1073
1145
|
}) })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
|
|
1074
1146
|
const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
|
|
1075
1147
|
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '🍄 ' : ' ', label] }, ctx));
|
|
1076
|
-
}) })] })) : mode === 'set-effort' && currentTasks.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsxs(MarioBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.set || 'Set effort for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((opt, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '🍄 ' : ' ', opt.label] }, opt.value || 'clear'))) })] }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '🍄 ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) :
|
|
1148
|
+
}) })] })) : mode === 'set-effort' && currentTasks.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsxs(MarioBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.set || 'Set effort for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((opt, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '🍄 ' : ' ', opt.label] }, opt.value || 'clear'))) })] }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '🍄 ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : mode === 'confirm-delete-project' && projectToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: projectChildCount > 0
|
|
1149
|
+
? fmt(i18n.tui.deleteProjectChildren || 'Project "{title}" has {count} task(s). t=delete all i=keep tasks (→Inbox) n=cancel', { title: projectToDelete.title, count: projectChildCount })
|
|
1150
|
+
: fmt(i18n.tui.deleteProjectConfirm || 'Delete project "{title}"? (y/n)', { title: projectToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(MarioBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(MarioBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
1077
1151
|
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
1078
1152
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.textMuted, children: [isSelected ? '🍄 ' : ' ', "[", comment.createdAt.toLocaleString(), "]"] }), _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [' ', comment.content] })] }, comment.id));
|
|
1079
1153
|
})) }) })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(MarioBoxInline, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : 'STAGE', width: leftPaneWidth, minHeight: 8, isActive: paneFocus === 'tabs' && mode !== 'project-detail', children: mode === 'project-detail' ? (_jsx(Text, { color: theme.colors.textMuted, children: "\u2190 Esc/b: back" })) : (TABS.map((tab, index) => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ProjectDeleteMode } from '../../../db/projectDelete.js';
|
|
2
|
+
import type { UndoableCommand, SerializedCommand } from '../types.js';
|
|
3
|
+
import type { Task, Comment } from '../../../db/schema.js';
|
|
4
|
+
interface DeleteProjectParams {
|
|
5
|
+
project: Task;
|
|
6
|
+
mode: ProjectDeleteMode;
|
|
7
|
+
description: string;
|
|
8
|
+
deletedTasks?: Task[];
|
|
9
|
+
movedTasks?: Task[];
|
|
10
|
+
deletedComments?: Comment[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Command to delete a project, handling its child tasks via `mode`
|
|
14
|
+
* (cascade = delete children too, keep = move children back to Inbox).
|
|
15
|
+
*/
|
|
16
|
+
export declare class DeleteProjectCommand implements UndoableCommand {
|
|
17
|
+
readonly description: string;
|
|
18
|
+
private readonly project;
|
|
19
|
+
private readonly mode;
|
|
20
|
+
private deletedTasks;
|
|
21
|
+
private movedTasks;
|
|
22
|
+
private deletedComments;
|
|
23
|
+
constructor(params: DeleteProjectParams);
|
|
24
|
+
execute(): Promise<void>;
|
|
25
|
+
undo(): Promise<void>;
|
|
26
|
+
toJSON(): SerializedCommand;
|
|
27
|
+
static fromJSON(json: {
|
|
28
|
+
data: Record<string, unknown>;
|
|
29
|
+
}): DeleteProjectCommand;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { getDb, schema } from '../../../db/index.js';
|
|
3
|
+
import { deleteProject } from '../../../db/projectDelete.js';
|
|
4
|
+
function taskToValues(task) {
|
|
5
|
+
return {
|
|
6
|
+
id: task.id,
|
|
7
|
+
title: task.title,
|
|
8
|
+
description: task.description,
|
|
9
|
+
status: task.status,
|
|
10
|
+
isProject: task.isProject,
|
|
11
|
+
parentId: task.parentId,
|
|
12
|
+
waitingFor: task.waitingFor,
|
|
13
|
+
context: task.context,
|
|
14
|
+
isFocused: task.isFocused,
|
|
15
|
+
effort: task.effort,
|
|
16
|
+
dueDate: task.dueDate,
|
|
17
|
+
completedAt: task.completedAt,
|
|
18
|
+
createdAt: task.createdAt,
|
|
19
|
+
updatedAt: task.updatedAt,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function serializeTask(task) {
|
|
23
|
+
return {
|
|
24
|
+
...task,
|
|
25
|
+
dueDate: task.dueDate ? task.dueDate.toISOString() : null,
|
|
26
|
+
completedAt: task.completedAt ? task.completedAt.toISOString() : null,
|
|
27
|
+
createdAt: task.createdAt.toISOString(),
|
|
28
|
+
updatedAt: task.updatedAt.toISOString(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function deserializeTask(data) {
|
|
32
|
+
return {
|
|
33
|
+
...data,
|
|
34
|
+
dueDate: data.dueDate ? new Date(data.dueDate) : null,
|
|
35
|
+
completedAt: data.completedAt ? new Date(data.completedAt) : null,
|
|
36
|
+
createdAt: new Date(data.createdAt),
|
|
37
|
+
updatedAt: new Date(data.updatedAt),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Command to delete a project, handling its child tasks via `mode`
|
|
42
|
+
* (cascade = delete children too, keep = move children back to Inbox).
|
|
43
|
+
*/
|
|
44
|
+
export class DeleteProjectCommand {
|
|
45
|
+
description;
|
|
46
|
+
project;
|
|
47
|
+
mode;
|
|
48
|
+
deletedTasks = [];
|
|
49
|
+
movedTasks = [];
|
|
50
|
+
deletedComments = [];
|
|
51
|
+
constructor(params) {
|
|
52
|
+
this.project = params.project;
|
|
53
|
+
this.mode = params.mode;
|
|
54
|
+
this.description = params.description;
|
|
55
|
+
if (params.deletedTasks)
|
|
56
|
+
this.deletedTasks = params.deletedTasks;
|
|
57
|
+
if (params.movedTasks)
|
|
58
|
+
this.movedTasks = params.movedTasks;
|
|
59
|
+
if (params.deletedComments)
|
|
60
|
+
this.deletedComments = params.deletedComments;
|
|
61
|
+
}
|
|
62
|
+
async execute() {
|
|
63
|
+
const result = await deleteProject(this.project, this.mode);
|
|
64
|
+
this.deletedTasks = result.deletedTasks;
|
|
65
|
+
this.movedTasks = result.movedTasks;
|
|
66
|
+
this.deletedComments = result.deletedComments;
|
|
67
|
+
}
|
|
68
|
+
async undo() {
|
|
69
|
+
const db = getDb();
|
|
70
|
+
// Restore the project.
|
|
71
|
+
await db.insert(schema.tasks).values(taskToValues(this.project));
|
|
72
|
+
if (this.mode === 'cascade') {
|
|
73
|
+
// Re-create deleted child tasks.
|
|
74
|
+
for (const task of this.deletedTasks) {
|
|
75
|
+
await db.insert(schema.tasks).values(taskToValues(task));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Re-link moved children and restore their original status.
|
|
80
|
+
for (const task of this.movedTasks) {
|
|
81
|
+
await db
|
|
82
|
+
.update(schema.tasks)
|
|
83
|
+
.set({
|
|
84
|
+
parentId: task.parentId,
|
|
85
|
+
status: task.status,
|
|
86
|
+
updatedAt: task.updatedAt,
|
|
87
|
+
})
|
|
88
|
+
.where(eq(schema.tasks.id, task.id));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Restore deleted comments.
|
|
92
|
+
for (const comment of this.deletedComments) {
|
|
93
|
+
await db.insert(schema.comments).values({
|
|
94
|
+
id: comment.id,
|
|
95
|
+
taskId: comment.taskId,
|
|
96
|
+
content: comment.content,
|
|
97
|
+
createdAt: comment.createdAt,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
toJSON() {
|
|
102
|
+
return {
|
|
103
|
+
type: 'delete_project',
|
|
104
|
+
data: {
|
|
105
|
+
project: serializeTask(this.project),
|
|
106
|
+
mode: this.mode,
|
|
107
|
+
deletedTasks: this.deletedTasks.map(serializeTask),
|
|
108
|
+
movedTasks: this.movedTasks.map(serializeTask),
|
|
109
|
+
deletedComments: this.deletedComments.map((c) => ({
|
|
110
|
+
...c,
|
|
111
|
+
createdAt: c.createdAt.toISOString(),
|
|
112
|
+
})),
|
|
113
|
+
description: this.description,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
static fromJSON(json) {
|
|
118
|
+
const deletedTasksData = json.data.deletedTasks || [];
|
|
119
|
+
const movedTasksData = json.data.movedTasks || [];
|
|
120
|
+
const commentsData = json.data.deletedComments || [];
|
|
121
|
+
return new DeleteProjectCommand({
|
|
122
|
+
project: deserializeTask(json.data.project),
|
|
123
|
+
mode: json.data.mode,
|
|
124
|
+
deletedTasks: deletedTasksData.map(deserializeTask),
|
|
125
|
+
movedTasks: movedTasksData.map(deserializeTask),
|
|
126
|
+
deletedComments: commentsData.map((c) => ({
|
|
127
|
+
...c,
|
|
128
|
+
createdAt: new Date(c.createdAt),
|
|
129
|
+
})),
|
|
130
|
+
description: json.data.description,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { CreateTaskCommand } from './CreateTaskCommand.js';
|
|
2
2
|
export { DeleteTaskCommand } from './DeleteTaskCommand.js';
|
|
3
|
+
export { DeleteProjectCommand } from './DeleteProjectCommand.js';
|
|
3
4
|
export { MoveTaskCommand } from './MoveTaskCommand.js';
|
|
4
5
|
export { LinkTaskCommand } from './LinkTaskCommand.js';
|
|
5
6
|
export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { CreateTaskCommand } from './CreateTaskCommand.js';
|
|
2
2
|
export { DeleteTaskCommand } from './DeleteTaskCommand.js';
|
|
3
|
+
export { DeleteProjectCommand } from './DeleteProjectCommand.js';
|
|
3
4
|
export { MoveTaskCommand } from './MoveTaskCommand.js';
|
|
4
5
|
export { LinkTaskCommand } from './LinkTaskCommand.js';
|
|
5
6
|
export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CreateTaskCommand } from './CreateTaskCommand.js';
|
|
2
2
|
import { DeleteTaskCommand } from './DeleteTaskCommand.js';
|
|
3
|
+
import { DeleteProjectCommand } from './DeleteProjectCommand.js';
|
|
3
4
|
import { MoveTaskCommand } from './MoveTaskCommand.js';
|
|
4
5
|
import { LinkTaskCommand } from './LinkTaskCommand.js';
|
|
5
6
|
import { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
|
|
@@ -11,6 +12,7 @@ import { SetEffortCommand } from './SetEffortCommand.js';
|
|
|
11
12
|
const registry = {
|
|
12
13
|
'create_task': (data) => CreateTaskCommand.fromJSON({ data }),
|
|
13
14
|
'delete_task': (data) => DeleteTaskCommand.fromJSON({ data }),
|
|
15
|
+
'delete_project': (data) => DeleteProjectCommand.fromJSON({ data }),
|
|
14
16
|
'move_task': (data) => MoveTaskCommand.fromJSON({ data }),
|
|
15
17
|
'link_task': (data) => LinkTaskCommand.fromJSON({ data }),
|
|
16
18
|
'convert_to_project': (data) => ConvertToProjectCommand.fromJSON({ data }),
|
|
@@ -3,4 +3,4 @@ export { MAX_HISTORY_SIZE } from './types.js';
|
|
|
3
3
|
export { HistoryManager, getHistoryManager } from './HistoryManager.js';
|
|
4
4
|
export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
|
|
5
5
|
export { useHistory } from './useHistory.js';
|
|
6
|
-
export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
|
|
6
|
+
export { CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
|
package/dist/ui/history/index.js
CHANGED
|
@@ -5,4 +5,4 @@ export { HistoryManager, getHistoryManager } from './HistoryManager.js';
|
|
|
5
5
|
export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
|
|
6
6
|
export { useHistory } from './useHistory.js';
|
|
7
7
|
// Commands
|
|
8
|
-
export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
|
|
8
|
+
export { CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "floq",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Floq - Getting Things Done Task Manager with MS-DOS style themes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -39,24 +39,25 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@inkjs/ui": "^2.0.0",
|
|
41
41
|
"@libsql/client": "^0.17.0",
|
|
42
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
43
|
"better-sqlite3": "^11.7.0",
|
|
44
44
|
"commander": "^13.1.0",
|
|
45
|
-
"drizzle-orm": "^0.
|
|
46
|
-
"googleapis": "^144.0.0",
|
|
45
|
+
"drizzle-orm": "^0.45.2",
|
|
47
46
|
"ical.js": "^2.1.0",
|
|
48
47
|
"ink": "^5.1.0",
|
|
49
48
|
"ink-text-input": "^6.0.0",
|
|
50
49
|
"open": "^10.1.0",
|
|
50
|
+
"qrcode": "^1.5.4",
|
|
51
51
|
"react": "^18.3.1",
|
|
52
|
-
"uuid": "^
|
|
52
|
+
"uuid": "^14.0.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/better-sqlite3": "^7.6.12",
|
|
56
56
|
"@types/node": "^22.10.7",
|
|
57
|
+
"@types/qrcode": "^1.5.6",
|
|
57
58
|
"@types/react": "^18.3.18",
|
|
58
59
|
"@types/uuid": "^10.0.0",
|
|
59
|
-
"drizzle-kit": "^0.31.
|
|
60
|
+
"drizzle-kit": "^0.31.10",
|
|
60
61
|
"typescript": "^5.7.3"
|
|
61
62
|
},
|
|
62
63
|
"overrides": {
|