Command Builder
Define atomic command values with types, permissions, transforms, visibility, disabled state, and render.
Use commandBuilder (or createCommandBuilder<TMeta>()) to define a single reusable command value.
import { commandBuilder, createCommandBuilder, ensureArray } from "@tulip-systems/commands";Builder flow
Every command ends with .render(fn). The builder starts with an unknown data type. Use .$type<T>() to lock in the data shape, then optionally chain .permission(...), .transform(...), .visibleWhen(...), and .disabledWhen(...) before calling .render(...).
Supported chains:
.$type<T>().render(fn).$type<T>().permission(permission).render(fn).$type<T>().visibleWhen(fn).render(fn).$type<T>().disabledWhen(fn).render(fn).$type<T>().transform(fn).render(fn).$type<T>().permission(permission).visibleWhen(fn).disabledWhen(fn).render(fn)
.$type<T>()
Sets the data type the command works with. This is a compile-time annotation only.
type ProjectData = { id: string; name: string };
export const projectEditCommand = commandBuilder
.$type<ProjectData>()
.render(({ data }) => <div>{data.name}</div>);.transform(fn)
Transforms input data before it reaches visibleWhen, disabledWhen, and render.
Use this to normalize single-item and multi-item actions into one array shape:
type ProjectInput = { id: string } | { id: string }[];
export const projectDeleteCommand = commandBuilder
.$type<ProjectInput>()
.transform((data) => (Array.isArray(data) ? data : [data]))
.render(({ data }) => <div>{data.length}</div>);ensureArray
ensureArray is a small helper for the common T | T[] -> T[] case.
import { commandBuilder, ensureArray } from "@tulip-systems/commands";
type ProjectInput = { id: string } | { id: string }[];
export const projectArchiveCommand = commandBuilder
.$type<ProjectInput>()
.transform(ensureArray)
.render(({ data }) => <div>{data.length}</div>);.permission(permission)
Attaches authorization requirements to the command.
export const customerCreateCommand = commandBuilder
.$type<null>()
.permission({ customer: ["create"] })
.render(() => <div>Create</div>);Permission is also a render gate: if the current user does not satisfy the permission, the command does not render.
.visibleWhen(({ data, meta }) => ...)
Controls whether the command is rendered for the current context.
export const projectArchiveCommand = commandBuilder
.$type<{ id: string; deletedAt: string | null }[]>()
.visibleWhen(({ data }) => data.every((item) => item.deletedAt === null))
.render(() => <div>Archive</div>);Use visibility for runtime eligibility, such as archived state, readonly rows, or whether all selected records support the action.
.disabledWhen(({ data, meta }) => ...)
Controls whether a command is rendered but disabled.
export const projectArchiveCommand = commandBuilder
.$type<{ id: string; locked: boolean }[]>()
.visibleWhen(({ data }) => data.length > 0)
.disabledWhen(({ data }) => data.some((item) => item.locked))
.render(() => <div>Archive</div>);.render(({ data, meta, ui }) => ...)
Returns the UI for the command.
data: typed command datameta: optional contextual meta passed by the menuui: current surface ("inline","dropdown","context","table","custom")
export const projectCreateCommand = commandBuilder
.$type<null>()
.render(({ ui }) => <div>Rendered in: {ui}</div>);createCommandBuilder<TMeta>()
Use this when commands need extra context that is not in data.
import type { VisibilityState } from "@tanstack/react-table";
const builder = createCommandBuilder<{ fieldVisibility: VisibilityState }>();
export const timeEntryCreateCommand = builder
.$type<{ taskId: string }>()
.render(({ data, meta }) => (
<div>{meta.fieldVisibility.taskId ? data.taskId : "hidden"}</div>
));Pass meta at render site:
<InlineCommandMenu
data={{ taskId: "task_1" }}
meta={{ fieldVisibility: { taskId: false } }}
commands={[timeEntryCreateCommand]}
/>Recommended definition pattern
Export atomic command values from *_config/commands.tsx:
export const customerCreateCommand = commandBuilder
.$type<null>()
.permission({ customer: ["create"] })
.render(() => <CreateCustomerCommand />);
export const customerArchiveCommand = commandBuilder
.$type<{ id: string } | { id: string }[]>()
.transform(ensureArray)
.permission({ customer: ["archive"] })
.render(({ data }) => <ArchiveCommand ids={data.map((item) => item.id)} />);Compose arrays where commands are rendered:
commands={[customerArchiveCommand]}
commands={[
customerArchiveCommand,
customerRestoreCommand,
customerDeleteCommand,
]}