From 45a7c2362ebecf1d7327e7952aa9518a4eb024ce Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Tue, 27 Oct 2026 14:52:09 +0800 Subject: [PATCH 1/5] Remove the unused BoardView and point the board tests at UnifiedBoard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1345: BoardView stopped rendering when the task and delivery boards became one view, so it was an implementation nothing reached — only the kanban tests still built it. Delete it together with the helpers that existed for it alone (BoardColumn, dropLabel, columnTitle, lanesOf, cardProps), and let the tests render UnifiedBoard, the board the dialog actually shows. Behaviour is unchanged: every assertion the removed view carried is made against the shipped one, and the two it no longer satisfies — subtask nesting and the drag hint labels — are asserted as they behave today and tracked in #1358 rather than dropped silently. Docs-not-needed: dead-code removal; UnifiedBoard and the shipped layout are unchanged. --- core/src/client/kanban.js | 91 +-------------------------------------- core/test/kanban.test.js | 30 ++++++++----- 2 files changed, 21 insertions(+), 100 deletions(-) diff --git a/core/src/client/kanban.js b/core/src/client/kanban.js index cee4c91db..1a0c48d21 100644 --- a/core/src/client/kanban.js +++ b/core/src/client/kanban.js @@ -134,33 +134,10 @@ function statusLabel(status) { } } -function dropLabel(column) { - if (column === 'in-progress') return t('Start task') - if (column === 'done') return t('Complete task') - return t('Not started') -} - -function columnTitle(column) { - if (column === 'in-progress') return t('In progress') - if (column === 'done') return t('Done') - return t('Not started') -} - function botName(roster, task) { return roster?.byId?.[task.ownerBotId]?.name || task.ownerBotName || '' } -function lanesOf(tasks, roster) { - const order = [] - const map = new Map() - for (const task of tasks) { - const id = task.ownerBotId ?? '' - if (!map.has(id)) { map.set(id, []); order.push(id) } - map.get(id).push(task) - } - return order.map(id => ({ id, name: botName(roster, map.get(id)[0]) || id, tasks: map.get(id) })) -} - const upsert = (list, task) => (list.some(item => item.id === task.id) ? list.map(item => item.id === task.id ? task : item) : [...list, task]) const without = (list, id) => list.filter(item => item.id !== id) @@ -275,72 +252,6 @@ export function BoardCard({ task, roster, movable, onMove, onDrag, partOf }) { }) } -function cardProps(task, roster, onMove, onDrag, partOf) { - return { task, roster, movable: canMoveTask(roster, task), onMove, onDrag, partOf } -} - -function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag }) { - const tree = lanes ? columnTree(tasks) : null - const linked = tree ? tree.loose.filter(task => task.parentId || task.parentTitle) : [] - const plain = tree ? tree.loose.filter(task => !task.parentId && !task.parentTitle) : tasks - return h('section', { - className: 'bt-board-column', - 'data-column': column, - 'aria-label': columnTitle(column), - onDragOver: (event) => { if (!dragging) return; event.preventDefault() }, - onDrop: (event) => { - event.preventDefault() - const id = event.dataTransfer?.getData('text/plain') - if (id) onMove?.(id, column) - onDrag?.(null) - }, - }, - h('h3', { className: 'bt-board-column-head' }, - h('span', null, columnTitle(column)), - h('span', { className: 'bt-board-count' }, String(tasks.length))), - dragging ? h('div', { className: 'bt-board-drop' }, dropLabel(column)) : null, - tasks.length === 0 && !dragging ? h('p', { className: 'bt-board-empty' }, t('Nothing in this column.')) : null, - tree ? tree.nests.map(nest => h('div', { key: nest.task.id, className: 'bt-board-nest' }, - h(BoardCard, cardProps(nest.task, roster, onMove, onDrag)), - nest.children.map(child => h('div', { key: child.id, className: 'bt-board-subtask', 'data-parent': nest.task.id }, - h(BoardCard, cardProps(child, roster, onMove, onDrag)))))) : null, - linked.map(task => h(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || '') })), - lanes - ? lanesOf(plain, roster).map(lane => h('div', { key: lane.id, className: 'bt-board-lane' }, - h('span', { className: 'bt-board-lane-name' }, lane.name), - lane.tasks.map(task => h(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })))) - : plain.map(task => h(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) }))) -} - -export function BoardView({ roster, tasks, rail, scope, counts, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate }) { - const columns = boardColumns(tasks) - const lanes = scope?.kind === 'group' - const selectedBot = scope?.kind === 'bot' ? scope.botId : '' - const selectedRoom = scope?.kind === 'group' ? scope.roomId : '' - return h('div', { className: 'bt-board-body' }, - h('aside', { className: 'bt-board-rail', 'aria-label': t('Task board') }, - h('h3', { className: 'bt-board-rail-head' }, t('Your Bots')), - rail.bots.length === 0 ? h('p', { className: 'bt-board-empty' }, t('No tasks yet.')) : rail.bots.map(row => { - const count = counts?.[row.id] ?? { done: 0, total: 0 } - return h(RailButton, { - key: row.id, row, selected: row.id === selectedBot, onSelect, - meta: count.total > 0 ? t('{done}/{total}', { done: count.done, total: count.total }) : '', - }) - }), - rail.groups.length > 0 ? h('h3', { className: 'bt-board-rail-head' }, t('Groups you admin')) : null, - rail.groups.map(row => h(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect }))), - h('div', { className: 'bt-board-main' }, - error ? h('p', { className: 'bt-board-error', role: 'alert' }, error) : null, - lanes ? h('p', { className: 'bt-board-hint' }, t('Assign, claim, and progress live on this board: one overall task, a subtask per part, an assignee, a column, and progress. @ only wakes someone.')) : null, - scope?.kind === 'bot' && onCreate ? h('form', { className: 'bt-board-add', onSubmit: (event) => { event.preventDefault(); onCreate(draft) } }, - h('input', { 'aria-label': t('Task title'), placeholder: t('New task'), value: draft ?? '', onChange: (event) => onDraft?.(event.target.value) }), - h('button', { type: 'submit', className: 'bt-soft bt-soft-sm' }, t('Add task'))) : null, - h('div', { className: 'bt-board-columns' }, - ['not-started', 'in-progress', 'done'].map(column => h(BoardColumn, { - key: column, column, tasks: columns[column], roster, lanes, dragging, onMove, onDrag, - }))))) -} - export function KanbanRow({ wide, useUi, actions }) { const open = useUi ? useUi(value => Boolean(value.board)) : false const label = t('Tasks') @@ -360,7 +271,7 @@ function scopeName(roster, scope) { // #1326: one board that shows both task cards and delivery cards together, no tab split. // Columns come from workflow definitions; task cards map into the same columns as delivery cards. -function UnifiedBoard({ roster, tasks, rail, scope, counts, dragging, error, draft, actions, onDraft, onSelect, onMove, onDrag, onCreate }) { +export function UnifiedBoard({ roster, tasks, rail, scope, counts, dragging, error, draft, actions, onDraft, onSelect, onMove, onDrag, onCreate }) { const selectedBot = scope?.kind === 'bot' ? scope.botId : '' const selectedRoom = scope?.kind === 'group' ? scope.roomId : '' const lanes = scope?.kind === 'group' diff --git a/core/test/kanban.test.js b/core/test/kanban.test.js index 20bb087ee..806e925c7 100644 --- a/core/test/kanban.test.js +++ b/core/test/kanban.test.js @@ -31,7 +31,7 @@ runInNewContext(bundle.outputFiles[0].text, { return platform[name] }, }) -const { ARCHIVE_SHOWN, BoardView, KanbanDialog, boardColumns, boardTasksFor, boardRail, canMoveTask, columnTree, shortId, useBoardTasks } = module.exports +const { ARCHIVE_SHOWN, UnifiedBoard, KanbanDialog, boardColumns, boardTasksFor, boardRail, canMoveTask, columnTree, shortId, useBoardTasks } = module.exports const expand = (node) => { if (Array.isArray(node)) return node.flatMap(expand) @@ -85,10 +85,10 @@ test('columns follow the task state and a card carries id, priority and progress assert.equal(canMoveTask(roster, tasks[3]), false) }) -test('the board draws the three columns, nests the child, and moves only the owner\'s card', () => { +test('the unified board draws the three columns and moves only the owner\'s card', () => { const moved = [] const tree = expand({ - type: BoardView, + type: UnifiedBoard, props: { roster, tasks: tasks.filter(item => item.ownerBotId === 's'), @@ -109,8 +109,15 @@ test('the board draws the three columns, nests the child, and moves only the own assert.match(textOf(tree), /High/) assert.match(textOf(tree), /40%/) assert.match(textOf(tree), /Halfway/) - assert.match(textOf(tree), /Start task/) - assert.match(textOf(tree), /Complete task/) + // #1345: the removed board view printed "Start task"/"Complete task" over a column while a card + // was dragged. + // The one board draws no such hint (#1326) and the two strings now render nowhere, so the coverage + // kept here is the part that carried the behaviour: every column is still a drop target, and a + // drop stays a request the board\'s own onMove decides. + for (const column of byClass(tree, 'bt-board-column')) { + assert.equal(typeof column.props.onDrop, 'function', `column ${column.props['data-column']} takes a drop`) + assert.equal(typeof column.props.onDragOver, 'function') + } // A task card names its source and its identity on the one card model (#1265 step 1). const card = all(tree).find(node => node.props?.['data-task'] === 'task-abcdef123456') assert.equal(card.props['data-source'], 'task') @@ -125,7 +132,7 @@ test('the board draws the three columns, nests the child, and moves only the own progress.props.onDrop({ preventDefault() {}, dataTransfer: { getData: () => 'task-abcdef123456' } }) assert.deepEqual(moved[1], ['task-abcdef123456', 'in-progress']) const group = expand({ - type: BoardView, + type: UnifiedBoard, props: { roster, tasks: [tasks[3]], @@ -143,7 +150,7 @@ test('the board draws the three columns, nests the child, and moves only the own assert.equal(byClass(group, 'bt-board-card')[0].props.draggable, undefined) }) -test('a group column nests subtasks under the overall task', () => { +test('a group board shows an overall task and its subtasks in their own columns', () => { const overall = task('task-aaaaaa111111', 'open', { title: 'Ship it', kind: 'group', ownerBotId: 'a', priority: 'high', updatedAt: 5 }) const child = task('task-bbbbbb222222', 'open', { title: 'Write it', ownerBotId: 's', parentId: overall.id, parentTitle: 'Ship it', priority: 'urgent', updatedAt: 6 }) const later = task('task-cccccc333333', 'working', { title: 'Check it', ownerBotId: 'b', ownerBotName: 'Other', parentId: overall.id, parentTitle: 'Ship it', progress: 10, updatedAt: 7 }) @@ -154,7 +161,7 @@ test('a group column nests subtasks under the overall task', () => { assert.equal(open.nests[0].children.map(item => item.id).join('|'), child.id) assert.equal(open.loose.map(item => item.id).join('|'), tasks[3].id) const tree = expand({ - type: BoardView, + type: UnifiedBoard, props: { roster, tasks: [overall, child, later, tasks[3]], @@ -169,8 +176,11 @@ test('a group column nests subtasks under the overall task', () => { assert.match(textOf(notStarted), /Group task/) assert.match(textOf(notStarted), /Ship it/) assert.match(textOf(notStarted), /Write it/) - assert.equal(all(notStarted).some(node => node.props?.['data-parent'] === overall.id), true) - assert.match(textOf(progress), /Part of Ship it/) + // #1345: the removed board view nested a subtask under its overall card (`data-parent`, "Part of X"). The one + // board draws every task card flat in the column its own state names (#1326), so the DOM no longer + // nests; the model still does (asserted above). Recorded, not silently dropped: #1358 tracks it. + assert.equal(all(tree).some(node => node.props?.['data-parent'] !== undefined), false) + assert.equal(textOf(notStarted).includes('Part of Ship it'), false) assert.match(textOf(progress), /Check it/) assert.match(textOf(notStarted), /Theirs/) }) From 1389e3cbea29d27af2a5ad71a7d759eff369bce5 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Tue, 27 Oct 2026 14:54:15 +0800 Subject: [PATCH 2/5] chore(#1345): regenerate the kanban whole-file mirrors `node scripts/ds-bot-patches.mjs generate` on 45a7c236 rewrites two files: `patches/ds-bot/files/src/client/kanban.js` and `patches/ds-bot/files/test/kanban.test.js` now carry the branch's text, byte for byte (`cmp` clean against `core/`). Both are whole-file mirrors and belong to no numbered group in `topics.json`, so no numbered patch moves. `check` at this tree: pin 2b10c70, 10 patches apply in order, core/ equals ds-bot/ + patches/ds-bot/. `node --test test/kanban.test.js` is 8/8. --- patches/ds-bot/files/src/client/kanban.js | 91 +---------------------- patches/ds-bot/files/test/kanban.test.js | 30 +++++--- 2 files changed, 21 insertions(+), 100 deletions(-) diff --git a/patches/ds-bot/files/src/client/kanban.js b/patches/ds-bot/files/src/client/kanban.js index cee4c91db..1a0c48d21 100644 --- a/patches/ds-bot/files/src/client/kanban.js +++ b/patches/ds-bot/files/src/client/kanban.js @@ -134,33 +134,10 @@ function statusLabel(status) { } } -function dropLabel(column) { - if (column === 'in-progress') return t('Start task') - if (column === 'done') return t('Complete task') - return t('Not started') -} - -function columnTitle(column) { - if (column === 'in-progress') return t('In progress') - if (column === 'done') return t('Done') - return t('Not started') -} - function botName(roster, task) { return roster?.byId?.[task.ownerBotId]?.name || task.ownerBotName || '' } -function lanesOf(tasks, roster) { - const order = [] - const map = new Map() - for (const task of tasks) { - const id = task.ownerBotId ?? '' - if (!map.has(id)) { map.set(id, []); order.push(id) } - map.get(id).push(task) - } - return order.map(id => ({ id, name: botName(roster, map.get(id)[0]) || id, tasks: map.get(id) })) -} - const upsert = (list, task) => (list.some(item => item.id === task.id) ? list.map(item => item.id === task.id ? task : item) : [...list, task]) const without = (list, id) => list.filter(item => item.id !== id) @@ -275,72 +252,6 @@ export function BoardCard({ task, roster, movable, onMove, onDrag, partOf }) { }) } -function cardProps(task, roster, onMove, onDrag, partOf) { - return { task, roster, movable: canMoveTask(roster, task), onMove, onDrag, partOf } -} - -function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag }) { - const tree = lanes ? columnTree(tasks) : null - const linked = tree ? tree.loose.filter(task => task.parentId || task.parentTitle) : [] - const plain = tree ? tree.loose.filter(task => !task.parentId && !task.parentTitle) : tasks - return h('section', { - className: 'bt-board-column', - 'data-column': column, - 'aria-label': columnTitle(column), - onDragOver: (event) => { if (!dragging) return; event.preventDefault() }, - onDrop: (event) => { - event.preventDefault() - const id = event.dataTransfer?.getData('text/plain') - if (id) onMove?.(id, column) - onDrag?.(null) - }, - }, - h('h3', { className: 'bt-board-column-head' }, - h('span', null, columnTitle(column)), - h('span', { className: 'bt-board-count' }, String(tasks.length))), - dragging ? h('div', { className: 'bt-board-drop' }, dropLabel(column)) : null, - tasks.length === 0 && !dragging ? h('p', { className: 'bt-board-empty' }, t('Nothing in this column.')) : null, - tree ? tree.nests.map(nest => h('div', { key: nest.task.id, className: 'bt-board-nest' }, - h(BoardCard, cardProps(nest.task, roster, onMove, onDrag)), - nest.children.map(child => h('div', { key: child.id, className: 'bt-board-subtask', 'data-parent': nest.task.id }, - h(BoardCard, cardProps(child, roster, onMove, onDrag)))))) : null, - linked.map(task => h(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || '') })), - lanes - ? lanesOf(plain, roster).map(lane => h('div', { key: lane.id, className: 'bt-board-lane' }, - h('span', { className: 'bt-board-lane-name' }, lane.name), - lane.tasks.map(task => h(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })))) - : plain.map(task => h(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) }))) -} - -export function BoardView({ roster, tasks, rail, scope, counts, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate }) { - const columns = boardColumns(tasks) - const lanes = scope?.kind === 'group' - const selectedBot = scope?.kind === 'bot' ? scope.botId : '' - const selectedRoom = scope?.kind === 'group' ? scope.roomId : '' - return h('div', { className: 'bt-board-body' }, - h('aside', { className: 'bt-board-rail', 'aria-label': t('Task board') }, - h('h3', { className: 'bt-board-rail-head' }, t('Your Bots')), - rail.bots.length === 0 ? h('p', { className: 'bt-board-empty' }, t('No tasks yet.')) : rail.bots.map(row => { - const count = counts?.[row.id] ?? { done: 0, total: 0 } - return h(RailButton, { - key: row.id, row, selected: row.id === selectedBot, onSelect, - meta: count.total > 0 ? t('{done}/{total}', { done: count.done, total: count.total }) : '', - }) - }), - rail.groups.length > 0 ? h('h3', { className: 'bt-board-rail-head' }, t('Groups you admin')) : null, - rail.groups.map(row => h(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect }))), - h('div', { className: 'bt-board-main' }, - error ? h('p', { className: 'bt-board-error', role: 'alert' }, error) : null, - lanes ? h('p', { className: 'bt-board-hint' }, t('Assign, claim, and progress live on this board: one overall task, a subtask per part, an assignee, a column, and progress. @ only wakes someone.')) : null, - scope?.kind === 'bot' && onCreate ? h('form', { className: 'bt-board-add', onSubmit: (event) => { event.preventDefault(); onCreate(draft) } }, - h('input', { 'aria-label': t('Task title'), placeholder: t('New task'), value: draft ?? '', onChange: (event) => onDraft?.(event.target.value) }), - h('button', { type: 'submit', className: 'bt-soft bt-soft-sm' }, t('Add task'))) : null, - h('div', { className: 'bt-board-columns' }, - ['not-started', 'in-progress', 'done'].map(column => h(BoardColumn, { - key: column, column, tasks: columns[column], roster, lanes, dragging, onMove, onDrag, - }))))) -} - export function KanbanRow({ wide, useUi, actions }) { const open = useUi ? useUi(value => Boolean(value.board)) : false const label = t('Tasks') @@ -360,7 +271,7 @@ function scopeName(roster, scope) { // #1326: one board that shows both task cards and delivery cards together, no tab split. // Columns come from workflow definitions; task cards map into the same columns as delivery cards. -function UnifiedBoard({ roster, tasks, rail, scope, counts, dragging, error, draft, actions, onDraft, onSelect, onMove, onDrag, onCreate }) { +export function UnifiedBoard({ roster, tasks, rail, scope, counts, dragging, error, draft, actions, onDraft, onSelect, onMove, onDrag, onCreate }) { const selectedBot = scope?.kind === 'bot' ? scope.botId : '' const selectedRoom = scope?.kind === 'group' ? scope.roomId : '' const lanes = scope?.kind === 'group' diff --git a/patches/ds-bot/files/test/kanban.test.js b/patches/ds-bot/files/test/kanban.test.js index 20bb087ee..806e925c7 100644 --- a/patches/ds-bot/files/test/kanban.test.js +++ b/patches/ds-bot/files/test/kanban.test.js @@ -31,7 +31,7 @@ runInNewContext(bundle.outputFiles[0].text, { return platform[name] }, }) -const { ARCHIVE_SHOWN, BoardView, KanbanDialog, boardColumns, boardTasksFor, boardRail, canMoveTask, columnTree, shortId, useBoardTasks } = module.exports +const { ARCHIVE_SHOWN, UnifiedBoard, KanbanDialog, boardColumns, boardTasksFor, boardRail, canMoveTask, columnTree, shortId, useBoardTasks } = module.exports const expand = (node) => { if (Array.isArray(node)) return node.flatMap(expand) @@ -85,10 +85,10 @@ test('columns follow the task state and a card carries id, priority and progress assert.equal(canMoveTask(roster, tasks[3]), false) }) -test('the board draws the three columns, nests the child, and moves only the owner\'s card', () => { +test('the unified board draws the three columns and moves only the owner\'s card', () => { const moved = [] const tree = expand({ - type: BoardView, + type: UnifiedBoard, props: { roster, tasks: tasks.filter(item => item.ownerBotId === 's'), @@ -109,8 +109,15 @@ test('the board draws the three columns, nests the child, and moves only the own assert.match(textOf(tree), /High/) assert.match(textOf(tree), /40%/) assert.match(textOf(tree), /Halfway/) - assert.match(textOf(tree), /Start task/) - assert.match(textOf(tree), /Complete task/) + // #1345: the removed board view printed "Start task"/"Complete task" over a column while a card + // was dragged. + // The one board draws no such hint (#1326) and the two strings now render nowhere, so the coverage + // kept here is the part that carried the behaviour: every column is still a drop target, and a + // drop stays a request the board\'s own onMove decides. + for (const column of byClass(tree, 'bt-board-column')) { + assert.equal(typeof column.props.onDrop, 'function', `column ${column.props['data-column']} takes a drop`) + assert.equal(typeof column.props.onDragOver, 'function') + } // A task card names its source and its identity on the one card model (#1265 step 1). const card = all(tree).find(node => node.props?.['data-task'] === 'task-abcdef123456') assert.equal(card.props['data-source'], 'task') @@ -125,7 +132,7 @@ test('the board draws the three columns, nests the child, and moves only the own progress.props.onDrop({ preventDefault() {}, dataTransfer: { getData: () => 'task-abcdef123456' } }) assert.deepEqual(moved[1], ['task-abcdef123456', 'in-progress']) const group = expand({ - type: BoardView, + type: UnifiedBoard, props: { roster, tasks: [tasks[3]], @@ -143,7 +150,7 @@ test('the board draws the three columns, nests the child, and moves only the own assert.equal(byClass(group, 'bt-board-card')[0].props.draggable, undefined) }) -test('a group column nests subtasks under the overall task', () => { +test('a group board shows an overall task and its subtasks in their own columns', () => { const overall = task('task-aaaaaa111111', 'open', { title: 'Ship it', kind: 'group', ownerBotId: 'a', priority: 'high', updatedAt: 5 }) const child = task('task-bbbbbb222222', 'open', { title: 'Write it', ownerBotId: 's', parentId: overall.id, parentTitle: 'Ship it', priority: 'urgent', updatedAt: 6 }) const later = task('task-cccccc333333', 'working', { title: 'Check it', ownerBotId: 'b', ownerBotName: 'Other', parentId: overall.id, parentTitle: 'Ship it', progress: 10, updatedAt: 7 }) @@ -154,7 +161,7 @@ test('a group column nests subtasks under the overall task', () => { assert.equal(open.nests[0].children.map(item => item.id).join('|'), child.id) assert.equal(open.loose.map(item => item.id).join('|'), tasks[3].id) const tree = expand({ - type: BoardView, + type: UnifiedBoard, props: { roster, tasks: [overall, child, later, tasks[3]], @@ -169,8 +176,11 @@ test('a group column nests subtasks under the overall task', () => { assert.match(textOf(notStarted), /Group task/) assert.match(textOf(notStarted), /Ship it/) assert.match(textOf(notStarted), /Write it/) - assert.equal(all(notStarted).some(node => node.props?.['data-parent'] === overall.id), true) - assert.match(textOf(progress), /Part of Ship it/) + // #1345: the removed board view nested a subtask under its overall card (`data-parent`, "Part of X"). The one + // board draws every task card flat in the column its own state names (#1326), so the DOM no longer + // nests; the model still does (asserted above). Recorded, not silently dropped: #1358 tracks it. + assert.equal(all(tree).some(node => node.props?.['data-parent'] !== undefined), false) + assert.equal(textOf(notStarted).includes('Part of Ship it'), false) assert.match(textOf(progress), /Check it/) assert.match(textOf(notStarted), /Theirs/) }) From df995b2517b1e9a03da7b4a2200a9a28beeef6d2 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Tue, 27 Oct 2026 14:56:44 +0800 Subject: [PATCH 3/5] Assert only the board shape that holds with or without nesting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1345: pinning today's flat DOM — no `data-parent`, no "Part of X" — would red the moment the group nesting comes back on the product path (#1358, #1361). Keep the assertions that survive either shape instead: every subtask card is drawn, and a `data-parent` never names a card that is not on the board. Docs-not-needed: test-only change; no owned path and no shipped behaviour moves. --- core/test/kanban.test.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/core/test/kanban.test.js b/core/test/kanban.test.js index 806e925c7..e3274b26c 100644 --- a/core/test/kanban.test.js +++ b/core/test/kanban.test.js @@ -109,11 +109,9 @@ test('the unified board draws the three columns and moves only the owner\'s card assert.match(textOf(tree), /High/) assert.match(textOf(tree), /40%/) assert.match(textOf(tree), /Halfway/) - // #1345: the removed board view printed "Start task"/"Complete task" over a column while a card - // was dragged. - // The one board draws no such hint (#1326) and the two strings now render nowhere, so the coverage - // kept here is the part that carried the behaviour: every column is still a drop target, and a - // drop stays a request the board\'s own onMove decides. + // #1345: the removed board view printed a drag hint over a column while a card was dragged. The + // hint's wording is not this test's business, so what is kept is the behaviour it carried: every + // column is still a drop target, and a drop stays a request the board's own onMove decides. for (const column of byClass(tree, 'bt-board-column')) { assert.equal(typeof column.props.onDrop, 'function', `column ${column.props['data-column']} takes a drop`) assert.equal(typeof column.props.onDragOver, 'function') @@ -176,11 +174,17 @@ test('a group board shows an overall task and its subtasks in their own columns' assert.match(textOf(notStarted), /Group task/) assert.match(textOf(notStarted), /Ship it/) assert.match(textOf(notStarted), /Write it/) - // #1345: the removed board view nested a subtask under its overall card (`data-parent`, "Part of X"). The one - // board draws every task card flat in the column its own state names (#1326), so the DOM no longer - // nests; the model still does (asserted above). Recorded, not silently dropped: #1358 tracks it. - assert.equal(all(tree).some(node => node.props?.['data-parent'] !== undefined), false) - assert.equal(textOf(notStarted).includes('Part of Ship it'), false) + // #1345: the board view that nested a subtask under its overall card is gone, and the one board + // draws every card flat in the column its own state names (#1326); #1358 tracks that loss. Whether + // nesting returns is not this test's business (#1361 works on it), so only the shape that holds + // either way is asserted: every subtask card is drawn, and a `data-parent` never names a card that + // is not on the board. + const drawn = new Set(all(tree).map(node => node.props?.['data-task']).filter(Boolean)) + assert.equal(drawn.has(child.id), true, 'the subtask in its parent\'s column is drawn') + assert.equal(drawn.has(later.id), true, 'the subtask in another column is drawn') + for (const node of all(tree).filter(node => node.props?.['data-parent'] !== undefined)) { + assert.equal(drawn.has(node.props['data-parent']), true, `data-parent ${node.props['data-parent']} names a drawn card`) + } assert.match(textOf(progress), /Check it/) assert.match(textOf(notStarted), /Theirs/) }) From 060dad44312987c71108f32857a2b34232a8af9e Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Tue, 27 Oct 2026 14:59:35 +0800 Subject: [PATCH 4/5] chore(#1345): regenerate the kanban test mirror on df995b25 Dev4's "Assert only the board shape that holds with or without nesting" changed `core/test/kanban.test.js` after the previous mirror commit, so `node scripts/ds-bot-patches.mjs generate` rewrites one file: `patches/ds-bot/files/test/kanban.test.js` now carries the same text as `core/test/kanban.test.js`, byte for byte (`cmp` clean). The kanban.js mirror was already equal and stays untouched; no numbered patch moves. `check` at this tree: pin 2b10c70, 10 patches apply in order, core/ equals ds-bot/ + patches/ds-bot/. `node --test test/kanban.test.js` is 8/8. --- patches/ds-bot/files/test/kanban.test.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/patches/ds-bot/files/test/kanban.test.js b/patches/ds-bot/files/test/kanban.test.js index 806e925c7..e3274b26c 100644 --- a/patches/ds-bot/files/test/kanban.test.js +++ b/patches/ds-bot/files/test/kanban.test.js @@ -109,11 +109,9 @@ test('the unified board draws the three columns and moves only the owner\'s card assert.match(textOf(tree), /High/) assert.match(textOf(tree), /40%/) assert.match(textOf(tree), /Halfway/) - // #1345: the removed board view printed "Start task"/"Complete task" over a column while a card - // was dragged. - // The one board draws no such hint (#1326) and the two strings now render nowhere, so the coverage - // kept here is the part that carried the behaviour: every column is still a drop target, and a - // drop stays a request the board\'s own onMove decides. + // #1345: the removed board view printed a drag hint over a column while a card was dragged. The + // hint's wording is not this test's business, so what is kept is the behaviour it carried: every + // column is still a drop target, and a drop stays a request the board's own onMove decides. for (const column of byClass(tree, 'bt-board-column')) { assert.equal(typeof column.props.onDrop, 'function', `column ${column.props['data-column']} takes a drop`) assert.equal(typeof column.props.onDragOver, 'function') @@ -176,11 +174,17 @@ test('a group board shows an overall task and its subtasks in their own columns' assert.match(textOf(notStarted), /Group task/) assert.match(textOf(notStarted), /Ship it/) assert.match(textOf(notStarted), /Write it/) - // #1345: the removed board view nested a subtask under its overall card (`data-parent`, "Part of X"). The one - // board draws every task card flat in the column its own state names (#1326), so the DOM no longer - // nests; the model still does (asserted above). Recorded, not silently dropped: #1358 tracks it. - assert.equal(all(tree).some(node => node.props?.['data-parent'] !== undefined), false) - assert.equal(textOf(notStarted).includes('Part of Ship it'), false) + // #1345: the board view that nested a subtask under its overall card is gone, and the one board + // draws every card flat in the column its own state names (#1326); #1358 tracks that loss. Whether + // nesting returns is not this test's business (#1361 works on it), so only the shape that holds + // either way is asserted: every subtask card is drawn, and a `data-parent` never names a card that + // is not on the board. + const drawn = new Set(all(tree).map(node => node.props?.['data-task']).filter(Boolean)) + assert.equal(drawn.has(child.id), true, 'the subtask in its parent\'s column is drawn') + assert.equal(drawn.has(later.id), true, 'the subtask in another column is drawn') + for (const node of all(tree).filter(node => node.props?.['data-parent'] !== undefined)) { + assert.equal(drawn.has(node.props['data-parent']), true, `data-parent ${node.props['data-parent']} names a drawn card`) + } assert.match(textOf(progress), /Check it/) assert.match(textOf(notStarted), /Theirs/) }) From d2f122d74148b7285d3322ebc9f15af3c26daee4 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Tue, 27 Oct 2026 15:02:31 +0800 Subject: [PATCH 5/5] Rebuild client.js after the BoardView removal #1345: the browser bundle is generated from src/client, so deleting BoardView there left core/client.js stale and CI's build-client check red. Rebuilt with `npm run build`; the bundle loses exactly the removed view and nothing else. Docs-not-needed: generated bundle follows the source change already described in this PR. --- core/client.js | 130 ------------------------------------------------- 1 file changed, 130 deletions(-) diff --git a/core/client.js b/core/client.js index 5ad3ec779..fea27778f 100644 --- a/core/client.js +++ b/core/client.js @@ -9227,32 +9227,9 @@ function statusLabel(status) { return t("Not started"); } } -function dropLabel(column) { - if (column === "in-progress") return t("Start task"); - if (column === "done") return t("Complete task"); - return t("Not started"); -} -function columnTitle(column) { - if (column === "in-progress") return t("In progress"); - if (column === "done") return t("Done"); - return t("Not started"); -} function botName(roster, task) { return roster?.byId?.[task.ownerBotId]?.name || task.ownerBotName || ""; } -function lanesOf(tasks, roster) { - const order = []; - const map = /* @__PURE__ */ new Map(); - for (const task of tasks) { - const id = task.ownerBotId ?? ""; - if (!map.has(id)) { - map.set(id, []); - order.push(id); - } - map.get(id).push(task); - } - return order.map((id) => ({ id, name: botName(roster, map.get(id)[0]) || id, tasks: map.get(id) })); -} var upsert = (list, task) => list.some((item) => item.id === task.id) ? list.map((item) => item.id === task.id ? task : item) : [...list, task]; var without = (list, id) => list.filter((item) => item.id !== id); function useBoardTasks(actions, scope) { @@ -9363,113 +9340,6 @@ function BoardCard2({ task, roster, movable, onMove, onDrag, partOf: partOf2 }) onDragEnd: () => onDrag?.(null) }); } -function cardProps(task, roster, onMove, onDrag, partOf2) { - return { task, roster, movable: canMoveTask(roster, task), onMove, onDrag, partOf: partOf2 }; -} -function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag }) { - const tree = lanes ? columnTree(tasks) : null; - const linked = tree ? tree.loose.filter((task) => task.parentId || task.parentTitle) : []; - const plain = tree ? tree.loose.filter((task) => !task.parentId && !task.parentTitle) : tasks; - return (0, import_react17.createElement)( - "section", - { - className: "bt-board-column", - "data-column": column, - "aria-label": columnTitle(column), - onDragOver: (event) => { - if (!dragging) return; - event.preventDefault(); - }, - onDrop: (event) => { - event.preventDefault(); - const id = event.dataTransfer?.getData("text/plain"); - if (id) onMove?.(id, column); - onDrag?.(null); - } - }, - (0, import_react17.createElement)( - "h3", - { className: "bt-board-column-head" }, - (0, import_react17.createElement)("span", null, columnTitle(column)), - (0, import_react17.createElement)("span", { className: "bt-board-count" }, String(tasks.length)) - ), - dragging ? (0, import_react17.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null, - tasks.length === 0 && !dragging ? (0, import_react17.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null, - tree ? tree.nests.map((nest) => (0, import_react17.createElement)( - "div", - { key: nest.task.id, className: "bt-board-nest" }, - (0, import_react17.createElement)(BoardCard2, cardProps(nest.task, roster, onMove, onDrag)), - nest.children.map((child) => (0, import_react17.createElement)( - "div", - { key: child.id, className: "bt-board-subtask", "data-parent": nest.task.id }, - (0, import_react17.createElement)(BoardCard2, cardProps(child, roster, onMove, onDrag)) - )) - )) : null, - linked.map((task) => (0, import_react17.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || "") })), - lanes ? lanesOf(plain, roster).map((lane) => (0, import_react17.createElement)( - "div", - { key: lane.id, className: "bt-board-lane" }, - (0, import_react17.createElement)("span", { className: "bt-board-lane-name" }, lane.name), - lane.tasks.map((task) => (0, import_react17.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) - )) : plain.map((task) => (0, import_react17.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) - ); -} -function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate }) { - const columns = boardColumns(tasks); - const lanes = scope?.kind === "group"; - const selectedBot = scope?.kind === "bot" ? scope.botId : ""; - const selectedRoom = scope?.kind === "group" ? scope.roomId : ""; - return (0, import_react17.createElement)( - "div", - { className: "bt-board-body" }, - (0, import_react17.createElement)( - "aside", - { className: "bt-board-rail", "aria-label": t("Task board") }, - (0, import_react17.createElement)("h3", { className: "bt-board-rail-head" }, t("Your Bots")), - rail.bots.length === 0 ? (0, import_react17.createElement)("p", { className: "bt-board-empty" }, t("No tasks yet.")) : rail.bots.map((row) => { - const count = counts2?.[row.id] ?? { done: 0, total: 0 }; - return (0, import_react17.createElement)(RailButton, { - key: row.id, - row, - selected: row.id === selectedBot, - onSelect, - meta: count.total > 0 ? t("{done}/{total}", { done: count.done, total: count.total }) : "" - }); - }), - rail.groups.length > 0 ? (0, import_react17.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null, - rail.groups.map((row) => (0, import_react17.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect })) - ), - (0, import_react17.createElement)( - "div", - { className: "bt-board-main" }, - error ? (0, import_react17.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null, - lanes ? (0, import_react17.createElement)("p", { className: "bt-board-hint" }, t("Assign, claim, and progress live on this board: one overall task, a subtask per part, an assignee, a column, and progress. @ only wakes someone.")) : null, - scope?.kind === "bot" && onCreate ? (0, import_react17.createElement)( - "form", - { className: "bt-board-add", onSubmit: (event) => { - event.preventDefault(); - onCreate(draft); - } }, - (0, import_react17.createElement)("input", { "aria-label": t("Task title"), placeholder: t("New task"), value: draft ?? "", onChange: (event) => onDraft?.(event.target.value) }), - (0, import_react17.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, t("Add task")) - ) : null, - (0, import_react17.createElement)( - "div", - { className: "bt-board-columns" }, - ["not-started", "in-progress", "done"].map((column) => (0, import_react17.createElement)(BoardColumn, { - key: column, - column, - tasks: columns[column], - roster, - lanes, - dragging, - onMove, - onDrag - })) - ) - ) - ); -} function KanbanRow({ wide, useUi, actions }) { const open = useUi ? useUi((value) => Boolean(value.board)) : false; const label = t("Tasks");