f8708da1e0
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
90 lines
7.9 KiB
Markdown
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).
|