From 6cd993e7d491e9612ee8f651118213b6c0043569 Mon Sep 17 00:00:00 2001 From: witbrock Date: Sat, 12 Sep 2026 21:22:37 -0400 Subject: [PATCH] Clarify task popup controls and open exact tasks in workspace --- .../static/js/components/taskPanel.js | 56 ++++++++++- .../web/von_interface/static/styles.css | 22 ++++- .../web/von_interface/templates/chat_tab.html | 6 +- tests/browser/taskPopupControls.cjs | 92 +++++++++++++++++++ tests/frontend/taskPanelReference.test.js | 48 ++++++++++ 5 files changed, 215 insertions(+), 9 deletions(-) create mode 100644 tests/browser/taskPopupControls.cjs diff --git a/src/frontend/web/von_interface/static/js/components/taskPanel.js b/src/frontend/web/von_interface/static/js/components/taskPanel.js index 603b6dd99..03aa87d04 100644 --- a/src/frontend/web/von_interface/static/js/components/taskPanel.js +++ b/src/frontend/web/von_interface/static/js/components/taskPanel.js @@ -30,6 +30,9 @@ let _filterTaskSourceId = 'all'; let _isGlobalTabMode = false; // True when rendering into global tasks tab let _taskDetailState = {}; // taskId -> detail panel state let _selectedTaskId = ''; +let _pendingTaskNavigation = null; +let _referencedGlobalTaskId = ''; +let _panelReturnFocus = null; let _panelTaskIds = null; let _panelLoadGeneration = 0; let _bulkTaskVisibility = 'exclude'; @@ -182,6 +185,8 @@ function resetTaskPanelScopedState(activeOrganisationConceptId, { actorChanged = _taskQueueActivityGeneration += 1; stopTaskQueueActivityPolling(); _taskQueueActivityLoadPromise = null; + _pendingTaskNavigation = null; + _referencedGlobalTaskId = ''; _activeOrganisationConceptId = activeOrganisationConceptId; _globalTaskLoadGeneration += 1; _isLoading = false; @@ -757,6 +762,20 @@ export function initializeTaskPanel() { ensureTaskPanelAuthStatusListener(); renderTaskGroupFilterControls(); + _panelEl.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + event.preventDefault(); + event.stopPropagation(); + hideTaskPanel(); + } + }); + document.addEventListener('pointerdown', (event) => { + if (_isVisible && !_panelEl.contains(event.target) + && !event.target.closest('#taskPanelToggleBtn')) { + hideTaskPanel({ restoreFocus: false }); + } + }); + // Set up close button const closeBtn = document.getElementById('closeTaskPanel'); if (closeBtn) { @@ -823,12 +842,14 @@ export function showTaskPanel(options = {}) { _isGlobalTabMode = false; syncTaskQueueActivityPolling(); _taskListEl = document.getElementById('taskList') || _taskListEl; + if (!_isVisible) _panelReturnFocus = document.activeElement; _panelEl.classList.remove('hidden'); _panelEl.setAttribute('aria-hidden', 'false'); _panelEl.querySelectorAll('.task-create-form, .task-filter-row').forEach(element => { element.classList.toggle('hidden', _panelTaskIds !== null); }); _isVisible = true; + document.getElementById('closeTaskPanel')?.focus(); // Auto-refresh tasks when panel becomes visible return refreshTasks(); } @@ -870,11 +891,12 @@ async function loadReferencedPanelTasks() { /** * Hide the task panel. */ -export function hideTaskPanel() { +export function hideTaskPanel({ restoreFocus = true } = {}) { if (_panelEl) { _panelEl.classList.add('hidden'); _panelEl.setAttribute('aria-hidden', 'true'); _isVisible = false; + if (restoreFocus && _panelReturnFocus?.isConnected) _panelReturnFocus.focus(); } } @@ -909,6 +931,7 @@ async function openGlobalTasks() { _tasks = []; _taskDetailState = {}; _selectedTaskId = ''; + _referencedGlobalTaskId = ''; ensureTaskPanelOrganisationSwitchListener(); ensureTaskPanelAuthStatusListener(); ensureTaskQueueActivityPollingListeners(); @@ -939,6 +962,17 @@ async function openGlobalTasks() { loadGlobalTasks({ telemetry }), loadTaskQueueActivity(), ]); + if (_pendingTaskNavigation) { + const task = _pendingTaskNavigation; + _pendingTaskNavigation = null; + const taskId = getTaskId(task); + if (!_tasks.some(item => getTaskId(item) === taskId)) _tasks.push(task); + _referencedGlobalTaskId = taskId; + await selectTask(taskId); + const inspector = _globalTasksContainer.querySelector('#globalTaskInspector'); + inspector?.setAttribute('tabindex', '-1'); + inspector?.focus(); + } syncTaskQueueActivityPolling(); } @@ -1508,6 +1542,7 @@ function getFilteredTasks() { // Exact references remain reachable regardless of saved list filters. if (!_isGlobalTabMode && _panelTaskIds !== null) return _tasks; return _tasks.filter((task) => { + if (_isGlobalTabMode && getTaskId(task) === _referencedGlobalTaskId) return true; if (!taskMatchesGlobalScope(task)) { return false; } @@ -3440,9 +3475,10 @@ function renderTaskItem(task) {
${renderTaskExecuteWithVonButton(task)} ${renderTaskDiscussButton(task)} - + ${!_isGlobalTabMode ? `` : ''}
'; + const { getJson } = require(base + 'apiService.js'); + getJson.mockImplementation(async url => { + if (url === '/api/tasks/%23V%23exact_task') return { task_concept_id: '#V#exact_task', title: 'Exact completed task', status: 'completed' }; + if (url.includes('limit=50')) return { tasks: [{ task_concept_id: '#V#other', title: 'Other task', status: 'pending' }] }; + return {}; + }); + const panel = require(base + 'components/taskPanel.js'); + await panel.openTaskInPanel('#V#exact_task'); + document.getElementById('taskStatusFilter').dispatchEvent(new Event('change')); + document.querySelector('.task-open-in-tab-btn').click(); + expect(document.getElementById('taskPanel').classList.contains('hidden')).toBe(true); + expect(require(base + 'tabNavigation.js').activateTab).toHaveBeenCalledWith('globalTasksTab'); + await panel.showGlobalTasks(); + expect(document.querySelector('#globalTaskInspector').textContent).toContain('Exact completed task'); + expect(document.querySelector('.task-item.is-selected').dataset.taskId).toBe('#V#exact_task'); + expect(document.getElementById('globalTaskStatusFilter').value).toBe('pending'); + expect(document.activeElement.id).toBe('globalTaskInspector'); +});