MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-89

2.3.4 Filtered type + parent picker — inline type-parent validation surface

Done
Description

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.

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; explicit workspaceId WHERE 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's parentRules matrix 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 + icons
  • lib/issues/parentRules.ts (2.1.2) — canParent, allowedChildTypes; this Subtask uses the inverse: derive allowedParentKinds(childType) by inverting the matrix
  • lib/services/workItemsService.ts + lib/repositories/workItemRepository.ts (1.4) — the entity-naming pattern, the workspaceId-filter convention
  • components/ui/Combobox.tsx or equivalent (1.0.5 design system) — the primitive to compose
  • motir-core/CLAUDE.md — 4-layer, entity-naming, workspaceId-filter rules