Tulip Logo IconTulip
Commands

Command Filtering

Tag-based and key-based command filtering API.

Commands can be filtered by their registry keys or by the tags assigned during definition. Both filter namespaces are available on every command registry.

Goals

  • Keep commands centrally defined.
  • Select commands cheaply before rendering menus.
  • Support more contexts than only single and bulk.
  • Keep exact command selection available when needed.
  • Make tag filters and key filters clearly separate.

Command definitions

Commands are still defined in a registry. The object key is the command identity. Tags describe where or how a command can be used.

import { commandBuilder, defineCommands } from "@tulip-systems/core/commands";

export const projectCommands = defineCommands({
  edit: commandBuilder
    .tags("single", "table", "safe")
    .render(({ data }) => <EditCommand data={data} />),

  duplicate: commandBuilder
    .tags("single", "table", "safe")
    .render(({ data }) => <DuplicateCommand data={data} />),

  archive: commandBuilder
    .tags("single", "bulk", "danger")
    .render(({ data }) => <ArchiveCommand data={data} />),

  delete: commandBuilder
    .tags("single", "bulk", "danger")
    .render(({ data }) => <DeleteCommand data={data} />),

  export: commandBuilder
    .tags("single", "bulk", "safe")
    .render(({ data }) => <ExportCommand data={data} />),
});

The keys are edit, duplicate, archive, delete, and export. The tags are values such as single, bulk, table, safe, and danger.

Two filter namespaces

Command filtering has two separate namespaces:

projectCommands.keys.pick(...)
projectCommands.keys.omit(...)

projectCommands.tags.every(...)
projectCommands.tags.some(...)
projectCommands.tags.exact(...)
projectCommands.tags.without(...)

Use keys when you want exact command names from defineCommands. Use tags when you want context-based filtering.

Key filters chain only with key filters. Tag filters chain only with tag filters. This keeps the behavior clear and avoids mixing two different selection models.

keys.pick(...)

keys.pick(...) selects commands by their exact registry keys.

projectCommands.keys.pick({ edit: true, duplicate: true })

Result:

["edit", "duplicate"]

Use this when a menu needs a curated list of exact commands.

<InlineCommandMenu
  data={project}
  commands={projectCommands.keys.pick({ edit: true, duplicate: true }).toArray()}
/>

keys.omit(...)

keys.omit(...) removes commands by their exact registry keys.

projectCommands.keys.omit({ delete: true })

Result:

["edit", "duplicate", "archive", "export"]

Use this when a menu should receive almost everything except specific commands.

<DropdownCommandMenu
  data={project}
  commands={projectCommands.keys.omit({ delete: true }).toArray()}
/>

tags.every(...)

tags.every(...) selects commands that contain every requested tag. Extra tags are allowed.

projectCommands.tags.every("single", "table")

Result:

["edit", "duplicate"]

Why:

edit.tags = ["single", "table", "safe"] // matches
archive.tags = ["single", "bulk", "danger"] // does not match: missing "table"

Use every when the command must fit every requested context.

<InlineCommandMenu
  data={row}
  commands={projectCommands.tags.every("single", "table").toArray()}
/>

tags.some(...)

tags.some(...) selects commands that contain at least one requested tag.

projectCommands.tags.some("bulk", "table")

Result:

["edit", "duplicate", "archive", "delete", "export"]

Why:

edit.tags = ["single", "table", "safe"] // matches "table"
archive.tags = ["single", "bulk", "danger"] // matches "bulk"

Use some when multiple contexts are acceptable.

<ResponsiveCommandMenu
  data={data}
  commands={projectCommands.tags.some("single", "bulk").toArray()}
/>

tags.exact(...)

tags.exact(...) selects commands whose tag set is exactly equal to the requested tags. Extra tags are not allowed.

projectCommands.tags.exact("single", "table", "safe")

Result:

["edit", "duplicate"]

This does not match a command with extra tags:

["single", "table", "safe"] // matches
["single", "table", "safe", "special"] // does not match

Use exact when you want to guarantee that only commands with precisely that tag set are included. Most menus should prefer all because it composes better with additional tags.

tags.without(...)

tags.without(...) removes commands that contain any requested tag.

projectCommands.tags.without("danger")

Result:

["edit", "duplicate", "export"]

Use this to exclude a group such as destructive actions.

<InlineCommandMenu
  data={row}
  commands={projectCommands.tags.every("single").without("danger").toArray()}
/>

Result:

["edit", "duplicate", "export"]

Chaining

Key filters can chain with key filters:

projectCommands.keys
  .pick({ edit: true, duplicate: true, delete: true })
  .omit({ delete: true })
  .toArray();

Result:

["edit", "duplicate"]

Tag filters can chain with tag filters:

projectCommands.tags
  .all("single")
  .without("danger")
  .toArray();

Result:

["edit", "duplicate", "export"]

Do not mix key and tag filtering in one chain. If you need exact command selection, use keys. If you need contextual selection, use tags.

Common patterns

Single row actions:

projectCommands.tags.every("single")

Bulk selection actions:

projectCommands.tags.every("bulk")

Single table row actions:

projectCommands.tags.every("single", "table")

Single or bulk actions:

projectCommands.tags.some("single", "bulk")

Safe single actions:

projectCommands.tags.every("single").without("danger")

Exact curated menu:

projectCommands.keys.pick({ edit: true, duplicate: true })

On this page