floq 0.2.3 → 0.3.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 +4 -0
- package/README.md +4 -0
- package/dist/i18n/en.d.ts +16 -0
- package/dist/i18n/en.js +9 -0
- package/dist/i18n/ja.js +9 -0
- package/dist/ui/App.js +116 -58
- package/dist/ui/SplashScreen.js +1 -1
- package/dist/ui/components/HelpModal.js +3 -2
- package/dist/ui/components/KanbanBoard.js +100 -40
- package/dist/ui/history/HistoryContext.d.ts +29 -0
- package/dist/ui/history/HistoryContext.js +44 -0
- package/dist/ui/history/HistoryManager.d.ts +56 -0
- package/dist/ui/history/HistoryManager.js +137 -0
- package/dist/ui/history/commands/ConvertToProjectCommand.d.ts +19 -0
- package/dist/ui/history/commands/ConvertToProjectCommand.js +37 -0
- package/dist/ui/history/commands/CreateCommentCommand.d.ts +18 -0
- package/dist/ui/history/commands/CreateCommentCommand.js +23 -0
- package/dist/ui/history/commands/CreateTaskCommand.d.ts +18 -0
- package/dist/ui/history/commands/CreateTaskCommand.js +24 -0
- package/dist/ui/history/commands/DeleteCommentCommand.d.ts +17 -0
- package/dist/ui/history/commands/DeleteCommentCommand.js +26 -0
- package/dist/ui/history/commands/DeleteTaskCommand.d.ts +18 -0
- package/dist/ui/history/commands/DeleteTaskCommand.js +51 -0
- package/dist/ui/history/commands/LinkTaskCommand.d.ts +20 -0
- package/dist/ui/history/commands/LinkTaskCommand.js +37 -0
- package/dist/ui/history/commands/MoveTaskCommand.d.ts +25 -0
- package/dist/ui/history/commands/MoveTaskCommand.js +43 -0
- package/dist/ui/history/commands/index.d.ts +7 -0
- package/dist/ui/history/commands/index.js +7 -0
- package/dist/ui/history/index.d.ts +6 -0
- package/dist/ui/history/index.js +8 -0
- package/dist/ui/history/types.d.ts +26 -0
- package/dist/ui/history/types.js +4 -0
- package/dist/ui/history/useHistory.d.ts +24 -0
- package/dist/ui/history/useHistory.js +31 -0
- package/package.json +1 -1
|
@@ -14,10 +14,12 @@ import { t, fmt } from '../../i18n/index.js';
|
|
|
14
14
|
import { useTheme } from '../theme/index.js';
|
|
15
15
|
import { isTursoEnabled } from '../../config.js';
|
|
16
16
|
import { VERSION } from '../../version.js';
|
|
17
|
+
import { useHistory, CreateTaskCommand, MoveTaskCommand, LinkTaskCommand, CreateCommentCommand, DeleteCommentCommand, } from '../history/index.js';
|
|
17
18
|
const COLUMNS = ['todo', 'doing', 'done'];
|
|
18
19
|
export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
19
20
|
const theme = useTheme();
|
|
20
21
|
const { exit } = useApp();
|
|
22
|
+
const history = useHistory();
|
|
21
23
|
const [mode, setMode] = useState('normal');
|
|
22
24
|
const [inputValue, setInputValue] = useState('');
|
|
23
25
|
const [currentColumnIndex, setCurrentColumnIndex] = useState(0);
|
|
@@ -87,32 +89,42 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
87
89
|
setTaskComments(comments);
|
|
88
90
|
}, []);
|
|
89
91
|
const addCommentToTask = useCallback(async (task, content) => {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
const commentId = uuidv4();
|
|
93
|
+
const command = new CreateCommentCommand({
|
|
94
|
+
comment: {
|
|
95
|
+
id: commentId,
|
|
96
|
+
taskId: task.id,
|
|
97
|
+
content: content.trim(),
|
|
98
|
+
createdAt: new Date(),
|
|
99
|
+
},
|
|
100
|
+
description: i18n.tui.commentAdded || 'Comment added',
|
|
96
101
|
});
|
|
102
|
+
await history.execute(command);
|
|
97
103
|
setMessage(i18n.tui.commentAdded || 'Comment added');
|
|
98
104
|
await loadTaskComments(task.id);
|
|
99
|
-
}, [i18n.tui.commentAdded, loadTaskComments]);
|
|
105
|
+
}, [i18n.tui.commentAdded, loadTaskComments, history]);
|
|
100
106
|
const deleteComment = useCallback(async (comment) => {
|
|
101
|
-
const
|
|
102
|
-
|
|
107
|
+
const command = new DeleteCommentCommand({
|
|
108
|
+
comment,
|
|
109
|
+
description: i18n.tui.commentDeleted || 'Comment deleted',
|
|
110
|
+
});
|
|
111
|
+
await history.execute(command);
|
|
103
112
|
setMessage(i18n.tui.commentDeleted || 'Comment deleted');
|
|
104
113
|
if (selectedTask) {
|
|
105
114
|
await loadTaskComments(selectedTask.id);
|
|
106
115
|
}
|
|
107
|
-
}, [i18n.tui.commentDeleted, loadTaskComments, selectedTask]);
|
|
116
|
+
}, [i18n.tui.commentDeleted, loadTaskComments, selectedTask, history]);
|
|
108
117
|
const linkTaskToProject = useCallback(async (task, project) => {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.
|
|
118
|
+
const command = new LinkTaskCommand({
|
|
119
|
+
taskId: task.id,
|
|
120
|
+
fromParentId: task.parentId,
|
|
121
|
+
toParentId: project.id,
|
|
122
|
+
description: fmt(i18n.tui.linkedToProject || 'Linked "{title}" to {project}', { title: task.title, project: project.title }),
|
|
123
|
+
});
|
|
124
|
+
await history.execute(command);
|
|
113
125
|
setMessage(fmt(i18n.tui.linkedToProject || 'Linked "{title}" to {project}', { title: task.title, project: project.title }));
|
|
114
126
|
await loadTasks();
|
|
115
|
-
}, [i18n.tui.linkedToProject, loadTasks]);
|
|
127
|
+
}, [i18n.tui.linkedToProject, loadTasks, history]);
|
|
116
128
|
const currentColumn = COLUMNS[currentColumnIndex];
|
|
117
129
|
const currentTasks = tasks[currentColumn];
|
|
118
130
|
const selectedTaskIndex = selectedTaskIndices[currentColumn];
|
|
@@ -167,19 +179,22 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
167
179
|
const addTask = useCallback(async (title) => {
|
|
168
180
|
if (!title.trim())
|
|
169
181
|
return;
|
|
170
|
-
const db = getDb();
|
|
171
182
|
const now = new Date();
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
const taskId = uuidv4();
|
|
184
|
+
const command = new CreateTaskCommand({
|
|
185
|
+
task: {
|
|
186
|
+
id: taskId,
|
|
187
|
+
title: title.trim(),
|
|
188
|
+
status: 'inbox', // New tasks go to inbox (which maps to TODO)
|
|
189
|
+
createdAt: now,
|
|
190
|
+
updatedAt: now,
|
|
191
|
+
},
|
|
192
|
+
description: fmt(i18n.tui.added, { title: title.trim() }),
|
|
179
193
|
});
|
|
194
|
+
await history.execute(command);
|
|
180
195
|
setMessage(fmt(i18n.tui.added, { title: title.trim() }));
|
|
181
196
|
await loadTasks();
|
|
182
|
-
}, [i18n.tui.added, loadTasks]);
|
|
197
|
+
}, [i18n.tui.added, loadTasks, history]);
|
|
183
198
|
const handleInputSubmit = async (value) => {
|
|
184
199
|
if (mode === 'add-comment' && selectedTask) {
|
|
185
200
|
if (value.trim()) {
|
|
@@ -210,7 +225,6 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
210
225
|
setMode('normal');
|
|
211
226
|
};
|
|
212
227
|
const moveTaskRight = useCallback(async (task) => {
|
|
213
|
-
const db = getDb();
|
|
214
228
|
let newStatus;
|
|
215
229
|
// Determine new status based on current status
|
|
216
230
|
if (task.status === 'inbox' || task.status === 'someday') {
|
|
@@ -225,14 +239,19 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
225
239
|
// Already done, do nothing
|
|
226
240
|
return;
|
|
227
241
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
242
|
+
const command = new MoveTaskCommand({
|
|
243
|
+
taskId: task.id,
|
|
244
|
+
fromStatus: task.status,
|
|
245
|
+
toStatus: newStatus,
|
|
246
|
+
fromWaitingFor: task.waitingFor,
|
|
247
|
+
toWaitingFor: null,
|
|
248
|
+
description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[newStatus] }),
|
|
249
|
+
});
|
|
250
|
+
await history.execute(command);
|
|
231
251
|
setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[newStatus] }));
|
|
232
252
|
await loadTasks();
|
|
233
|
-
}, [i18n.tui.movedTo, i18n.status, loadTasks]);
|
|
253
|
+
}, [i18n.tui.movedTo, i18n.status, loadTasks, history]);
|
|
234
254
|
const moveTaskLeft = useCallback(async (task) => {
|
|
235
|
-
const db = getDb();
|
|
236
255
|
let newStatus;
|
|
237
256
|
// Determine new status based on current status
|
|
238
257
|
if (task.status === 'next' || task.status === 'waiting') {
|
|
@@ -247,20 +266,31 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
247
266
|
// Already in TODO, do nothing
|
|
248
267
|
return;
|
|
249
268
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
269
|
+
const command = new MoveTaskCommand({
|
|
270
|
+
taskId: task.id,
|
|
271
|
+
fromStatus: task.status,
|
|
272
|
+
toStatus: newStatus,
|
|
273
|
+
fromWaitingFor: task.waitingFor,
|
|
274
|
+
toWaitingFor: null,
|
|
275
|
+
description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[newStatus] }),
|
|
276
|
+
});
|
|
277
|
+
await history.execute(command);
|
|
253
278
|
setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[newStatus] }));
|
|
254
279
|
await loadTasks();
|
|
255
|
-
}, [i18n.tui.movedTo, i18n.status, loadTasks]);
|
|
280
|
+
}, [i18n.tui.movedTo, i18n.status, loadTasks, history]);
|
|
256
281
|
const markTaskDone = useCallback(async (task) => {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
.
|
|
260
|
-
|
|
282
|
+
const command = new MoveTaskCommand({
|
|
283
|
+
taskId: task.id,
|
|
284
|
+
fromStatus: task.status,
|
|
285
|
+
toStatus: 'done',
|
|
286
|
+
fromWaitingFor: task.waitingFor,
|
|
287
|
+
toWaitingFor: null,
|
|
288
|
+
description: fmt(i18n.tui.completed, { title: task.title }),
|
|
289
|
+
});
|
|
290
|
+
await history.execute(command);
|
|
261
291
|
setMessage(fmt(i18n.tui.completed, { title: task.title }));
|
|
262
292
|
await loadTasks();
|
|
263
|
-
}, [i18n.tui.completed, loadTasks]);
|
|
293
|
+
}, [i18n.tui.completed, loadTasks, history]);
|
|
264
294
|
const getColumnLabel = (column) => {
|
|
265
295
|
return i18n.kanban[column];
|
|
266
296
|
};
|
|
@@ -500,11 +530,41 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
500
530
|
return;
|
|
501
531
|
}
|
|
502
532
|
// Refresh
|
|
503
|
-
if (input === 'r') {
|
|
533
|
+
if (input === 'r' && !(key.ctrl)) {
|
|
504
534
|
loadTasks();
|
|
505
535
|
setMessage(i18n.tui.refreshed);
|
|
506
536
|
return;
|
|
507
537
|
}
|
|
538
|
+
// Undo (u key) - only in normal mode
|
|
539
|
+
if (input === 'u' && mode === 'normal') {
|
|
540
|
+
history.undo().then((didUndo) => {
|
|
541
|
+
if (didUndo) {
|
|
542
|
+
setMessage(fmt(i18n.tui.undone, { action: history.undoDescription || '' }));
|
|
543
|
+
loadTasks();
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
setMessage(i18n.tui.nothingToUndo);
|
|
547
|
+
}
|
|
548
|
+
}).catch(() => {
|
|
549
|
+
setMessage(i18n.tui.undoFailed);
|
|
550
|
+
});
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
// Redo (Ctrl+r) - only in normal mode
|
|
554
|
+
if (key.ctrl && input === 'r' && mode === 'normal') {
|
|
555
|
+
history.redo().then((didRedo) => {
|
|
556
|
+
if (didRedo) {
|
|
557
|
+
setMessage(fmt(i18n.tui.redone, { action: history.redoDescription || '' }));
|
|
558
|
+
loadTasks();
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
setMessage(i18n.tui.nothingToRedo);
|
|
562
|
+
}
|
|
563
|
+
}).catch(() => {
|
|
564
|
+
setMessage(i18n.tui.redoFailed);
|
|
565
|
+
});
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
508
568
|
});
|
|
509
569
|
// Help modal overlay
|
|
510
570
|
if (mode === 'help') {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { UndoableCommand, HistoryState } from './types.js';
|
|
3
|
+
interface HistoryContextValue {
|
|
4
|
+
/** Execute a command and add to history */
|
|
5
|
+
execute: (command: UndoableCommand) => Promise<void>;
|
|
6
|
+
/** Undo the last command */
|
|
7
|
+
undo: () => Promise<boolean>;
|
|
8
|
+
/** Redo the last undone command */
|
|
9
|
+
redo: () => Promise<boolean>;
|
|
10
|
+
/** Check if undo is available */
|
|
11
|
+
canUndo: boolean;
|
|
12
|
+
/** Check if redo is available */
|
|
13
|
+
canRedo: boolean;
|
|
14
|
+
/** Get the current history state */
|
|
15
|
+
state: HistoryState;
|
|
16
|
+
/** Description of command that would be undone */
|
|
17
|
+
undoDescription: string | null;
|
|
18
|
+
/** Description of command that would be redone */
|
|
19
|
+
redoDescription: string | null;
|
|
20
|
+
}
|
|
21
|
+
interface HistoryProviderProps {
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
export declare function HistoryProvider({ children }: HistoryProviderProps): React.ReactElement;
|
|
25
|
+
/**
|
|
26
|
+
* Hook to access history context
|
|
27
|
+
*/
|
|
28
|
+
export declare function useHistoryContext(): HistoryContextValue;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react';
|
|
3
|
+
import { getHistoryManager } from './HistoryManager.js';
|
|
4
|
+
const HistoryContext = createContext(null);
|
|
5
|
+
export function HistoryProvider({ children }) {
|
|
6
|
+
const manager = useMemo(() => getHistoryManager(), []);
|
|
7
|
+
const [state, setState] = useState(manager.getState());
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const unsubscribe = manager.subscribe(() => {
|
|
10
|
+
setState(manager.getState());
|
|
11
|
+
});
|
|
12
|
+
return unsubscribe;
|
|
13
|
+
}, [manager]);
|
|
14
|
+
const execute = useCallback(async (command) => {
|
|
15
|
+
await manager.execute(command);
|
|
16
|
+
}, [manager]);
|
|
17
|
+
const undo = useCallback(async () => {
|
|
18
|
+
return manager.undo();
|
|
19
|
+
}, [manager]);
|
|
20
|
+
const redo = useCallback(async () => {
|
|
21
|
+
return manager.redo();
|
|
22
|
+
}, [manager]);
|
|
23
|
+
const value = useMemo(() => ({
|
|
24
|
+
execute,
|
|
25
|
+
undo,
|
|
26
|
+
redo,
|
|
27
|
+
canUndo: manager.canUndo(),
|
|
28
|
+
canRedo: manager.canRedo(),
|
|
29
|
+
state,
|
|
30
|
+
undoDescription: manager.getUndoDescription(),
|
|
31
|
+
redoDescription: manager.getRedoDescription(),
|
|
32
|
+
}), [execute, undo, redo, state, manager]);
|
|
33
|
+
return _jsx(HistoryContext.Provider, { value: value, children: children });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Hook to access history context
|
|
37
|
+
*/
|
|
38
|
+
export function useHistoryContext() {
|
|
39
|
+
const context = useContext(HistoryContext);
|
|
40
|
+
if (!context) {
|
|
41
|
+
throw new Error('useHistoryContext must be used within a HistoryProvider');
|
|
42
|
+
}
|
|
43
|
+
return context;
|
|
44
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { UndoableCommand, HistoryState } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Manages undo/redo history using the Command Pattern
|
|
4
|
+
*/
|
|
5
|
+
export declare class HistoryManager {
|
|
6
|
+
private undoStack;
|
|
7
|
+
private redoStack;
|
|
8
|
+
private listeners;
|
|
9
|
+
/**
|
|
10
|
+
* Execute a command and add it to the undo stack
|
|
11
|
+
*/
|
|
12
|
+
execute(command: UndoableCommand): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Undo the last command
|
|
15
|
+
* @returns true if undo was performed, false if nothing to undo
|
|
16
|
+
*/
|
|
17
|
+
undo(): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Redo the last undone command
|
|
20
|
+
* @returns true if redo was performed, false if nothing to redo
|
|
21
|
+
*/
|
|
22
|
+
redo(): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Get the current history state
|
|
25
|
+
*/
|
|
26
|
+
getState(): HistoryState;
|
|
27
|
+
/**
|
|
28
|
+
* Check if undo is available
|
|
29
|
+
*/
|
|
30
|
+
canUndo(): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Check if redo is available
|
|
33
|
+
*/
|
|
34
|
+
canRedo(): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Get description of the command that would be undone
|
|
37
|
+
*/
|
|
38
|
+
getUndoDescription(): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Get description of the command that would be redone
|
|
41
|
+
*/
|
|
42
|
+
getRedoDescription(): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Clear all history
|
|
45
|
+
*/
|
|
46
|
+
clear(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Subscribe to history changes
|
|
49
|
+
*/
|
|
50
|
+
subscribe(listener: () => void): () => void;
|
|
51
|
+
private notifyListeners;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the singleton HistoryManager instance
|
|
55
|
+
*/
|
|
56
|
+
export declare function getHistoryManager(): HistoryManager;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { MAX_HISTORY_SIZE } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Manages undo/redo history using the Command Pattern
|
|
4
|
+
*/
|
|
5
|
+
export class HistoryManager {
|
|
6
|
+
undoStack = [];
|
|
7
|
+
redoStack = [];
|
|
8
|
+
listeners = new Set();
|
|
9
|
+
/**
|
|
10
|
+
* Execute a command and add it to the undo stack
|
|
11
|
+
*/
|
|
12
|
+
async execute(command) {
|
|
13
|
+
await command.execute();
|
|
14
|
+
// Add to undo stack
|
|
15
|
+
this.undoStack.push(command);
|
|
16
|
+
// Clear redo stack (new action invalidates redo history)
|
|
17
|
+
this.redoStack = [];
|
|
18
|
+
// Enforce max history size
|
|
19
|
+
if (this.undoStack.length > MAX_HISTORY_SIZE) {
|
|
20
|
+
this.undoStack.shift();
|
|
21
|
+
}
|
|
22
|
+
this.notifyListeners();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Undo the last command
|
|
26
|
+
* @returns true if undo was performed, false if nothing to undo
|
|
27
|
+
*/
|
|
28
|
+
async undo() {
|
|
29
|
+
const command = this.undoStack.pop();
|
|
30
|
+
if (!command) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await command.undo();
|
|
35
|
+
this.redoStack.push(command);
|
|
36
|
+
this.notifyListeners();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
// Re-add command to undo stack if undo fails
|
|
41
|
+
this.undoStack.push(command);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Redo the last undone command
|
|
47
|
+
* @returns true if redo was performed, false if nothing to redo
|
|
48
|
+
*/
|
|
49
|
+
async redo() {
|
|
50
|
+
const command = this.redoStack.pop();
|
|
51
|
+
if (!command) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
await command.execute();
|
|
56
|
+
this.undoStack.push(command);
|
|
57
|
+
this.notifyListeners();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Re-add command to redo stack if redo fails
|
|
62
|
+
this.redoStack.push(command);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the current history state
|
|
68
|
+
*/
|
|
69
|
+
getState() {
|
|
70
|
+
return {
|
|
71
|
+
undoCount: this.undoStack.length,
|
|
72
|
+
redoCount: this.redoStack.length,
|
|
73
|
+
lastCommandDescription: this.undoStack.length > 0
|
|
74
|
+
? this.undoStack[this.undoStack.length - 1].description
|
|
75
|
+
: null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if undo is available
|
|
80
|
+
*/
|
|
81
|
+
canUndo() {
|
|
82
|
+
return this.undoStack.length > 0;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check if redo is available
|
|
86
|
+
*/
|
|
87
|
+
canRedo() {
|
|
88
|
+
return this.redoStack.length > 0;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get description of the command that would be undone
|
|
92
|
+
*/
|
|
93
|
+
getUndoDescription() {
|
|
94
|
+
return this.undoStack.length > 0
|
|
95
|
+
? this.undoStack[this.undoStack.length - 1].description
|
|
96
|
+
: null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get description of the command that would be redone
|
|
100
|
+
*/
|
|
101
|
+
getRedoDescription() {
|
|
102
|
+
return this.redoStack.length > 0
|
|
103
|
+
? this.redoStack[this.redoStack.length - 1].description
|
|
104
|
+
: null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Clear all history
|
|
108
|
+
*/
|
|
109
|
+
clear() {
|
|
110
|
+
this.undoStack = [];
|
|
111
|
+
this.redoStack = [];
|
|
112
|
+
this.notifyListeners();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Subscribe to history changes
|
|
116
|
+
*/
|
|
117
|
+
subscribe(listener) {
|
|
118
|
+
this.listeners.add(listener);
|
|
119
|
+
return () => this.listeners.delete(listener);
|
|
120
|
+
}
|
|
121
|
+
notifyListeners() {
|
|
122
|
+
for (const listener of this.listeners) {
|
|
123
|
+
listener();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Singleton instance
|
|
128
|
+
let historyManagerInstance = null;
|
|
129
|
+
/**
|
|
130
|
+
* Get the singleton HistoryManager instance
|
|
131
|
+
*/
|
|
132
|
+
export function getHistoryManager() {
|
|
133
|
+
if (!historyManagerInstance) {
|
|
134
|
+
historyManagerInstance = new HistoryManager();
|
|
135
|
+
}
|
|
136
|
+
return historyManagerInstance;
|
|
137
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { UndoableCommand } from '../types.js';
|
|
2
|
+
import type { TaskStatus } from '../../../db/schema.js';
|
|
3
|
+
interface ConvertToProjectParams {
|
|
4
|
+
taskId: string;
|
|
5
|
+
originalStatus: TaskStatus;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Command to convert a task to a project
|
|
10
|
+
*/
|
|
11
|
+
export declare class ConvertToProjectCommand implements UndoableCommand {
|
|
12
|
+
readonly description: string;
|
|
13
|
+
private readonly taskId;
|
|
14
|
+
private readonly originalStatus;
|
|
15
|
+
constructor(params: ConvertToProjectParams);
|
|
16
|
+
execute(): Promise<void>;
|
|
17
|
+
undo(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { getDb, schema } from '../../../db/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Command to convert a task to a project
|
|
5
|
+
*/
|
|
6
|
+
export class ConvertToProjectCommand {
|
|
7
|
+
description;
|
|
8
|
+
taskId;
|
|
9
|
+
originalStatus;
|
|
10
|
+
constructor(params) {
|
|
11
|
+
this.taskId = params.taskId;
|
|
12
|
+
this.originalStatus = params.originalStatus;
|
|
13
|
+
this.description = params.description;
|
|
14
|
+
}
|
|
15
|
+
async execute() {
|
|
16
|
+
const db = getDb();
|
|
17
|
+
await db
|
|
18
|
+
.update(schema.tasks)
|
|
19
|
+
.set({
|
|
20
|
+
isProject: true,
|
|
21
|
+
status: 'next',
|
|
22
|
+
updatedAt: new Date(),
|
|
23
|
+
})
|
|
24
|
+
.where(eq(schema.tasks.id, this.taskId));
|
|
25
|
+
}
|
|
26
|
+
async undo() {
|
|
27
|
+
const db = getDb();
|
|
28
|
+
await db
|
|
29
|
+
.update(schema.tasks)
|
|
30
|
+
.set({
|
|
31
|
+
isProject: false,
|
|
32
|
+
status: this.originalStatus,
|
|
33
|
+
updatedAt: new Date(),
|
|
34
|
+
})
|
|
35
|
+
.where(eq(schema.tasks.id, this.taskId));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { UndoableCommand } from '../types.js';
|
|
2
|
+
import type { NewComment } from '../../../db/schema.js';
|
|
3
|
+
interface CreateCommentParams {
|
|
4
|
+
comment: NewComment;
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Command to create a new comment
|
|
9
|
+
*/
|
|
10
|
+
export declare class CreateCommentCommand implements UndoableCommand {
|
|
11
|
+
readonly description: string;
|
|
12
|
+
private readonly comment;
|
|
13
|
+
private createdCommentId;
|
|
14
|
+
constructor(params: CreateCommentParams);
|
|
15
|
+
execute(): Promise<void>;
|
|
16
|
+
undo(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { getDb, schema } from '../../../db/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Command to create a new comment
|
|
5
|
+
*/
|
|
6
|
+
export class CreateCommentCommand {
|
|
7
|
+
description;
|
|
8
|
+
comment;
|
|
9
|
+
createdCommentId;
|
|
10
|
+
constructor(params) {
|
|
11
|
+
this.comment = params.comment;
|
|
12
|
+
this.description = params.description;
|
|
13
|
+
this.createdCommentId = params.comment.id;
|
|
14
|
+
}
|
|
15
|
+
async execute() {
|
|
16
|
+
const db = getDb();
|
|
17
|
+
await db.insert(schema.comments).values(this.comment);
|
|
18
|
+
}
|
|
19
|
+
async undo() {
|
|
20
|
+
const db = getDb();
|
|
21
|
+
await db.delete(schema.comments).where(eq(schema.comments.id, this.createdCommentId));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { UndoableCommand } from '../types.js';
|
|
2
|
+
import type { NewTask } from '../../../db/schema.js';
|
|
3
|
+
interface CreateTaskParams {
|
|
4
|
+
task: NewTask;
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Command to create a new task
|
|
9
|
+
*/
|
|
10
|
+
export declare class CreateTaskCommand implements UndoableCommand {
|
|
11
|
+
readonly description: string;
|
|
12
|
+
private readonly task;
|
|
13
|
+
private createdTaskId;
|
|
14
|
+
constructor(params: CreateTaskParams);
|
|
15
|
+
execute(): Promise<void>;
|
|
16
|
+
undo(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { getDb, schema } from '../../../db/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Command to create a new task
|
|
5
|
+
*/
|
|
6
|
+
export class CreateTaskCommand {
|
|
7
|
+
description;
|
|
8
|
+
task;
|
|
9
|
+
createdTaskId;
|
|
10
|
+
constructor(params) {
|
|
11
|
+
this.task = params.task;
|
|
12
|
+
this.description = params.description;
|
|
13
|
+
this.createdTaskId = params.task.id;
|
|
14
|
+
}
|
|
15
|
+
async execute() {
|
|
16
|
+
const db = getDb();
|
|
17
|
+
await db.insert(schema.tasks).values(this.task);
|
|
18
|
+
}
|
|
19
|
+
async undo() {
|
|
20
|
+
const db = getDb();
|
|
21
|
+
// Delete the created task
|
|
22
|
+
await db.delete(schema.tasks).where(eq(schema.tasks.id, this.createdTaskId));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { UndoableCommand } from '../types.js';
|
|
2
|
+
import type { Comment } from '../../../db/schema.js';
|
|
3
|
+
interface DeleteCommentParams {
|
|
4
|
+
comment: Comment;
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Command to delete a comment
|
|
9
|
+
*/
|
|
10
|
+
export declare class DeleteCommentCommand implements UndoableCommand {
|
|
11
|
+
readonly description: string;
|
|
12
|
+
private readonly comment;
|
|
13
|
+
constructor(params: DeleteCommentParams);
|
|
14
|
+
execute(): Promise<void>;
|
|
15
|
+
undo(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { getDb, schema } from '../../../db/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Command to delete a comment
|
|
5
|
+
*/
|
|
6
|
+
export class DeleteCommentCommand {
|
|
7
|
+
description;
|
|
8
|
+
comment;
|
|
9
|
+
constructor(params) {
|
|
10
|
+
this.comment = params.comment;
|
|
11
|
+
this.description = params.description;
|
|
12
|
+
}
|
|
13
|
+
async execute() {
|
|
14
|
+
const db = getDb();
|
|
15
|
+
await db.delete(schema.comments).where(eq(schema.comments.id, this.comment.id));
|
|
16
|
+
}
|
|
17
|
+
async undo() {
|
|
18
|
+
const db = getDb();
|
|
19
|
+
await db.insert(schema.comments).values({
|
|
20
|
+
id: this.comment.id,
|
|
21
|
+
taskId: this.comment.taskId,
|
|
22
|
+
content: this.comment.content,
|
|
23
|
+
createdAt: this.comment.createdAt,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|