opencode-backlog 0.1.6 → 0.2.1
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 +39 -15
- package/dist/backlog.d.ts +21 -6
- package/dist/backlog.js +144 -24
- package/dist/backlog.js.map +1 -1
- package/dist/index.js +152 -31
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts +3 -2
- package/dist/input.js +12 -4
- package/dist/input.js.map +1 -1
- package/dist/tui.js +410 -158
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -11,12 +11,17 @@ 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,
|
|
14
|
+
import { addCategory, BACKLOG_FILE, CATEGORY_COLORS, EMPTY_BACKLOG, moveItem, moveCategory, purgeCategory, removeCategory, renameCategory, setCategoryColor } from "./backlog.js";
|
|
15
15
|
import { readBacklog, readBacklogSync, updateBacklog } from "./store.js";
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
function categoryTitle(categories, status) {
|
|
17
|
+
return categories.find(category => category.id === status)?.title ?? status;
|
|
18
|
+
}
|
|
19
|
+
function categoryColorName(category) {
|
|
20
|
+
if (category.color) return category.color;
|
|
21
|
+
if (category.id === "todo") return "subdued";
|
|
22
|
+
if (category.id === "doing") return "warning";
|
|
23
|
+
if (category.id === "done") return "success";
|
|
24
|
+
return "info";
|
|
20
25
|
}
|
|
21
26
|
function readSnapshot(directory) {
|
|
22
27
|
try {
|
|
@@ -48,11 +53,6 @@ function BacklogView(props) {
|
|
|
48
53
|
clearTimeout(refresh);
|
|
49
54
|
watcher.close();
|
|
50
55
|
});
|
|
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
56
|
return (() => {
|
|
57
57
|
var _el$ = _$createElement("box"),
|
|
58
58
|
_el$2 = _$createElement("text"),
|
|
@@ -83,93 +83,81 @@ function BacklogView(props) {
|
|
|
83
83
|
}
|
|
84
84
|
}), null);
|
|
85
85
|
_$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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
});
|
|
86
|
+
get each() {
|
|
87
|
+
return backlog().categories;
|
|
88
|
+
},
|
|
89
|
+
children: category => {
|
|
90
|
+
const items = createMemo(() => backlog().items.filter(item => item.status === category.id));
|
|
91
|
+
const color = () => {
|
|
92
|
+
const name = categoryColorName(category);
|
|
93
|
+
if (name === "default") return theme.text.default;
|
|
94
|
+
if (name === "subdued") return theme.text.subdued;
|
|
95
|
+
return theme.text.feedback[name].default;
|
|
96
|
+
};
|
|
97
|
+
return (() => {
|
|
98
|
+
var _el$8 = _$createElement("box"),
|
|
99
|
+
_el$9 = _$createElement("text"),
|
|
100
|
+
_el$0 = _$createElement("b"),
|
|
101
|
+
_el$1 = _$createTextNode(` (`),
|
|
102
|
+
_el$10 = _$createTextNode(`)`);
|
|
103
|
+
_$insertNode(_el$8, _el$9);
|
|
104
|
+
_$setProp(_el$8, "marginTop", 1);
|
|
105
|
+
_$insertNode(_el$9, _el$0);
|
|
106
|
+
_$insertNode(_el$9, _el$1);
|
|
107
|
+
_$insertNode(_el$9, _el$10);
|
|
108
|
+
_$insert(_el$0, () => category.title);
|
|
109
|
+
_$insert(_el$9, () => items().length, _el$10);
|
|
110
|
+
_$insert(_el$8, _$createComponent(For, {
|
|
111
|
+
get each() {
|
|
112
|
+
return items();
|
|
113
|
+
},
|
|
114
|
+
children: item => (() => {
|
|
115
|
+
var _el$11 = _$createElement("box"),
|
|
116
|
+
_el$12 = _$createElement("text");
|
|
117
|
+
_$insertNode(_el$11, _el$12);
|
|
118
|
+
_$setProp(_el$11, "minWidth", 0);
|
|
119
|
+
_$setProp(_el$11, "onMouseUp", () => void showTaskDetails(props.context, item, backlog().categories));
|
|
120
|
+
_$setProp(_el$12, "wrapMode", "none");
|
|
121
|
+
_$setProp(_el$12, "truncate", true);
|
|
122
|
+
_$setProp(_el$12, "flexGrow", 1);
|
|
123
|
+
_$setProp(_el$12, "minWidth", 0);
|
|
124
|
+
_$insert(_el$12, () => item.title);
|
|
125
|
+
_$effect(_$p => _$setProp(_el$12, "fg", theme.text.default, _$p));
|
|
126
|
+
return _el$11;
|
|
127
|
+
})()
|
|
128
|
+
}), null);
|
|
129
|
+
_$effect(_$p => _$setProp(_el$9, "fg", color(), _$p));
|
|
130
|
+
return _el$8;
|
|
131
|
+
})();
|
|
144
132
|
}
|
|
145
133
|
}), null);
|
|
146
134
|
_$effect(_$p => _$setProp(_el$2, "fg", theme.text.default, _$p));
|
|
147
135
|
return _el$;
|
|
148
136
|
})();
|
|
149
137
|
}
|
|
150
|
-
function taskDetails(item) {
|
|
151
|
-
return [`
|
|
138
|
+
function taskDetails(item, categories) {
|
|
139
|
+
return [`Category: ${categoryTitle(categories, item.status)}`, `ID: ${item.id}`, "", item.notes ?? "No notes"].join("\n");
|
|
152
140
|
}
|
|
153
141
|
function TaskAction(props) {
|
|
154
142
|
return (() => {
|
|
155
|
-
var _el$
|
|
156
|
-
_el$
|
|
157
|
-
_el$
|
|
158
|
-
_$insertNode(_el$
|
|
159
|
-
_$insertNode(_el$
|
|
160
|
-
_$insert(_el$
|
|
161
|
-
_$insert(_el$
|
|
143
|
+
var _el$13 = _$createElement("text"),
|
|
144
|
+
_el$14 = _$createElement("b"),
|
|
145
|
+
_el$15 = _$createTextNode(` `);
|
|
146
|
+
_$insertNode(_el$13, _el$14);
|
|
147
|
+
_$insertNode(_el$13, _el$15);
|
|
148
|
+
_$insert(_el$14, () => props.shortcut);
|
|
149
|
+
_$insert(_el$13, () => props.label, null);
|
|
162
150
|
_$effect(_p$ => {
|
|
163
|
-
var _v$
|
|
164
|
-
_v$
|
|
165
|
-
_v$
|
|
166
|
-
_v$
|
|
151
|
+
var _v$ = props.danger ? props.context.theme.text.feedback.error.default : props.context.theme.text.subdued,
|
|
152
|
+
_v$2 = props.run;
|
|
153
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$13, "fg", _v$, _p$.e));
|
|
154
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$13, "onMouseUp", _v$2, _p$.t));
|
|
167
155
|
return _p$;
|
|
168
156
|
}, {
|
|
169
157
|
e: undefined,
|
|
170
158
|
t: undefined
|
|
171
159
|
});
|
|
172
|
-
return _el$
|
|
160
|
+
return _el$13;
|
|
173
161
|
})();
|
|
174
162
|
}
|
|
175
163
|
function TaskDetailsDialog(props) {
|
|
@@ -207,37 +195,37 @@ function TaskDetailsDialog(props) {
|
|
|
207
195
|
}]
|
|
208
196
|
}));
|
|
209
197
|
return (() => {
|
|
210
|
-
var _el$
|
|
211
|
-
_el$
|
|
212
|
-
_el$
|
|
213
|
-
_el$
|
|
214
|
-
_el$
|
|
215
|
-
_el$
|
|
216
|
-
_el$
|
|
198
|
+
var _el$16 = _$createElement("box"),
|
|
199
|
+
_el$17 = _$createElement("box"),
|
|
200
|
+
_el$18 = _$createElement("text"),
|
|
201
|
+
_el$19 = _$createElement("b"),
|
|
202
|
+
_el$20 = _$createElement("text"),
|
|
203
|
+
_el$22 = _$createElement("text"),
|
|
204
|
+
_el$23 = _$createElement("box");
|
|
205
|
+
_$insertNode(_el$16, _el$17);
|
|
206
|
+
_$insertNode(_el$16, _el$22);
|
|
207
|
+
_$insertNode(_el$16, _el$23);
|
|
208
|
+
_$setProp(_el$16, "paddingLeft", 2);
|
|
209
|
+
_$setProp(_el$16, "paddingRight", 2);
|
|
210
|
+
_$setProp(_el$16, "gap", 1);
|
|
217
211
|
_$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);
|
|
212
|
+
_$insertNode(_el$17, _el$20);
|
|
213
|
+
_$setProp(_el$17, "flexDirection", "row");
|
|
214
|
+
_$setProp(_el$17, "justifyContent", "space-between");
|
|
215
|
+
_$setProp(_el$17, "gap", 2);
|
|
223
216
|
_$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, {
|
|
217
|
+
_$setProp(_el$18, "wrapMode", "word");
|
|
218
|
+
_$setProp(_el$18, "flexGrow", 1);
|
|
219
|
+
_$insert(_el$19, () => props.item.title);
|
|
220
|
+
_$insertNode(_el$20, _$createTextNode(`esc`));
|
|
221
|
+
_$setProp(_el$20, "onMouseUp", () => props.context.ui.dialog.clear());
|
|
222
|
+
_$setProp(_el$22, "wrapMode", "word");
|
|
223
|
+
_$insert(_el$22, () => taskDetails(props.item, props.categories));
|
|
224
|
+
_$setProp(_el$23, "flexDirection", "row");
|
|
225
|
+
_$setProp(_el$23, "justifyContent", "flex-end");
|
|
226
|
+
_$setProp(_el$23, "gap", 2);
|
|
227
|
+
_$setProp(_el$23, "paddingBottom", 1);
|
|
228
|
+
_$insert(_el$23, _$createComponent(TaskAction, {
|
|
241
229
|
get context() {
|
|
242
230
|
return props.context;
|
|
243
231
|
},
|
|
@@ -245,7 +233,7 @@ function TaskDetailsDialog(props) {
|
|
|
245
233
|
label: "status",
|
|
246
234
|
run: changeStatus
|
|
247
235
|
}), null);
|
|
248
|
-
_$insert(_el$
|
|
236
|
+
_$insert(_el$23, _$createComponent(TaskAction, {
|
|
249
237
|
get context() {
|
|
250
238
|
return props.context;
|
|
251
239
|
},
|
|
@@ -253,7 +241,7 @@ function TaskDetailsDialog(props) {
|
|
|
253
241
|
label: "edit",
|
|
254
242
|
run: edit
|
|
255
243
|
}), null);
|
|
256
|
-
_$insert(_el$
|
|
244
|
+
_$insert(_el$23, _$createComponent(TaskAction, {
|
|
257
245
|
get context() {
|
|
258
246
|
return props.context;
|
|
259
247
|
},
|
|
@@ -263,25 +251,26 @@ function TaskDetailsDialog(props) {
|
|
|
263
251
|
run: remove
|
|
264
252
|
}), null);
|
|
265
253
|
_$effect(_p$ => {
|
|
266
|
-
var _v$
|
|
267
|
-
_v$
|
|
268
|
-
_v$
|
|
269
|
-
_v$
|
|
270
|
-
_v$
|
|
271
|
-
_v$
|
|
254
|
+
var _v$3 = props.context.theme.text.default,
|
|
255
|
+
_v$4 = props.context.theme.text.subdued,
|
|
256
|
+
_v$5 = props.context.theme.text.subdued;
|
|
257
|
+
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$18, "fg", _v$3, _p$.e));
|
|
258
|
+
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$20, "fg", _v$4, _p$.t));
|
|
259
|
+
_v$5 !== _p$.a && (_p$.a = _$setProp(_el$22, "fg", _v$5, _p$.a));
|
|
272
260
|
return _p$;
|
|
273
261
|
}, {
|
|
274
262
|
e: undefined,
|
|
275
263
|
t: undefined,
|
|
276
264
|
a: undefined
|
|
277
265
|
});
|
|
278
|
-
return _el$
|
|
266
|
+
return _el$16;
|
|
279
267
|
})();
|
|
280
268
|
}
|
|
281
|
-
function showTaskDetails(context, item) {
|
|
269
|
+
function showTaskDetails(context, item, categories) {
|
|
282
270
|
context.ui.dialog.show(() => _$createComponent(TaskDetailsDialog, {
|
|
283
271
|
context: context,
|
|
284
|
-
item: item
|
|
272
|
+
item: item,
|
|
273
|
+
categories: categories
|
|
285
274
|
}));
|
|
286
275
|
}
|
|
287
276
|
async function browseBacklog(context) {
|
|
@@ -297,6 +286,9 @@ function backlogLocation(context) {
|
|
|
297
286
|
};
|
|
298
287
|
}
|
|
299
288
|
async function addBacklogItem(context) {
|
|
289
|
+
const {
|
|
290
|
+
path
|
|
291
|
+
} = backlogLocation(context);
|
|
300
292
|
const title = await context.ui.dialog.prompt({
|
|
301
293
|
title: "New backlog task",
|
|
302
294
|
placeholder: "Task title"
|
|
@@ -308,48 +300,52 @@ async function addBacklogItem(context) {
|
|
|
308
300
|
placeholder: "Leave empty for no notes"
|
|
309
301
|
});
|
|
310
302
|
if (notes === undefined) return;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
303
|
+
let item;
|
|
304
|
+
await updateBacklog(path, current => {
|
|
305
|
+
const firstCategory = current.categories[0];
|
|
306
|
+
if (!firstCategory) throw new Error("Add a backlog category before adding a task");
|
|
307
|
+
item = {
|
|
308
|
+
id: randomUUID(),
|
|
309
|
+
title: title.trim(),
|
|
310
|
+
...(notes.trim() ? {
|
|
311
|
+
notes: notes.trim()
|
|
312
|
+
} : {}),
|
|
313
|
+
status: firstCategory.id
|
|
314
|
+
};
|
|
315
|
+
return {
|
|
316
|
+
...current,
|
|
317
|
+
items: moveItem([...current.items, item], item.id, firstCategory.id, 0, current.categories)
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
if (!item) throw new Error("Backlog update did not add a task");
|
|
326
321
|
context.ui.toast.show({
|
|
327
322
|
message: `Added "${item.title}".`,
|
|
328
323
|
variant: "success"
|
|
329
324
|
});
|
|
330
325
|
}
|
|
331
326
|
async function changeTaskStatus(context, item) {
|
|
327
|
+
const {
|
|
328
|
+
path
|
|
329
|
+
} = backlogLocation(context);
|
|
330
|
+
const backlog = await readBacklog(path);
|
|
332
331
|
const status = await context.ui.dialog.select({
|
|
333
|
-
title: `Change
|
|
332
|
+
title: `Change category: ${item.title}`,
|
|
334
333
|
current: item.status,
|
|
335
|
-
options:
|
|
336
|
-
title:
|
|
337
|
-
value:
|
|
338
|
-
...(
|
|
339
|
-
description: "Current
|
|
334
|
+
options: backlog.categories.map(category => ({
|
|
335
|
+
title: category.title,
|
|
336
|
+
value: category.id,
|
|
337
|
+
...(category.id === item.status ? {
|
|
338
|
+
description: "Current category"
|
|
340
339
|
} : {})
|
|
341
340
|
}))
|
|
342
341
|
});
|
|
343
342
|
if (!status || status === item.status) return;
|
|
344
|
-
const {
|
|
345
|
-
path
|
|
346
|
-
} = backlogLocation(context);
|
|
347
343
|
await updateBacklog(path, current => ({
|
|
348
|
-
|
|
349
|
-
items: moveItem(current.items, item.id, status, undefined)
|
|
344
|
+
...current,
|
|
345
|
+
items: moveItem(current.items, item.id, status, undefined, current.categories)
|
|
350
346
|
}));
|
|
351
347
|
context.ui.toast.show({
|
|
352
|
-
message: `Moved "${item.title}" to ${
|
|
348
|
+
message: `Moved "${item.title}" to ${categoryTitle(backlog.categories, status)}.`,
|
|
353
349
|
variant: "success"
|
|
354
350
|
});
|
|
355
351
|
}
|
|
@@ -371,7 +367,7 @@ async function editBacklogItem(context, item) {
|
|
|
371
367
|
path
|
|
372
368
|
} = backlogLocation(context);
|
|
373
369
|
await updateBacklog(path, current => ({
|
|
374
|
-
|
|
370
|
+
...current,
|
|
375
371
|
items: current.items.map(candidate => {
|
|
376
372
|
if (candidate.id !== item.id) return candidate;
|
|
377
373
|
return {
|
|
@@ -403,7 +399,7 @@ async function removeBacklogItem(context, item) {
|
|
|
403
399
|
path
|
|
404
400
|
} = backlogLocation(context);
|
|
405
401
|
await updateBacklog(path, current => ({
|
|
406
|
-
|
|
402
|
+
...current,
|
|
407
403
|
items: current.items.filter(candidate => candidate.id !== item.id)
|
|
408
404
|
}));
|
|
409
405
|
context.ui.toast.show({
|
|
@@ -411,6 +407,234 @@ async function removeBacklogItem(context, item) {
|
|
|
411
407
|
variant: "success"
|
|
412
408
|
});
|
|
413
409
|
}
|
|
410
|
+
async function purgeConfirmedCategory(path, status, confirmedIDs) {
|
|
411
|
+
await updateBacklog(path, current => {
|
|
412
|
+
const currentIDs = current.items.filter(item => item.status === status).map(item => item.id);
|
|
413
|
+
const changed = currentIDs.length !== confirmedIDs.length || currentIDs.some(id => !confirmedIDs.includes(id));
|
|
414
|
+
if (changed) throw new Error("The category changed. Review its tasks and confirm the purge again.");
|
|
415
|
+
return purgeCategory(current, status);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async function purgeBacklogCategory(context) {
|
|
419
|
+
const {
|
|
420
|
+
path
|
|
421
|
+
} = backlogLocation(context);
|
|
422
|
+
const backlog = await readBacklog(path);
|
|
423
|
+
const status = await context.ui.dialog.select({
|
|
424
|
+
title: "Purge backlog category",
|
|
425
|
+
placeholder: "Select a category",
|
|
426
|
+
options: backlog.categories.map(category => {
|
|
427
|
+
const count = backlog.items.filter(item => item.status === category.id).length;
|
|
428
|
+
return {
|
|
429
|
+
title: category.title,
|
|
430
|
+
value: category.id,
|
|
431
|
+
description: `${count} ${count === 1 ? "task" : "tasks"}`
|
|
432
|
+
};
|
|
433
|
+
})
|
|
434
|
+
});
|
|
435
|
+
if (!status) return;
|
|
436
|
+
const category = backlog.categories.find(candidate => candidate.id === status);
|
|
437
|
+
if (!category) return;
|
|
438
|
+
const taskIDs = backlog.items.filter(item => item.status === status).map(item => item.id);
|
|
439
|
+
const count = taskIDs.length;
|
|
440
|
+
const confirmed = await context.ui.dialog.confirm({
|
|
441
|
+
title: `Purge ${category.title}`,
|
|
442
|
+
message: `Permanently delete ${count} ${count === 1 ? "task" : "tasks"} from this category?`,
|
|
443
|
+
label: {
|
|
444
|
+
confirm: "Purge",
|
|
445
|
+
cancel: "Cancel"
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
if (!confirmed) return;
|
|
449
|
+
await purgeConfirmedCategory(path, status, taskIDs);
|
|
450
|
+
context.ui.toast.show({
|
|
451
|
+
message: `Purged ${count} ${count === 1 ? "task" : "tasks"} from ${category.title}.`,
|
|
452
|
+
variant: "success"
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
async function addBacklogCategory(context) {
|
|
456
|
+
const title = await context.ui.dialog.prompt({
|
|
457
|
+
title: "New backlog category",
|
|
458
|
+
placeholder: "Category title"
|
|
459
|
+
});
|
|
460
|
+
if (!title?.trim()) return;
|
|
461
|
+
const suggestedID = title.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
462
|
+
const id = await context.ui.dialog.prompt({
|
|
463
|
+
title: title.trim(),
|
|
464
|
+
description: "Stable category ID",
|
|
465
|
+
placeholder: "Category ID",
|
|
466
|
+
value: suggestedID
|
|
467
|
+
});
|
|
468
|
+
if (!id?.trim()) return;
|
|
469
|
+
const color = await context.ui.dialog.select({
|
|
470
|
+
title: title.trim(),
|
|
471
|
+
placeholder: "Category color",
|
|
472
|
+
current: "info",
|
|
473
|
+
options: CATEGORY_COLORS.map(value => ({
|
|
474
|
+
title: value,
|
|
475
|
+
value
|
|
476
|
+
}))
|
|
477
|
+
});
|
|
478
|
+
if (!color) return;
|
|
479
|
+
const category = {
|
|
480
|
+
id: id.trim(),
|
|
481
|
+
title: title.trim(),
|
|
482
|
+
color
|
|
483
|
+
};
|
|
484
|
+
const {
|
|
485
|
+
path
|
|
486
|
+
} = backlogLocation(context);
|
|
487
|
+
await updateBacklog(path, current => addCategory(current, category));
|
|
488
|
+
context.ui.toast.show({
|
|
489
|
+
message: `Added category "${category.title}".`,
|
|
490
|
+
variant: "success"
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
async function renameBacklogCategory(context, category) {
|
|
494
|
+
const title = await context.ui.dialog.prompt({
|
|
495
|
+
title: "Rename backlog category",
|
|
496
|
+
placeholder: "Category title",
|
|
497
|
+
value: category.title
|
|
498
|
+
});
|
|
499
|
+
if (!title?.trim() || title.trim() === category.title) return;
|
|
500
|
+
const {
|
|
501
|
+
path
|
|
502
|
+
} = backlogLocation(context);
|
|
503
|
+
await updateBacklog(path, current => renameCategory(current, category.id, title.trim()));
|
|
504
|
+
context.ui.toast.show({
|
|
505
|
+
message: `Renamed category to "${title.trim()}".`,
|
|
506
|
+
variant: "success"
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
async function colorBacklogCategory(context, category) {
|
|
510
|
+
const color = await context.ui.dialog.select({
|
|
511
|
+
title: `Color: ${category.title}`,
|
|
512
|
+
placeholder: "Category color",
|
|
513
|
+
current: categoryColorName(category),
|
|
514
|
+
options: CATEGORY_COLORS.map(value => ({
|
|
515
|
+
title: value,
|
|
516
|
+
value
|
|
517
|
+
}))
|
|
518
|
+
});
|
|
519
|
+
if (!color || color === categoryColorName(category)) return;
|
|
520
|
+
const {
|
|
521
|
+
path
|
|
522
|
+
} = backlogLocation(context);
|
|
523
|
+
await updateBacklog(path, current => setCategoryColor(current, category.id, color));
|
|
524
|
+
context.ui.toast.show({
|
|
525
|
+
message: `Updated color for "${category.title}".`,
|
|
526
|
+
variant: "success"
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
async function moveBacklogCategory(context, category) {
|
|
530
|
+
const {
|
|
531
|
+
path
|
|
532
|
+
} = backlogLocation(context);
|
|
533
|
+
const backlog = await readBacklog(path);
|
|
534
|
+
const position = await context.ui.dialog.select({
|
|
535
|
+
title: `Move ${category.title}`,
|
|
536
|
+
placeholder: "Select a position",
|
|
537
|
+
options: backlog.categories.map((candidate, index) => ({
|
|
538
|
+
title: `${index + 1}. ${candidate.title}`,
|
|
539
|
+
value: index,
|
|
540
|
+
...(candidate.id === category.id ? {
|
|
541
|
+
description: "Current position"
|
|
542
|
+
} : {})
|
|
543
|
+
}))
|
|
544
|
+
});
|
|
545
|
+
if (position === undefined || backlog.categories[position]?.id === category.id) return;
|
|
546
|
+
await updateBacklog(path, current => moveCategory(current, category.id, position));
|
|
547
|
+
context.ui.toast.show({
|
|
548
|
+
message: `Moved category "${category.title}".`,
|
|
549
|
+
variant: "success"
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
async function deleteBacklogCategory(context, category) {
|
|
553
|
+
const confirmed = await context.ui.dialog.confirm({
|
|
554
|
+
title: "Delete backlog category",
|
|
555
|
+
message: category.title,
|
|
556
|
+
label: {
|
|
557
|
+
confirm: "Delete",
|
|
558
|
+
cancel: "Cancel"
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
if (!confirmed) return;
|
|
562
|
+
const {
|
|
563
|
+
path
|
|
564
|
+
} = backlogLocation(context);
|
|
565
|
+
await updateBacklog(path, current => removeCategory(current, category.id));
|
|
566
|
+
context.ui.toast.show({
|
|
567
|
+
message: `Deleted category "${category.title}".`,
|
|
568
|
+
variant: "success"
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
async function manageBacklogCategories(context) {
|
|
572
|
+
const {
|
|
573
|
+
path
|
|
574
|
+
} = backlogLocation(context);
|
|
575
|
+
const backlog = await readBacklog(path);
|
|
576
|
+
let addSelection = `__add:${randomUUID()}`;
|
|
577
|
+
while (backlog.categories.some(category => category.id === addSelection)) {
|
|
578
|
+
addSelection = `__add:${randomUUID()}`;
|
|
579
|
+
}
|
|
580
|
+
const selection = await context.ui.dialog.select({
|
|
581
|
+
title: "Backlog categories",
|
|
582
|
+
placeholder: "Add or select a category",
|
|
583
|
+
options: [{
|
|
584
|
+
title: "Add category",
|
|
585
|
+
value: addSelection
|
|
586
|
+
}, ...backlog.categories.map(category => ({
|
|
587
|
+
title: category.title,
|
|
588
|
+
value: category.id,
|
|
589
|
+
description: `${category.id} - ${backlog.items.filter(item => item.status === category.id).length} tasks`
|
|
590
|
+
}))]
|
|
591
|
+
});
|
|
592
|
+
if (!selection) return;
|
|
593
|
+
if (selection === addSelection) return addBacklogCategory(context);
|
|
594
|
+
const category = backlog.categories.find(candidate => candidate.id === selection);
|
|
595
|
+
if (!category) return;
|
|
596
|
+
const taskIDs = backlog.items.filter(item => item.status === category.id).map(item => item.id);
|
|
597
|
+
const count = taskIDs.length;
|
|
598
|
+
const action = await context.ui.dialog.select({
|
|
599
|
+
title: category.title,
|
|
600
|
+
placeholder: "Select an action",
|
|
601
|
+
options: [{
|
|
602
|
+
title: "Rename",
|
|
603
|
+
value: "rename"
|
|
604
|
+
}, {
|
|
605
|
+
title: "Change color",
|
|
606
|
+
value: "color"
|
|
607
|
+
}, {
|
|
608
|
+
title: "Move",
|
|
609
|
+
value: "move"
|
|
610
|
+
}, ...(count === 0 ? [{
|
|
611
|
+
title: "Delete empty category",
|
|
612
|
+
value: "delete"
|
|
613
|
+
}] : [{
|
|
614
|
+
title: `Purge category and ${count} ${count === 1 ? "task" : "tasks"}`,
|
|
615
|
+
value: "purge"
|
|
616
|
+
}])]
|
|
617
|
+
});
|
|
618
|
+
if (action === "rename") return renameBacklogCategory(context, category);
|
|
619
|
+
if (action === "color") return colorBacklogCategory(context, category);
|
|
620
|
+
if (action === "move") return moveBacklogCategory(context, category);
|
|
621
|
+
if (action === "delete") return deleteBacklogCategory(context, category);
|
|
622
|
+
if (action !== "purge") return;
|
|
623
|
+
const confirmed = await context.ui.dialog.confirm({
|
|
624
|
+
title: `Purge ${category.title}`,
|
|
625
|
+
message: `Permanently delete ${count} ${count === 1 ? "task" : "tasks"} from this category?`,
|
|
626
|
+
label: {
|
|
627
|
+
confirm: "Purge",
|
|
628
|
+
cancel: "Cancel"
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
if (!confirmed) return;
|
|
632
|
+
await purgeConfirmedCategory(path, category.id, taskIDs);
|
|
633
|
+
context.ui.toast.show({
|
|
634
|
+
message: `Purged category "${category.title}".`,
|
|
635
|
+
variant: "success"
|
|
636
|
+
});
|
|
637
|
+
}
|
|
414
638
|
async function moveBacklogItem(context) {
|
|
415
639
|
const {
|
|
416
640
|
path
|
|
@@ -432,7 +656,7 @@ async function moveBacklogItem(context) {
|
|
|
432
656
|
...(item.notes === undefined ? {} : {
|
|
433
657
|
description: item.notes
|
|
434
658
|
}),
|
|
435
|
-
category:
|
|
659
|
+
category: categoryTitle(backlog.categories, item.status)
|
|
436
660
|
}))
|
|
437
661
|
});
|
|
438
662
|
if (!id) return;
|
|
@@ -462,7 +686,7 @@ async function browseBacklogWithState(context, setOpen, action = () => "details"
|
|
|
462
686
|
...(item.notes === undefined ? {} : {
|
|
463
687
|
description: item.notes
|
|
464
688
|
}),
|
|
465
|
-
category:
|
|
689
|
+
category: categoryTitle(backlog.categories, item.status)
|
|
466
690
|
}))
|
|
467
691
|
}).finally(() => setOpen(false));
|
|
468
692
|
if (!id) return;
|
|
@@ -472,7 +696,7 @@ async function browseBacklogWithState(context, setOpen, action = () => "details"
|
|
|
472
696
|
if (selectedAction === "status") return changeTaskStatus(context, item);
|
|
473
697
|
if (selectedAction === "edit") return editBacklogItem(context, item);
|
|
474
698
|
if (selectedAction === "delete") return removeBacklogItem(context, item);
|
|
475
|
-
showTaskDetails(context, item);
|
|
699
|
+
showTaskDetails(context, item, backlog.categories);
|
|
476
700
|
}
|
|
477
701
|
function Commands(props) {
|
|
478
702
|
const [browseOpen, setBrowseOpen] = createSignal(false);
|
|
@@ -492,7 +716,7 @@ function Commands(props) {
|
|
|
492
716
|
commands: [{
|
|
493
717
|
id: "backlog.browse",
|
|
494
718
|
title: "Browse backlog",
|
|
495
|
-
description: "View backlog tasks and change their
|
|
719
|
+
description: "View backlog tasks and change their category",
|
|
496
720
|
group: "Backlog",
|
|
497
721
|
palette: true,
|
|
498
722
|
slash: {
|
|
@@ -506,7 +730,7 @@ function Commands(props) {
|
|
|
506
730
|
}, {
|
|
507
731
|
id: "backlog.add",
|
|
508
732
|
title: "Add backlog task",
|
|
509
|
-
description: "Create a
|
|
733
|
+
description: "Create a task at the top of the first category",
|
|
510
734
|
group: "Backlog",
|
|
511
735
|
palette: true,
|
|
512
736
|
slash: {
|
|
@@ -517,7 +741,7 @@ function Commands(props) {
|
|
|
517
741
|
}, {
|
|
518
742
|
id: "backlog.move",
|
|
519
743
|
title: "Move backlog task",
|
|
520
|
-
description: "Change a backlog task
|
|
744
|
+
description: "Change a backlog task category",
|
|
521
745
|
group: "Backlog",
|
|
522
746
|
palette: true,
|
|
523
747
|
slash: {
|
|
@@ -525,6 +749,26 @@ function Commands(props) {
|
|
|
525
749
|
aliases: ["task-move"]
|
|
526
750
|
},
|
|
527
751
|
run: () => run(() => moveBacklogItem(props.context))
|
|
752
|
+
}, {
|
|
753
|
+
id: "backlog.purge",
|
|
754
|
+
title: "Purge backlog category",
|
|
755
|
+
description: "Permanently delete every task in a category",
|
|
756
|
+
group: "Backlog",
|
|
757
|
+
palette: true,
|
|
758
|
+
slash: {
|
|
759
|
+
name: "backlog-purge"
|
|
760
|
+
},
|
|
761
|
+
run: () => run(() => purgeBacklogCategory(props.context))
|
|
762
|
+
}, {
|
|
763
|
+
id: "backlog.categories",
|
|
764
|
+
title: "Manage backlog categories",
|
|
765
|
+
description: "Add, rename, move, purge, or delete a category",
|
|
766
|
+
group: "Backlog",
|
|
767
|
+
palette: true,
|
|
768
|
+
slash: {
|
|
769
|
+
name: "backlog-categories"
|
|
770
|
+
},
|
|
771
|
+
run: () => run(() => manageBacklogCategories(props.context))
|
|
528
772
|
}]
|
|
529
773
|
}));
|
|
530
774
|
props.context.keymap.layer(() => ({
|
|
@@ -539,9 +783,17 @@ function Commands(props) {
|
|
|
539
783
|
setBrowseOpen(false);
|
|
540
784
|
return run(() => addBacklogItem(props.context));
|
|
541
785
|
}
|
|
786
|
+
}, {
|
|
787
|
+
bind: "p",
|
|
788
|
+
title: "Purge backlog category",
|
|
789
|
+
group: "Backlog",
|
|
790
|
+
run() {
|
|
791
|
+
setBrowseOpen(false);
|
|
792
|
+
return run(() => purgeBacklogCategory(props.context));
|
|
793
|
+
}
|
|
542
794
|
}, {
|
|
543
795
|
bind: "c",
|
|
544
|
-
title: "Change selected task
|
|
796
|
+
title: "Change selected task category",
|
|
545
797
|
group: "Backlog",
|
|
546
798
|
run() {
|
|
547
799
|
setBrowseAction("status");
|