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 a826a531e..603b6dd99 100644 --- a/src/frontend/web/von_interface/static/js/components/taskPanel.js +++ b/src/frontend/web/von_interface/static/js/components/taskPanel.js @@ -10,7 +10,7 @@ import { deleteJson, getJson, patchJson, postJson, getUserContext, ensureUniqueW import { activateTab } from '../tabNavigation.js'; import { showToast } from '../utils/toast.js'; import { renderMarkdownViaServer } from '../markdownUtils.js'; -import { selectBestNameForContext } from '../utils/nameSelection.js'; +import { selectBestNameForContext, selectShortestNameForContext } from '../utils/nameSelection.js'; // Task panel state let _panelEl = null; @@ -49,6 +49,7 @@ let _globalTaskLoadGeneration = 0; let _activeOrganisationConceptId; let _taskOrganisationOptions = []; let _taskOrganisationOptionsLoaded = false; +const _taskOrganisationNameRequests = new Map(); let _taskTaxonomy = { task_types: [], task_sources: [], @@ -198,6 +199,7 @@ function resetTaskPanelScopedState(activeOrganisationConceptId, { actorChanged = _hiddenBulkTaskCollections = []; _taskQueueActivity = []; _taskQueueActivityError = ''; + _taskOrganisationNameRequests.clear(); if (actorChanged) { _taskOrganisationOptions = []; _taskOrganisationOptionsLoaded = false; @@ -1888,6 +1890,7 @@ function renderTaskList() { } _taskListEl.innerHTML = html; + hydrateTaskOrganisationChips(); if (isBoardView) { _taskListEl.scrollLeft = previousScrollLeft; _taskListEl.scrollTop = previousScrollTop; @@ -2645,6 +2648,26 @@ function renderTaskHistoryRows(history) { `).join(''); } +function hydrateTaskOrganisationChips() { + _taskListEl.querySelectorAll('.task-organisation-chip[data-organisation-id]').forEach((chip) => { + const conceptId = chip.dataset.organisationId; + if (!conceptId) return; + let request = _taskOrganisationNameRequests.get(conceptId); + if (!request) { + request = getJson(`/api/concepts/${encodeURIComponent(conceptId)}`) + .then((concept) => Array.isArray(concept?.names) && concept.names.length + ? concept.names : concept?.raw_doc?.names) + .catch(() => null); + _taskOrganisationNameRequests.set(conceptId, request); + } + void request.then((names) => { + if (!chip.isConnected || _taskOrganisationNameRequests.get(conceptId) !== request) return; + const shortestName = selectShortestNameForContext(names); + if (shortestName) chip.textContent = shortestName; + }); + }); +} + function getTaskOrganisationDisplayName(organisationConceptId) { const conceptId = normaliseTaskConceptId(organisationConceptId || ''); if (!conceptId) return 'Unscoped'; @@ -3361,7 +3384,8 @@ function renderTaskItem(task) { const organisationChipHtml = `
diff --git a/tests/frontend/taskPanelConceptLinks.test.js b/tests/frontend/taskPanelConceptLinks.test.js index 26bcde67a..d597f7906 100644 --- a/tests/frontend/taskPanelConceptLinks.test.js +++ b/tests/frontend/taskPanelConceptLinks.test.js @@ -52,6 +52,34 @@ describe('task panel concept links', () => { jest.restoreAllMocks(); }); + test('organisation chips use shortest names, language ranking and safe fallbacks', async () => { + const { getJson } = require(apiServiceModulePath); + const labels = ['University Of Auckland Strong Ai Lab', 'Research Institute', 'Single', 'Fallback', 'Unavailable']; + getJson.mockImplementation((url) => { + if (url === '/von/api/organisations/my_organisations') return Promise.resolve({ + organisations: labels.map((name, i) => ({ concept_id: `#V#org_${i}`, name })) }); + if (url.startsWith('/api/tasks/?')) return Promise.resolve({ tasks: labels.map((name, i) => ({ + task_concept_id: `#V#task_${i}`, title: name, status: 'pending', priority: 'low', + organisation_concept_id: `#V#org_${i}` })) }); + if (url.startsWith('/api/concepts/')) { + const i = Number(url.slice(-1)); + if (i === 4) return Promise.reject(new Error('Unavailable')); + const names = i === 3 ? [] : [{ name: labels[i], language: 'en-NZ' }]; + if (i < 2) names.push({ name: ['SAIL', 'RI'][i], language: 'en-NZ', type: 'ABBR' }, + { name: 'X', language: 'fr' }); + return Promise.resolve({ raw_doc: { names } }); + } + return Promise.resolve({}); + }); + const { showGlobalTasks } = require(taskPanelModulePath); + await showGlobalTasks(); + await flushMicrotasks(); + const chips = Array.from(document.querySelectorAll('.task-organisation-chip')); + expect(chips.map((chip) => chip.textContent.trim())).toEqual(['SAIL', 'RI', 'Single', 'Fallback', 'Unavailable']); + expect(chips[0].title).toContain(labels[0]); + expect(chips[0].dataset.organisationId).toBe('#V#org_0'); + }); + test('renders task title and parent as clickable concept links and dispatches concept navigation', async () => { const { getJson } = require(apiServiceModulePath); getJson.mockImplementation((url) => {