feat(saved): saved-message labels + filter chips (LC-479) #484

Merged
longjacksonle merged 4 commits from feat/LC-479-saved-labels into main 2026-06-28 19:55:43 +02:00

LC-479 - Saved-message labels / folders

Lets users tag saved messages (e.g. follow-up, read-later) and filter the /saved page by label. Buckets in the spirit of Slack's "Later".

Data

  • Migration 0074_bookmark_labels.sql: ALTER TABLE bookmarks ADD COLUMN label TEXT (nullable; NULL = unlabeled). The table is already user-scoped via its composite PK, so a label is private to the owning user.
  • db::bookmarks::set_label(user, message, Option<&str>) updates/clears only the owner's row; bookmarks_for_user selects the label into BookmarkRow.

Editing

  • Each /saved row has an inline label <input> that posts on change to POST /messages/{id}/bookmark/label. The handler trims, caps at 40 chars (empty = clear), stores, and broadcasts SavedChanged so every /saved tab re-renders its list + chips over the existing WebSocket path (request returns 204; the form is hx-swap="none").

Filtering

  • Filter chips (All / one per distinct label / Unlabeled) render inside the live region from SavedPage::labels() + has_unlabeled() (shared by the page and the OOB fragment, so a live refresh keeps them in sync).
  • Filtering is client-side over data-lc-saved-label, so the server never needs to know each tab's active filter. The active filter is stored on the stable #lc-saved-wrap (which the live swap does not replace) and re-applied on every htmx:afterSettle, so it survives a SavedChanged refresh. A filter whose label disappears falls back to All.

Tests

db_bookmarks: set_label round-trip (set -> clear) and owner-scoping (one user's label does not touch another's bookmark of the same message). just check, just test, just test-saas all pass (i18n catalog parity covers the four new en/es keys). No test hand-builds these view/db structs, so no contract drift; new migration is picked up automatically by the common::chat_pool() migrator.

Not operator-visible (no env/config/contract change).

🤖 Generated with Claude Code

## LC-479 - Saved-message labels / folders Lets users tag saved messages (e.g. `follow-up`, `read-later`) and filter the `/saved` page by label. Buckets in the spirit of Slack's "Later". ### Data - Migration `0074_bookmark_labels.sql`: `ALTER TABLE bookmarks ADD COLUMN label TEXT` (nullable; NULL = unlabeled). The table is already user-scoped via its composite PK, so a label is private to the owning user. - `db::bookmarks::set_label(user, message, Option<&str>)` updates/clears only the owner's row; `bookmarks_for_user` selects the label into `BookmarkRow`. ### Editing - Each `/saved` row has an inline label `<input>` that posts on change to `POST /messages/{id}/bookmark/label`. The handler trims, caps at 40 chars (empty = clear), stores, and broadcasts `SavedChanged` so every `/saved` tab re-renders its list + chips over the existing WebSocket path (request returns 204; the form is `hx-swap="none"`). ### Filtering - Filter chips (All / one per distinct label / Unlabeled) render inside the live region from `SavedPage::labels()` + `has_unlabeled()` (shared by the page and the OOB fragment, so a live refresh keeps them in sync). - Filtering is client-side over `data-lc-saved-label`, so the server never needs to know each tab's active filter. The active filter is stored on the stable `#lc-saved-wrap` (which the live swap does not replace) and re-applied on every `htmx:afterSettle`, so it survives a `SavedChanged` refresh. A filter whose label disappears falls back to All. ### Tests `db_bookmarks`: `set_label` round-trip (set -> clear) and owner-scoping (one user's label does not touch another's bookmark of the same message). `just check`, `just test`, `just test-saas` all pass (i18n catalog parity covers the four new en/es keys). No test hand-builds these view/db structs, so no contract drift; new migration is picked up automatically by the `common::chat_pool()` migrator. Not operator-visible (no env/config/contract change). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Migration 0074 adds bookmarks.label (NULL = unlabeled). bookmarks_for_user selects it into BookmarkRow; new set_label() updates/clears the owning user's row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /messages/{id}/bookmark/label sets/clears the label (trim, 40-char cap, empty = clear) and broadcasts SavedChanged so every /saved tab re-renders over WS (request returns 204; form is hx-swap=none). SavedListRow carries the label; SavedPage/SavedListFragment expose labels()/has_unlabeled() for the shared items partial.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each saved row gets an inline label input (posts on change); a chip bar filters the list client-side over data-lc-saved-label (All / each label / Unlabeled). The active filter lives on the stable #lc-saved-wrap so it survives the SavedChanged live refresh, re-applied on every htmx settle. New i18n keys (en+es) and .lc-saved-chip styling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(saved): set_label round-trip + owner-scoping (LC-479)
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 5s
Check / clippy + fmt + tests (pull_request) Successful in 2m57s
f3a09c1afa
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/LC-479-saved-labels 2026-06-28 19:55:43 +02:00
Sign in to join this conversation.
No reviewers
No labels
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
psa-systems/lets-chat!484
No description provided.