Commands
Examples
Practical command patterns using atomic command values and local composition.
This page shows the recommended command architecture used in Tulip apps.
Atomic commands per feature
Export one command per action.
// app/admin/projects/_config/commands.tsx
export const projectCreateCommand = commandBuilder
.$type<null>()
.render(() => <CreateProjectCommand />);
export const projectUpdateStatusCommand = commandBuilder
.$type<{ id: string } | { id: string }[]>()
.transform(ensureArray)
.render(({ data }) => <UpdateProjectStatusCommand projects={data} />);
export const projectArchiveCommand = commandBuilder
.$type<{ id: string } | { id: string }[]>()
.transform(ensureArray)
.render(({ data }) => <ArchiveProjectCommand projects={data} />);
export const projectDeleteCommand = commandBuilder
.$type<{ id: string } | { id: string }[]>()
.transform(ensureArray)
.render(({ data }) => <DeleteProjectCommand projects={data} />);Local composition at usage site
Compose the command list where it is rendered.
<ResponsiveCommandMenu
data={project}
commands={[
projectUpdateStatusCommand,
projectArchiveCommand,
projectDeleteCommand,
]}
/>Create action in a toolbar:
<InlineCommandMenu data={null} commands={[projectCreateCommand]} />Single and bulk with one command
Use .transform(ensureArray) when one command should support row actions and selection actions.
type CustomerInput = { id: string } | { id: string }[];
export const customerDeleteCommand = commandBuilder
.$type<CustomerInput>()
.transform(ensureArray)
.render(({ data }) => (
<DeleteCommand variables={{ ids: data.map((item) => item.id) }} mutation={...} />
));The same command can then be used in both places:
<DropdownCommandMenu data={row} commands={[customerDeleteCommand]} />
<FloatingCommandMenu data={selectedRows} commands={[customerDeleteCommand]} state="open" />Meta-aware commands
Use createCommandBuilder<TMeta>() when commands need extra view context.
const timeBuilder = createCommandBuilder<{ fieldVisibility: { projectId?: boolean } }>();
export const timeEntryCreateCommand = timeBuilder
.$type<{ projectId: string }>()
.render(({ data, meta }) => (
<CreateTimeEntryCommand
defaultValues={data}
fieldVisibility={{ projectId: meta.fieldVisibility.projectId ?? true }}
/>
));
<InlineCommandMenu
data={{ projectId: "p_1" }}
meta={{ fieldVisibility: { projectId: false } }}
commands={[timeEntryCreateCommand]}
/>Full create command example
import { commandBuilder } from "@tulip-systems/commands";
import {
CommandFormDialog,
CommandFormDialogCancel,
CommandFormDialogContent,
CommandFormDialogFields,
CommandFormDialogFooter,
CommandFormDialogHeader,
CommandFormDialogSubmit,
CommandFormDialogTitle,
CommandFormDialogTrigger,
CommandLabel,
} from "@tulip-systems/commands/client";
import { Input } from "@tulip-systems/ui/input";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from "@tulip-systems/ui/react-hook-form";
import { PlusIcon } from "lucide-react";
import { useForm } from "react-hook-form";
import { orpc } from "@/server/router/client";
type CreateProjectInput = {
name: string;
customerId: string;
};
function CreateProjectCommand({ customerId }: { customerId: string }) {
const form = useForm<CreateProjectInput>({ defaultValues: { name: "", customerId } });
return (
<CommandFormDialog>
<CommandFormDialogTrigger label="Create project">
<PlusIcon className="w-4" />
<CommandLabel />
</CommandFormDialogTrigger>
<Form {...form}>
<CommandFormDialogContent
variables={(values) => values}
mutation={orpc.projects.create.mutationOptions()}
>
<CommandFormDialogHeader>
<CommandFormDialogTitle>Create project</CommandFormDialogTitle>
</CommandFormDialogHeader>
<CommandFormDialogFields>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
</CommandFormDialogFields>
<CommandFormDialogFooter>
<CommandFormDialogCancel>Cancel</CommandFormDialogCancel>
<CommandFormDialogSubmit>Create</CommandFormDialogSubmit>
</CommandFormDialogFooter>
</CommandFormDialogContent>
</Form>
</CommandFormDialog>
);
}
export const projectCreateCommand = commandBuilder
.$type<{ customerId: string }>()
.permission({ project: ["create"] })
.render(({ data }) => <CreateProjectCommand customerId={data.customerId} />);Render it where the action belongs:
<InlineCommandMenu
data={{ customerId: customer.id }}
commands={[projectCreateCommand]}
/>