feat(#1007): unify M0V3 → ROOT and reparent into single picker
Remove the separate '⇡ M0V3 → ROOT' button from the selection bar. Add '(no parent)' as the first real option in the existing M0V3 dropdown — selecting it submits an empty new_parent, which the backend already treats as 'promote to root'. The select placeholder label changes to '⇢ M0V3 →' so the combined control reads naturally without the old standalone button. The submit button enables as soon as any option past the placeholder is chosen (selectedIndex > 0), which correctly covers both '(no parent)' and named-parent selections.
This commit is contained in:
parent
a7d31046a6
commit
1f5197a0a5
1 changed files with 21 additions and 39 deletions
|
|
@ -956,57 +956,39 @@ window.marked = marked;
|
||||||
// included) and the refusal surfaces in the bulk-action error
|
// included) and the refusal surfaces in the bulk-action error
|
||||||
// roll-up.
|
// roll-up.
|
||||||
function addMoveActions(parent, selected, containers) {
|
function addMoveActions(parent, selected, containers) {
|
||||||
// M0V3 → ROOT: parent=null for every selected agent. Only meaningful
|
// Unified M0V3 picker: a single `<select>` with "(no parent)" as the
|
||||||
// when at least one selected agent currently has a non-null parent;
|
// first real choice (backend: empty new_parent → promotes to root) plus
|
||||||
// otherwise it's a no-op for everything.
|
// all valid candidate agents. Removes the need for a separate ROOT
|
||||||
const someNotAtRoot = selected.some((c) => c.parent);
|
// button — picking "(no parent)" achieves the same outcome.
|
||||||
addBulkButton(parent, 'btn-move', '⇡ M0V3 → ROOT', someNotAtRoot, selected, {
|
|
||||||
action: '/api/topology/set-parent',
|
|
||||||
perAgentBodyFor: (name) => ({ child: name, new_parent: '' }),
|
|
||||||
confirm: (names) => `promote ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')}) to top-level (parent → root)?`,
|
|
||||||
disabledTitle: !someNotAtRoot
|
|
||||||
? '⇡ M0V3 → ROOT not available — all selected agents are already at root'
|
|
||||||
: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// M0V3 → <pick>: inline `<select>` of candidate parents + submit
|
|
||||||
// button. Available for any selection size. Picker omits each
|
|
||||||
// selected agent itself plus the union of every selected agent's
|
|
||||||
// descendants (cycle-safe; backend `topology::set_parent`
|
|
||||||
// re-checks). Empty candidate list ⇒ disable the picker.
|
|
||||||
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
|
||||||
? `change ${selected[0].name}'s parent`
|
? `change ${selected[0].name}'s parent`
|
||||||
: `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: '' }, '— pick parent —'));
|
sel.append(el('option', { value: '' }, '⇢ M0V3 →'));
|
||||||
|
// "(no parent)" promotes selected agent(s) to top-level.
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
if (!candidates.length) {
|
const btn = el('button', { type: 'button', class: 'btn btn-move' }, 'M0V3');
|
||||||
sel.disabled = true;
|
|
||||||
sel.title = selected.length === 1
|
|
||||||
? `no valid parents for ${selected[0].name} (every other agent is its descendant)`
|
|
||||||
: `no valid parents — every other agent is a descendant of one of the selected`;
|
|
||||||
}
|
|
||||||
const btn = el('button', { type: 'button', class: 'btn btn-move' }, '⇢ M0V3');
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
sel.addEventListener('change', () => { btn.disabled = !sel.value; });
|
// Enable once any real choice is made (selectedIndex > 0).
|
||||||
|
sel.addEventListener('change', () => { btn.disabled = sel.selectedIndex === 0; });
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const newParent = sel.value;
|
if (sel.selectedIndex === 0) return;
|
||||||
if (!newParent) return;
|
// '__root__' → empty new_parent (promotes to root / no parent).
|
||||||
|
const newParent = sel.value === '__root__' ? '' : 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]} under ${newParent}?`
|
? `move ${names[0]} → ${newParentLabel}?`
|
||||||
: `move ${names.length} agents (${names.join(', ')}) under ${newParent}?`;
|
: `move ${names.length} agents (${names.join(', ')}) → ${newParentLabel}?`;
|
||||||
if (!confirm(promptMsg)) return;
|
if (!confirm(promptMsg)) return;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const original = btn.innerHTML;
|
const original = btn.textContent;
|
||||||
btn.innerHTML = '<span class="spinner">◐</span> ⇢ M0V3';
|
btn.innerHTML = '<span class="spinner">◐</span>';
|
||||||
// Sequential POSTs — same shape as the bulk-button loop. Each
|
|
||||||
// call is small + backend serialises topology writes via the
|
|
||||||
// file-lock anyway.
|
|
||||||
const failures = [];
|
const failures = [];
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1027,10 +1009,10 @@ window.marked = marked;
|
||||||
failures.push(`${name}: ${err}`);
|
failures.push(`${name}: ${err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btn.innerHTML = original;
|
btn.textContent = original;
|
||||||
btn.disabled = !sel.value;
|
btn.disabled = 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, btn);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue