floq 1.7.0 → 1.9.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/README.ja.md +24 -9
- package/README.md +24 -9
- package/dist/calendar/index.d.ts +4 -1
- package/dist/calendar/index.js +49 -41
- package/dist/calendar/oauth.js +5 -9
- package/dist/cli.js +43 -15
- package/dist/commands/calendar.d.ts +12 -12
- package/dist/commands/calendar.js +196 -87
- package/dist/commands/config.js +5 -13
- package/dist/commands/project.d.ts +6 -0
- package/dist/commands/project.js +95 -0
- package/dist/commands/schedule.d.ts +6 -0
- package/dist/commands/schedule.js +109 -0
- package/dist/config.d.ts +19 -5
- package/dist/config.js +101 -23
- 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/CalendarModal.js +8 -7
- 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 +5 -6
|
@@ -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.9.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,18 +39,17 @@
|
|
|
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",
|
|
51
50
|
"qrcode": "^1.5.4",
|
|
52
51
|
"react": "^18.3.1",
|
|
53
|
-
"uuid": "^
|
|
52
|
+
"uuid": "^14.0.0"
|
|
54
53
|
},
|
|
55
54
|
"devDependencies": {
|
|
56
55
|
"@types/better-sqlite3": "^7.6.12",
|
|
@@ -58,7 +57,7 @@
|
|
|
58
57
|
"@types/qrcode": "^1.5.6",
|
|
59
58
|
"@types/react": "^18.3.18",
|
|
60
59
|
"@types/uuid": "^10.0.0",
|
|
61
|
-
"drizzle-kit": "^0.31.
|
|
60
|
+
"drizzle-kit": "^0.31.10",
|
|
62
61
|
"typescript": "^5.7.3"
|
|
63
62
|
},
|
|
64
63
|
"overrides": {
|