Kanban column controls, header menu and card order: a recruiting pipeline board
A recruiting pipeline has stages: Applied, Screen, Interview, Offer. Each stage has its own small jobs. Choose the interview kit for the people in it. Add a trigger that fires when a candidate arrives. Those jobs belong to a stage, so the controls belong under its header.
The board above holds eleven candidates. Under the Screen and Interview headers sit a “Select interview kit” select and an “Add trigger” button. Every stage has a header menu, behind the button at the right of its header, and a right-click on a header opens the same menu. Inside every column the candidates run from the highest score to the lowest. The grid underneath holds the same rows.
Controls under the header
A column definition takes controls: a button, a select or a custom element, drawn between the column header and its cards.
{
id: 'Screen',
title: 'Screen',
controls: [
{ type: 'select', id: 'kit', placeholder: 'Select interview kit',
options: ['Engineering', 'Design', 'Sales', 'Analytics'] },
{ type: 'button', id: 'trigger', label: 'Add trigger' },
],
}
A column with no controls of its own takes what columnControls(column) on the board returns, so the Applied and Offer stages above share one button, and so do columns the board creates from the data. A select is drawn by the Form module’s select when you inject it as form: { createSelect }, and by a native select otherwise.
A click or a change fires column:control, and the event names the column and the control. controlValue(columnId, controlId, value?) reads a select, or sets it when you pass a value, and the value survives a re-render. Try it: pick a kit under Screen, then drag a candidate to another stage, and the pick is still there.
The header menu
A column definition also takes menu: items behind a menu button in the header. Items you give to columnMenu on the board serve every column that has no menu of its own.
columnMenu: [
{ id: 'only-here', label: 'Show only this stage in the grid',
action: ({ column }) => grid.filters.quick(column) },
{ id: 'all', label: 'Show every stage',
action: () => grid.filters.quick('') },
],
Picking an item calls its action and then fires column:menu, naming the column and the item. In the example the first item narrows the grid to one stage. Look at the board while you do it: it narrows too, which is the last section.
Cards in the order you choose
Until now a column ordered its cards by one property. An order(a, b, column) comparator orders them by any rule you can write: a score, a due date, a computed rank the rows do not carry as a single field.
pointsProperty: 'score',
order: (a, b, column) => b.row.score - a.row.score,
Each argument is a card, and the function returns a number as Array.sort does. A card’s row holds the grid row, so a.row.score reads a column the card itself does not carry; here the score is also the card’s points, which the card shows as a small number. The order property, if you have one, stays the tie-break when the comparator returns 0. board.setOrder(fn) swaps the rule while the board is on screen, and board.setOrder(null) clears it. Press “Order by hand” above and the candidates fall back to the order the rows were loaded; press it again and the scores take over. Selection, focus and scroll stay where they were.
One dataset, several views
The board does not hold candidates of its own. It reads the rows the grid displays and writes a move back through the grid, the same way the charts beside a grid do. So the controls act on the rows every other view shows. The menu item that filters the grid is the whole point in one line: the board followed because it was never a separate copy.
Open the example, pick a kit, filter to a stage from its menu and switch the ordering off and on. The changelog has the rest.