fix(kanban): show unassigned tasks as explicit profile lane on mobile
- Add KANBAN_UNASSIGNED_LANE constant and _kanbanLaneKey/_kanbanLaneLabel helpers so unassigned tasks map to a stable internal key '__unassigned__' rather than a locale-dependent translated string - _kanbanLaneNames now places 'default' first, other assignees alphabetically, and the Unassigned lane last - _kanbanRenderProfileLanes filters by _kanbanLaneKey, renders label via _kanbanLaneLabel, and adds kanban-profile-lane-unassigned CSS class - _kanbanTaskMeta always emits the unassigned label so sidebar shows it too - _kanbanRenderBoard checks unfiltered total and shows a 'Tasks hidden by filters' state with a Clear filters button when text search hides all tasks - Add kanban_tasks_hidden_by_filters and kanban_clear_filters i18n keys to all 13 locale blocks - Add .kanban-profile-lane-unassigned header border using warning token Fixes: on Dynasty board mobile, Ready=3 CLI but default lane showed Ready=0 because those tasks were unassigned and had no visible lane. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b8cf8dfc4e
commit
dfcd94a808
3 changed files with 59 additions and 7 deletions
|
|
@ -849,6 +849,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
|
|
@ -2445,6 +2447,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: 'Nessun task corrispondente',
|
||||
kanban_no_data: 'Nessun dato Kanban',
|
||||
kanban_work_queue_hint: 'Questa è la coda di lavoro di Hermes Agent. Crea o classifica un task, assegnalo, spostalo in Pronto, poi lascia che il dispatcher lo reclami.',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: 'Kanban non disponibile',
|
||||
kanban_read_only: 'Vista sola lettura',
|
||||
kanban_empty: 'Vuoto',
|
||||
|
|
@ -4032,6 +4036,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: '一致するタスクがありません',
|
||||
kanban_no_data: 'カンバンデータがありません',
|
||||
kanban_work_queue_hint: 'これは Hermes Agent のワークキューです。タスクを作成またはトリアージし、担当者を割り当て、Ready に移動すると、ディスパッチャーがそれをクレームします。',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: 'カンバンを利用できません',
|
||||
kanban_read_only: '読み取り専用',
|
||||
kanban_empty: '空',
|
||||
|
|
@ -5353,6 +5359,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
|
|
@ -6877,6 +6885,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
|
|
@ -8379,6 +8389,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
|
|
@ -9935,7 +9947,9 @@ const LOCALES = {
|
|||
kanban_include_archived: '包含已归档',
|
||||
kanban_no_matching_tasks: '没有匹配的任务',
|
||||
kanban_no_data: '无看板数据',
|
||||
kanban_work_queue_hint: '这是 Hermes Agent 的工作队列。创建或分类任务,分配任务,将其移至“就绪”状态,然后让调度员认领任务。',
|
||||
kanban_work_queue_hint: '这是 Hermes Agent 的工作队列。创建或分类任务,分配任务,将其移至”就绪”状态,然后让调度员认领任务。',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: '看板不可用',
|
||||
kanban_read_only: '只读视图',
|
||||
kanban_empty: '空',
|
||||
|
|
@ -11718,6 +11732,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: '沒有符合的任務',
|
||||
kanban_no_data: '沒有看板資料',
|
||||
kanban_work_queue_hint: '這是 Hermes Agent 的工作佇列。建立或分類任務、指派對象、移至 Ready,然後讓調度器認領。',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: '看板無法使用',
|
||||
kanban_read_only: '唯讀檢視',
|
||||
kanban_empty: '空',
|
||||
|
|
@ -13192,6 +13208,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
|
|
@ -14680,6 +14698,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
|
|
@ -16197,6 +16217,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: 'Aucune tâche correspondante',
|
||||
kanban_no_data: 'Aucune donnée Kanban',
|
||||
kanban_work_queue_hint: 'Il s\'agit de la file d\'attente de travail de l\'agent Hermes. Créez ou triez une tâche, attribuez-la, déplacez-la vers Prêt, puis laissez le répartiteur la réclamer.',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: 'Kanban indisponible',
|
||||
kanban_read_only: 'Vue en lecture seule',
|
||||
kanban_empty: 'Vide',
|
||||
|
|
@ -17795,6 +17817,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: 'Eşleşen görev yok',
|
||||
kanban_no_data: 'Kanban verisi yok',
|
||||
kanban_work_queue_hint: 'Bu Hermes Ajanının iş kuyruğudur. Bir görev oluşturun veya önceliklendirin, atayın, Hazır\'a taşıyın ve sevk görevlisinin görevi talep etmesine izin verin.',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: 'Kanban kullanılamıyor',
|
||||
kanban_read_only: 'Salt okunur görünüm',
|
||||
kanban_empty: 'Boş',
|
||||
|
|
@ -19397,6 +19421,8 @@ const LOCALES = {
|
|||
kanban_no_matching_tasks: 'Brak pasujących zadań',
|
||||
kanban_no_data: 'Brak danych Kanban',
|
||||
kanban_work_queue_hint: 'To jest kolejka pracy Agenta Hermes. Utwórz zadanie lub dokonaj triażu, przypisz je, przenieś do statusu "Gotowe", a następnie pozwól dyspozytorowi je pobrać.',
|
||||
kanban_tasks_hidden_by_filters: 'Tasks hidden by filters',
|
||||
kanban_clear_filters: 'Clear filters',
|
||||
kanban_unavailable: 'Kanban niedostępny',
|
||||
kanban_read_only: 'Widok tylko do odczytu',
|
||||
kanban_empty: 'Puste',
|
||||
|
|
|
|||
|
|
@ -1541,7 +1541,7 @@ function _kanbanTaskTitle(task){ return task.title || task.summary || task.id ||
|
|||
function _kanbanTaskBody(task){ return task.body || task.description || task.prompt || ''; }
|
||||
function _kanbanTaskMeta(task){
|
||||
const bits = [];
|
||||
if (task.assignee) bits.push(task.assignee);
|
||||
bits.push(task.assignee ? task.assignee : t('kanban_unassigned'));
|
||||
if (task.tenant) bits.push(task.tenant);
|
||||
if (task.priority !== undefined && task.priority !== null) bits.push('P' + task.priority);
|
||||
if (task.comment_count) bits.push('💬 ' + task.comment_count);
|
||||
|
|
@ -1920,10 +1920,20 @@ async function dropKanbanTask(event, status){
|
|||
_kanbanSuppressNextCardClick();
|
||||
}
|
||||
|
||||
const KANBAN_UNASSIGNED_LANE = '__unassigned__';
|
||||
function _kanbanLaneKey(task){ return task && task.assignee ? String(task.assignee) : KANBAN_UNASSIGNED_LANE; }
|
||||
function _kanbanLaneLabel(lane){ return lane === KANBAN_UNASSIGNED_LANE ? t('kanban_unassigned') : lane; }
|
||||
|
||||
function _kanbanLaneNames(columns){
|
||||
const names = new Set();
|
||||
columns.forEach(col => (col.tasks || []).forEach(task => names.add(task.assignee || t('kanban_unassigned'))));
|
||||
return Array.from(names).sort((a, b) => String(a).localeCompare(String(b)));
|
||||
columns.forEach(col => (col.tasks || []).forEach(task => names.add(_kanbanLaneKey(task))));
|
||||
const assigned = Array.from(names).filter(n => n !== KANBAN_UNASSIGNED_LANE).sort((a, b) => {
|
||||
if (a === 'default') return -1;
|
||||
if (b === 'default') return 1;
|
||||
return String(a).localeCompare(String(b));
|
||||
});
|
||||
if (names.has(KANBAN_UNASSIGNED_LANE)) assigned.push(KANBAN_UNASSIGNED_LANE);
|
||||
return assigned;
|
||||
}
|
||||
|
||||
function _kanbanRenderColumn(col){
|
||||
|
|
@ -1943,9 +1953,10 @@ function _kanbanRenderProfileLanes(columns){
|
|||
const lanes = _kanbanLaneNames(columns);
|
||||
if (!lanes.length) return columns.map(_kanbanRenderColumn).join('');
|
||||
return `<div class="kanban-profile-lanes">${lanes.map(lane => {
|
||||
const laneCols = columns.map(col => ({...col, tasks: (col.tasks || []).filter(task => (task.assignee || t('kanban_unassigned')) === lane)}));
|
||||
const laneCols = columns.map(col => ({...col, tasks: (col.tasks || []).filter(task => _kanbanLaneKey(task) === lane)}));
|
||||
const count = laneCols.reduce((sum, col) => sum + (col.tasks || []).length, 0);
|
||||
return `<section class="kanban-profile-lane" data-kanban-lane="${esc(lane)}"><header class="kanban-profile-lane-head"><span>${esc(lane)}</span><span class="kanban-count">${count}</span></header><div class="kanban-board kanban-board-in-lane">${laneCols.map(_kanbanRenderColumn).join('')}</div></section>`;
|
||||
const laneClass = lane === KANBAN_UNASSIGNED_LANE ? ' kanban-profile-lane-unassigned' : '';
|
||||
return `<section class="kanban-profile-lane${laneClass}" data-kanban-lane="${esc(lane)}"><header class="kanban-profile-lane-head"><span>${esc(_kanbanLaneLabel(lane))}</span><span class="kanban-count">${count}</span></header><div class="kanban-board kanban-board-in-lane">${laneCols.map(_kanbanRenderColumn).join('')}</div></section>`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -1953,6 +1964,19 @@ function _kanbanEmptyBoardHtml(){
|
|||
return `<div class="main-view-empty"><div class="main-view-empty-title">${esc(t('kanban_no_data'))}</div><div class="main-view-empty-sub">${esc(t('kanban_work_queue_hint'))}</div></div>`;
|
||||
}
|
||||
|
||||
function _kanbanHiddenByFiltersHtml(){
|
||||
return `<div class="main-view-empty"><div class="main-view-empty-title">${esc(t('kanban_tasks_hidden_by_filters'))}</div><div class="main-view-empty-sub"><button class="btn-link" onclick="clearKanbanFilters()">${esc(t('kanban_clear_filters'))}</button></div></div>`;
|
||||
}
|
||||
|
||||
function clearKanbanFilters(){
|
||||
const s = $('kanbanSearch'); if (s) s.value = '';
|
||||
const a = $('kanbanAssigneeFilter'); if (a) { a.value = ''; a.dataset.defaultValue = ''; }
|
||||
const te = $('kanbanTenantFilter'); if (te) { te.value = ''; te.dataset.defaultValue = ''; }
|
||||
const ai = $('kanbanIncludeArchived'); if (ai) ai.checked = false;
|
||||
const om = $('kanbanOnlyMine'); if (om) om.checked = false;
|
||||
loadKanban(true);
|
||||
}
|
||||
|
||||
function _kanbanRenderBoard(){
|
||||
const board = $('kanbanBoard');
|
||||
if (!board) return;
|
||||
|
|
@ -1965,7 +1989,8 @@ function _kanbanRenderBoard(){
|
|||
if ($('kanbanSummary')) $('kanbanSummary').textContent = String(t('kanban_visible_tasks')).replace('{0}', total);
|
||||
_kanbanRenderSidebar(columns);
|
||||
if (total === 0) {
|
||||
board.innerHTML = _kanbanEmptyBoardHtml();
|
||||
const unfilteredTotal = (_kanbanBoard.columns || []).reduce((n, col) => n + (col.tasks || []).length, 0);
|
||||
board.innerHTML = unfilteredTotal > 0 ? _kanbanHiddenByFiltersHtml() : _kanbanEmptyBoardHtml();
|
||||
return;
|
||||
}
|
||||
board.innerHTML = _kanbanLanesByProfile ? _kanbanRenderProfileLanes(columns) : columns.map(_kanbanRenderColumn).join('');
|
||||
|
|
|
|||
|
|
@ -6037,6 +6037,7 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
|
|||
.kanban-profile-lanes{display:flex;flex-direction:column;gap:18px;min-width:100%}
|
||||
.kanban-profile-lane{border:1px solid var(--border);border-radius:12px;background:rgba(255,255,255,.02);padding:10px}
|
||||
.kanban-profile-lane-head{display:flex;justify-content:space-between;align-items:center;color:var(--text);font-size:13px;font-weight:600;margin-bottom:8px}
|
||||
.kanban-profile-lane-unassigned .kanban-profile-lane-head{border-bottom:1px solid color-mix(in srgb,var(--warning,#f5c542) 45%,var(--border));padding-bottom:6px}
|
||||
.kanban-board-in-lane{min-height:0;overflow-x:auto}
|
||||
.kanban-card-topline{display:flex;gap:6px;align-items:center;margin-bottom:4px;font-size:10px;color:var(--muted)}
|
||||
.kanban-card-id{font-family:var(--mono);opacity:.8}
|
||||
|
|
|
|||
Loading…
Reference in a new issue