Action groups
Put related actions under one name. Components decide how to show the group.
Owner: logical groups in @bitakit/core; query bindings in @bitakit/ui. UI placement uses slots.
An action group is a name for a set of related actions. todos.bulk, for example, can hold every action that works on the whole todo list.
A group only groups. It does not render anything and does not decide where its actions appear. Components read a group and choose what to do with it: an explicitly bound menu can show its members, and search can use its title as a heading.
Shell navigation and organization member menus now use explicit slots. Joining a group alone does not place an Action in either region. The group bindings below remain available for explicit logical queries.
Put actions in a group
Name the group in groups:
// src/plugins/todos/actions/clear.ts
import { defineAction } from '@bitakit/core';
export default defineAction({
title: 'Clear all',
groups: ['todos.bulk'],
execute: ({ services }) => services.todos.list.clear(),
});// src/plugins/todos/actions/export.ts
import { defineAction } from '@bitakit/core';
export default defineAction({
title: 'Export todos',
groups: ['todos.bulk'],
execute: ({ services }) => JSON.stringify(services.todos.list.state.get().todos),
});Both actions are now in the group todos.bulk. You do not create a group first; naming it is enough. An action can be in several groups, and one group can hold actions from different plugins. Group names follow the action ID rules.
These examples use the todos.list service from the Actions guide.
Show a group
Pass the group's name to a component:
<ActionMenu group="todos.bulk" label="Bulk actions" />The menu lists every visible action of todos.bulk and updates when they change. Hidden and internal actions are left out.
The same group can look different in another place. Here it is a dropdown menu and a toolbar at the same time:
// src/plugins/todos/ui/bulk-actions.tsx
'use client';
import { ActionMenu, useActionGroup } from '@bitakit/ui';
import { Button } from '@bitakit/ui/components';
export function BulkActions() {
const bulk = useActionGroup('todos.bulk');
return (
<>
{/* The group as a dropdown menu… */}
<ActionMenu group="todos.bulk" label="Bulk actions" />
{/* …and the same group as a toolbar. */}
<div role="toolbar">
{bulk.items.map((item) => (
<Button key={item.id} {...item.props()}>
{item.title}
</Button>
))}
</div>
</>
);
}useActionGroup(group, input?) gives your component the group's current visible actions as controls, ready to spread on any element. You decide the markup. useActionGroups([...]) returns several groups at once.
How components use groups
| Component | Groups | What happens |
|---|---|---|
ActionMenu (@bitakit/ui) | the group you name | Shows a dropdown menu |
useActionGroup, useActionGroups (@bitakit/ui) | the groups you pass | Hands the controls to your own component |
ActionSidebarNavigation, ActionNavigationMenu (@bitakit/ui/components) | navigation unless you pass groups | Sidebar or top navigation, one section per group |
| Quick Search | the first group of each result | Section heading; group titles are searchable |
| App Preferences | puts each settings page in its category's group | Search shows settings under their category |
For Shell navigation, target the owner's slot explicitly (Shell must be installed and mounted):
// src/plugins/todos/actions/open.ts
import { defineAction } from '@bitakit/core';
export default defineAction({ title: 'Todos', href: '/todos', slots: ['shell.navigation'] });Give a group a title
Components that show headings, such as navigation and search, use the group's title. Without a title they show the group's name, and "General" for navigation.
Create the group with its title once and use it in your actions:
// src/plugins/todos/groups.ts
import { ActionGroup } from '@bitakit/core';
export const bulk = new ActionGroup('todos.bulk', 'Bulk actions');// src/plugins/todos/actions/clear.ts
import { defineAction } from '@bitakit/core';
import { bulk } from '../groups';
export default defineAction({
title: 'Clear all',
groups: [bulk],
execute: ({ services }) => services.todos.list.clear(),
});Other actions can keep the plain name 'todos.bulk'. They are in the same group, and the runtime keeps the first title given for an ID, whichever action is registered first. If two places give one group different titles, the first title wins, so set it in one place. The title is shown as written.
More options
Order and separators
order and dividerBefore are hints on the action for components that show a group. They are not part of the group itself:
// src/plugins/todos/actions/clear.ts
import { defineAction } from '@bitakit/core';
export default defineAction({
title: 'Clear all',
groups: ['todos.bulk'],
order: 100, // after items with smaller numbers
dividerBefore: true,
execute: ({ services }) => services.todos.list.clear(),
});ActionMenu, useActionGroup and the navigation components sort by ascending order (default 0), then by ID. dividerBefore asks for a separator before the item; the settings overview draws it, the standard ActionMenu does not yet. Both hints belong to the action, so they apply in every group it is in.
One group, a different input per place
useActionGroup passes its input to every action of the group. One set of row actions can then serve every row of a table:
function TodoRow({ todo }: { todo: Todo }) {
const row = useActionGroup('todos.row', todo.id);
return (
<li>
{todo.title}
{row.items.map((item) => (
<button key={item.id} {...item.props()}>
{item.title}
</button>
))}
</li>
);
}Each row passes its own todo ID, so the function-valued properties and execute see that row's value, and concurrency: 'per-input' shows pending per row. <ActionMenu group="todos.row" label="More" input={todo.id} /> does the same as a menu.
Add an action you do not own
A setup.ts file can put an existing action into a group without changing that action. Here the app collects a plugin action in its own group:
// src/setup.ts
import type { Setup } from '@bitakit/core';
const setup: Setup = ({ scope }) => {
scope.actions.getGroup('app.quick').attach('todos.add'); // undone when the scope stops
};
export default setup;Add a new action straight into a group
add registers a new action in a group from setup:
// src/plugins/help/setup.ts
import type { Setup } from '@bitakit/core';
const setup: Setup = ({ scope }) => {
scope.actions.getGroup('navigation').add({ id: 'help.open', title: 'Help', href: '/help' });
};
export default setup;add takes the low-level action format: id and title are required. The action is removed when the plugin stops.
Read a group in code
A group is plain data on each action, so code can read it without any component:
import type { ActionList } from '@bitakit/core';
/** IDs of the public actions in a group. */
export function actionsInGroup(actions: ActionList, group: string) {
return Object.values(actions.getState().actions)
.filter((action) => !action.internal && action.groups?.some((item) => item.id === group))
.map((action) => action.id);
}actionsInGroup(runtime.actions, 'todos.bulk') returns ['todos.clear', 'todos.export']. The action list comes from runtime.actions, from useActionList() in React, or from mountPlugin(...).actions in tests.