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.
Files changed (36) hide show
  1. package/README.ja.md +4 -0
  2. package/README.md +4 -0
  3. package/dist/i18n/en.d.ts +16 -0
  4. package/dist/i18n/en.js +9 -0
  5. package/dist/i18n/ja.js +9 -0
  6. package/dist/ui/App.js +116 -58
  7. package/dist/ui/SplashScreen.js +1 -1
  8. package/dist/ui/components/HelpModal.js +3 -2
  9. package/dist/ui/components/KanbanBoard.js +100 -40
  10. package/dist/ui/history/HistoryContext.d.ts +29 -0
  11. package/dist/ui/history/HistoryContext.js +44 -0
  12. package/dist/ui/history/HistoryManager.d.ts +56 -0
  13. package/dist/ui/history/HistoryManager.js +137 -0
  14. package/dist/ui/history/commands/ConvertToProjectCommand.d.ts +19 -0
  15. package/dist/ui/history/commands/ConvertToProjectCommand.js +37 -0
  16. package/dist/ui/history/commands/CreateCommentCommand.d.ts +18 -0
  17. package/dist/ui/history/commands/CreateCommentCommand.js +23 -0
  18. package/dist/ui/history/commands/CreateTaskCommand.d.ts +18 -0
  19. package/dist/ui/history/commands/CreateTaskCommand.js +24 -0
  20. package/dist/ui/history/commands/DeleteCommentCommand.d.ts +17 -0
  21. package/dist/ui/history/commands/DeleteCommentCommand.js +26 -0
  22. package/dist/ui/history/commands/DeleteTaskCommand.d.ts +18 -0
  23. package/dist/ui/history/commands/DeleteTaskCommand.js +51 -0
  24. package/dist/ui/history/commands/LinkTaskCommand.d.ts +20 -0
  25. package/dist/ui/history/commands/LinkTaskCommand.js +37 -0
  26. package/dist/ui/history/commands/MoveTaskCommand.d.ts +25 -0
  27. package/dist/ui/history/commands/MoveTaskCommand.js +43 -0
  28. package/dist/ui/history/commands/index.d.ts +7 -0
  29. package/dist/ui/history/commands/index.js +7 -0
  30. package/dist/ui/history/index.d.ts +6 -0
  31. package/dist/ui/history/index.js +8 -0
  32. package/dist/ui/history/types.d.ts +26 -0
  33. package/dist/ui/history/types.js +4 -0
  34. package/dist/ui/history/useHistory.d.ts +24 -0
  35. package/dist/ui/history/useHistory.js +31 -0
  36. 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 db = getDb();
91
- await db.insert(schema.comments).values({
92
- id: uuidv4(),
93
- taskId: task.id,
94
- content: content.trim(),
95
- createdAt: new Date(),
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 db = getDb();
102
- await db.delete(schema.comments).where(eq(schema.comments.id, comment.id));
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 db = getDb();
110
- await db.update(schema.tasks)
111
- .set({ parentId: project.id, updatedAt: new Date() })
112
- .where(eq(schema.tasks.id, task.id));
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
- await db.insert(schema.tasks)
173
- .values({
174
- id: uuidv4(),
175
- title: title.trim(),
176
- status: 'inbox', // New tasks go to inbox (which maps to TODO)
177
- createdAt: now,
178
- updatedAt: now,
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
- await db.update(schema.tasks)
229
- .set({ status: newStatus, waitingFor: null, updatedAt: new Date() })
230
- .where(eq(schema.tasks.id, task.id));
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
- await db.update(schema.tasks)
251
- .set({ status: newStatus, waitingFor: null, updatedAt: new Date() })
252
- .where(eq(schema.tasks.id, task.id));
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 db = getDb();
258
- await db.update(schema.tasks)
259
- .set({ status: 'done', updatedAt: new Date() })
260
- .where(eq(schema.tasks.id, task.id));
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
+ }