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

7.9 KiB

status, system, created, owners
status system created owners
draft plugins 2026-09-23
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.