feat(#1007): select-as-action — picking a parent directly triggers M0V3
Selecting any option in the M0V3 dropdown now fires confirm+POST immediately; no separate button needed. On cancel or after the request completes, the select resets to the placeholder. Removes the now-redundant btn-move button and its CSS rule.
This commit is contained in:
parent
1f5197a0a5
commit
912f9c5ed2
2 changed files with 24 additions and 39 deletions
|
|
@ -2520,6 +2520,4 @@ body.dashboard-shell.has-selection { padding-bottom: 4.5em; }
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.move-picker .btn-move {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -933,33 +933,24 @@ window.marked = marked;
|
||||||
disabledTitle: why('PURG3', managerNames.map((n) => `\`${n}\` is the manager`)),
|
disabledTitle: why('PURG3', managerNames.map((n) => `\`${n}\` is the manager`)),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Move agent(s) in the topology tree — see
|
// Move agent(s) in the topology tree — selecting an option in the
|
||||||
// docs/web-ui.md::Selection bar for the two affordances:
|
// M0V3 dropdown immediately confirms + executes the move. "(no parent)"
|
||||||
|
// promotes to root (empty new_parent on the backend). Cycle-safe:
|
||||||
|
// dropdown filters out self and descendants on the client side; the
|
||||||
|
// backend rechecks via `topology::set_parent`.
|
||||||
//
|
//
|
||||||
// ⇡ M0V3 → ROOT promote selected agent(s) to top-level (parent=null)
|
// Backend: POST /api/topology/set-parent (dashboard.rs),
|
||||||
// ⇢ M0V3 → [sel] reparent the single selected agent under a picked
|
// form-encoded `child=<name>&new_parent=<target-or-empty>`. Re-emits
|
||||||
// parent (cycle-safe — the dropdown filters out self
|
// container snapshots on success so the tree repaints without a
|
||||||
// and own descendants on the client side; the
|
// separate refresh.
|
||||||
// backend rechecks via `topology::set_parent`).
|
|
||||||
//
|
|
||||||
// Backend lives at POST /api/topology/set-parent (dashboard.rs),
|
|
||||||
// form-encoded `child=<name>&new_parent=<target-or-empty>`. The
|
|
||||||
// backend re-emits container snapshots on success, so the tree
|
|
||||||
// repaints without a separate refresh.
|
|
||||||
addMoveActions(actions, selected, containers);
|
addMoveActions(actions, selected, containers);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the M0V3 affordances inside the selection bar — separate
|
// Render the M0V3 picker in the selection bar. Selecting any real option
|
||||||
// helper because the picker variant needs a select + button pair,
|
// (including "(no parent)") immediately fires a confirm + POST — no
|
||||||
// not the single-button shape addBulkButton ships. Backend
|
// separate button. Backend `topology::set_parent` refuses invalid moves
|
||||||
// `topology::set_parent` refuses moves it can't satisfy (manager
|
// and the refusal surfaces in the alert roll-up.
|
||||||
// included) and the refusal surfaces in the bulk-action error
|
|
||||||
// roll-up.
|
|
||||||
function addMoveActions(parent, selected, containers) {
|
function addMoveActions(parent, selected, containers) {
|
||||||
// Unified M0V3 picker: a single `<select>` with "(no parent)" as the
|
|
||||||
// first real choice (backend: empty new_parent → promotes to root) plus
|
|
||||||
// all valid candidate agents. Removes the need for a separate ROOT
|
|
||||||
// button — picking "(no parent)" achieves the same outcome.
|
|
||||||
const candidates = validReparentCandidates(selected, containers);
|
const candidates = validReparentCandidates(selected, containers);
|
||||||
const wrap = el('span', { class: 'move-picker' });
|
const wrap = el('span', { class: 'move-picker' });
|
||||||
const selectTitle = selected.length === 1
|
const selectTitle = selected.length === 1
|
||||||
|
|
@ -967,28 +958,24 @@ window.marked = marked;
|
||||||
: `change ${selected.length} agents' parent`;
|
: `change ${selected.length} agents' parent`;
|
||||||
const sel = el('select', { class: 'move-picker-select', title: selectTitle });
|
const sel = el('select', { class: 'move-picker-select', title: selectTitle });
|
||||||
sel.append(el('option', { value: '' }, '⇢ M0V3 →'));
|
sel.append(el('option', { value: '' }, '⇢ M0V3 →'));
|
||||||
// "(no parent)" promotes selected agent(s) to top-level.
|
// "(no parent)" → empty new_parent on the backend (promotes to root).
|
||||||
sel.append(el('option', { value: '__root__' }, '(no parent)'));
|
sel.append(el('option', { value: '__root__' }, '(no parent)'));
|
||||||
for (const name of candidates) {
|
for (const name of candidates) {
|
||||||
sel.append(el('option', { value: name }, name));
|
sel.append(el('option', { value: name }, name));
|
||||||
}
|
}
|
||||||
const btn = el('button', { type: 'button', class: 'btn btn-move' }, 'M0V3');
|
sel.addEventListener('change', async () => {
|
||||||
btn.disabled = true;
|
|
||||||
// Enable once any real choice is made (selectedIndex > 0).
|
|
||||||
sel.addEventListener('change', () => { btn.disabled = sel.selectedIndex === 0; });
|
|
||||||
btn.addEventListener('click', async () => {
|
|
||||||
if (sel.selectedIndex === 0) return;
|
if (sel.selectedIndex === 0) return;
|
||||||
// '__root__' → empty new_parent (promotes to root / no parent).
|
|
||||||
const newParent = sel.value === '__root__' ? '' : sel.value;
|
const newParent = sel.value === '__root__' ? '' : sel.value;
|
||||||
const newParentLabel = sel.value === '__root__' ? '(no parent)' : sel.value;
|
const newParentLabel = sel.value === '__root__' ? '(no parent)' : sel.value;
|
||||||
const names = selected.map((c) => c.name);
|
const names = selected.map((c) => c.name);
|
||||||
const promptMsg = names.length === 1
|
const promptMsg = names.length === 1
|
||||||
? `move ${names[0]} → ${newParentLabel}?`
|
? `move ${names[0]} → ${newParentLabel}?`
|
||||||
: `move ${names.length} agents (${names.join(', ')}) → ${newParentLabel}?`;
|
: `move ${names.length} agents (${names.join(', ')}) → ${newParentLabel}?`;
|
||||||
if (!confirm(promptMsg)) return;
|
if (!confirm(promptMsg)) {
|
||||||
btn.disabled = true;
|
sel.selectedIndex = 0;
|
||||||
const original = btn.textContent;
|
return;
|
||||||
btn.innerHTML = '<span class="spinner">◐</span>';
|
}
|
||||||
|
sel.disabled = true;
|
||||||
const failures = [];
|
const failures = [];
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1009,13 +996,13 @@ window.marked = marked;
|
||||||
failures.push(`${name}: ${err}`);
|
failures.push(`${name}: ${err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btn.textContent = original;
|
sel.disabled = false;
|
||||||
btn.disabled = sel.selectedIndex === 0;
|
sel.selectedIndex = 0;
|
||||||
if (failures.length) {
|
if (failures.length) {
|
||||||
alert(`M0V3 completed with ${failures.length} failure${failures.length === 1 ? '' : 's'}:\n\n` + failures.join('\n'));
|
alert(`M0V3 completed with ${failures.length} failure${failures.length === 1 ? '' : 's'}:\n\n` + failures.join('\n'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
wrap.append(sel, btn);
|
wrap.append(sel);
|
||||||
parent.append(wrap);
|
parent.append(wrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue