taskchef 7.10.0 → 7.11.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/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/dashboard/app.js +33 -13
- package/src/dashboard/index.html +25 -6
- package/src/dashboard/state.js +36 -1
- package/src/dashboard/styles.css +11 -0
package/package.json
CHANGED
package/src/dashboard/app.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
clearNotifications,
|
|
3
3
|
dismissNotification,
|
|
4
|
+
filterTasks,
|
|
4
5
|
findCurrentTask,
|
|
5
|
-
KNOWN_TASK_STATUSES,
|
|
6
6
|
latestTurnPresentation,
|
|
7
7
|
mergeProjectedTurns,
|
|
8
8
|
nextDateFilterRefreshDelay,
|
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
notificationOpenLabel,
|
|
11
11
|
notificationTitle,
|
|
12
12
|
reconcileNotifications,
|
|
13
|
+
statusFilterCounts,
|
|
14
|
+
statusFilterText,
|
|
13
15
|
taskStatusLabel,
|
|
14
|
-
taskWithinDateFilter,
|
|
15
16
|
turnPresentation,
|
|
16
17
|
} from "./state.js";
|
|
17
18
|
import { openTaskFromControl } from "./actions.js";
|
|
@@ -388,20 +389,43 @@ function taskCard(task) {
|
|
|
388
389
|
|
|
389
390
|
function render() {
|
|
390
391
|
const project = elements.projectFilter.value;
|
|
391
|
-
const status = elements.statusFilter.value;
|
|
392
|
+
const status = elements.statusFilter.querySelector("input:checked")?.value ?? "";
|
|
392
393
|
const date = elements.dateFilter.value;
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
394
|
+
const now = Date.now();
|
|
395
|
+
const contextualTasks = filterTasks(state.tasks, { project, date, now });
|
|
396
|
+
const visible = filterTasks(contextualTasks, { status, now });
|
|
397
|
+
const counts = statusFilterCounts(state.tasks, { project, date, now });
|
|
398
|
+
for (const label of elements.statusFilter.querySelectorAll("[data-status-label]")) {
|
|
399
|
+
const value = label.dataset.statusLabel;
|
|
400
|
+
label.textContent = statusFilterText(value, counts[value]);
|
|
401
|
+
}
|
|
397
402
|
elements.taskList.replaceChildren(...visible.map(taskCard));
|
|
398
403
|
elements.emptyState.hidden = visible.length > 0;
|
|
399
404
|
elements.taskCount.textContent = `${visible.length} of ${state.tasks.length} task${state.tasks.length === 1 ? "" : "s"}`;
|
|
400
405
|
clearTimeout(dateRefreshTimer);
|
|
401
|
-
const refreshDelay = nextDateFilterRefreshDelay(
|
|
406
|
+
const refreshDelay = nextDateFilterRefreshDelay(contextualTasks, date, now);
|
|
402
407
|
dateRefreshTimer = refreshDelay === null ? null : setTimeout(render, refreshDelay);
|
|
403
408
|
}
|
|
404
409
|
|
|
410
|
+
function selectStatusFromKeyboard(event) {
|
|
411
|
+
if (event.target?.name !== "status-filter") return;
|
|
412
|
+
const inputs = [...elements.statusFilter.querySelectorAll('input[name="status-filter"]')];
|
|
413
|
+
const currentIndex = inputs.indexOf(event.target);
|
|
414
|
+
if (currentIndex < 0) return;
|
|
415
|
+
let nextIndex = currentIndex;
|
|
416
|
+
if (["ArrowRight", "ArrowDown"].includes(event.key)) nextIndex = (currentIndex + 1) % inputs.length;
|
|
417
|
+
else if (["ArrowLeft", "ArrowUp"].includes(event.key)) {
|
|
418
|
+
nextIndex = (currentIndex - 1 + inputs.length) % inputs.length;
|
|
419
|
+
} else if (event.key === "Home") nextIndex = 0;
|
|
420
|
+
else if (event.key === "End") nextIndex = inputs.length - 1;
|
|
421
|
+
else if (![" ", "Enter"].includes(event.key)) return;
|
|
422
|
+
event.preventDefault();
|
|
423
|
+
for (const input of inputs) input.checked = false;
|
|
424
|
+
inputs[nextIndex].checked = true;
|
|
425
|
+
inputs[nextIndex].focus();
|
|
426
|
+
render();
|
|
427
|
+
}
|
|
428
|
+
|
|
405
429
|
function applySnapshot(snapshot) {
|
|
406
430
|
const reconciled = reconcileNotifications({
|
|
407
431
|
initialized: state.initialized,
|
|
@@ -424,11 +448,6 @@ function applySnapshot(snapshot) {
|
|
|
424
448
|
[...new Set(state.tasks.map((task) => task.project.name))].sort(),
|
|
425
449
|
"All projects",
|
|
426
450
|
);
|
|
427
|
-
replaceOptions(
|
|
428
|
-
elements.statusFilter,
|
|
429
|
-
[...new Set([...KNOWN_TASK_STATUSES, ...state.tasks.map(taskStatusLabel)])],
|
|
430
|
-
"All statuses",
|
|
431
|
-
);
|
|
432
451
|
if (state.selectedTask) {
|
|
433
452
|
const updated = state.tasks.find((task) => task.id === state.selectedTask.id);
|
|
434
453
|
if (updated && elements.dialog.open) openDialog(updated);
|
|
@@ -457,6 +476,7 @@ events.addEventListener("dashboard-error", (event) => {
|
|
|
457
476
|
|
|
458
477
|
elements.projectFilter.addEventListener("change", render);
|
|
459
478
|
elements.statusFilter.addEventListener("change", render);
|
|
479
|
+
elements.statusFilter.addEventListener("keydown", selectStatusFromKeyboard);
|
|
460
480
|
elements.dateFilter.addEventListener("change", render);
|
|
461
481
|
elements.dismissDashboardMessage.addEventListener("click", () => {
|
|
462
482
|
elements.dashboardMessage.hidden = true;
|
package/src/dashboard/index.html
CHANGED
|
@@ -35,12 +35,31 @@
|
|
|
35
35
|
<option value="">All projects</option>
|
|
36
36
|
</select>
|
|
37
37
|
</label>
|
|
38
|
-
<
|
|
39
|
-
Status
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
<fieldset class="status-filter-fieldset">
|
|
39
|
+
<legend>Status</legend>
|
|
40
|
+
<div id="status-filter" class="status-filter-options">
|
|
41
|
+
<label class="status-filter-option">
|
|
42
|
+
<input type="radio" name="status-filter" value="" checked>
|
|
43
|
+
<span data-status-label="">All</span>
|
|
44
|
+
</label>
|
|
45
|
+
<label class="status-filter-option">
|
|
46
|
+
<input type="radio" name="status-filter" value="working">
|
|
47
|
+
<span data-status-label="working">Working</span>
|
|
48
|
+
</label>
|
|
49
|
+
<label class="status-filter-option">
|
|
50
|
+
<input type="radio" name="status-filter" value="needs input">
|
|
51
|
+
<span data-status-label="needs input">Needs input</span>
|
|
52
|
+
</label>
|
|
53
|
+
<label class="status-filter-option">
|
|
54
|
+
<input type="radio" name="status-filter" value="completed">
|
|
55
|
+
<span data-status-label="completed">Completed</span>
|
|
56
|
+
</label>
|
|
57
|
+
<label class="status-filter-option">
|
|
58
|
+
<input type="radio" name="status-filter" value="failed">
|
|
59
|
+
<span data-status-label="failed">Failed</span>
|
|
60
|
+
</label>
|
|
61
|
+
</div>
|
|
62
|
+
</fieldset>
|
|
44
63
|
<label>
|
|
45
64
|
Updated
|
|
46
65
|
<select id="date-filter">
|
package/src/dashboard/state.js
CHANGED
|
@@ -5,7 +5,13 @@ export const KNOWN_TASK_STATUSES = [
|
|
|
5
5
|
"needs input",
|
|
6
6
|
"completed",
|
|
7
7
|
"failed",
|
|
8
|
-
|
|
8
|
+
];
|
|
9
|
+
export const STATUS_FILTERS = [
|
|
10
|
+
{ value: "", label: "All" },
|
|
11
|
+
{ value: "working", label: "Working" },
|
|
12
|
+
{ value: "needs input", label: "Needs input" },
|
|
13
|
+
{ value: "completed", label: "Completed" },
|
|
14
|
+
{ value: "failed", label: "Failed" },
|
|
9
15
|
];
|
|
10
16
|
|
|
11
17
|
const NOTIFICATION_TITLES = new Map([
|
|
@@ -98,6 +104,35 @@ export function taskWithinDateFilter(task, filter, now = Date.now()) {
|
|
|
98
104
|
return Number.isFinite(meaningfulTime) && meaningfulTime >= now - windowMs;
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
export function filterTasks(
|
|
108
|
+
tasks,
|
|
109
|
+
{ project = "", status = "", date = "all", now = Date.now() } = {},
|
|
110
|
+
) {
|
|
111
|
+
return tasks.filter((task) =>
|
|
112
|
+
(!project || task.project.name === project)
|
|
113
|
+
&& (!status || taskStatusLabel(task) === status)
|
|
114
|
+
&& taskWithinDateFilter(task, date, now));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function statusFilterCounts(
|
|
118
|
+
tasks,
|
|
119
|
+
{ project = "", date = "all", now = Date.now() } = {},
|
|
120
|
+
) {
|
|
121
|
+
const contextualTasks = filterTasks(tasks, { project, date, now });
|
|
122
|
+
return Object.fromEntries(STATUS_FILTERS.map(({ value }) => [
|
|
123
|
+
value,
|
|
124
|
+
value
|
|
125
|
+
? contextualTasks.filter((task) => taskStatusLabel(task) === value).length
|
|
126
|
+
: contextualTasks.length,
|
|
127
|
+
]));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function statusFilterText(value, count) {
|
|
131
|
+
const option = STATUS_FILTERS.find((filter) => filter.value === value);
|
|
132
|
+
if (!option) return "";
|
|
133
|
+
return count > 0 ? `${option.label} (${count})` : option.label;
|
|
134
|
+
}
|
|
135
|
+
|
|
101
136
|
export function nextDateFilterRefreshDelay(tasks, filter, now = Date.now()) {
|
|
102
137
|
const windowMs = DATE_WINDOWS_MS.get(filter);
|
|
103
138
|
if (windowMs === null || windowMs === undefined) return null;
|
package/src/dashboard/styles.css
CHANGED
|
@@ -70,6 +70,15 @@ main { padding-block: 28px 80px; }
|
|
|
70
70
|
|
|
71
71
|
label { display: grid; gap: 6px; color: var(--muted); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
72
72
|
select { min-width: 180px; padding: 9px 34px 9px 11px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); color: var(--text); }
|
|
73
|
+
.status-filter-fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
74
|
+
.status-filter-fieldset legend { margin-bottom: 6px; padding: 0; color: var(--muted); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
75
|
+
.status-filter-options { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
76
|
+
.status-filter-option { position: relative; display: block; color: var(--text); font-size: 0.82rem; letter-spacing: 0; text-transform: none; cursor: pointer; }
|
|
77
|
+
.status-filter-option input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
|
78
|
+
.status-filter-option span { display: block; padding: 7px 11px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); white-space: nowrap; }
|
|
79
|
+
.status-filter-option:hover span { border-color: var(--accent); }
|
|
80
|
+
.status-filter-option:has(input:checked) span { border-color: var(--accent); background: var(--accent); color: white; box-shadow: 0 1px 2px rgb(24 33 29 / 16%); }
|
|
81
|
+
.status-filter-option:has(input:focus-visible) span { outline: 3px solid var(--accent); outline-offset: 3px; }
|
|
73
82
|
.task-count { margin: 0 0 9px auto; color: var(--muted); font-size: 0.9rem; }
|
|
74
83
|
|
|
75
84
|
.task-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
|
|
@@ -152,6 +161,7 @@ pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius
|
|
|
152
161
|
.title-row { align-items: flex-start; }
|
|
153
162
|
.toolbar { align-items: stretch; flex-direction: column; }
|
|
154
163
|
.toolbar label, .toolbar select { width: 100%; }
|
|
164
|
+
.toolbar .status-filter-option { width: auto; }
|
|
155
165
|
.task-count { margin: 0; }
|
|
156
166
|
.task-heading { align-items: flex-start; }
|
|
157
167
|
.task-footer { align-items: flex-start; flex-direction: column; }
|
|
@@ -176,6 +186,7 @@ pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius
|
|
|
176
186
|
--warning: #e4bd77;
|
|
177
187
|
}
|
|
178
188
|
.dashboard-message { border-color: #66592e; background: #38321d; color: #f1d98c; }
|
|
189
|
+
.status-filter-option:has(input:checked) span { color: var(--background); }
|
|
179
190
|
.status-failed { background: #4e2927; }
|
|
180
191
|
.status-needs_input { background: #4c3b20; }
|
|
181
192
|
.status-interrupted { background: #37332f; color: #d6d0c7; }
|