opencode-backlog 0.1.6 → 0.2.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.md +37 -15
- package/dist/backlog.d.ts +16 -6
- package/dist/backlog.js +124 -24
- package/dist/backlog.js.map +1 -1
- package/dist/index.js +141 -30
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts +2 -2
- package/dist/input.js +3 -3
- package/dist/input.js.map +1 -1
- package/dist/tui.js +362 -158
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -11,12 +11,10 @@ import { watch } from "node:fs";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { Plugin } from "@opencode-ai/plugin/tui";
|
|
13
13
|
import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
|
|
14
|
-
import { BACKLOG_FILE, EMPTY_BACKLOG,
|
|
14
|
+
import { addCategory, BACKLOG_FILE, EMPTY_BACKLOG, moveItem, moveCategory, purgeCategory, removeCategory, renameCategory } from "./backlog.js";
|
|
15
15
|
import { readBacklog, readBacklogSync, updateBacklog } from "./store.js";
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
if (status === "doing") return "Doing";
|
|
19
|
-
return "Done";
|
|
16
|
+
function categoryTitle(categories, status) {
|
|
17
|
+
return categories.find(category => category.id === status)?.title ?? status;
|
|
20
18
|
}
|
|
21
19
|
function readSnapshot(directory) {
|
|
22
20
|
try {
|
|
@@ -48,11 +46,6 @@ function BacklogView(props) {
|
|
|
48
46
|
clearTimeout(refresh);
|
|
49
47
|
watcher.close();
|
|
50
48
|
});
|
|
51
|
-
const color = status => {
|
|
52
|
-
if (status === "doing") return theme.text.feedback.warning.default;
|
|
53
|
-
if (status === "done") return theme.text.feedback.success.default;
|
|
54
|
-
return theme.text.subdued;
|
|
55
|
-
};
|
|
56
49
|
return (() => {
|
|
57
50
|
var _el$ = _$createElement("box"),
|
|
58
51
|
_el$2 = _$createElement("text"),
|
|
@@ -83,93 +76,75 @@ function BacklogView(props) {
|
|
|
83
76
|
}
|
|
84
77
|
}), null);
|
|
85
78
|
_$insert(_el$, _$createComponent(For, {
|
|
86
|
-
each
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
_$insert(_el$13, () => item.title);
|
|
127
|
-
_$effect(_p$ => {
|
|
128
|
-
var _v$ = color(status),
|
|
129
|
-
_v$2 = theme.text.default;
|
|
130
|
-
_v$ !== _p$.e && (_p$.e = _$setProp(_el$12, "fg", _v$, _p$.e));
|
|
131
|
-
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$13, "fg", _v$2, _p$.t));
|
|
132
|
-
return _p$;
|
|
133
|
-
}, {
|
|
134
|
-
e: undefined,
|
|
135
|
-
t: undefined
|
|
136
|
-
});
|
|
137
|
-
return _el$11;
|
|
138
|
-
})()
|
|
139
|
-
}), null);
|
|
140
|
-
_$effect(_$p => _$setProp(_el$9, "fg", color(status), _$p));
|
|
141
|
-
return _el$8;
|
|
142
|
-
}
|
|
143
|
-
});
|
|
79
|
+
get each() {
|
|
80
|
+
return backlog().categories;
|
|
81
|
+
},
|
|
82
|
+
children: category => {
|
|
83
|
+
const items = createMemo(() => backlog().items.filter(item => item.status === category.id));
|
|
84
|
+
return (() => {
|
|
85
|
+
var _el$8 = _$createElement("box"),
|
|
86
|
+
_el$9 = _$createElement("text"),
|
|
87
|
+
_el$0 = _$createElement("b"),
|
|
88
|
+
_el$1 = _$createTextNode(` (`),
|
|
89
|
+
_el$10 = _$createTextNode(`)`);
|
|
90
|
+
_$insertNode(_el$8, _el$9);
|
|
91
|
+
_$setProp(_el$8, "marginTop", 1);
|
|
92
|
+
_$insertNode(_el$9, _el$0);
|
|
93
|
+
_$insertNode(_el$9, _el$1);
|
|
94
|
+
_$insertNode(_el$9, _el$10);
|
|
95
|
+
_$insert(_el$0, () => category.title);
|
|
96
|
+
_$insert(_el$9, () => items().length, _el$10);
|
|
97
|
+
_$insert(_el$8, _$createComponent(For, {
|
|
98
|
+
get each() {
|
|
99
|
+
return items();
|
|
100
|
+
},
|
|
101
|
+
children: item => (() => {
|
|
102
|
+
var _el$11 = _$createElement("box"),
|
|
103
|
+
_el$12 = _$createElement("text");
|
|
104
|
+
_$insertNode(_el$11, _el$12);
|
|
105
|
+
_$setProp(_el$11, "minWidth", 0);
|
|
106
|
+
_$setProp(_el$11, "onMouseUp", () => void showTaskDetails(props.context, item, backlog().categories));
|
|
107
|
+
_$setProp(_el$12, "wrapMode", "none");
|
|
108
|
+
_$setProp(_el$12, "truncate", true);
|
|
109
|
+
_$setProp(_el$12, "flexGrow", 1);
|
|
110
|
+
_$setProp(_el$12, "minWidth", 0);
|
|
111
|
+
_$insert(_el$12, () => item.title);
|
|
112
|
+
_$effect(_$p => _$setProp(_el$12, "fg", theme.text.default, _$p));
|
|
113
|
+
return _el$11;
|
|
114
|
+
})()
|
|
115
|
+
}), null);
|
|
116
|
+
_$effect(_$p => _$setProp(_el$9, "fg", theme.text.subdued, _$p));
|
|
117
|
+
return _el$8;
|
|
118
|
+
})();
|
|
144
119
|
}
|
|
145
120
|
}), null);
|
|
146
121
|
_$effect(_$p => _$setProp(_el$2, "fg", theme.text.default, _$p));
|
|
147
122
|
return _el$;
|
|
148
123
|
})();
|
|
149
124
|
}
|
|
150
|
-
function taskDetails(item) {
|
|
151
|
-
return [`
|
|
125
|
+
function taskDetails(item, categories) {
|
|
126
|
+
return [`Category: ${categoryTitle(categories, item.status)}`, `ID: ${item.id}`, "", item.notes ?? "No notes"].join("\n");
|
|
152
127
|
}
|
|
153
128
|
function TaskAction(props) {
|
|
154
129
|
return (() => {
|
|
155
|
-
var _el$
|
|
156
|
-
_el$
|
|
157
|
-
_el$
|
|
158
|
-
_$insertNode(_el$
|
|
159
|
-
_$insertNode(_el$
|
|
160
|
-
_$insert(_el$
|
|
161
|
-
_$insert(_el$
|
|
130
|
+
var _el$13 = _$createElement("text"),
|
|
131
|
+
_el$14 = _$createElement("b"),
|
|
132
|
+
_el$15 = _$createTextNode(` `);
|
|
133
|
+
_$insertNode(_el$13, _el$14);
|
|
134
|
+
_$insertNode(_el$13, _el$15);
|
|
135
|
+
_$insert(_el$14, () => props.shortcut);
|
|
136
|
+
_$insert(_el$13, () => props.label, null);
|
|
162
137
|
_$effect(_p$ => {
|
|
163
|
-
var _v$
|
|
164
|
-
_v$
|
|
165
|
-
_v$
|
|
166
|
-
_v$
|
|
138
|
+
var _v$ = props.danger ? props.context.theme.text.feedback.error.default : props.context.theme.text.subdued,
|
|
139
|
+
_v$2 = props.run;
|
|
140
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$13, "fg", _v$, _p$.e));
|
|
141
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$13, "onMouseUp", _v$2, _p$.t));
|
|
167
142
|
return _p$;
|
|
168
143
|
}, {
|
|
169
144
|
e: undefined,
|
|
170
145
|
t: undefined
|
|
171
146
|
});
|
|
172
|
-
return _el$
|
|
147
|
+
return _el$13;
|
|
173
148
|
})();
|
|
174
149
|
}
|
|
175
150
|
function TaskDetailsDialog(props) {
|
|
@@ -207,37 +182,37 @@ function TaskDetailsDialog(props) {
|
|
|
207
182
|
}]
|
|
208
183
|
}));
|
|
209
184
|
return (() => {
|
|
210
|
-
var _el$
|
|
211
|
-
_el$
|
|
212
|
-
_el$
|
|
213
|
-
_el$
|
|
214
|
-
_el$
|
|
215
|
-
_el$
|
|
216
|
-
_el$
|
|
185
|
+
var _el$16 = _$createElement("box"),
|
|
186
|
+
_el$17 = _$createElement("box"),
|
|
187
|
+
_el$18 = _$createElement("text"),
|
|
188
|
+
_el$19 = _$createElement("b"),
|
|
189
|
+
_el$20 = _$createElement("text"),
|
|
190
|
+
_el$22 = _$createElement("text"),
|
|
191
|
+
_el$23 = _$createElement("box");
|
|
192
|
+
_$insertNode(_el$16, _el$17);
|
|
193
|
+
_$insertNode(_el$16, _el$22);
|
|
194
|
+
_$insertNode(_el$16, _el$23);
|
|
195
|
+
_$setProp(_el$16, "paddingLeft", 2);
|
|
196
|
+
_$setProp(_el$16, "paddingRight", 2);
|
|
197
|
+
_$setProp(_el$16, "gap", 1);
|
|
217
198
|
_$insertNode(_el$17, _el$18);
|
|
218
|
-
_$insertNode(_el$17, _el$
|
|
219
|
-
_$
|
|
220
|
-
_$setProp(_el$17, "
|
|
221
|
-
_$setProp(_el$17, "
|
|
222
|
-
_$setProp(_el$17, "gap", 1);
|
|
199
|
+
_$insertNode(_el$17, _el$20);
|
|
200
|
+
_$setProp(_el$17, "flexDirection", "row");
|
|
201
|
+
_$setProp(_el$17, "justifyContent", "space-between");
|
|
202
|
+
_$setProp(_el$17, "gap", 2);
|
|
223
203
|
_$insertNode(_el$18, _el$19);
|
|
224
|
-
_$
|
|
225
|
-
_$setProp(_el$18, "
|
|
226
|
-
_$
|
|
227
|
-
_$
|
|
228
|
-
_$
|
|
229
|
-
_$setProp(_el$
|
|
230
|
-
_$
|
|
231
|
-
_$
|
|
232
|
-
_$
|
|
233
|
-
_$setProp(_el$
|
|
234
|
-
_$setProp(_el$23, "
|
|
235
|
-
_$insert(_el$23, (
|
|
236
|
-
_$setProp(_el$24, "flexDirection", "row");
|
|
237
|
-
_$setProp(_el$24, "justifyContent", "flex-end");
|
|
238
|
-
_$setProp(_el$24, "gap", 2);
|
|
239
|
-
_$setProp(_el$24, "paddingBottom", 1);
|
|
240
|
-
_$insert(_el$24, _$createComponent(TaskAction, {
|
|
204
|
+
_$setProp(_el$18, "wrapMode", "word");
|
|
205
|
+
_$setProp(_el$18, "flexGrow", 1);
|
|
206
|
+
_$insert(_el$19, () => props.item.title);
|
|
207
|
+
_$insertNode(_el$20, _$createTextNode(`esc`));
|
|
208
|
+
_$setProp(_el$20, "onMouseUp", () => props.context.ui.dialog.clear());
|
|
209
|
+
_$setProp(_el$22, "wrapMode", "word");
|
|
210
|
+
_$insert(_el$22, () => taskDetails(props.item, props.categories));
|
|
211
|
+
_$setProp(_el$23, "flexDirection", "row");
|
|
212
|
+
_$setProp(_el$23, "justifyContent", "flex-end");
|
|
213
|
+
_$setProp(_el$23, "gap", 2);
|
|
214
|
+
_$setProp(_el$23, "paddingBottom", 1);
|
|
215
|
+
_$insert(_el$23, _$createComponent(TaskAction, {
|
|
241
216
|
get context() {
|
|
242
217
|
return props.context;
|
|
243
218
|
},
|
|
@@ -245,7 +220,7 @@ function TaskDetailsDialog(props) {
|
|
|
245
220
|
label: "status",
|
|
246
221
|
run: changeStatus
|
|
247
222
|
}), null);
|
|
248
|
-
_$insert(_el$
|
|
223
|
+
_$insert(_el$23, _$createComponent(TaskAction, {
|
|
249
224
|
get context() {
|
|
250
225
|
return props.context;
|
|
251
226
|
},
|
|
@@ -253,7 +228,7 @@ function TaskDetailsDialog(props) {
|
|
|
253
228
|
label: "edit",
|
|
254
229
|
run: edit
|
|
255
230
|
}), null);
|
|
256
|
-
_$insert(_el$
|
|
231
|
+
_$insert(_el$23, _$createComponent(TaskAction, {
|
|
257
232
|
get context() {
|
|
258
233
|
return props.context;
|
|
259
234
|
},
|
|
@@ -263,25 +238,26 @@ function TaskDetailsDialog(props) {
|
|
|
263
238
|
run: remove
|
|
264
239
|
}), null);
|
|
265
240
|
_$effect(_p$ => {
|
|
266
|
-
var _v$
|
|
267
|
-
_v$
|
|
268
|
-
_v$
|
|
269
|
-
_v$
|
|
270
|
-
_v$
|
|
271
|
-
_v$
|
|
241
|
+
var _v$3 = props.context.theme.text.default,
|
|
242
|
+
_v$4 = props.context.theme.text.subdued,
|
|
243
|
+
_v$5 = props.context.theme.text.subdued;
|
|
244
|
+
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$18, "fg", _v$3, _p$.e));
|
|
245
|
+
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$20, "fg", _v$4, _p$.t));
|
|
246
|
+
_v$5 !== _p$.a && (_p$.a = _$setProp(_el$22, "fg", _v$5, _p$.a));
|
|
272
247
|
return _p$;
|
|
273
248
|
}, {
|
|
274
249
|
e: undefined,
|
|
275
250
|
t: undefined,
|
|
276
251
|
a: undefined
|
|
277
252
|
});
|
|
278
|
-
return _el$
|
|
253
|
+
return _el$16;
|
|
279
254
|
})();
|
|
280
255
|
}
|
|
281
|
-
function showTaskDetails(context, item) {
|
|
256
|
+
function showTaskDetails(context, item, categories) {
|
|
282
257
|
context.ui.dialog.show(() => _$createComponent(TaskDetailsDialog, {
|
|
283
258
|
context: context,
|
|
284
|
-
item: item
|
|
259
|
+
item: item,
|
|
260
|
+
categories: categories
|
|
285
261
|
}));
|
|
286
262
|
}
|
|
287
263
|
async function browseBacklog(context) {
|
|
@@ -297,6 +273,9 @@ function backlogLocation(context) {
|
|
|
297
273
|
};
|
|
298
274
|
}
|
|
299
275
|
async function addBacklogItem(context) {
|
|
276
|
+
const {
|
|
277
|
+
path
|
|
278
|
+
} = backlogLocation(context);
|
|
300
279
|
const title = await context.ui.dialog.prompt({
|
|
301
280
|
title: "New backlog task",
|
|
302
281
|
placeholder: "Task title"
|
|
@@ -308,48 +287,52 @@ async function addBacklogItem(context) {
|
|
|
308
287
|
placeholder: "Leave empty for no notes"
|
|
309
288
|
});
|
|
310
289
|
if (notes === undefined) return;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
290
|
+
let item;
|
|
291
|
+
await updateBacklog(path, current => {
|
|
292
|
+
const firstCategory = current.categories[0];
|
|
293
|
+
if (!firstCategory) throw new Error("Add a backlog category before adding a task");
|
|
294
|
+
item = {
|
|
295
|
+
id: randomUUID(),
|
|
296
|
+
title: title.trim(),
|
|
297
|
+
...(notes.trim() ? {
|
|
298
|
+
notes: notes.trim()
|
|
299
|
+
} : {}),
|
|
300
|
+
status: firstCategory.id
|
|
301
|
+
};
|
|
302
|
+
return {
|
|
303
|
+
...current,
|
|
304
|
+
items: moveItem([...current.items, item], item.id, firstCategory.id, 0, current.categories)
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
if (!item) throw new Error("Backlog update did not add a task");
|
|
326
308
|
context.ui.toast.show({
|
|
327
309
|
message: `Added "${item.title}".`,
|
|
328
310
|
variant: "success"
|
|
329
311
|
});
|
|
330
312
|
}
|
|
331
313
|
async function changeTaskStatus(context, item) {
|
|
314
|
+
const {
|
|
315
|
+
path
|
|
316
|
+
} = backlogLocation(context);
|
|
317
|
+
const backlog = await readBacklog(path);
|
|
332
318
|
const status = await context.ui.dialog.select({
|
|
333
|
-
title: `Change
|
|
319
|
+
title: `Change category: ${item.title}`,
|
|
334
320
|
current: item.status,
|
|
335
|
-
options:
|
|
336
|
-
title:
|
|
337
|
-
value:
|
|
338
|
-
...(
|
|
339
|
-
description: "Current
|
|
321
|
+
options: backlog.categories.map(category => ({
|
|
322
|
+
title: category.title,
|
|
323
|
+
value: category.id,
|
|
324
|
+
...(category.id === item.status ? {
|
|
325
|
+
description: "Current category"
|
|
340
326
|
} : {})
|
|
341
327
|
}))
|
|
342
328
|
});
|
|
343
329
|
if (!status || status === item.status) return;
|
|
344
|
-
const {
|
|
345
|
-
path
|
|
346
|
-
} = backlogLocation(context);
|
|
347
330
|
await updateBacklog(path, current => ({
|
|
348
|
-
|
|
349
|
-
items: moveItem(current.items, item.id, status, undefined)
|
|
331
|
+
...current,
|
|
332
|
+
items: moveItem(current.items, item.id, status, undefined, current.categories)
|
|
350
333
|
}));
|
|
351
334
|
context.ui.toast.show({
|
|
352
|
-
message: `Moved "${item.title}" to ${
|
|
335
|
+
message: `Moved "${item.title}" to ${categoryTitle(backlog.categories, status)}.`,
|
|
353
336
|
variant: "success"
|
|
354
337
|
});
|
|
355
338
|
}
|
|
@@ -371,7 +354,7 @@ async function editBacklogItem(context, item) {
|
|
|
371
354
|
path
|
|
372
355
|
} = backlogLocation(context);
|
|
373
356
|
await updateBacklog(path, current => ({
|
|
374
|
-
|
|
357
|
+
...current,
|
|
375
358
|
items: current.items.map(candidate => {
|
|
376
359
|
if (candidate.id !== item.id) return candidate;
|
|
377
360
|
return {
|
|
@@ -403,7 +386,7 @@ async function removeBacklogItem(context, item) {
|
|
|
403
386
|
path
|
|
404
387
|
} = backlogLocation(context);
|
|
405
388
|
await updateBacklog(path, current => ({
|
|
406
|
-
|
|
389
|
+
...current,
|
|
407
390
|
items: current.items.filter(candidate => candidate.id !== item.id)
|
|
408
391
|
}));
|
|
409
392
|
context.ui.toast.show({
|
|
@@ -411,6 +394,199 @@ async function removeBacklogItem(context, item) {
|
|
|
411
394
|
variant: "success"
|
|
412
395
|
});
|
|
413
396
|
}
|
|
397
|
+
async function purgeConfirmedCategory(path, status, confirmedIDs) {
|
|
398
|
+
await updateBacklog(path, current => {
|
|
399
|
+
const currentIDs = current.items.filter(item => item.status === status).map(item => item.id);
|
|
400
|
+
const changed = currentIDs.length !== confirmedIDs.length || currentIDs.some(id => !confirmedIDs.includes(id));
|
|
401
|
+
if (changed) throw new Error("The category changed. Review its tasks and confirm the purge again.");
|
|
402
|
+
return purgeCategory(current, status);
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
async function purgeBacklogCategory(context) {
|
|
406
|
+
const {
|
|
407
|
+
path
|
|
408
|
+
} = backlogLocation(context);
|
|
409
|
+
const backlog = await readBacklog(path);
|
|
410
|
+
const status = await context.ui.dialog.select({
|
|
411
|
+
title: "Purge backlog category",
|
|
412
|
+
placeholder: "Select a category",
|
|
413
|
+
options: backlog.categories.map(category => {
|
|
414
|
+
const count = backlog.items.filter(item => item.status === category.id).length;
|
|
415
|
+
return {
|
|
416
|
+
title: category.title,
|
|
417
|
+
value: category.id,
|
|
418
|
+
description: `${count} ${count === 1 ? "task" : "tasks"}`
|
|
419
|
+
};
|
|
420
|
+
})
|
|
421
|
+
});
|
|
422
|
+
if (!status) return;
|
|
423
|
+
const category = backlog.categories.find(candidate => candidate.id === status);
|
|
424
|
+
if (!category) return;
|
|
425
|
+
const taskIDs = backlog.items.filter(item => item.status === status).map(item => item.id);
|
|
426
|
+
const count = taskIDs.length;
|
|
427
|
+
const confirmed = await context.ui.dialog.confirm({
|
|
428
|
+
title: `Purge ${category.title}`,
|
|
429
|
+
message: `Permanently delete ${count} ${count === 1 ? "task" : "tasks"} from this category?`,
|
|
430
|
+
label: {
|
|
431
|
+
confirm: "Purge",
|
|
432
|
+
cancel: "Cancel"
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
if (!confirmed) return;
|
|
436
|
+
await purgeConfirmedCategory(path, status, taskIDs);
|
|
437
|
+
context.ui.toast.show({
|
|
438
|
+
message: `Purged ${count} ${count === 1 ? "task" : "tasks"} from ${category.title}.`,
|
|
439
|
+
variant: "success"
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async function addBacklogCategory(context) {
|
|
443
|
+
const title = await context.ui.dialog.prompt({
|
|
444
|
+
title: "New backlog category",
|
|
445
|
+
placeholder: "Category title"
|
|
446
|
+
});
|
|
447
|
+
if (!title?.trim()) return;
|
|
448
|
+
const suggestedID = title.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
449
|
+
const id = await context.ui.dialog.prompt({
|
|
450
|
+
title: title.trim(),
|
|
451
|
+
description: "Stable category ID",
|
|
452
|
+
placeholder: "Category ID",
|
|
453
|
+
value: suggestedID
|
|
454
|
+
});
|
|
455
|
+
if (!id?.trim()) return;
|
|
456
|
+
const category = {
|
|
457
|
+
id: id.trim(),
|
|
458
|
+
title: title.trim()
|
|
459
|
+
};
|
|
460
|
+
const {
|
|
461
|
+
path
|
|
462
|
+
} = backlogLocation(context);
|
|
463
|
+
await updateBacklog(path, current => addCategory(current, category));
|
|
464
|
+
context.ui.toast.show({
|
|
465
|
+
message: `Added category "${category.title}".`,
|
|
466
|
+
variant: "success"
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
async function renameBacklogCategory(context, category) {
|
|
470
|
+
const title = await context.ui.dialog.prompt({
|
|
471
|
+
title: "Rename backlog category",
|
|
472
|
+
placeholder: "Category title",
|
|
473
|
+
value: category.title
|
|
474
|
+
});
|
|
475
|
+
if (!title?.trim() || title.trim() === category.title) return;
|
|
476
|
+
const {
|
|
477
|
+
path
|
|
478
|
+
} = backlogLocation(context);
|
|
479
|
+
await updateBacklog(path, current => renameCategory(current, category.id, title.trim()));
|
|
480
|
+
context.ui.toast.show({
|
|
481
|
+
message: `Renamed category to "${title.trim()}".`,
|
|
482
|
+
variant: "success"
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
async function moveBacklogCategory(context, category) {
|
|
486
|
+
const {
|
|
487
|
+
path
|
|
488
|
+
} = backlogLocation(context);
|
|
489
|
+
const backlog = await readBacklog(path);
|
|
490
|
+
const position = await context.ui.dialog.select({
|
|
491
|
+
title: `Move ${category.title}`,
|
|
492
|
+
placeholder: "Select a position",
|
|
493
|
+
options: backlog.categories.map((candidate, index) => ({
|
|
494
|
+
title: `${index + 1}. ${candidate.title}`,
|
|
495
|
+
value: index,
|
|
496
|
+
...(candidate.id === category.id ? {
|
|
497
|
+
description: "Current position"
|
|
498
|
+
} : {})
|
|
499
|
+
}))
|
|
500
|
+
});
|
|
501
|
+
if (position === undefined || backlog.categories[position]?.id === category.id) return;
|
|
502
|
+
await updateBacklog(path, current => moveCategory(current, category.id, position));
|
|
503
|
+
context.ui.toast.show({
|
|
504
|
+
message: `Moved category "${category.title}".`,
|
|
505
|
+
variant: "success"
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
async function deleteBacklogCategory(context, category) {
|
|
509
|
+
const confirmed = await context.ui.dialog.confirm({
|
|
510
|
+
title: "Delete backlog category",
|
|
511
|
+
message: category.title,
|
|
512
|
+
label: {
|
|
513
|
+
confirm: "Delete",
|
|
514
|
+
cancel: "Cancel"
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
if (!confirmed) return;
|
|
518
|
+
const {
|
|
519
|
+
path
|
|
520
|
+
} = backlogLocation(context);
|
|
521
|
+
await updateBacklog(path, current => removeCategory(current, category.id));
|
|
522
|
+
context.ui.toast.show({
|
|
523
|
+
message: `Deleted category "${category.title}".`,
|
|
524
|
+
variant: "success"
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async function manageBacklogCategories(context) {
|
|
528
|
+
const {
|
|
529
|
+
path
|
|
530
|
+
} = backlogLocation(context);
|
|
531
|
+
const backlog = await readBacklog(path);
|
|
532
|
+
let addSelection = `__add:${randomUUID()}`;
|
|
533
|
+
while (backlog.categories.some(category => category.id === addSelection)) {
|
|
534
|
+
addSelection = `__add:${randomUUID()}`;
|
|
535
|
+
}
|
|
536
|
+
const selection = await context.ui.dialog.select({
|
|
537
|
+
title: "Backlog categories",
|
|
538
|
+
placeholder: "Add or select a category",
|
|
539
|
+
options: [{
|
|
540
|
+
title: "Add category",
|
|
541
|
+
value: addSelection
|
|
542
|
+
}, ...backlog.categories.map(category => ({
|
|
543
|
+
title: category.title,
|
|
544
|
+
value: category.id,
|
|
545
|
+
description: `${category.id} - ${backlog.items.filter(item => item.status === category.id).length} tasks`
|
|
546
|
+
}))]
|
|
547
|
+
});
|
|
548
|
+
if (!selection) return;
|
|
549
|
+
if (selection === addSelection) return addBacklogCategory(context);
|
|
550
|
+
const category = backlog.categories.find(candidate => candidate.id === selection);
|
|
551
|
+
if (!category) return;
|
|
552
|
+
const taskIDs = backlog.items.filter(item => item.status === category.id).map(item => item.id);
|
|
553
|
+
const count = taskIDs.length;
|
|
554
|
+
const action = await context.ui.dialog.select({
|
|
555
|
+
title: category.title,
|
|
556
|
+
placeholder: "Select an action",
|
|
557
|
+
options: [{
|
|
558
|
+
title: "Rename",
|
|
559
|
+
value: "rename"
|
|
560
|
+
}, {
|
|
561
|
+
title: "Move",
|
|
562
|
+
value: "move"
|
|
563
|
+
}, ...(count === 0 ? [{
|
|
564
|
+
title: "Delete empty category",
|
|
565
|
+
value: "delete"
|
|
566
|
+
}] : [{
|
|
567
|
+
title: `Purge category and ${count} ${count === 1 ? "task" : "tasks"}`,
|
|
568
|
+
value: "purge"
|
|
569
|
+
}])]
|
|
570
|
+
});
|
|
571
|
+
if (action === "rename") return renameBacklogCategory(context, category);
|
|
572
|
+
if (action === "move") return moveBacklogCategory(context, category);
|
|
573
|
+
if (action === "delete") return deleteBacklogCategory(context, category);
|
|
574
|
+
if (action !== "purge") return;
|
|
575
|
+
const confirmed = await context.ui.dialog.confirm({
|
|
576
|
+
title: `Purge ${category.title}`,
|
|
577
|
+
message: `Permanently delete ${count} ${count === 1 ? "task" : "tasks"} from this category?`,
|
|
578
|
+
label: {
|
|
579
|
+
confirm: "Purge",
|
|
580
|
+
cancel: "Cancel"
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
if (!confirmed) return;
|
|
584
|
+
await purgeConfirmedCategory(path, category.id, taskIDs);
|
|
585
|
+
context.ui.toast.show({
|
|
586
|
+
message: `Purged category "${category.title}".`,
|
|
587
|
+
variant: "success"
|
|
588
|
+
});
|
|
589
|
+
}
|
|
414
590
|
async function moveBacklogItem(context) {
|
|
415
591
|
const {
|
|
416
592
|
path
|
|
@@ -432,7 +608,7 @@ async function moveBacklogItem(context) {
|
|
|
432
608
|
...(item.notes === undefined ? {} : {
|
|
433
609
|
description: item.notes
|
|
434
610
|
}),
|
|
435
|
-
category:
|
|
611
|
+
category: categoryTitle(backlog.categories, item.status)
|
|
436
612
|
}))
|
|
437
613
|
});
|
|
438
614
|
if (!id) return;
|
|
@@ -462,7 +638,7 @@ async function browseBacklogWithState(context, setOpen, action = () => "details"
|
|
|
462
638
|
...(item.notes === undefined ? {} : {
|
|
463
639
|
description: item.notes
|
|
464
640
|
}),
|
|
465
|
-
category:
|
|
641
|
+
category: categoryTitle(backlog.categories, item.status)
|
|
466
642
|
}))
|
|
467
643
|
}).finally(() => setOpen(false));
|
|
468
644
|
if (!id) return;
|
|
@@ -472,7 +648,7 @@ async function browseBacklogWithState(context, setOpen, action = () => "details"
|
|
|
472
648
|
if (selectedAction === "status") return changeTaskStatus(context, item);
|
|
473
649
|
if (selectedAction === "edit") return editBacklogItem(context, item);
|
|
474
650
|
if (selectedAction === "delete") return removeBacklogItem(context, item);
|
|
475
|
-
showTaskDetails(context, item);
|
|
651
|
+
showTaskDetails(context, item, backlog.categories);
|
|
476
652
|
}
|
|
477
653
|
function Commands(props) {
|
|
478
654
|
const [browseOpen, setBrowseOpen] = createSignal(false);
|
|
@@ -492,7 +668,7 @@ function Commands(props) {
|
|
|
492
668
|
commands: [{
|
|
493
669
|
id: "backlog.browse",
|
|
494
670
|
title: "Browse backlog",
|
|
495
|
-
description: "View backlog tasks and change their
|
|
671
|
+
description: "View backlog tasks and change their category",
|
|
496
672
|
group: "Backlog",
|
|
497
673
|
palette: true,
|
|
498
674
|
slash: {
|
|
@@ -506,7 +682,7 @@ function Commands(props) {
|
|
|
506
682
|
}, {
|
|
507
683
|
id: "backlog.add",
|
|
508
684
|
title: "Add backlog task",
|
|
509
|
-
description: "Create a
|
|
685
|
+
description: "Create a task at the top of the first category",
|
|
510
686
|
group: "Backlog",
|
|
511
687
|
palette: true,
|
|
512
688
|
slash: {
|
|
@@ -517,7 +693,7 @@ function Commands(props) {
|
|
|
517
693
|
}, {
|
|
518
694
|
id: "backlog.move",
|
|
519
695
|
title: "Move backlog task",
|
|
520
|
-
description: "Change a backlog task
|
|
696
|
+
description: "Change a backlog task category",
|
|
521
697
|
group: "Backlog",
|
|
522
698
|
palette: true,
|
|
523
699
|
slash: {
|
|
@@ -525,6 +701,26 @@ function Commands(props) {
|
|
|
525
701
|
aliases: ["task-move"]
|
|
526
702
|
},
|
|
527
703
|
run: () => run(() => moveBacklogItem(props.context))
|
|
704
|
+
}, {
|
|
705
|
+
id: "backlog.purge",
|
|
706
|
+
title: "Purge backlog category",
|
|
707
|
+
description: "Permanently delete every task in a category",
|
|
708
|
+
group: "Backlog",
|
|
709
|
+
palette: true,
|
|
710
|
+
slash: {
|
|
711
|
+
name: "backlog-purge"
|
|
712
|
+
},
|
|
713
|
+
run: () => run(() => purgeBacklogCategory(props.context))
|
|
714
|
+
}, {
|
|
715
|
+
id: "backlog.categories",
|
|
716
|
+
title: "Manage backlog categories",
|
|
717
|
+
description: "Add, rename, move, purge, or delete a category",
|
|
718
|
+
group: "Backlog",
|
|
719
|
+
palette: true,
|
|
720
|
+
slash: {
|
|
721
|
+
name: "backlog-categories"
|
|
722
|
+
},
|
|
723
|
+
run: () => run(() => manageBacklogCategories(props.context))
|
|
528
724
|
}]
|
|
529
725
|
}));
|
|
530
726
|
props.context.keymap.layer(() => ({
|
|
@@ -539,9 +735,17 @@ function Commands(props) {
|
|
|
539
735
|
setBrowseOpen(false);
|
|
540
736
|
return run(() => addBacklogItem(props.context));
|
|
541
737
|
}
|
|
738
|
+
}, {
|
|
739
|
+
bind: "p",
|
|
740
|
+
title: "Purge backlog category",
|
|
741
|
+
group: "Backlog",
|
|
742
|
+
run() {
|
|
743
|
+
setBrowseOpen(false);
|
|
744
|
+
return run(() => purgeBacklogCategory(props.context));
|
|
745
|
+
}
|
|
542
746
|
}, {
|
|
543
747
|
bind: "c",
|
|
544
|
-
title: "Change selected task
|
|
748
|
+
title: "Change selected task category",
|
|
545
749
|
group: "Backlog",
|
|
546
750
|
run() {
|
|
547
751
|
setBrowseAction("status");
|