floq 1.4.1 → 1.6.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/cli.js +32 -2
- package/dist/commands/config.js +2 -1
- package/dist/commands/done.js +1 -0
- package/dist/commands/insights.js +13 -10
- package/dist/commands/move.js +2 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +11 -0
- package/dist/db/index.js +36 -0
- package/dist/db/schema.d.ts +116 -0
- package/dist/db/schema.js +8 -0
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/ja.js +1 -0
- package/dist/index.js +2 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +186 -0
- package/dist/ui/App.js +7 -4
- package/dist/ui/components/GtdDQ.js +4 -1
- package/dist/ui/components/GtdMario.js +4 -1
- package/dist/ui/components/InsightsModal.js +15 -8
- package/dist/ui/components/KanbanBoard.js +3 -0
- package/dist/ui/components/KanbanDQ.js +2 -0
- package/dist/ui/components/KanbanMario.js +2 -0
- package/dist/ui/components/TaskItem.d.ts +2 -1
- package/dist/ui/components/TaskItem.js +4 -3
- package/dist/ui/history/HistoryContext.js +8 -0
- package/dist/ui/history/HistoryManager.d.ts +9 -1
- package/dist/ui/history/HistoryManager.js +140 -16
- package/dist/ui/history/commands/ConvertToProjectCommand.d.ts +5 -1
- package/dist/ui/history/commands/ConvertToProjectCommand.js +13 -0
- package/dist/ui/history/commands/CreateCommentCommand.d.ts +5 -1
- package/dist/ui/history/commands/CreateCommentCommand.js +22 -0
- package/dist/ui/history/commands/CreateTaskCommand.d.ts +5 -1
- package/dist/ui/history/commands/CreateTaskCommand.js +26 -1
- package/dist/ui/history/commands/DeleteCommentCommand.d.ts +5 -1
- package/dist/ui/history/commands/DeleteCommentCommand.js +22 -0
- package/dist/ui/history/commands/DeleteTaskCommand.d.ts +7 -2
- package/dist/ui/history/commands/DeleteTaskCommand.js +41 -0
- package/dist/ui/history/commands/LinkTaskCommand.d.ts +5 -1
- package/dist/ui/history/commands/LinkTaskCommand.js +14 -0
- package/dist/ui/history/commands/MoveTaskCommand.d.ts +7 -1
- package/dist/ui/history/commands/MoveTaskCommand.js +25 -0
- package/dist/ui/history/commands/SetContextCommand.d.ts +5 -1
- package/dist/ui/history/commands/SetContextCommand.js +14 -0
- package/dist/ui/history/commands/SetEffortCommand.d.ts +5 -1
- package/dist/ui/history/commands/SetEffortCommand.js +14 -0
- package/dist/ui/history/commands/SetFocusCommand.d.ts +5 -1
- package/dist/ui/history/commands/SetFocusCommand.js +14 -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.d.ts +2 -0
- package/dist/ui/history/commands/registry.js +28 -0
- package/dist/ui/history/index.d.ts +2 -2
- package/dist/ui/history/index.js +1 -1
- package/dist/ui/history/types.d.ts +9 -0
- package/package.json +6 -5
|
@@ -1,23 +1,70 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { eq, desc, asc } from 'drizzle-orm';
|
|
3
|
+
import { getDb, schema } from '../../db/index.js';
|
|
4
|
+
import { deserializeCommand } from './commands/registry.js';
|
|
1
5
|
import { MAX_HISTORY_SIZE } from './types.js';
|
|
2
6
|
/**
|
|
3
7
|
* Manages undo/redo history using the Command Pattern
|
|
8
|
+
* with DB persistence for crash-safe undo
|
|
4
9
|
*/
|
|
5
10
|
export class HistoryManager {
|
|
6
11
|
undoStack = [];
|
|
7
12
|
redoStack = [];
|
|
8
13
|
listeners = new Set();
|
|
14
|
+
initialized = false;
|
|
15
|
+
/**
|
|
16
|
+
* Initialize by loading history from DB
|
|
17
|
+
*/
|
|
18
|
+
async init() {
|
|
19
|
+
if (this.initialized)
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
await this.loadFromDb();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// DB may not have the table yet - that's OK, just use in-memory
|
|
26
|
+
}
|
|
27
|
+
this.initialized = true;
|
|
28
|
+
}
|
|
9
29
|
/**
|
|
10
30
|
* Execute a command and add it to the undo stack
|
|
11
31
|
*/
|
|
12
32
|
async execute(command) {
|
|
13
33
|
await command.execute();
|
|
34
|
+
const dbId = uuidv4();
|
|
35
|
+
// Persist to DB
|
|
36
|
+
try {
|
|
37
|
+
const serialized = command.toJSON();
|
|
38
|
+
const db = getDb();
|
|
39
|
+
await db.insert(schema.operationHistory).values({
|
|
40
|
+
id: dbId,
|
|
41
|
+
commandType: serialized.type,
|
|
42
|
+
commandData: JSON.stringify(serialized.data),
|
|
43
|
+
executedAt: new Date(),
|
|
44
|
+
isUndone: false,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// If DB write fails, in-memory undo still works
|
|
49
|
+
}
|
|
14
50
|
// Add to undo stack
|
|
15
|
-
this.undoStack.push(command);
|
|
51
|
+
this.undoStack.push({ dbId, command });
|
|
16
52
|
// Clear redo stack (new action invalidates redo history)
|
|
53
|
+
// Also clean up DB records for redo items
|
|
54
|
+
await this.clearRedoFromDb();
|
|
17
55
|
this.redoStack = [];
|
|
18
56
|
// Enforce max history size
|
|
19
57
|
if (this.undoStack.length > MAX_HISTORY_SIZE) {
|
|
20
|
-
this.undoStack.shift();
|
|
58
|
+
const removed = this.undoStack.shift();
|
|
59
|
+
if (removed) {
|
|
60
|
+
try {
|
|
61
|
+
const db = getDb();
|
|
62
|
+
await db.delete(schema.operationHistory).where(eq(schema.operationHistory.id, removed.dbId));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// ignore
|
|
66
|
+
}
|
|
67
|
+
}
|
|
21
68
|
}
|
|
22
69
|
this.notifyListeners();
|
|
23
70
|
}
|
|
@@ -26,19 +73,29 @@ export class HistoryManager {
|
|
|
26
73
|
* @returns true if undo was performed, false if nothing to undo
|
|
27
74
|
*/
|
|
28
75
|
async undo() {
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
76
|
+
const tracked = this.undoStack.pop();
|
|
77
|
+
if (!tracked) {
|
|
31
78
|
return false;
|
|
32
79
|
}
|
|
33
80
|
try {
|
|
34
|
-
await command.undo();
|
|
35
|
-
|
|
81
|
+
await tracked.command.undo();
|
|
82
|
+
// Mark as undone in DB
|
|
83
|
+
try {
|
|
84
|
+
const db = getDb();
|
|
85
|
+
await db.update(schema.operationHistory)
|
|
86
|
+
.set({ isUndone: true })
|
|
87
|
+
.where(eq(schema.operationHistory.id, tracked.dbId));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// ignore DB errors
|
|
91
|
+
}
|
|
92
|
+
this.redoStack.push(tracked);
|
|
36
93
|
this.notifyListeners();
|
|
37
94
|
return true;
|
|
38
95
|
}
|
|
39
96
|
catch (error) {
|
|
40
97
|
// Re-add command to undo stack if undo fails
|
|
41
|
-
this.undoStack.push(
|
|
98
|
+
this.undoStack.push(tracked);
|
|
42
99
|
throw error;
|
|
43
100
|
}
|
|
44
101
|
}
|
|
@@ -47,19 +104,29 @@ export class HistoryManager {
|
|
|
47
104
|
* @returns true if redo was performed, false if nothing to redo
|
|
48
105
|
*/
|
|
49
106
|
async redo() {
|
|
50
|
-
const
|
|
51
|
-
if (!
|
|
107
|
+
const tracked = this.redoStack.pop();
|
|
108
|
+
if (!tracked) {
|
|
52
109
|
return false;
|
|
53
110
|
}
|
|
54
111
|
try {
|
|
55
|
-
await command.execute();
|
|
56
|
-
|
|
112
|
+
await tracked.command.execute();
|
|
113
|
+
// Mark as not undone in DB
|
|
114
|
+
try {
|
|
115
|
+
const db = getDb();
|
|
116
|
+
await db.update(schema.operationHistory)
|
|
117
|
+
.set({ isUndone: false })
|
|
118
|
+
.where(eq(schema.operationHistory.id, tracked.dbId));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// ignore DB errors
|
|
122
|
+
}
|
|
123
|
+
this.undoStack.push(tracked);
|
|
57
124
|
this.notifyListeners();
|
|
58
125
|
return true;
|
|
59
126
|
}
|
|
60
127
|
catch (error) {
|
|
61
128
|
// Re-add command to redo stack if redo fails
|
|
62
|
-
this.redoStack.push(
|
|
129
|
+
this.redoStack.push(tracked);
|
|
63
130
|
throw error;
|
|
64
131
|
}
|
|
65
132
|
}
|
|
@@ -71,7 +138,7 @@ export class HistoryManager {
|
|
|
71
138
|
undoCount: this.undoStack.length,
|
|
72
139
|
redoCount: this.redoStack.length,
|
|
73
140
|
lastCommandDescription: this.undoStack.length > 0
|
|
74
|
-
? this.undoStack[this.undoStack.length - 1].description
|
|
141
|
+
? this.undoStack[this.undoStack.length - 1].command.description
|
|
75
142
|
: null,
|
|
76
143
|
};
|
|
77
144
|
}
|
|
@@ -92,7 +159,7 @@ export class HistoryManager {
|
|
|
92
159
|
*/
|
|
93
160
|
getUndoDescription() {
|
|
94
161
|
return this.undoStack.length > 0
|
|
95
|
-
? this.undoStack[this.undoStack.length - 1].description
|
|
162
|
+
? this.undoStack[this.undoStack.length - 1].command.description
|
|
96
163
|
: null;
|
|
97
164
|
}
|
|
98
165
|
/**
|
|
@@ -100,15 +167,22 @@ export class HistoryManager {
|
|
|
100
167
|
*/
|
|
101
168
|
getRedoDescription() {
|
|
102
169
|
return this.redoStack.length > 0
|
|
103
|
-
? this.redoStack[this.redoStack.length - 1].description
|
|
170
|
+
? this.redoStack[this.redoStack.length - 1].command.description
|
|
104
171
|
: null;
|
|
105
172
|
}
|
|
106
173
|
/**
|
|
107
174
|
* Clear all history
|
|
108
175
|
*/
|
|
109
|
-
clear() {
|
|
176
|
+
async clear() {
|
|
110
177
|
this.undoStack = [];
|
|
111
178
|
this.redoStack = [];
|
|
179
|
+
try {
|
|
180
|
+
const db = getDb();
|
|
181
|
+
await db.delete(schema.operationHistory);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// ignore DB errors
|
|
185
|
+
}
|
|
112
186
|
this.notifyListeners();
|
|
113
187
|
}
|
|
114
188
|
/**
|
|
@@ -118,6 +192,56 @@ export class HistoryManager {
|
|
|
118
192
|
this.listeners.add(listener);
|
|
119
193
|
return () => this.listeners.delete(listener);
|
|
120
194
|
}
|
|
195
|
+
async loadFromDb() {
|
|
196
|
+
const db = getDb();
|
|
197
|
+
// Load undo stack: non-undone operations, oldest first
|
|
198
|
+
const undoRows = await db
|
|
199
|
+
.select()
|
|
200
|
+
.from(schema.operationHistory)
|
|
201
|
+
.where(eq(schema.operationHistory.isUndone, false))
|
|
202
|
+
.orderBy(asc(schema.operationHistory.executedAt))
|
|
203
|
+
.limit(MAX_HISTORY_SIZE);
|
|
204
|
+
for (const row of undoRows) {
|
|
205
|
+
try {
|
|
206
|
+
const data = JSON.parse(row.commandData);
|
|
207
|
+
const command = deserializeCommand(row.commandType, data);
|
|
208
|
+
this.undoStack.push({ dbId: row.id, command });
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Skip commands that can't be deserialized
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Load redo stack: undone operations, most recent first (so most recent undo is on top)
|
|
215
|
+
const redoRows = await db
|
|
216
|
+
.select()
|
|
217
|
+
.from(schema.operationHistory)
|
|
218
|
+
.where(eq(schema.operationHistory.isUndone, true))
|
|
219
|
+
.orderBy(desc(schema.operationHistory.executedAt))
|
|
220
|
+
.limit(MAX_HISTORY_SIZE);
|
|
221
|
+
for (const row of redoRows) {
|
|
222
|
+
try {
|
|
223
|
+
const data = JSON.parse(row.commandData);
|
|
224
|
+
const command = deserializeCommand(row.commandType, data);
|
|
225
|
+
this.redoStack.push({ dbId: row.id, command });
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Skip commands that can't be deserialized
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async clearRedoFromDb() {
|
|
233
|
+
if (this.redoStack.length === 0)
|
|
234
|
+
return;
|
|
235
|
+
try {
|
|
236
|
+
const db = getDb();
|
|
237
|
+
for (const tracked of this.redoStack) {
|
|
238
|
+
await db.delete(schema.operationHistory).where(eq(schema.operationHistory.id, tracked.dbId));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// ignore
|
|
243
|
+
}
|
|
244
|
+
}
|
|
121
245
|
notifyListeners() {
|
|
122
246
|
for (const listener of this.listeners) {
|
|
123
247
|
listener();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UndoableCommand } from '../types.js';
|
|
1
|
+
import type { UndoableCommand, SerializedCommand } from '../types.js';
|
|
2
2
|
import type { TaskStatus } from '../../../db/schema.js';
|
|
3
3
|
interface ConvertToProjectParams {
|
|
4
4
|
taskId: string;
|
|
@@ -15,5 +15,9 @@ export declare class ConvertToProjectCommand implements UndoableCommand {
|
|
|
15
15
|
constructor(params: ConvertToProjectParams);
|
|
16
16
|
execute(): Promise<void>;
|
|
17
17
|
undo(): Promise<void>;
|
|
18
|
+
toJSON(): SerializedCommand;
|
|
19
|
+
static fromJSON(json: {
|
|
20
|
+
data: Record<string, unknown>;
|
|
21
|
+
}): ConvertToProjectCommand;
|
|
18
22
|
}
|
|
19
23
|
export {};
|
|
@@ -34,4 +34,17 @@ export class ConvertToProjectCommand {
|
|
|
34
34
|
})
|
|
35
35
|
.where(eq(schema.tasks.id, this.taskId));
|
|
36
36
|
}
|
|
37
|
+
toJSON() {
|
|
38
|
+
return {
|
|
39
|
+
type: 'convert_to_project',
|
|
40
|
+
data: {
|
|
41
|
+
taskId: this.taskId,
|
|
42
|
+
originalStatus: this.originalStatus,
|
|
43
|
+
description: this.description,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
static fromJSON(json) {
|
|
48
|
+
return new ConvertToProjectCommand(json.data);
|
|
49
|
+
}
|
|
37
50
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UndoableCommand } from '../types.js';
|
|
1
|
+
import type { UndoableCommand, SerializedCommand } from '../types.js';
|
|
2
2
|
import type { NewComment } from '../../../db/schema.js';
|
|
3
3
|
interface CreateCommentParams {
|
|
4
4
|
comment: NewComment;
|
|
@@ -14,5 +14,9 @@ export declare class CreateCommentCommand implements UndoableCommand {
|
|
|
14
14
|
constructor(params: CreateCommentParams);
|
|
15
15
|
execute(): Promise<void>;
|
|
16
16
|
undo(): Promise<void>;
|
|
17
|
+
toJSON(): SerializedCommand;
|
|
18
|
+
static fromJSON(json: {
|
|
19
|
+
data: Record<string, unknown>;
|
|
20
|
+
}): CreateCommentCommand;
|
|
17
21
|
}
|
|
18
22
|
export {};
|
|
@@ -20,4 +20,26 @@ export class CreateCommentCommand {
|
|
|
20
20
|
const db = getDb();
|
|
21
21
|
await db.delete(schema.comments).where(eq(schema.comments.id, this.createdCommentId));
|
|
22
22
|
}
|
|
23
|
+
toJSON() {
|
|
24
|
+
return {
|
|
25
|
+
type: 'create_comment',
|
|
26
|
+
data: {
|
|
27
|
+
comment: {
|
|
28
|
+
...this.comment,
|
|
29
|
+
createdAt: this.comment.createdAt instanceof Date ? this.comment.createdAt.toISOString() : this.comment.createdAt,
|
|
30
|
+
},
|
|
31
|
+
description: this.description,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
static fromJSON(json) {
|
|
36
|
+
const commentData = json.data.comment;
|
|
37
|
+
return new CreateCommentCommand({
|
|
38
|
+
comment: {
|
|
39
|
+
...commentData,
|
|
40
|
+
createdAt: new Date(commentData.createdAt),
|
|
41
|
+
},
|
|
42
|
+
description: json.data.description,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
23
45
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UndoableCommand } from '../types.js';
|
|
1
|
+
import type { UndoableCommand, SerializedCommand } from '../types.js';
|
|
2
2
|
import type { NewTask } from '../../../db/schema.js';
|
|
3
3
|
interface CreateTaskParams {
|
|
4
4
|
task: NewTask;
|
|
@@ -14,5 +14,9 @@ export declare class CreateTaskCommand implements UndoableCommand {
|
|
|
14
14
|
constructor(params: CreateTaskParams);
|
|
15
15
|
execute(): Promise<void>;
|
|
16
16
|
undo(): Promise<void>;
|
|
17
|
+
toJSON(): SerializedCommand;
|
|
18
|
+
static fromJSON(json: {
|
|
19
|
+
data: Record<string, unknown>;
|
|
20
|
+
}): CreateTaskCommand;
|
|
17
21
|
}
|
|
18
22
|
export {};
|
|
@@ -18,7 +18,32 @@ export class CreateTaskCommand {
|
|
|
18
18
|
}
|
|
19
19
|
async undo() {
|
|
20
20
|
const db = getDb();
|
|
21
|
-
// Delete the created task
|
|
22
21
|
await db.delete(schema.tasks).where(eq(schema.tasks.id, this.createdTaskId));
|
|
23
22
|
}
|
|
23
|
+
toJSON() {
|
|
24
|
+
return {
|
|
25
|
+
type: 'create_task',
|
|
26
|
+
data: {
|
|
27
|
+
task: {
|
|
28
|
+
...this.task,
|
|
29
|
+
dueDate: this.task.dueDate instanceof Date ? this.task.dueDate.toISOString() : this.task.dueDate ?? null,
|
|
30
|
+
createdAt: this.task.createdAt instanceof Date ? this.task.createdAt.toISOString() : this.task.createdAt,
|
|
31
|
+
updatedAt: this.task.updatedAt instanceof Date ? this.task.updatedAt.toISOString() : this.task.updatedAt,
|
|
32
|
+
},
|
|
33
|
+
description: this.description,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
static fromJSON(json) {
|
|
38
|
+
const taskData = json.data.task;
|
|
39
|
+
return new CreateTaskCommand({
|
|
40
|
+
task: {
|
|
41
|
+
...taskData,
|
|
42
|
+
dueDate: taskData.dueDate ? new Date(taskData.dueDate) : null,
|
|
43
|
+
createdAt: new Date(taskData.createdAt),
|
|
44
|
+
updatedAt: new Date(taskData.updatedAt),
|
|
45
|
+
},
|
|
46
|
+
description: json.data.description,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
24
49
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UndoableCommand } from '../types.js';
|
|
1
|
+
import type { UndoableCommand, SerializedCommand } from '../types.js';
|
|
2
2
|
import type { Comment } from '../../../db/schema.js';
|
|
3
3
|
interface DeleteCommentParams {
|
|
4
4
|
comment: Comment;
|
|
@@ -13,5 +13,9 @@ export declare class DeleteCommentCommand implements UndoableCommand {
|
|
|
13
13
|
constructor(params: DeleteCommentParams);
|
|
14
14
|
execute(): Promise<void>;
|
|
15
15
|
undo(): Promise<void>;
|
|
16
|
+
toJSON(): SerializedCommand;
|
|
17
|
+
static fromJSON(json: {
|
|
18
|
+
data: Record<string, unknown>;
|
|
19
|
+
}): DeleteCommentCommand;
|
|
16
20
|
}
|
|
17
21
|
export {};
|
|
@@ -23,4 +23,26 @@ export class DeleteCommentCommand {
|
|
|
23
23
|
createdAt: this.comment.createdAt,
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
+
toJSON() {
|
|
27
|
+
return {
|
|
28
|
+
type: 'delete_comment',
|
|
29
|
+
data: {
|
|
30
|
+
comment: {
|
|
31
|
+
...this.comment,
|
|
32
|
+
createdAt: this.comment.createdAt.toISOString(),
|
|
33
|
+
},
|
|
34
|
+
description: this.description,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
static fromJSON(json) {
|
|
39
|
+
const commentData = json.data.comment;
|
|
40
|
+
return new DeleteCommentCommand({
|
|
41
|
+
comment: {
|
|
42
|
+
...commentData,
|
|
43
|
+
createdAt: new Date(commentData.createdAt),
|
|
44
|
+
},
|
|
45
|
+
description: json.data.description,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
26
48
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { UndoableCommand } from '../types.js';
|
|
2
|
-
import type { Task } from '../../../db/schema.js';
|
|
1
|
+
import type { UndoableCommand, SerializedCommand } from '../types.js';
|
|
2
|
+
import type { Task, Comment } from '../../../db/schema.js';
|
|
3
3
|
interface DeleteTaskParams {
|
|
4
4
|
task: Task;
|
|
5
5
|
description: string;
|
|
6
|
+
savedComments?: Comment[];
|
|
6
7
|
}
|
|
7
8
|
/**
|
|
8
9
|
* Command to delete a task (and its comments)
|
|
@@ -14,5 +15,9 @@ export declare class DeleteTaskCommand implements UndoableCommand {
|
|
|
14
15
|
constructor(params: DeleteTaskParams);
|
|
15
16
|
execute(): Promise<void>;
|
|
16
17
|
undo(): Promise<void>;
|
|
18
|
+
toJSON(): SerializedCommand;
|
|
19
|
+
static fromJSON(json: {
|
|
20
|
+
data: Record<string, unknown>;
|
|
21
|
+
}): DeleteTaskCommand;
|
|
17
22
|
}
|
|
18
23
|
export {};
|
|
@@ -10,6 +10,9 @@ export class DeleteTaskCommand {
|
|
|
10
10
|
constructor(params) {
|
|
11
11
|
this.task = params.task;
|
|
12
12
|
this.description = params.description;
|
|
13
|
+
if (params.savedComments) {
|
|
14
|
+
this.savedComments = params.savedComments;
|
|
15
|
+
}
|
|
13
16
|
}
|
|
14
17
|
async execute() {
|
|
15
18
|
const db = getDb();
|
|
@@ -35,6 +38,7 @@ export class DeleteTaskCommand {
|
|
|
35
38
|
parentId: this.task.parentId,
|
|
36
39
|
waitingFor: this.task.waitingFor,
|
|
37
40
|
dueDate: this.task.dueDate,
|
|
41
|
+
completedAt: this.task.completedAt,
|
|
38
42
|
createdAt: this.task.createdAt,
|
|
39
43
|
updatedAt: this.task.updatedAt,
|
|
40
44
|
});
|
|
@@ -48,4 +52,41 @@ export class DeleteTaskCommand {
|
|
|
48
52
|
});
|
|
49
53
|
}
|
|
50
54
|
}
|
|
55
|
+
toJSON() {
|
|
56
|
+
return {
|
|
57
|
+
type: 'delete_task',
|
|
58
|
+
data: {
|
|
59
|
+
task: {
|
|
60
|
+
...this.task,
|
|
61
|
+
dueDate: this.task.dueDate ? this.task.dueDate.toISOString() : null,
|
|
62
|
+
completedAt: this.task.completedAt ? this.task.completedAt.toISOString() : null,
|
|
63
|
+
createdAt: this.task.createdAt.toISOString(),
|
|
64
|
+
updatedAt: this.task.updatedAt.toISOString(),
|
|
65
|
+
},
|
|
66
|
+
savedComments: this.savedComments.map(c => ({
|
|
67
|
+
...c,
|
|
68
|
+
createdAt: c.createdAt.toISOString(),
|
|
69
|
+
})),
|
|
70
|
+
description: this.description,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
static fromJSON(json) {
|
|
75
|
+
const taskData = json.data.task;
|
|
76
|
+
const commentsData = json.data.savedComments || [];
|
|
77
|
+
return new DeleteTaskCommand({
|
|
78
|
+
task: {
|
|
79
|
+
...taskData,
|
|
80
|
+
dueDate: taskData.dueDate ? new Date(taskData.dueDate) : null,
|
|
81
|
+
completedAt: taskData.completedAt ? new Date(taskData.completedAt) : null,
|
|
82
|
+
createdAt: new Date(taskData.createdAt),
|
|
83
|
+
updatedAt: new Date(taskData.updatedAt),
|
|
84
|
+
},
|
|
85
|
+
savedComments: commentsData.map(c => ({
|
|
86
|
+
...c,
|
|
87
|
+
createdAt: new Date(c.createdAt),
|
|
88
|
+
})),
|
|
89
|
+
description: json.data.description,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
51
92
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UndoableCommand } from '../types.js';
|
|
1
|
+
import type { UndoableCommand, SerializedCommand } from '../types.js';
|
|
2
2
|
interface LinkTaskParams {
|
|
3
3
|
taskId: string;
|
|
4
4
|
fromParentId: string | null;
|
|
@@ -16,5 +16,9 @@ export declare class LinkTaskCommand implements UndoableCommand {
|
|
|
16
16
|
constructor(params: LinkTaskParams);
|
|
17
17
|
execute(): Promise<void>;
|
|
18
18
|
undo(): Promise<void>;
|
|
19
|
+
toJSON(): SerializedCommand;
|
|
20
|
+
static fromJSON(json: {
|
|
21
|
+
data: Record<string, unknown>;
|
|
22
|
+
}): LinkTaskCommand;
|
|
19
23
|
}
|
|
20
24
|
export {};
|
|
@@ -34,4 +34,18 @@ export class LinkTaskCommand {
|
|
|
34
34
|
})
|
|
35
35
|
.where(eq(schema.tasks.id, this.taskId));
|
|
36
36
|
}
|
|
37
|
+
toJSON() {
|
|
38
|
+
return {
|
|
39
|
+
type: 'link_task',
|
|
40
|
+
data: {
|
|
41
|
+
taskId: this.taskId,
|
|
42
|
+
fromParentId: this.fromParentId,
|
|
43
|
+
toParentId: this.toParentId,
|
|
44
|
+
description: this.description,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
static fromJSON(json) {
|
|
49
|
+
return new LinkTaskCommand(json.data);
|
|
50
|
+
}
|
|
37
51
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UndoableCommand } from '../types.js';
|
|
1
|
+
import type { UndoableCommand, SerializedCommand } from '../types.js';
|
|
2
2
|
import type { TaskStatus } from '../../../db/schema.js';
|
|
3
3
|
interface MoveTaskParams {
|
|
4
4
|
taskId: string;
|
|
@@ -6,6 +6,7 @@ interface MoveTaskParams {
|
|
|
6
6
|
toStatus: TaskStatus;
|
|
7
7
|
fromWaitingFor: string | null;
|
|
8
8
|
toWaitingFor: string | null;
|
|
9
|
+
fromCompletedAt: Date | null;
|
|
9
10
|
description: string;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
@@ -18,8 +19,13 @@ export declare class MoveTaskCommand implements UndoableCommand {
|
|
|
18
19
|
private readonly toStatus;
|
|
19
20
|
private readonly fromWaitingFor;
|
|
20
21
|
private readonly toWaitingFor;
|
|
22
|
+
private readonly fromCompletedAt;
|
|
21
23
|
constructor(params: MoveTaskParams);
|
|
22
24
|
execute(): Promise<void>;
|
|
23
25
|
undo(): Promise<void>;
|
|
26
|
+
toJSON(): SerializedCommand;
|
|
27
|
+
static fromJSON(json: {
|
|
28
|
+
data: Record<string, unknown>;
|
|
29
|
+
}): MoveTaskCommand;
|
|
24
30
|
}
|
|
25
31
|
export {};
|
|
@@ -10,12 +10,14 @@ export class MoveTaskCommand {
|
|
|
10
10
|
toStatus;
|
|
11
11
|
fromWaitingFor;
|
|
12
12
|
toWaitingFor;
|
|
13
|
+
fromCompletedAt;
|
|
13
14
|
constructor(params) {
|
|
14
15
|
this.taskId = params.taskId;
|
|
15
16
|
this.fromStatus = params.fromStatus;
|
|
16
17
|
this.toStatus = params.toStatus;
|
|
17
18
|
this.fromWaitingFor = params.fromWaitingFor;
|
|
18
19
|
this.toWaitingFor = params.toWaitingFor;
|
|
20
|
+
this.fromCompletedAt = params.fromCompletedAt;
|
|
19
21
|
this.description = params.description;
|
|
20
22
|
}
|
|
21
23
|
async execute() {
|
|
@@ -25,6 +27,7 @@ export class MoveTaskCommand {
|
|
|
25
27
|
.set({
|
|
26
28
|
status: this.toStatus,
|
|
27
29
|
waitingFor: this.toWaitingFor,
|
|
30
|
+
completedAt: this.toStatus === 'done' ? new Date() : null,
|
|
28
31
|
updatedAt: new Date(),
|
|
29
32
|
})
|
|
30
33
|
.where(eq(schema.tasks.id, this.taskId));
|
|
@@ -36,8 +39,30 @@ export class MoveTaskCommand {
|
|
|
36
39
|
.set({
|
|
37
40
|
status: this.fromStatus,
|
|
38
41
|
waitingFor: this.fromWaitingFor,
|
|
42
|
+
completedAt: this.fromCompletedAt,
|
|
39
43
|
updatedAt: new Date(),
|
|
40
44
|
})
|
|
41
45
|
.where(eq(schema.tasks.id, this.taskId));
|
|
42
46
|
}
|
|
47
|
+
toJSON() {
|
|
48
|
+
return {
|
|
49
|
+
type: 'move_task',
|
|
50
|
+
data: {
|
|
51
|
+
taskId: this.taskId,
|
|
52
|
+
fromStatus: this.fromStatus,
|
|
53
|
+
toStatus: this.toStatus,
|
|
54
|
+
fromWaitingFor: this.fromWaitingFor,
|
|
55
|
+
toWaitingFor: this.toWaitingFor,
|
|
56
|
+
fromCompletedAt: this.fromCompletedAt ? this.fromCompletedAt.toISOString() : null,
|
|
57
|
+
description: this.description,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
static fromJSON(json) {
|
|
62
|
+
const data = json.data;
|
|
63
|
+
return new MoveTaskCommand({
|
|
64
|
+
...data,
|
|
65
|
+
fromCompletedAt: data.fromCompletedAt ? new Date(data.fromCompletedAt) : null,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
43
68
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UndoableCommand } from '../types.js';
|
|
1
|
+
import type { UndoableCommand, SerializedCommand } from '../types.js';
|
|
2
2
|
interface SetContextParams {
|
|
3
3
|
taskId: string;
|
|
4
4
|
fromContext: string | null;
|
|
@@ -16,5 +16,9 @@ export declare class SetContextCommand implements UndoableCommand {
|
|
|
16
16
|
constructor(params: SetContextParams);
|
|
17
17
|
execute(): Promise<void>;
|
|
18
18
|
undo(): Promise<void>;
|
|
19
|
+
toJSON(): SerializedCommand;
|
|
20
|
+
static fromJSON(json: {
|
|
21
|
+
data: Record<string, unknown>;
|
|
22
|
+
}): SetContextCommand;
|
|
19
23
|
}
|
|
20
24
|
export {};
|
|
@@ -34,4 +34,18 @@ export class SetContextCommand {
|
|
|
34
34
|
})
|
|
35
35
|
.where(eq(schema.tasks.id, this.taskId));
|
|
36
36
|
}
|
|
37
|
+
toJSON() {
|
|
38
|
+
return {
|
|
39
|
+
type: 'set_context',
|
|
40
|
+
data: {
|
|
41
|
+
taskId: this.taskId,
|
|
42
|
+
fromContext: this.fromContext,
|
|
43
|
+
toContext: this.toContext,
|
|
44
|
+
description: this.description,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
static fromJSON(json) {
|
|
49
|
+
return new SetContextCommand(json.data);
|
|
50
|
+
}
|
|
37
51
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UndoableCommand } from '../types.js';
|
|
1
|
+
import type { UndoableCommand, SerializedCommand } from '../types.js';
|
|
2
2
|
interface SetEffortParams {
|
|
3
3
|
taskId: string;
|
|
4
4
|
fromEffort: string | null;
|
|
@@ -16,5 +16,9 @@ export declare class SetEffortCommand implements UndoableCommand {
|
|
|
16
16
|
constructor(params: SetEffortParams);
|
|
17
17
|
execute(): Promise<void>;
|
|
18
18
|
undo(): Promise<void>;
|
|
19
|
+
toJSON(): SerializedCommand;
|
|
20
|
+
static fromJSON(json: {
|
|
21
|
+
data: Record<string, unknown>;
|
|
22
|
+
}): SetEffortCommand;
|
|
19
23
|
}
|
|
20
24
|
export {};
|
|
@@ -34,4 +34,18 @@ export class SetEffortCommand {
|
|
|
34
34
|
})
|
|
35
35
|
.where(eq(schema.tasks.id, this.taskId));
|
|
36
36
|
}
|
|
37
|
+
toJSON() {
|
|
38
|
+
return {
|
|
39
|
+
type: 'set_effort',
|
|
40
|
+
data: {
|
|
41
|
+
taskId: this.taskId,
|
|
42
|
+
fromEffort: this.fromEffort,
|
|
43
|
+
toEffort: this.toEffort,
|
|
44
|
+
description: this.description,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
static fromJSON(json) {
|
|
49
|
+
return new SetEffortCommand(json.data);
|
|
50
|
+
}
|
|
37
51
|
}
|