feat(categories): assign categories to entries & motifs + display (#6, slice 2) #51

Merged
rbrooks merged 2 commits from feat/v6.2-categories-assign into main 2026-06-26 03:22:20 +00:00
Owner

Slice 2 of the categories feature (#6). Builds on slice 1's data model + CRUD API (#50).

Done when: you can assign categories to entries and motifs from their forms, and see them as colored badges across the app.

API

  • New lib/categoryAssign.ts:
    • applyCategories(target, userId, ownerId, categoryIds) — project-contained replace-set into entry_categories / motif_categories. Only category ids belonging to the owner's own project are applied (prevents cross-project assignment / IDOR); unknown and malformed ids are silently dropped rather than failing the write. Diffs are written to the audit log by category name. No-ops when category_ids is absent (so an omitted field leaves assignments untouched).
    • fetchCategoriesFor for accurate write responses, and categoriesJsonSql for embedding categories in read payloads.
    • Table/column identifiers come from a compile-time allowlist, never user input (per the dynamic-SQL invariant).
  • Entry & motif create/update accept category_ids: string[].
  • Read payloads include categories (id/name/color): entry list + detail, motif list + detail, and entries-within-a-motif. Embedded via a correlated subquery, not a JOIN, so it never cartesian-multiplies the existing tag aggregation.

UI

  • CategoryBadge — colored chip with automatic text-contrast; neutral fallback when a category has no color.
  • CategorySelect — multi-select that picks existing categories or creates one on the fly (POST /api/categories), mirroring the tag input. Selection is by id; the component owns the id→name/color lookup.
  • Wired into EntryForm and the motif edit form; badges shown on EntryCard, EntryDetailPage, MotifListPage, MotifDetailPage (including entries-within).
  • categories added to the Entry / MotifSummary / MotifEntry types; test fixtures and the public-project graph adapter updated to match.

Tests

11 new integration tests in categoryAssign.test.ts: assignment on create/update, replace-set semantics, empty-array clear, omitted-field no-op, list/detail read payloads, usage counts, project containment, and malformed-id tolerance.

Not in this slice

Filtering lists by category and the Settings management UI land in slice 3. CHANGELOG entry is deferred to the end of slice 3, rolling into the v6.2.0 release notes (per the #6 plan).

🤖 Generated with Claude Code

Slice 2 of the categories feature (#6). Builds on slice 1's data model + CRUD API (#50). **Done when:** you can assign categories to entries and motifs from their forms, and see them as colored badges across the app. ## API - New `lib/categoryAssign.ts`: - `applyCategories(target, userId, ownerId, categoryIds)` — project-contained replace-set into `entry_categories` / `motif_categories`. Only category ids belonging to the owner's **own project** are applied (prevents cross-project assignment / IDOR); unknown and malformed ids are silently dropped rather than failing the write. Diffs are written to the audit log by category name. No-ops when `category_ids` is absent (so an omitted field leaves assignments untouched). - `fetchCategoriesFor` for accurate write responses, and `categoriesJsonSql` for embedding categories in read payloads. - Table/column identifiers come from a compile-time allowlist, never user input (per the dynamic-SQL invariant). - Entry & motif **create/update** accept `category_ids: string[]`. - Read payloads include `categories` (id/name/color): entry list + detail, motif list + detail, and entries-within-a-motif. Embedded via a **correlated subquery, not a JOIN**, so it never cartesian-multiplies the existing tag aggregation. ## UI - `CategoryBadge` — colored chip with automatic text-contrast; neutral fallback when a category has no color. - `CategorySelect` — multi-select that picks existing categories or **creates one on the fly** (`POST /api/categories`), mirroring the tag input. Selection is by id; the component owns the id→name/color lookup. - Wired into `EntryForm` and the motif edit form; badges shown on `EntryCard`, `EntryDetailPage`, `MotifListPage`, `MotifDetailPage` (including entries-within). - `categories` added to the `Entry` / `MotifSummary` / `MotifEntry` types; test fixtures and the public-project graph adapter updated to match. ## Tests 11 new integration tests in `categoryAssign.test.ts`: assignment on create/update, replace-set semantics, empty-array clear, omitted-field no-op, list/detail read payloads, usage counts, **project containment**, and malformed-id tolerance. ## Not in this slice Filtering lists by category and the Settings management UI land in **slice 3**. CHANGELOG entry is deferred to the end of slice 3, rolling into the v6.2.0 release notes (per the #6 plan). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(categories): assign categories to entries & motifs + display (#6, slice 2)
Some checks failed
CI / App — typecheck, test & build (push) Failing after 1m19s
CI / API — typecheck & tests (push) Successful in 2m25s
CI / App — typecheck, test & build (pull_request) Failing after 1m12s
CI / API — typecheck & tests (pull_request) Successful in 2m17s
d15e4d93d4
Slice 2 of the categories feature. Entry & motif create/update now accept
`category_ids: string[]` and replace-set them into the junction tables; read
payloads (list + detail, plus entries-within-motif) include each item's
`categories` (id/name/color). Categories are surfaced as colored badges on
entry cards, the entry detail view, the motif list, and the motif detail view,
and are editable from the entry & motif forms via a new picker.

API:
- New `lib/categoryAssign.ts`: shared, project-contained replace-set helper
  (`applyCategories`) + `fetchCategoriesFor` + `categoriesJsonSql`. Only category
  ids belonging to the owner's own project are applied (prevents cross-project
  assignment / IDOR); diffs are written to the audit log by category name.
  Table/column identifiers come from a compile-time allowlist, never user input.
- Categories embedded in reads via a correlated subquery, not a JOIN, so they
  never cartesian-multiply the existing tag aggregation.
- 11 integration tests: assignment on create/update, replace-set semantics,
  omitted-field no-op, read payloads, usage counts, project containment, and
  malformed-id tolerance.

UI:
- New `CategoryBadge` (color-contrast aware) and `CategorySelect` (multi-select
  with create-on-the-fly via POST /api/categories, mirroring the tag input).
- Wired into EntryForm and the motif edit form; badges on EntryCard,
  EntryDetailPage, MotifListPage, MotifDetailPage (incl. entries-within).
- `categories` added to the Entry/MotifSummary/MotifEntry types; test fixtures
  and the public-project graph adapter updated to match.

CHANGELOG deferred to the end of slice 3 (per the #6 plan), rolling into v6.2.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(categories): satisfy MotifEntry type in public graph adapters
All checks were successful
CI / App — typecheck, test & build (pull_request) Successful in 1m43s
CI / App — typecheck, test & build (push) Successful in 1m47s
CI / API — typecheck & tests (pull_request) Successful in 2m33s
CI / API — typecheck & tests (push) Successful in 2m34s
1a0494c83b
The added `categories` field on MotifEntry broke the production tsc build in
two public pages that adapt their own entry shape into MotifEntry[] for the
graph: EmbedPage (gains a categories field on EmbedEntry) and PublicMotifPage
(literal categories: [] in the inline map). Public pages don't surface
categories, so both supply an empty array. Verified on the dev server: api +
app builds and the full test suites (168 api, 63 app) all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rbrooks deleted branch feat/v6.2-categories-assign 2026-06-26 03:22:20 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
rbrooks/TeaLeaves!51
No description provided.