2.3.4 Filtered type + parent picker — inline type-parent validation surface
Estimate: 14m
The reusable picker that surfaces 2.1.2's assertValidParent rule
inline as constructibility, not as post-submit error. This is the durable
shape — illegal parent selections are not present in the option list, so the user can't
construct them. The post-submit error remains as a defense-in-depth backstop (the
Server Action still calls the service, which still throws on bypass), but in normal UI
flow it never fires.
Two components, sharing a Combobox primitive:
components/issues/TypePicker.tsx: simple Radix Select / Combobox over the 5 types from 2.1.1'sWORK_ITEM_TYPE_METADATA(epic/story/task/bug/subtask), with each option showing the type icon + label.components/issues/ParentPicker.tsx: async combobox takingchildType: WorkItemKindas a prop. On mount, fetches candidate parents via a new server actionlistCandidateParents(projectId, childType)that calls a newworkItemsService.listCandidateParents(projectId, childType, workspaceId)(single-op repo read filtering bykind IN allowedParentKinds(childType)— derived from 2.1.2'sparentRulesinverted — andarchivedAt IS NULL). Type-ahead filter is client-side over the returned list (up to ~500 items per project at v1 scale; pagination is deferred per Epic-6 search). Each row shows[PROD-N]+ title + kind icon. "No parent" is the first option.
The PARENT picker re-fetches whenever childType changes (the parent slot
for an Epic differs from the parent slot for a Subtask). Selecting a parent in the picker
can NEVER produce an illegal pair — the list is pre-filtered. If a child-type change
would invalidate the current parent selection, the parent field is cleared with a one-line
notice ("Parent cleared — <parent> can't hold a <new-child-type>"). This is
the user-facing materialization of 2.1.2's matrix.
Why a new service method. 1.4's workItemsService currently
exposes listChildren + getWorkItem but no "candidates" query.
A bespoke method keeps the picker's filter logic in the service layer (CLAUDE.md
single-source-of-truth rule) and lets Epic 7's planning layer reuse it.
Acceptance criteria
- Both components exported from
components/issues/; both consumed by 2.3.3 (modal) and 2.3.6 (edit form). - New
workItemsService.listCandidateParents+workItemRepository.findByProjectAndKinds; explicitworkspaceIdWHERE clause per finding #26. - Vitest: the service returns ONLY items whose kind is in
allowedParentKinds(childType)for every (childType, projectFixture) cell; excludes archived; cross-workspace returns []; reuses 2.1.2'sparentRulesmatrix without re-encoding it. - Component spec (RTL): selecting type=Story shows only Epic candidates in the parent list; switching type=Subtask re-fetches and shows Story/Task/Bug; switching type clears an invalidated parent with the documented inline notice.
- Illegal parent IDs from a forged client payload are still rejected by the service (defense-in-depth) — proved by a server-side test, not just a UI assertion.
- Picker is keyboard-navigable + screen-reader labeled; the STRICT shell-a11y sweep extended to a story page that renders both pickers — zero violations.
Context refs
lib/issues/issueTypes.ts(2.1.1) — the 5-type metadata + iconslib/issues/parentRules.ts(2.1.2) —canParent,allowedChildTypes; this Subtask uses the inverse: deriveallowedParentKinds(childType)by inverting the matrixlib/services/workItemsService.ts+lib/repositories/workItemRepository.ts(1.4) — the entity-naming pattern, the workspaceId-filter conventioncomponents/ui/Combobox.tsxor equivalent (1.0.5 design system) — the primitive to composemotir-core/CLAUDE.md— 4-layer, entity-naming, workspaceId-filter rules