Files
kandev/docs/specs/plugins/requirements/plugin-task-menu-submenus.md
T
2026-09-23 11:00:23 +01:00

90 lines
7.9 KiB
Markdown

---
status: draft
system: plugins
created: 2026-09-23
owners:
- kandev
---
# Plugin task menu submenus Requirements
## Overview
A plugin task-menu action has always been exactly one selectable item. A plugin
whose natural contribution is a short list of frequent choices therefore has
nowhere to put them: the Tags plugin opens a modal picker so the user can add one
of the workspace's recent tags to a card, one menu open plus one modal plus one
click. Registering five sibling actions instead would flood every card's menu
with rows that are mostly noise.
`registerTaskMenuAction` gains an optional `items(context)` beside its required
`run`. Declaring it renders the action as a one-level submenu whose children the
plugin supplies at menu-build time; the children reach the card menu, the shared
desktop/mobile task-row menu, the command palette and the sidebar's task
commands. `run` stays required and stays the flat behavior, which is the whole
compatibility story: a host that predates the field ignores it, and a build whose
`items()` yields nothing usable falls back to the same flat item, so an action
can ship both a quick child list and its existing behavior.
Plugin bundles are plain JavaScript, so the registration types are a promise
rather than a guarantee. Everything here is shaped by that: the host reads plugin
values defensively at one boundary, degrades to the flat item or drops a single
child instead of failing a render, and reports what it dropped.
## Requirements
### REQ-PLUGINS-TASK-MENU-SUBMENUS-001: A task menu action can declare a one-level submenu
**Intent:** A plugin with a short list of frequent, per-card choices needs to
offer them in one click each without adding sibling rows to every card's menu.
The host renders the action as a submenu when, and only when, the plugin supplies
children it can actually render.
#### Acceptance criteria
- **AC-PLUGINS-TASK-MENU-SUBMENUS-001.1:** `TaskMenuActionRegistration.items` SHALL be optional and synchronous, returning `readonly TaskMenuSubItemRegistration[]`; `run` SHALL remain required. A host that predates `items` SHALL ignore the unknown field and render the flat item it has always rendered, reading only `label`, `icon` and `run`.
- **AC-PLUGINS-TASK-MENU-SUBMENUS-001.2:** When `items()` returns at least one usable child, the action SHALL render as a submenu: `label` becomes an unselectable trigger, each child becomes a selectable entry in the returned order, each child's `run` is invoked with the same `PluginTaskMenuContext` as the action, and a child is unselectable when either the child's own `disabled` or the host's entry-level `disabled` is set. Nesting SHALL stop at one level: a child is never itself a submenu.
- **AC-PLUGINS-TASK-MENU-SUBMENUS-001.3:** A result with nothing usable SHALL fall back to the action's flat `run` item rather than producing a trigger nothing can open. Nothing usable means: `items` absent, `null`/`undefined`, a non-array, an empty array, a thrown exception, a thenable (the contract is synchronous), or an array whose every element is unusable. A thrown `items()` SHALL be caught and reported.
- **AC-PLUGINS-TASK-MENU-SUBMENUS-001.4:** A child the host cannot render SHALL be dropped, and the drop SHALL be reported once per action and defect kind rather than once per menu build. Unusable means: `id` or `label` absent, not a string, or blank after trimming; `run` not callable; `disabled` present and neither boolean nor `null`; `icon` present and neither a curated name, a component (including React's `memo` and `forwardRef`, which are objects at run time), a ready-made element, nor `null`. A child may be read only inside a guard, so a throwing getter or a Proxy fails that child instead of the card's render.
- **AC-PLUGINS-TASK-MENU-SUBMENUS-001.5:** A repeated child `id` SHALL keep its first occurrence and report the repeat, so the host never builds two children with one key. `icon: null` and `disabled: null` SHALL both mean "absent", matching how a JavaScript bundle spells an omitted field.
- **AC-PLUGINS-TASK-MENU-SUBMENUS-001.6:** `items()` SHALL be evaluated while the host builds that card's or row's menu entries, which happens on every render and for both the dropdown and the context variant whether or not a menu is open. The API documentation SHALL say so and SHALL tell authors to read cached state and memoize anything expensive.
### REQ-PLUGINS-TASK-MENU-SUBMENUS-002: An unrenderable registration never fails a host render
**Intent:** One plugin's malformed registration must not take down a card, a
board, or a command surface. The host's own contract says unusable input degrades;
these criteria make that true for the values a real bundle can produce, including
values whose own accessors throw.
#### Acceptance criteria
- **AC-PLUGINS-TASK-MENU-SUBMENUS-002.1:** A registration whose own `label` or `id` is not a usable string SHALL contribute no entry at all, and SHALL be reported once, instead of rendering an unselectable or unkeyable entry.
- **AC-PLUGINS-TASK-MENU-SUBMENUS-002.2:** The host's defect report SHALL NOT throw while reporting: a `Symbol` id, an object without a string form, or a throwing accessor on the registration SHALL still be reported or skipped without an exception escaping the builder.
- **AC-PLUGINS-TASK-MENU-SUBMENUS-002.3:** A group `edit` registration the host cannot render SHALL leave the card's native `Edit` item flat, exactly as if no plugin action were registered, rather than wrapping it in a submenu with no plugin child under it.
- **AC-PLUGINS-TASK-MENU-SUBMENUS-002.4:** An entry key SHALL be unique for any plugin-authored `pluginId`, action `id` and child `id`, without throwing for any string a bundle can produce, including a lone surrogate (the shape `slice` and `[0]` produce from an astral character). Because the key is also a palette row's React key and cmdk value, two entries SHALL never share one.
- **AC-PLUGINS-TASK-MENU-SUBMENUS-002.5:** A plugin-provided value that is not a usable icon SHALL resolve to the surface's fallback glyph rather than reaching `createElement` as an element type. Context menus SHALL additionally render a ready-made element unchanged, which is the shape plugins registered before icon resolution existed still send; that tolerance SHALL be documented as menu-only.
### REQ-PLUGINS-TASK-MENU-SUBMENUS-003: Submenu children are reachable from the command surfaces
**Intent:** A card's plugin contribution should be usable from the same
task-action surfaces that already list card actions, rather than only inside the
card's own menu.
#### Acceptance criteria
- **AC-PLUGINS-TASK-MENU-SUBMENUS-003.1:** A `primary`-group submenu SHALL contribute one command per child to the command palette and the sidebar's task commands, each carrying the trigger's label as its `context` so a bare child label stays identifiable; the sidebar SHALL keep the task title as the default `context` for commands that do not set their own.
- **AC-PLUGINS-TASK-MENU-SUBMENUS-003.2:** Group `edit` SHALL remain card-only: its submenu children SHALL NOT appear in the command palette or the sidebar's task commands.
- **AC-PLUGINS-TASK-MENU-SUBMENUS-003.3:** A surface that reuses already-built plugin entries SHALL produce them from the same inputs it renders with, and a caller that forces the flat `Edit` form SHALL NOT receive a prebuilt set that could contain the nested `Edit` submenu.
## Exclusions
- No nested plugin submenus: a child is a selectable item, never another submenu.
- No asynchronous `items()`: a menu entry cannot await, so a thenable is treated as a contract violation and falls back.
- The palette lists submenu children as flat commands; it does not render a nested palette submenu.
- Group `edit` gains no palette or sidebar placement.
- The host does not validate or repair a plugin's ids, labels or icons at registration time; malformed input is handled where it is used.
## System design
The technical source for these requirements is
[plugin-task-menu-submenus](../system-design/plugin-task-menu-submenus.md).