Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
7.9 KiB
status, system, created, owners
| status | system | created | owners | |
|---|---|---|---|---|
| draft | plugins | 2026-09-23 |
|
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.itemsSHALL be optional and synchronous, returningreadonly TaskMenuSubItemRegistration[];runSHALL remain required. A host that predatesitemsSHALL ignore the unknown field and render the flat item it has always rendered, reading onlylabel,iconandrun. - AC-PLUGINS-TASK-MENU-SUBMENUS-001.2: When
items()returns at least one usable child, the action SHALL render as a submenu:labelbecomes an unselectable trigger, each child becomes a selectable entry in the returned order, each child'srunis invoked with the samePluginTaskMenuContextas the action, and a child is unselectable when either the child's owndisabledor the host's entry-leveldisabledis 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
runitem rather than producing a trigger nothing can open. Nothing usable means:itemsabsent,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 thrownitems()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:
idorlabelabsent, not a string, or blank after trimming;runnot callable;disabledpresent and neither boolean nornull;iconpresent and neither a curated name, a component (including React'smemoandforwardRef, which are objects at run time), a ready-made element, nornull. 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
idSHALL keep its first occurrence and report the repeat, so the host never builds two children with one key.icon: nullanddisabled: nullSHALL 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
labeloridis 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
Symbolid, 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
editregistration the host cannot render SHALL leave the card's nativeEdititem 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, actionidand childid, without throwing for any string a bundle can produce, including a lone surrogate (the shapesliceand[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
createElementas 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 itscontextso a bare child label stays identifiable; the sidebar SHALL keep the task title as the defaultcontextfor commands that do not set their own. - AC-PLUGINS-TASK-MENU-SUBMENUS-003.2: Group
editSHALL 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
Editform SHALL NOT receive a prebuilt set that could contain the nestedEditsubmenu.
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
editgains 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.