@@ -8,6 +8,7 @@
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
### Fixed
|
||||
|
||||
- Board selector menu labels no longer overlap board titles when board metadata uses a longer text label in the icon/color slot.
|
||||
@@ -42,6 +43,11 @@
|
||||
|
||||
- **PR #2528** (closes #2518) — Guard New Conversation creation while a previous `/api/session/new` request is still in flight, so cold model/provider catalog resolution gives immediate pending feedback and rapid repeated clicks reuse the same create request instead of enqueueing duplicate blank sessions.
|
||||
>>>>>>> pr-2528
|
||||
=======
|
||||
### Fixed
|
||||
|
||||
- Keep Kanban drag/drop status updates from also opening the task detail pane via a trailing card click or the generic task-update helper (refs #2529).
|
||||
>>>>>>> pr-2530
|
||||
|
||||
## [v0.51.89] — 2026-05-18 — Release BM (stage-382 — 6-PR full sweep batch — runtime adapter approval/clarify seam + SOUL.md memory panel + #1855 resolve_model_provider fast-path + PWA sidebar spinner fix + /model active-provider preference + contributor contract docs index)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ let _kanbanCurrentBoard = null;
|
||||
let _kanbanBoardsList = null;
|
||||
let _kanbanBoardMenuOpen = false;
|
||||
let _kanbanIsDispatching = false;
|
||||
let _kanbanSuppressCardClickUntil = 0;
|
||||
// SSE event stream — replaces the 30s polling cadence with a long-lived
|
||||
// /api/kanban/events/stream connection. Falls back to polling when the
|
||||
// EventSource fails to connect (proxy that strips text/event-stream, etc).
|
||||
@@ -1264,12 +1265,33 @@ async function quickKanbanCardAction(event, taskId, status){
|
||||
return updateKanbanTask(taskId, {status});
|
||||
}
|
||||
|
||||
function _kanbanSuppressNextCardClick(){
|
||||
_kanbanSuppressCardClickUntil = Date.now() + 700;
|
||||
}
|
||||
|
||||
function dragKanbanTask(event, taskId){
|
||||
_kanbanSuppressNextCardClick();
|
||||
if (!event.dataTransfer) return;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', taskId);
|
||||
}
|
||||
|
||||
function finishKanbanDrag(event){
|
||||
if (event) _kanbanSuppressNextCardClick();
|
||||
}
|
||||
|
||||
function openKanbanCard(event, taskId){
|
||||
if (Date.now() < _kanbanSuppressCardClickUntil) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
loadKanbanTask(taskId);
|
||||
return false;
|
||||
}
|
||||
|
||||
function allowKanbanDrop(event){
|
||||
// Don't accept drops into the 'running' column. Entering 'running' is owned
|
||||
// by the dispatcher/claim_task path (sets claim_lock + claim_expires +
|
||||
@@ -1290,10 +1312,13 @@ function clearKanbanDrop(event){
|
||||
}
|
||||
|
||||
async function dropKanbanTask(event, status){
|
||||
_kanbanSuppressNextCardClick();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clearKanbanDrop(event);
|
||||
const taskId = event.dataTransfer ? event.dataTransfer.getData('text/plain') : '';
|
||||
if (taskId && status) await updateKanbanTask(taskId, {status});
|
||||
if (taskId && status) await updateKanbanTask(taskId, {status}, {openDetail: false});
|
||||
_kanbanSuppressNextCardClick();
|
||||
}
|
||||
|
||||
function _kanbanLaneNames(columns){
|
||||
@@ -1356,7 +1381,7 @@ function _kanbanCard(task, status){
|
||||
const stale = _kanbanCardStalenessClass(task);
|
||||
const body = _kanbanTaskBody(task);
|
||||
const assignee = task.assignee ? `<span class="kanban-card-assignee">@${esc(task.assignee)}</span>` : `<span class="kanban-card-unassigned">${esc(t('kanban_unassigned'))}</span>`;
|
||||
return `<article class="kanban-card ${esc(stale)}" data-kanban-task-id="${esc(task.id)}" draggable="true" ondragstart="dragKanbanTask(event, '${esc(task.id)}')" onclick="loadKanbanTask('${esc(task.id)}')" tabindex="0" role="button" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();loadKanbanTask('${esc(task.id)}')}">
|
||||
return `<article class="kanban-card ${esc(stale)}" data-kanban-task-id="${esc(task.id)}" draggable="true" ondragstart="dragKanbanTask(event, '${esc(task.id)}')" ondragend="finishKanbanDrag(event)" onclick="return openKanbanCard(event, '${esc(task.id)}')" tabindex="0" role="button" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();loadKanbanTask('${esc(task.id)}')}">
|
||||
<div class="kanban-card-topline"><span class="kanban-card-id">${esc(task.id || '')}</span>${priority ? `<span class="kanban-badge priority">P${priority}</span>` : ''}${task.tenant ? `<span class="kanban-badge tenant">${esc(task.tenant)}</span>` : ''}</div>
|
||||
<div class="kanban-card-title">${esc(_kanbanTaskTitle(task))}</div>
|
||||
${body ? `<div class="kanban-card-body">${_kanbanRenderMarkdown(body)}</div>` : ''}
|
||||
@@ -2269,15 +2294,16 @@ async function submitKanbanTaskModal(){
|
||||
}
|
||||
}
|
||||
|
||||
async function updateKanbanTask(taskId, patch){
|
||||
async function updateKanbanTask(taskId, patch, opts){
|
||||
if (!taskId || !patch) return;
|
||||
try {
|
||||
const openDetail = !opts || opts.openDetail !== false;
|
||||
const updated = await api('/api/kanban/tasks/' + encodeURIComponent(taskId) + _kanbanBoardQuery(), {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
await loadKanban(true);
|
||||
await loadKanbanTask((updated && updated.task && updated.task.id) || taskId);
|
||||
if (openDetail) await loadKanbanTask((updated && updated.task && updated.task.id) || taskId);
|
||||
} catch(e) { showToast(t('kanban_unavailable') + ': ' + (e.message || e), 'error'); }
|
||||
}
|
||||
|
||||
|
||||
@@ -692,6 +692,65 @@ def test_kanban_ui_parity_polish_adds_card_metadata_quick_actions_and_swimlanes(
|
||||
assert "javascript:" not in PANELS.lower()
|
||||
|
||||
|
||||
def test_kanban_dragging_card_does_not_open_detail_on_drop_click():
|
||||
"""Regression: drag/drop should move a card without opening task detail."""
|
||||
assert "function _kanbanSuppressNextCardClick" in PANELS
|
||||
assert "let _kanbanSuppressCardClickUntil" in PANELS
|
||||
assert "function openKanbanCard" in PANELS
|
||||
assert "function finishKanbanDrag" in PANELS
|
||||
|
||||
drag_fn = re.search(r"function dragKanbanTask\([^)]*\)\{(.*?)\n\}", PANELS, re.DOTALL)
|
||||
assert drag_fn, "dragKanbanTask() not found"
|
||||
assert "_kanbanSuppressNextCardClick" in drag_fn.group(1), (
|
||||
"drag start must arm the click suppressor so the trailing click after "
|
||||
"drop cannot open the task detail pane"
|
||||
)
|
||||
|
||||
finish_fn = re.search(r"function finishKanbanDrag\([^)]*\)\{(.*?)\n\}", PANELS, re.DOTALL)
|
||||
assert finish_fn, "finishKanbanDrag() not found"
|
||||
assert "_kanbanSuppressNextCardClick" in finish_fn.group(1), (
|
||||
"drag end must refresh the suppressor window before browsers emit a "
|
||||
"trailing synthetic click"
|
||||
)
|
||||
|
||||
drop_fn = re.search(r"async function dropKanbanTask\([^)]*\)\{(.*?)\n\}", PANELS, re.DOTALL)
|
||||
assert drop_fn, "dropKanbanTask() not found"
|
||||
drop_body = drop_fn.group(1)
|
||||
assert "_kanbanSuppressNextCardClick" in drop_body
|
||||
assert "event.stopPropagation()" in drop_body
|
||||
assert "updateKanbanTask(taskId, {status}, {openDetail: false})" in drop_body, (
|
||||
"drag/drop status updates must refresh the board without opening the task detail"
|
||||
)
|
||||
|
||||
update_fn = re.search(r"async function updateKanbanTask\([^)]*\)\{(.*?)\n\}", PANELS, re.DOTALL)
|
||||
assert update_fn, "updateKanbanTask() not found"
|
||||
update_body = update_fn.group(1)
|
||||
assert "const openDetail = !opts || opts.openDetail !== false;" in update_body
|
||||
assert "if (openDetail) await loadKanbanTask" in update_body
|
||||
|
||||
card_template = re.search(r"return `<article class=\"kanban-card.*?</article>`;", PANELS, re.DOTALL)
|
||||
assert card_template, "Kanban card template not found"
|
||||
card_html = card_template.group(0)
|
||||
assert "ondragend=\"finishKanbanDrag(event)\"" in card_html
|
||||
assert "onclick=\"return openKanbanCard(event," in card_html
|
||||
assert "onclick=\"loadKanbanTask" not in card_html, (
|
||||
"Kanban cards must not call loadKanbanTask directly from onclick; "
|
||||
"drag/drop needs a guarded click path"
|
||||
)
|
||||
|
||||
open_fn = re.search(r"function openKanbanCard\([^)]*\)\{(.*?)\n\}", PANELS, re.DOTALL)
|
||||
assert open_fn, "openKanbanCard() not found"
|
||||
open_body = open_fn.group(1)
|
||||
for token in (
|
||||
"Date.now()",
|
||||
"_kanbanSuppressCardClickUntil",
|
||||
"preventDefault",
|
||||
"stopPropagation",
|
||||
"loadKanbanTask",
|
||||
):
|
||||
assert token in open_body
|
||||
|
||||
|
||||
def test_kanban_lifecycle_controls_do_not_offer_manual_running_start():
|
||||
assert "quickKanbanCardAction(event,'${id}','running')" not in PANELS
|
||||
assert "kanban_card_start" not in PANELS
|
||||
|
||||
Reference in New Issue
Block a user