Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions src/frontend/web/von_interface/static/js/components/taskPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,6 +49,7 @@ let _globalTaskLoadGeneration = 0;
let _activeOrganisationConceptId;
let _taskOrganisationOptions = [];
let _taskOrganisationOptionsLoaded = false;
const _taskOrganisationNameRequests = new Map();
let _taskTaxonomy = {
task_types: [],
task_sources: [],
Expand Down Expand Up @@ -198,6 +199,7 @@ function resetTaskPanelScopedState(activeOrganisationConceptId, { actorChanged =
_hiddenBulkTaskCollections = [];
_taskQueueActivity = [];
_taskQueueActivityError = '';
_taskOrganisationNameRequests.clear();
if (actorChanged) {
_taskOrganisationOptions = [];
_taskOrganisationOptionsLoaded = false;
Expand Down Expand Up @@ -1888,6 +1890,7 @@ function renderTaskList() {
}

_taskListEl.innerHTML = html;
hydrateTaskOrganisationChips();
if (isBoardView) {
_taskListEl.scrollLeft = previousScrollLeft;
_taskListEl.scrollTop = previousScrollTop;
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -3361,7 +3384,8 @@ function renderTaskItem(task) {
const organisationChipHtml = `
<div class="task-meta-chip-row">
<span class="task-source-chip task-organisation-chip ${task.organisation_concept_id ? '' : 'task-organisation-unscoped'}"
title="Task organisation scope">
data-organisation-id="${escapeHtml(normaliseTaskConceptId(task.organisation_concept_id || ''))}"
title="Task organisation scope: ${escapeHtml(getTaskOrganisationDisplayName(task.organisation_concept_id))}">
${escapeHtml(getTaskOrganisationDisplayName(task.organisation_concept_id))}
</span>
</div>
Expand Down
28 changes: 28 additions & 0 deletions tests/frontend/taskPanelConceptLinks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading