Move Rules
Use rules when a move needs a business decision: Done work cannot move backward, locked cards cannot be dragged, or approval is required before completion.
Require work to start before completion
A task cannot move directly from To do to Done.
Try it: Try Complete research, then Start research and complete it again.
What to notice: The rule rejects the first move and accepts the valid workflow.
TypeScript source
Install the packages from Getting Started, then use this source in your app.
import type { KanbanPlugin, KanbanConfig } from '@revolist/kanban';import type { WorkCard } from './first-board';
export function moveResearch(kanban: KanbanPlugin<WorkCard>, columnProp: string) { return kanban.moveCards(['task-1'], { columnProp });}export function moveResearchToPlatform(kanban: KanbanPlugin<WorkCard>) { return kanban.moveCards(['task-1'], { columnProp: 'todo', swimlaneId: 'platform' });}export function selectTodoCards(kanban: KanbanPlugin<WorkCard>) { kanban.setSelectedCardIds(kanban.getCards().filter(card => card.status === 'todo').map(card => card.id));}export const workInProgress: Partial<KanbanConfig<WorkCard>> = { columns: [{ prop: 'todo', name: 'To do' }, { prop: 'doing', name: 'In progress', wipLimit: 1 }, { prop: 'done', name: 'Done' }], wipBehavior: 'block',};export const startBeforeDone: Partial<KanbanConfig<WorkCard>> = { rules: { canDrop: ({ cards, targetColumn }) => targetColumn.prop === 'done' && cards.some(card => card.status === 'todo') ? 'Start the task before marking it done.' : true },};export function frenchBoard(config: KanbanConfig<WorkCard>): KanbanConfig<WorkCard> { return { ...config, locale: 'fr-FR', columns: config.columns?.map(column => ({ ...column, name: ({ todo: 'À faire', doing: 'En cours', done: 'Terminé' } as Record<string, string>)[String(column.prop)] ?? column.name })), labels: { ...config.labels, emptyColumn: 'Aucune carte', dropCardHere: 'Déposer une carte ici', moveCancelled: 'Déplacement annulé', editCard: 'Modifier la carte', deleteCard: 'Supprimer la carte' } };}import { defineCustomElements } from '@revolist/revogrid/loader';import { KanbanPlugin, type KanbanConfig } from '@revolist/kanban';import '@revolist/revogrid-pro/styles.css';import '@revolist/kanban/styles.css';
export interface WorkCard { id: string; title: string; status: string; order: number; description?: string; team?: string; owner?: string; priority?: string; tags?: string[]; progress?: number; startDate?: string; endDate?: string;}export const demoCards: WorkCard[] = [ { id: 'task-1', title: 'Research customer needs', description: 'Collect feedback for the next release.', status: 'todo', team: 'product', owner: 'Ana', priority: 'normal', order: 1000 }, { id: 'task-2', title: 'Build onboarding', description: 'Make the first session useful.', status: 'doing', team: 'product', owner: 'Mia', priority: 'high', order: 1000 }, { id: 'task-3', title: 'Improve API errors', description: 'Return actionable validation messages.', status: 'todo', team: 'platform', owner: 'Rui', priority: 'normal', order: 2000 }, { id: 'task-4', title: 'Release checklist', description: 'Ready for the next deployment.', status: 'done', team: 'platform', owner: 'Sam', priority: 'normal', order: 1000 },];export const boardConfig: KanbanConfig<WorkCard> = { columns: [{ prop: 'todo', name: 'To do', size: 210 }, { prop: 'doing', name: 'In progress', size: 210 }, { prop: 'done', name: 'Done', size: 210 }], card: { titleField: 'title' }, cardRowHeight: 88, addCard: false, swimlaneColumn: false,};
export async function configureBoard( grid: HTMLRevoGridElement, config: KanbanConfig<WorkCard> = boardConfig, cards: readonly WorkCard[] = demoCards, extraPlugins: HTMLRevoGridElement['plugins'] = [],) { await defineCustomElements(); grid.plugins = [KanbanPlugin, ...extraPlugins]; // The packaged editor and history install automatically. grid.columns = [{ prop: 'title', name: 'Title' }, { prop: 'status', name: 'Status' }, { prop: 'owner', name: 'Owner' }]; // Cached remote data can hydrate immediately: finish initial grid setup first. grid.kanban = config.remote ? { ...config, remote: false } : config; grid.source = cards.map(card => ({ ...card })); await grid.componentOnReady(); const plugins = await grid.getPlugins(); const kanban = plugins.find((plugin): plugin is KanbanPlugin<WorkCard> => plugin instanceof KanbanPlugin); if (!kanban) throw new Error('Kanban could not start.'); if (config.remote) grid.kanban = config; return kanban;}
// Give your container a width and mount once after it exists in the DOM.export async function createKanbanBoard(container: HTMLElement) { await defineCustomElements(); const grid = document.createElement('revo-grid'); grid.style.cssText = 'display:block;width:100%;height:400px'; container.append(grid); try { const kanban = await configureBoard(grid); return { grid, kanban, destroy() { grid.remove(); } }; } catch (error) { grid.remove(); throw error; }}Try it: choose Complete research before starting it: the rule blocks the move. Choose Start research, then Complete research again. Local rules help users understand the workflow; your backend still enforces authorization when saving.
Movement permissions
Section titled “Movement permissions”Use allowedFrom for simple workflow transitions and rules for application policy. This extends the grid and config from Your First Board:
grid.kanban = { ...config, rules: { canDrop: ({ cards, targetColumn }) => targetColumn.prop === 'done' && cards.some(card => card.status === 'todo') ? 'Start the task before marking it done.' : true, },};canDrag and canDrop accept false or a localized reason string to reject an action. canSelect and canEdit return booleans. These hooks receive your application cards, so you can base decisions on fields such as a lock flag or permissions.
Visual card rules
Section titled “Visual card rules”cardRules is an ordered declarative layer for badges, classes, styles, and permissions. Matching classes and badges accumulate, later styles override earlier properties, and any explicit permission denial wins.
grid.kanban = { ...config, cardRules: [{ id: 'high-priority', when: ({ card }) => card.priority === 'high', result: { className: 'card--high-priority', badges: [{ label: 'High priority' }], style: { '--kanban-card-accent': '#dc2626' }, }, }],};Synchronous and server approval
Section titled “Synchronous and server approval”rules and beforekanbancardmove decide synchronously. For the optional complete-dataset managed adapter, approveMove can await approval. Approval is not a save, and it does not replace the final backend permission check.
Next steps
Section titled “Next steps”See WIP limits, card design and application saving. See the Kanban API for exact types and signatures.