BitaKit
How-to guides

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

ComponentGroupsWhat happens
ActionMenu (@bitakit/ui)the group you nameShows a dropdown menu
useActionGroup, useActionGroups (@bitakit/ui)the groups you passHands the controls to your own component
ActionSidebarNavigation, ActionNavigationMenu (@bitakit/ui/components)navigation unless you pass groupsSidebar or top navigation, one section per group
Quick Searchthe first group of each resultSection heading; group titles are searchable
App Preferencesputs each settings page in its category's groupSearch 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.

On this page