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
1 change: 1 addition & 0 deletions changelog.d/2045-board-archive-counts.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The task board's Done column is one archive page of 20 cards, and until now it presented that page as the whole story: a Bot with 45 finished tasks read **Done 20**, and its rail row read **20/22** as if 22 were everything. When the archive page is full the column heading now reads **20 newest** and the rail row reads **20+ done** — what was drawn, and that there is more; a page under the limit, and the empty board, read exactly as before ([#2045](https://github.com/ThinkFlowLab/FlyBot/issues/2045)).
21 changes: 15 additions & 6 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ var ZH = {
"{from} → {to}": "{from} → {to}",
"+{count} more": "另有 {count} 条",
"{count} more not shown": "还有 {count} 条未显示",
"{count} newest": "最新 {count} 张",
"{count}+ done": "{count}+ 已完成",
"Reading the decision…": "正在读取决策…",
"not listed": "不在列表里",
delegated: "委托转达",
Expand Down Expand Up @@ -8952,7 +8954,7 @@ function BoardCard2({ task, roster, movable, onMove, onDrag, partOf: partOf2 })
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 }) {
function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag, archiveFull }) {
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;
Expand All @@ -8977,7 +8979,9 @@ function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag })
"h3",
{ className: "bt-board-column-head" },
(0, import_react15.createElement)("span", null, columnTitle(column)),
(0, import_react15.createElement)("span", { className: "bt-board-count" }, String(tasks.length))
// #2045: the Done column is one archive page. `tasks.length` is what was drawn, not what the
// owner has, so a full page says `20 newest` instead of claiming the page is the total.
(0, import_react15.createElement)("span", { className: "bt-board-count" }, column === "done" && archiveFull ? t("{count} newest", { count: tasks.length }) : String(tasks.length))
),
dragging ? (0, import_react15.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null,
tasks.length === 0 && !dragging ? (0, import_react15.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null,
Expand All @@ -9000,7 +9004,7 @@ function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag })
)) : plain.map((task) => (0, import_react15.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 }) {
function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate, archiveFull }) {
const columns = boardColumns(tasks);
const lanes = scope?.kind === "group";
const selectedBot = scope?.kind === "bot" ? scope.botId : "";
Expand All @@ -9014,12 +9018,14 @@ function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, erro
(0, import_react15.createElement)("h3", { className: "bt-board-rail-head" }, t("Your Bots")),
rail.bots.length === 0 ? (0, import_react15.createElement)("p", { className: "bt-board-empty" }, t("No tasks yet.")) : rail.bots.map((row) => {
const count = counts2?.[row.id] ?? { done: 0, total: 0 };
const capped = archiveFull && count.done >= ARCHIVE_SHOWN;
const meta = count.total === 0 ? "" : capped ? t("{count}+ done", { count: count.done }) : t("{done}/{total}", { done: count.done, total: count.total });
return (0, import_react15.createElement)(RailButton, {
key: row.id,
row,
selected: row.id === selectedBot,
onSelect,
meta: count.total > 0 ? t("{done}/{total}", { done: count.done, total: count.total }) : ""
meta
});
}),
rail.groups.length > 0 ? (0, import_react15.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null,
Expand Down Expand Up @@ -9050,7 +9056,8 @@ function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, erro
lanes,
dragging,
onMove,
onDrag
onDrag,
archiveFull
}))
)
)
Expand Down Expand Up @@ -9098,8 +9105,9 @@ function KanbanDialog({ roster, actions, leaving = false }) {
if (kind === "bot" && roster.byId?.[id]) setScope({ kind: "bot", botId: id });
if (kind === "group" && roster.roomsById?.[id]) setScope({ kind: "group", roomId: id });
}, []);
const archiveFull = Array.isArray(archive) && archive.length >= ARCHIVE_SHOWN;
const counts2 = {};
for (const task of [...own, ...archive]) {
for (const task of [...own, ...(archive ?? []).slice(0, ARCHIVE_SHOWN)]) {
const id = task.ownerBotId;
const bucket = counts2[id] ?? (counts2[id] = { done: 0, total: 0 });
if (task.status === "cancelled" || task.status === "failed") continue;
Expand Down Expand Up @@ -9154,6 +9162,7 @@ function KanbanDialog({ roster, actions, leaving = false }) {
dragging,
error,
draft,
archiveFull,
onDraft: setDraft,
onSelect: setScope,
onMove: move,
Expand Down
2 changes: 2 additions & 0 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ const ZH = {
'{from} → {to}': '{from} → {to}',
'+{count} more': '另有 {count} 条',
'{count} more not shown': '还有 {count} 条未显示',
'{count} newest': '最新 {count} 张',
'{count}+ done': '{count}+ 已完成',
'Reading the decision…': '正在读取决策…',
'not listed': '不在列表里',
delegated: '委托转达',
Expand Down
29 changes: 22 additions & 7 deletions core/src/client/kanban.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ 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 }) {
function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag, archiveFull }) {
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
Expand All @@ -296,7 +296,9 @@ function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag })
},
h('h3', { className: 'bt-board-column-head' },
h('span', null, columnTitle(column)),
h('span', { className: 'bt-board-count' }, String(tasks.length))),
// #2045: the Done column is one archive page. `tasks.length` is what was drawn, not what the
// owner has, so a full page says `20 newest` instead of claiming the page is the total.
h('span', { className: 'bt-board-count' }, column === 'done' && archiveFull ? t('{count} newest', { count: tasks.length }) : 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' },
Expand All @@ -311,7 +313,7 @@ function BoardColumn({ column, tasks, roster, lanes, dragging, 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 }) {
export function BoardView({ roster, tasks, rail, scope, counts, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate, archiveFull }) {
const columns = boardColumns(tasks)
const lanes = scope?.kind === 'group'
const selectedBot = scope?.kind === 'bot' ? scope.botId : ''
Expand All @@ -321,9 +323,16 @@ export function BoardView({ roster, tasks, rail, scope, counts, dragging, error,
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 }
// #2045: a Bot whose Done column is a full archive page has at least that many finished
// cards, and the total the rail adds up is only the page — so it says `20+ done` rather than
// a fraction that reads as if the page were the history.
const capped = archiveFull && count.done >= ARCHIVE_SHOWN
const meta = count.total === 0
? ''
: capped ? t('{count}+ done', { count: count.done }) : t('{done}/{total}', { done: count.done, total: count.total })
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 }) : '',
meta,
})
}),
rail.groups.length > 0 ? h('h3', { className: 'bt-board-rail-head' }, t('Groups you admin')) : null,
Expand All @@ -336,7 +345,7 @@ export function BoardView({ roster, tasks, rail, scope, counts, dragging, error,
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,
key: column, column, tasks: columns[column], roster, lanes, dragging, onMove, onDrag, archiveFull,
})))))
}

Expand Down Expand Up @@ -379,8 +388,14 @@ export function KanbanDialog({ roster, actions, leaving = false }) {
if (kind === 'bot' && roster.byId?.[id]) setScope({ kind: 'bot', botId: id })
if (kind === 'group' && roster.roomsById?.[id]) setScope({ kind: 'group', roomId: id })
}, [])
// #2045: a full page is a cut — the host answers the newest `ARCHIVE_SHOWN` finished rows and no
// total — so this is the only signal the board has that more may exist.
const archiveFull = Array.isArray(archive) && archive.length >= ARCHIVE_SHOWN
const counts = {}
for (const task of [...own, ...archive]) {
// #2045: the rail counts what the board draws, so the archive contributes at most its one page.
// The list can hold a row more than the page after an optimistic move, and counting that row would
// read as a total the board never showed.
for (const task of [...own, ...(archive ?? []).slice(0, ARCHIVE_SHOWN)]) {
const id = task.ownerBotId
const bucket = counts[id] ?? (counts[id] = { done: 0, total: 0 })
if (task.status === 'cancelled' || task.status === 'failed') continue
Expand Down Expand Up @@ -420,7 +435,7 @@ export function KanbanDialog({ roster, actions, leaving = false }) {
h('span', { className: 'bt-mem-head-gap' }),
h('button', { type: 'button', className: 'bt-icon-btn', 'aria-label': t('Close board'), autoFocus: true, onClick: close }, h(CloseIcon))),
h(BoardView, {
roster, tasks: boardTasks, rail, scope, counts, dragging, error, draft,
roster, tasks: boardTasks, rail, scope, counts, dragging, error, draft, archiveFull,
onDraft: setDraft, onSelect: setScope, onMove: move, onDrag: setDragging, onCreate: create,
})))
}
Expand Down
61 changes: 61 additions & 0 deletions core/test/kanban.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,64 @@ test('#1252: a Bot board asks the host for that Bot\'s archive, not the whole sc
const done = boardColumns(boardTasksFor([], [otherBotCard].filter(task => task.ownerBotId === 'a'))).done
assert.equal(done.length, 1, 'the Bot\'s own finished card fills the Done column')
})

// #2045: the Done column is one archive page (`ARCHIVE_SHOWN` cards), and the page is a cut, not a
// count. The heading prints `tasks.length`, so a Bot with 45 finished tasks reads `Done · 20`, and
// the rail adds the page to the active list, so it reads `20/22`. Neither number is what the owner
// has; when the page is full the board must not pretend it is the total. It may still say how much
// it drew — `20 newest` — and that there are more where those came from — `20+ done`. A page that
// is not full is the whole history, so those two numbers stay exactly as they are today.
const settledTasks = (count, extra = {}) => Array.from({ length: count }, (_, index) => task(`task-done-${index}`, 'accepted', {
title: `Finished ${index}`, progress: 100, createdAt: index, updatedAt: 100 + index, ...extra,
}))
const boardWith = (scope, own, archive) => {
states.length = 0
states.push(scope, null, '', '', own, [], archive)
const view = expand(KanbanDialog({ roster, actions: { closeBoard: () => {}, boardClose: () => {} } }))
const column = name => byClass(view, 'bt-board-column').find(node => node.props['data-column'] === name)
const railMeta = id => {
const row = all(view).find(node => node.props?.['data-id'] === id)
const meta = byClass(row, 'bt-board-rail-meta')[0]
return meta ? textOf(meta) : ''
}
return { view, column, railMeta }
}

test('#2045: a full archive page says how many rows were drawn and that there are more, on a Bot board and a group board', () => {
const active = [
task('task-open-1', 'open', { title: 'Still to do', ownerBotId: 's' }),
task('task-work-1', 'working', { title: 'Being done', ownerBotId: 's' }),
]
const botBoard = boardWith({ kind: 'bot', botId: 's' }, active, settledTasks(45))
const done = botBoard.column('done')
assert.equal(byClass(done, 'bt-board-card').length, ARCHIVE_SHOWN, 'the column still draws one archive page')
assert.equal(textOf(done).includes('20 newest'), true, 'the heading says these are the newest twenty, not the total')
// The other two columns are the active list, not a page, so their counts stay plain numbers.
assert.equal(textOf(botBoard.column('not-started')).includes('newest'), false, 'the open column keeps a plain count')
assert.equal(textOf(botBoard.column('in-progress')).includes('newest'), false, 'the working column keeps a plain count')
assert.equal(botBoard.railMeta('s'), '20+ done', 'the rail says at least twenty are done, and stops claiming a total')

// The group board reads the same archive page and must say the same thing.
const groupBoard = boardWith({ kind: 'group', roomId: 'war' }, [], settledTasks(45, { ownerBotId: 'a' }))
const groupDone = groupBoard.column('done')
assert.equal(byClass(groupDone, 'bt-board-card').length, ARCHIVE_SHOWN)
assert.equal(textOf(groupDone).includes('20 newest'), true, 'a full group page is marked the same way')
})

test('#2045: a page that is not full still reads as the whole history, unchanged', () => {
const active = [
task('task-open-1', 'open', { title: 'Still to do', ownerBotId: 's' }),
task('task-work-1', 'working', { title: 'Being done', ownerBotId: 's' }),
]
const short = boardWith({ kind: 'bot', botId: 's' }, active, settledTasks(3))
assert.equal(byClass(short.column('done'), 'bt-board-card').length, 3)
assert.equal(textOf(short.column('done')).includes('newest'), false, 'three rows are three rows, with no hedge')
assert.equal(short.railMeta('s'), '3/5', 'the rail keeps {done}/{total} when nothing was cut')

// Exactly one page full is the boundary: the board cannot know whether a 21st card exists, so the
// full-page wording has to start at the page size itself, never a row later.
const exact = boardWith({ kind: 'bot', botId: 's' }, active, settledTasks(ARCHIVE_SHOWN))
assert.equal(exact.railMeta('s'), '20+ done', 'a page at the limit is treated as cut')
const under = boardWith({ kind: 'bot', botId: 's' }, active, settledTasks(ARCHIVE_SHOWN - 1))
assert.equal(under.railMeta('s'), '19/21', 'one row under the limit is the whole history')
})
Loading
Loading