feat: work-in-progress limits and swimlanes on the board

Each status can carry a WIP limit in the project settings; the board shows
it next to the count, marks a column that goes over it and says so when a
card is dropped there. The board can also be grouped in rows by person,
epic, priority or type, remembered per browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 15:09:21 +02:00
parent 6aec774c92
commit 09bd8c19a3
23 changed files with 563 additions and 74 deletions

View File

@@ -61,6 +61,11 @@ export interface WorkflowStatus {
requiresReason: boolean;
/** Archived: no new tickets or transitions into it; existing ones keep it. */
archived: boolean;
/**
* Work-in-progress limit: how many tickets the column should hold at most
* (null: no limit). A warning, not a lock: the board flags a full column.
*/
wipLimit?: number | null;
}
export interface IssueTypeOption {
@@ -193,6 +198,11 @@ export class ProjectWorkflow {
return this.settings.statuses.find((s) => s.id === id);
}
/** Most tickets the status should hold (null: no limit). */
wipLimit(id: string): number | null {
return this.status(id)?.wipLimit ?? null;
}
/** Name to show, also for ids no longer configured. */
statusName(id: string): string {
return (
@@ -287,6 +297,8 @@ export function validateProjectSettings(
color: s.color,
requiresReason: !!s.requiresReason,
archived: !!s.archived,
// Only when set: settings saved unchanged must compare equal.
...(s.wipLimit != null ? { wipLimit: s.wipLimit } : {}),
}));
const issueTypes = next.issueTypes.map((t) => ({
id: normId(t.id),
@@ -326,6 +338,11 @@ export function validateProjectSettings(
SETTING_COLORS.includes(s.color),
`Color no válido en el estado ${s.name}`,
);
assertThat(
s.wipLimit === undefined ||
(Number.isInteger(s.wipLimit) && s.wipLimit >= 1 && s.wipLimit <= 999),
`El límite WIP del estado ${s.name} tiene que ser un número entre 1 y 999`,
);
}
for (const t of issueTypes) {
assertThat(