Merge claude/1
This commit is contained in:
commit
13cafcb056
@ -382,6 +382,108 @@
|
||||
border-color: var(--status-danger);
|
||||
}
|
||||
|
||||
/* Multi-select checkbox sat to the right of the row's Delete button.
|
||||
Sized + framed to match the surrounding .task-btn buttons so the row
|
||||
reads as a single action group. Hides the native input and renders
|
||||
a custom box; `cursor: pointer` on the wrapping label keeps the
|
||||
whole 24×24 hit target clickable. */
|
||||
.task-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(var(--text-rgb), 0.12);
|
||||
background: rgba(var(--text-rgb), 0.04);
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
.task-select:hover {
|
||||
background: var(--surface-hover);
|
||||
border-color: var(--status-success);
|
||||
}
|
||||
.task-select input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
width: 0; height: 0;
|
||||
}
|
||||
.task-select-box {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
border: 1.5px solid rgba(var(--text-rgb), 0.45);
|
||||
background: transparent;
|
||||
position: relative;
|
||||
transition: background 0.12s ease, border-color 0.12s ease;
|
||||
}
|
||||
.task-select input[type="checkbox"]:checked + .task-select-box {
|
||||
background: var(--status-success);
|
||||
border-color: var(--status-success);
|
||||
}
|
||||
.task-select input[type="checkbox"]:checked + .task-select-box::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 4px;
|
||||
width: 3px;
|
||||
height: 7px;
|
||||
border: solid var(--text-on-accent, #fff);
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.task-select input[type="checkbox"]:focus-visible + .task-select-box {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Master "Select all" toggle in the status/action bar (left of Clear All).
|
||||
Uses the same custom-box visual as the row checkbox so the two reinforce
|
||||
each other; the label sits inline with the button text style. */
|
||||
.task-select-all {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
user-select: none;
|
||||
transition: background 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
.task-select-all:hover {
|
||||
background: var(--surface-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.task-select-all input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
width: 0; height: 0;
|
||||
}
|
||||
/* The indeterminate state (some-but-not-all visible rows ticked) shows
|
||||
a horizontal dash instead of a check. */
|
||||
.task-select-all input[type="checkbox"]:indeterminate + .task-select-box {
|
||||
background: var(--status-success);
|
||||
border-color: var(--status-success);
|
||||
}
|
||||
.task-select-all input[type="checkbox"]:indeterminate + .task-select-box::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 1px;
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
background: var(--text-on-accent, #fff);
|
||||
border: none;
|
||||
transform: none;
|
||||
}
|
||||
.task-select-all-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.task-details {
|
||||
border-top: 1px solid rgba(var(--text-rgb), 0.10);
|
||||
background: transparent;
|
||||
|
||||
@ -151,12 +151,17 @@
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
<button class="clear-btn" onclick="clearAllTasks()" title="Clear All Tasks">
|
||||
<label class="task-select-all" title="Select all visible tasks">
|
||||
<input type="checkbox" id="tasks-select-all" onchange="window.tasksManager && tasksManager.toggleSelectAll(this.checked)">
|
||||
<span class="task-select-box" aria-hidden="true"></span>
|
||||
<span class="task-select-all-label">Select all</span>
|
||||
</label>
|
||||
<button class="clear-btn" id="tasks-clear-btn" onclick="clearAllTasks()" title="Clear All Tasks">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 6h18"></path>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
Clear All
|
||||
<span class="clear-btn-label">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -8,6 +8,10 @@ class TasksManager {
|
||||
this.tasks = [];
|
||||
this.currentCategory = 'all';
|
||||
this.highlightedTaskId = null;
|
||||
// Multi-select: ids of tasks the user has ticked. When non-empty the
|
||||
// "Clear All" button morphs into "Delete Selected (N)". Both paths
|
||||
// share _showClearAllModal — same UX, different filter.
|
||||
this.selectedTaskIds = new Set();
|
||||
this.taskManager = new TaskManager();
|
||||
this.init();
|
||||
|
||||
@ -556,6 +560,11 @@ class TasksManager {
|
||||
if (scrollParent) {
|
||||
requestAnimationFrame(() => { scrollParent.scrollTop = savedScrollTop; });
|
||||
}
|
||||
|
||||
// Resync the multi-select UI after the render. The Clear All label
|
||||
// + master-checkbox tri-state need to reflect the just-rendered row
|
||||
// set (selections can become stale across category switches).
|
||||
if (typeof this._updateSelectionUI === 'function') this._updateSelectionUI();
|
||||
}
|
||||
|
||||
filterTasksByCategory(tasks, category) {
|
||||
@ -677,6 +686,11 @@ class TasksManager {
|
||||
</svg>
|
||||
<span class="task-btn-label">Delete</span>
|
||||
</button>
|
||||
<label class="task-select" onclick="event.stopPropagation();" title="Select for bulk delete">
|
||||
<input type="checkbox" data-task-select="${task.id}" ${this.selectedTaskIds.has(task.id) ? 'checked' : ''}
|
||||
onchange="event.stopPropagation(); window.tasksManager && tasksManager.toggleTaskSelection('${task.id}', this.checked)">
|
||||
<span class="task-select-box" aria-hidden="true"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2328,19 +2342,86 @@ class TasksManager {
|
||||
}
|
||||
|
||||
async clearAllTasks() {
|
||||
const result = await this._showClearAllModal(this.tasks);
|
||||
// Routes to one of two modes depending on whether any rows are ticked.
|
||||
// Both paths share _showClearAllModal — same UX, same modal, different
|
||||
// input list + title.
|
||||
const hasSelection = this.selectedTaskIds.size > 0;
|
||||
const targetTasks = hasSelection
|
||||
? this.tasks.filter(t => this.selectedTaskIds.has(t.id))
|
||||
: this.tasks;
|
||||
const result = await this._showClearAllModal(targetTasks, hasSelection ? 'selected' : 'all');
|
||||
if (!result || !result.confirmed) return false;
|
||||
await this.performClearAll({ cancelRunning: result.cancelRunning });
|
||||
await this.performClearAll({ cancelRunning: result.cancelRunning, targets: targetTasks });
|
||||
if (hasSelection) {
|
||||
// Drop any ids we just deleted from the selection set, then refresh
|
||||
// the button label + master-checkbox state.
|
||||
this.selectedTaskIds = new Set([...this.selectedTaskIds].filter(id => this.tasks.find(t => t.id === id)));
|
||||
this._updateSelectionUI();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Selection helpers wired by the row + master checkboxes.
|
||||
toggleTaskSelection(taskId, checked) {
|
||||
if (checked) this.selectedTaskIds.add(taskId);
|
||||
else this.selectedTaskIds.delete(taskId);
|
||||
this._updateSelectionUI();
|
||||
}
|
||||
|
||||
toggleSelectAll(checked) {
|
||||
if (checked) {
|
||||
// Tick every CURRENTLY VISIBLE row. We use the rendered checkboxes
|
||||
// rather than this.tasks so category-filtered views only select
|
||||
// what the user can see.
|
||||
const boxes = document.querySelectorAll('#tasks-list [data-task-select]');
|
||||
boxes.forEach((cb) => {
|
||||
this.selectedTaskIds.add(cb.dataset.taskSelect);
|
||||
cb.checked = true;
|
||||
});
|
||||
} else {
|
||||
this.selectedTaskIds.clear();
|
||||
document.querySelectorAll('#tasks-list [data-task-select]').forEach((cb) => { cb.checked = false; });
|
||||
}
|
||||
this._updateSelectionUI();
|
||||
}
|
||||
|
||||
// Rewrites the Clear All button label + the master-checkbox indeterminate
|
||||
// state to reflect the current selection. Cheap — only touches a few
|
||||
// DOM nodes, safe to call from any selection-change path.
|
||||
_updateSelectionUI() {
|
||||
const n = this.selectedTaskIds.size;
|
||||
const btn = document.getElementById('tasks-clear-btn');
|
||||
const btnLabel = btn && btn.querySelector('.clear-btn-label');
|
||||
if (btnLabel) btnLabel.textContent = n > 0 ? `Delete Selected (${n})` : 'Clear All';
|
||||
if (btn) btn.title = n > 0 ? `Delete ${n} selected task${n === 1 ? '' : 's'}` : 'Clear All Tasks';
|
||||
|
||||
// Master checkbox: checked when ALL visible are picked, indeterminate
|
||||
// when SOME are, unchecked when none.
|
||||
const master = document.getElementById('tasks-select-all');
|
||||
const visible = document.querySelectorAll('#tasks-list [data-task-select]');
|
||||
if (master) {
|
||||
if (n === 0 || visible.length === 0) {
|
||||
master.checked = false;
|
||||
master.indeterminate = false;
|
||||
} else if (n >= visible.length) {
|
||||
master.checked = true;
|
||||
master.indeterminate = false;
|
||||
} else {
|
||||
master.checked = false;
|
||||
master.indeterminate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmation modal for clearAllTasks. Same openEoModal shape as
|
||||
// _showDeleteTaskModal so visual identity matches every other destructive
|
||||
// confirmation (Uninstall, Delete Task, …). Adds a "Cancel running tasks
|
||||
// too" toggle (off by default) — when off, running/queued tasks are
|
||||
// skipped; when on, they're cancelled first then deleted.
|
||||
// Resolves {confirmed, cancelRunning}. Cancel/backdrop/close → confirmed=false.
|
||||
_showClearAllModal(tasks) {
|
||||
// mode: 'all' (everything) or 'selected' (user-picked subset) — just
|
||||
// shapes the title/copy.
|
||||
_showClearAllModal(tasks, mode) {
|
||||
return new Promise((resolve) => {
|
||||
const escHtml = (s) => String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
@ -2389,12 +2470,21 @@ class TasksManager {
|
||||
resolve(val);
|
||||
};
|
||||
|
||||
// Title shape depends on which path got us here:
|
||||
// selected → "Delete N selected task(s)?" (multi-select chip)
|
||||
// all → "Delete all N tasks?" (legacy Clear All)
|
||||
const isSelectedMode = mode === 'selected';
|
||||
const titleText = isSelectedMode
|
||||
? (total === 1 ? 'Delete 1 selected task?' : `Delete ${total} selected tasks?`)
|
||||
: (total === 1 ? 'Delete 1 task?' : `Delete all ${total} tasks?`);
|
||||
const m = window.openEoModal({
|
||||
id: 'clear-all-tasks-modal',
|
||||
size: 'sm',
|
||||
eyebrow: 'Delete Tasks',
|
||||
title: total === 1 ? 'Delete 1 task?' : `Delete all ${total} tasks?`,
|
||||
desc: 'Confirm to delete the selected tasks.',
|
||||
eyebrow: isSelectedMode ? 'Delete Selected' : 'Delete Tasks',
|
||||
title: titleText,
|
||||
desc: isSelectedMode
|
||||
? 'Confirm to delete the ticked tasks.'
|
||||
: 'Confirm to delete every task on the list.',
|
||||
body: bodyHtml,
|
||||
actions: [
|
||||
{
|
||||
@ -2432,6 +2522,10 @@ class TasksManager {
|
||||
async performClearAll(opts) {
|
||||
opts = opts || {};
|
||||
const cancelRunning = !!opts.cancelRunning;
|
||||
// Caller (clearAllTasks) passes the subset to operate on. Falls back
|
||||
// to this.tasks for backward compatibility — old direct callers
|
||||
// continue to mean "everything".
|
||||
const universe = Array.isArray(opts.targets) ? opts.targets : (this.tasks || []);
|
||||
|
||||
// Show progress notification with the trash type icon in the left slot
|
||||
// (this is conceptually a delete-all action, so 🗑️ matches the row icon).
|
||||
@ -2447,8 +2541,8 @@ class TasksManager {
|
||||
|
||||
const isActive = (t) => t && (t.status === 'running' || t.status === 'queued' || t.status === 'pending');
|
||||
// Partition: which tasks we attempt now, which we skip silently.
|
||||
const targets = (this.tasks || []).filter(t => cancelRunning || !isActive(t));
|
||||
const skipped = (this.tasks || []).filter(t => !targets.includes(t));
|
||||
const targets = universe.filter(t => cancelRunning || !isActive(t));
|
||||
const skipped = universe.filter(t => !targets.includes(t));
|
||||
|
||||
try {
|
||||
// For active tasks (only reached when the toggle is on): cancel first,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user