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's WORK_ITEM_TYPE_METADATA (epic/story/task/bug/subtask),
with each option showing the type icon + label.components/issues/ParentPicker.tsx: async combobox
taking childType: WorkItemKind as a prop. On mount, fetches candidate
parents via a new server action listCandidateParents(projectId, childType)
that calls a new workItemsService.listCandidateParents(projectId, childType, workspaceId) (single-op repo read filtering by kind IN allowedParentKinds(childType) — derived from 2.1.2's parentRules
inverted — and archivedAt 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 — can't hold a "). 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.
components/issues/; both consumed by 2.3.3 (modal) and 2.3.6 (edit form).workItemsService.listCandidateParents + workItemRepository.findByProjectAndKinds; explicit workspaceId WHERE clause per finding #26.allowedParentKinds(childType) for every (childType, projectFixture) cell; excludes archived; cross-workspace returns []; reuses 2.1.2's parentRules matrix without re-encoding it.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: derive allowedParentKinds(childType) by inverting the matrixlib/services/workItemsService.ts + lib/repositories/workItemRepository.ts (1.4) — the entity-naming pattern, the workspaceId-filter conventioncomponents/ui/Combobox.tsx or equivalent (1.0.5 design system) — the primitive to composemotir-core/CLAUDE.md — 4-layer, entity-naming, workspaceId-filter rules