feat: add batch task approval settings and fix modal dropdown scrolling

This commit is contained in:
Ed1s0nZ
2026-09-13 18:02:43 +08:00
parent c563567502
commit ac101d8476
13 changed files with 387 additions and 32 deletions
+72
View File
@@ -894,6 +894,8 @@ async function showBatchImportModal() {
if (titleInput) {
titleInput.value = '';
}
const hitlSelect = document.getElementById('batch-queue-hitl-policy');
if (hitlSelect) hitlSelect.value = '';
// 重置角色选择为默认
if (roleSelect) {
roleSelect.value = '';
@@ -1059,6 +1061,7 @@ async function createBatchQueue() {
tasks,
role,
agentMode,
hitlPolicy: document.getElementById('batch-queue-hitl-policy')?.value || '',
scheduleMode,
cronExpr,
executeNow,
@@ -1318,6 +1321,7 @@ const BATCH_IMPORT_FORM_SELECT_IDS = [
'batch-queue-role',
'batch-queue-project-id',
'batch-queue-agent-mode',
'batch-queue-hitl-policy',
'batch-queue-schedule-mode',
];
const batchFormSelectMap = {};
@@ -1333,6 +1337,21 @@ function closeAllBatchFormSelects() {
});
}
// Keep the menu inside the modal scrollport without changing the modal's overflow.
function positionBatchFormDropdown(wrapper, trigger, dropdown) {
const body = wrapper.closest('.modal-body');
const bounds = body ? body.getBoundingClientRect() : { top: 0, bottom: window.innerHeight };
const rect = trigger.getBoundingClientRect();
const above = Math.max(0, rect.top - Math.max(0, bounds.top) - 8);
const below = Math.max(0, Math.min(window.innerHeight, bounds.bottom) - rect.bottom - 8);
const desired = Math.min(280, dropdown.scrollHeight + 2);
const openAbove = below < desired && above > below;
dropdown.style.top = openAbove ? 'auto' : 'calc(100% + 4px)';
dropdown.style.bottom = openAbove ? 'calc(100% + 4px)' : 'auto';
dropdown.style.maxHeight = Math.min(280, openAbove ? above : below) + 'px';
dropdown.style.boxSizing = 'border-box';
}
function syncBatchFormSelect(selectId) {
const reg = batchFormSelectMap[selectId];
if (!reg) return;
@@ -1457,6 +1476,7 @@ function enhanceBatchFormSelect(selectId, options) {
if (!open) {
wrapper.classList.add('open');
trigger.setAttribute('aria-expanded', 'true');
positionBatchFormDropdown(wrapper, trigger, dropdown);
}
});
@@ -1857,6 +1877,7 @@ async function showBatchQueueDetail(queueId) {
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.queueTitle'))}</span><span class="bq-kv__v" id="bq-title-val">${allowSubtaskMutation ? `<span class="bq-inline-editable" onclick="startInlineEditTitle()" title="${escapeHtml(_t('common.edit'))}">${escapeHtml(queue.title || _t('tasks.batchQueueUntitled'))}</span>` : escapeHtml(queue.title || _t('tasks.batchQueueUntitled'))}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.role'))}</span><span class="bq-kv__v" id="bq-role-val">${allowSubtaskMutation ? `<span class="bq-inline-editable" onclick="startInlineEditRole()" title="${escapeHtml(_t('common.edit'))}">${roleLineVal}</span>` : roleLineVal}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchImportModal.agentMode'))}</span><span class="bq-kv__v" id="bq-agentmode-val">${allowSubtaskMutation ? `<span class="bq-inline-editable" onclick="startInlineEditAgentMode()" title="${escapeHtml(_t('common.edit'))}">${escapeHtml(agentModeText)}</span>` : escapeHtml(agentModeText)}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchImportModal.hitlPolicy'))}</span><span class="bq-kv__v" id="bq-hitl-val">${allowSubtaskMutation ? `<button type="button" class="btn-link" onclick="startInlineEditHITLPolicy()" title="${escapeAttr(_t('common.edit'))}">${escapeHtml(batchHITLPolicyLabel(queue.hitlPolicy))}</button>` : escapeHtml(batchHITLPolicyLabel(queue.hitlPolicy))}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchImportModal.scheduleMode'))}</span><span class="bq-kv__v" id="bq-schedule-val">${allowSubtaskMutation ? `<span class="bq-inline-editable" onclick="startInlineEditSchedule()" title="${escapeHtml(_t('common.edit'))}">${scheduleDetail}</span>` : scheduleDetail}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.concurrency'))}</span><span class="bq-kv__v" id="bq-concurrency-val">${allowSubtaskMutation ? `<span class="bq-inline-editable" onclick="startInlineEditConcurrency()" title="${escapeHtml(_t('common.edit'))}">${escapeHtml(String(queue.concurrency && queue.concurrency > 0 ? queue.concurrency : 1))}</span>` : escapeHtml(String(queue.concurrency && queue.concurrency > 0 ? queue.concurrency : 1))}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.taskTotal'))}</span><span class="bq-kv__v">${queue.tasks.length}</span></div>
@@ -2920,3 +2941,54 @@ document.addEventListener('DOMContentLoaded', function () {
initBatchQueuesFilterSelects();
initBatchFormSelects();
});
const BATCH_HITL_POLICIES = {
'': 'hitlInherit', off: 'hitlOff', human: 'hitlHuman',
audit_agent: 'hitlAgent', review_edit: 'hitlReviewEdit'
};
function batchHITLPolicyLabel(policy) {
return _t('batchImportModal.' + (BATCH_HITL_POLICIES[policy || ''] || 'hitlInherit'));
}
async function startInlineEditHITLPolicy() {
if (typeof requirePermission === 'function' && !requirePermission('tasks:write')) return;
const queueId = batchQueuesState.currentQueueId;
const container = document.getElementById('bq-hitl-val');
if (!queueId || !container) return;
try {
const response = await apiFetch(`/api/batch-tasks/${queueId}`);
if (!response.ok) throw new Error(_t('tasks.loadTaskListFailed'));
const { queue } = await response.json();
if (batchQueuesState.currentQueueId !== queueId || !batchQueueAllowsSubtaskMutation(queue)) return;
container.innerHTML = `<select id="bq-edit-hitl" aria-label="${escapeAttr(_t('batchImportModal.hitlPolicy'))}">${Object.keys(BATCH_HITL_POLICIES).map(policy => `<option value="${policy}" ${policy === (queue.hitlPolicy || '') ? 'selected' : ''}>${escapeHtml(batchHITLPolicyLabel(policy))}</option>`).join('')}</select>`;
const select = document.getElementById('bq-edit-hitl');
select.focus();
select.addEventListener('keydown', e => {
if (e.key === 'Escape') showBatchQueueDetail(queueId);
});
select.addEventListener('change', async () => {
select.disabled = true;
try {
const result = await apiFetch(`/api/batch-tasks/${queueId}/metadata`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: queue.title || '', role: queue.role || '',
hitlPolicy: select.value
})
});
if (!result.ok) {
const error = await result.json().catch(() => ({}));
throw new Error(error.error || _t('tasks.updateTaskFailed'));
}
if (batchQueuesState.currentQueueId === queueId) showBatchQueueDetail(queueId);
refreshBatchQueues();
} catch (error) {
select.disabled = false;
alert(error.message);
}
});
} catch (error) { alert(error.message); }
}
window.startInlineEditHITLPolicy = startInlineEditHITLPolicy;