useDocyrusKanban
One-call wiring of a Docyrus data source to a fully configured Kanban board with select/status/radio-group, user, and date columns, drag-to-update persistence, final-zone integration, and a standard Docyrus card layout.
Installation
pnpm dlx @docyrus/cli add @docyrus/hooks-use-docyrus-kanbanpnpm add @docyrus/app-utils @docyrus/api-client @tanstack/react-query @dnd-kit/core @dnd-kit/sortable react-querybuilderThis hook is distributed as source. It expects an authenticated RestApiClient from @docyrus/api-client and a QueryClientProvider from @tanstack/react-query somewhere above your component tree.
Overview
useDocyrusKanban is the kanban-first companion to useDocyrusDataGrid. It wires a Docyrus data source to a fully configured <Kanban> board with:
- saved-view filters, sorting, and search wiring (via
useDocyrusDataViewSelect) - automatic column derivation from the field type you point it at
- drag-to-update persistence (
PATCH /items/:id) - final-zone integration when the group-by field is a
field-status - a ready-to-render standard card layout (avatar, title, description, footer with user + audit info)
Supported group-by field types
The groupByFieldSlug you pass must point to one of these field types:
| Field type | Column derivation | Toolbar control |
|---|---|---|
field-select, field-radioGroup | One column per enum option (using the option's slug/id as the key, color/icon for column accent) | "Show all" switch (default true) |
field-status | Same as above, plus a <KanbanFinalZone> is rendered for any enum option whose is_final_option === true | "Show all" switch (default true) |
field-userSelect | One column per distinct user (or team) referenced by the rows | "Group by" picker — User / Team |
field-date | One column per bucketed date — time portion is always ignored | "Group by" picker — Day / Week / Month |
field-dateTime | Same as field-date — time portion is dropped before bucketing | "Group by" picker — Day / Week / Month |
For enum-backed fields, the hook automatically appends the field slug to the request expand array so each row's value carries the option's color and icon. The same is done for field-userSelect so the user payload (photo, name, team) is available for column headers and card avatars.
Card contract
Every card is rendered with the same standard layout — only the field bindings change:
┌─────────────────────────────────────────────┐
│ ◯ Title ⋮ │ ← avatarColumn + titleColumn + menu
│ Description │ ← descriptionColumn
│ │
│ {cardContent} │ ← free-form children
│ │
│ ◎ ℹ︎ │ ← userColumn + audit tooltip
└─────────────────────────────────────────────┘| Slot | Bound by | Notes |
|---|---|---|
avatarColumn | A field whose value resolves to { icon, color, image } (or a string icon/color) | Rendered with <AvatarThumbnail> so icon, color, and image inputs all work consistently |
titleColumn | Field slug | Single-line, truncated, bold |
descriptionColumn | Field slug | Two-line clamp, muted |
userColumn | Field slug pointing to a field-userSelect payload (or any object with firstname/lastname/photo) | Rendered as a circular avatar in the footer-left with a name tooltip |
cardContent | Render prop receiving { row, column } | Free-form region between header and footer — render whatever extra fields the app needs |
| Footer info icon | created_on, last_modified_on, created_by, last_modified_by | Always shown when at least one is present; rendered as an <Info> icon with a tooltip |
| Card menu (top-right) | cardMenuItems (or default Open / Edit / Delete) | Default actions wire onCardOpen, onCardEdit, and a built-in delete confirmation dialog |
Override the menu either by listing items directly or by passing a function that receives the defaults — the function form lets you keep Open/Edit/Delete and append additional entries.
Usage
Status-driven board (enum + final zone)
'use client';
import { useDocyrusAuth } from '@docyrus/signin';
import { useDocyrusKanban } from '@docyrus/ui/library/hooks/use-docyrus-kanban';
type LeadRow = {
id: string;
name: string;
description?: string;
status: { id: string; name: string };
assigned_to?: { id: string; firstname: string; lastname: string; photo?: string };
icon?: string;
color?: string;
};
export function LeadsKanban() {
const { client } = useDocyrusAuth();
if (!client) return null;
const { toolbar, board } = useDocyrusKanban<LeadRow>({
client,
appSlug: 'crm',
dataSourceSlug: 'lead',
groupByFieldSlug: 'status',
avatarColumn: 'icon', // any field whose value is an icon/color/image bag
titleColumn: 'name',
descriptionColumn: 'description',
userColumn: 'assigned_to',
onCardOpen: row => router.push(`/crm/leads/${row.id}`),
onCardEdit: row => openEditDialog(row.id)
});
return (
<div className="flex h-full flex-col">
{toolbar}
<div className="flex-1 overflow-hidden">{board}</div>
</div>
);
}Date board with the Group By picker
const { toolbar, board } = useDocyrusKanban<TaskRow>({
client,
appSlug: 'project',
dataSourceSlug: 'task',
groupByFieldSlug: 'due_date', // field-date or field-dateTime
dateGroupBy: 'week', // Day | Week | Month — switchable via toolbar
titleColumn: 'subject',
descriptionColumn: 'summary',
userColumn: 'assigned_to'
});When the group-by field is a date or datetime field the toolbar automatically renders a "Group by" Select with Day, Week, and Month. The time portion is always ignored — buckets are computed from the UTC date components only.
User board with User/Team toggle
const { toolbar, board } = useDocyrusKanban<TaskRow>({
client,
appSlug: 'project',
dataSourceSlug: 'task',
groupByFieldSlug: 'assigned_to', // field-userSelect
userGroupBy: 'team', // User | Team — switchable via toolbar
avatarColumn: 'icon',
titleColumn: 'subject',
descriptionColumn: 'description',
userColumn: 'assigned_to'
});userGroupBy: 'team' reads team_id (or a nested team.id) off the user payload returned by expand=assigned_to. Cards remain draggable in 'user' mode and become read-only in 'team' mode (since teams aren't a writable field on the user reference).
Show all columns (enum) toggle
For enum-backed fields the toolbar renders a Show all switch (default true) that mirrors the showAllColumns option:
true(default): every enum option is rendered as a column even when no record currently maps to it. Useful when you want users to be able to drop a card into a fresh stage.false: only columns with at least one record are rendered.
const { toolbar, board, showAllColumns, setShowAllColumns } = useDocyrusKanban({
/* … */
showAllColumns: false // initial value — the toolbar switch still controls it
});Final zone (status fields)
When the group-by field is field-status, every enum option flagged is_final_option is rendered inside a <KanbanFinalZone> below the main board instead of as a board column:
const { board } = useDocyrusKanban({
/* … */
groupByFieldSlug: 'status'
});Dragging a card into a final-zone column issues the same PATCH /items/:id as dragging between board columns — the only difference is that final columns aren't rendered side-by-side with the active stages.
API Reference
Parameters
The hook accepts every option from useDocyrusDataViewSelect (for view CRUD + filter fields), plus:
DB-free metadata. Because this hook forwards into useDocyrusDataViewSelect, it also accepts dataSource (inject a pre-resolved schema → skips the getBySlug fetch), enableDataViews: false (skip the /views fetch), and dataSourceExpand (tune/drop the schema expand param). Pass dataSource together with data (pre-resolved rows) to render the board with no metadata requests. See DB-free metadata.
| Option | Type | Default | Description |
|---|---|---|---|
groupByFieldSlug | string | — | Required. Slug of the field whose values become kanban columns. |
data | Array<TData> | — | Pre-resolved rows. When provided, the hook skips its internal items query. |
collection | DocyrusKanbanCollection<TData> | — | TanStack DB collection. Optional update and remove methods are wired to drag-move and the default delete action. |
listParams | DocyrusKanbanListParams | — | Extra query params merged on top of the view-derived payload. |
defaultLimit | number | 200 | Default page size when no limit is supplied via listParams. |
enableItemsQuery | boolean | true when no data | Toggle the internal items query. |
dateGroupBy | 'day' | 'week' | 'month' | 'day' | Initial date bucket granularity. The toolbar switch overrides this at runtime. |
userGroupBy | 'user' | 'team' | 'user' | Initial user grouping mode for field-userSelect. |
showAllColumns | boolean | true | Initial state of the "Show all" switch (enum group-by only). |
avatarColumn | string | — | Field slug bound to the card avatar (icon/color/image). |
titleColumn | string | — | Field slug bound to the card title. |
descriptionColumn | string | — | Field slug bound to the card description. |
userColumn | string | — | Field slug bound to the user shown in the card footer. |
cardContent | (ctx) => ReactNode | — | Free-form card body. Receives { row, column }. |
cardMenuItems | Array<DocyrusKanbanCardMenuItem<TData>> | (row, defaults) => Array<...> | — | Override the action menu. The function form keeps the default Open/Edit/Delete entries available. |
cardActions | Array<'open' | 'edit' | 'delete'> | ['open', 'edit', 'delete'] | Whitelist of the default actions. |
onCardOpen | (row) => void | — | Wired to the default Open menu item. |
onCardEdit | (row) => void | — | Wired to the default Edit menu item. |
onCardDelete | (row) => Promise<void> | void | — | Custom delete handler. When omitted the hook calls collection.remove(id) or DELETE /items/:id after a confirmation dialog. |
onCardClick | (row) => void | — | Click handler fired when the card body is clicked. |
enableViewSelect | boolean | true | Show the saved-view picker in the toolbar. |
enableSearchInput | boolean | true | Show the search input. |
enableDateGroupMenu | boolean | true | Show the Day/Week/Month picker when grouping by date. |
enableUserGroupMenu | boolean | true | Show the User/Team picker when grouping by user. |
enableShowAllColumnsSwitch | boolean | true | Show the "Show all" switch when grouping by enum. |
enableReloadButton | boolean | true | Show the reload button. |
onReload | () => void | — | Called when the reload button is clicked, after the internal refetch. |
searchPlaceholder | string | 'Search…' | Placeholder for the toolbar search input. |
searchDebounceMs | number | 300 | Debounce in ms before the search input is sent as filterKeyword. |
toolbarClassName | string | — | Extra className for the toolbar root. |
toolbarStartContent | ReactNode | — | Extra node prepended to the left side of the toolbar. |
toolbarEndContent | ReactNode | — | Extra node appended to the right side of the toolbar. |
onItemMove | (params) => void | — | Fired when a card is dropped into a different column. Runs alongside the built-in mutation. |
onItemMoveCommit | (params) => Promise<void> | void | — | Replace the built-in PATCH with a custom handler. Throw to abort the move. |
Return Value
| Property | Type | Description |
|---|---|---|
toolbar | ReactNode | Pre-wired toolbar element ready to render above the board. |
board | ReactNode | Pre-wired kanban board element. Render directly. |
items | Array<TData> | Resolved rows passed to the board. |
resolvedListParams | DocyrusKanbanListParams | The list params actually sent to the backend. |
groupByField | DataSourceField | undefined | The field metadata used to derive columns. |
columns | Array<DocyrusKanbanColumnMeta> | Ordered metadata for every rendered column (including counts). |
columnsItems | Record<string, Array<TData>> | Items grouped by column.id. |
dateGroupBy | 'day' | 'week' | 'month' | Active date grouping. |
setDateGroupBy | (value) => void | Programmatically switch date grouping. |
userGroupBy | 'user' | 'team' | Active user grouping. |
setUserGroupBy | (value) => void | Programmatically switch user grouping. |
showAllColumns | boolean | Active state of the "Show all" switch. |
setShowAllColumns | (value) => void | Programmatically toggle the switch. |
views | Array<SavedDataGridView> | Saved views mapped from the backend shape. |
fields | Array<FullField> | Fields mapped for react-querybuilder. |
dataSource | DataSource | undefined | Raw data source metadata response. |
activeViewId | string | Id of the currently active view. |
setActiveViewId | (viewId) => void | Programmatically switch views. |
reload | () => void | Triggers refetch of the data source, views, and items queries plus the optional onReload callback. |
isLoading | boolean | true until all queries have resolved. |
error | Error | null | First error from any of the queries. |
refetch | () => void | Alias for reload. |
Drag-and-drop persistence
When a card is dropped into a different column the hook computes a payload based on the field type and either:
- calls
collection.update(id, payload)if the consumer passed a TanStack DB collection with anupdatemethod, or - issues a
PATCH /v1/apps/:appSlug/data-sources/:dataSourceSlug/items/:idwith theRestApiClientfrom@docyrus/signin.
| Group-by field | Payload sent on drop |
|---|---|
field-select, field-radioGroup, field-status | { [fieldSlug]: <enumOptionId> } |
field-userSelect (userGroupBy: 'user') | { [fieldSlug]: <userId> } |
field-userSelect (userGroupBy: 'team') | (no-op — teams aren't writable on the user reference) |
field-date, field-dateTime | (no-op — bucketing is derived, not stored) |
Pass onItemMoveCommit to skip the built-in PATCH entirely (e.g. for optimistic updates with rollback) and onItemMove to mirror the change in local state without replacing the mutation.
Field type → column derivation matrix
Docyrus type | Column id | Column label | Color/icon source | Final-zone integration |
|---|---|---|---|---|
field-select | enum option id (or slug) | enum option name | enum option color / icon | — |
field-radioGroup | enum option id (or slug) | enum option name | enum option color / icon | — |
field-status | enum option id (or slug) | enum option name | enum option color / icon | options where is_final_option === true |
field-userSelect (user) | user id | full name (or email) | user photo | — |
field-userSelect (team) | team id | team name | — | — |
field-date, field-dateTime | bucket key (YYYY-MM-DD / YYYY-Www / YYYY-MM) | localized label | — | — |
Out of scope
field-relation,field-multiSelect, andfield-tagSelectare intentionally not surfaced as group-by candidates — relation fields would require additional metadata to render meaningful column headers and multi-value fields don't map cleanly onto a single column. If you need to group by one of these, render your own board with the sameuseDocyrusDataViewSelectwiring.