File tags: store + API + chips in the details pane + filter (#530, 3/4) #540

Closed
opened 2026-09-14 02:48:13 +00:00 by Cordy · 2 comments
Owner

Third slice of #530. Tags per file, entered as chips in the details pane's overview panel (per the OpenCloud reference), listable and filterable in browse/search.

Needs: a tag store (statestore pattern, survives redeploys per #153), GET/PUT API per path plus a tag-filter query, chips UI in #538's overview panel, and tag propagation on rename/move/copy — Cairn keys by path, not stable file IDs, so tags must be re-keyed exactly like the activity feed (#539); trash/restore and delete need defined semantics (tags die with the file? survive restore?).

Scope guard: instance-local flat tags, no hierarchies, no per-tag permissions. Backend TDD; i18n ×4.

Third slice of #530. Tags per file, entered as chips in the details pane's overview panel (per the OpenCloud reference), listable and filterable in browse/search. Needs: a tag store (statestore pattern, survives redeploys per #153), `GET/PUT` API per path plus a tag-filter query, chips UI in #538's overview panel, and tag propagation on rename/move/copy — Cairn keys by path, not stable file IDs, so tags must be re-keyed exactly like the activity feed (#539); trash/restore and delete need defined semantics (tags die with the file? survive restore?). Scope guard: instance-local flat tags, no hierarchies, no per-tag permissions. Backend TDD; i18n ×4.
Author
Owner

Design note (build follows on approval-by-silence — flag anything off):

  • Tag store: statestore-pattern map path → [tags] in .cairn-state/tags.json; flat instance-local tags, lowercase-normalized, per-file cap 20, simple charset rule (letters, digits, dash, space). No hierarchies, no per-tag permissions (the issue's scope guard).
  • API: GET/PUT /api/v1/tags?path= (whole-list PUT, matching the settings idiom) and GET /api/v1/tags/find?tag= returning the caller's matching paths for the filter; resolveOwned-class handling, owner-only in v1.
  • Path semantics (the issue's open questions):
    • rename/move: tags re-key through the move chokepoint (with #539's events and #541's versions);
    • copy: tags carry over — a copy of an "invoice · 2026" file is still an invoice from 2026, and the issue lists copy as a propagation target;
    • trash: tags re-key into trash with the file and back on restore; purge drops them. Tags die only when the file truly dies.
  • UI: chips row in the pane's overview (the mockup + OpenCloud pattern: chips + inline "+ tag" input), and the filter surfaced through the existing search box (tag:foo syntax) rather than a new control.
**Design note (build follows on approval-by-silence — flag anything off):** - **Tag store**: statestore-pattern map `path → [tags]` in `.cairn-state/tags.json`; flat instance-local tags, lowercase-normalized, per-file cap 20, simple charset rule (letters, digits, dash, space). No hierarchies, no per-tag permissions (the issue's scope guard). - **API**: `GET/PUT /api/v1/tags?path=` (whole-list PUT, matching the settings idiom) and `GET /api/v1/tags/find?tag=` returning the caller's matching paths for the filter; `resolveOwned`-class handling, owner-only in v1. - **Path semantics** (the issue's open questions): - rename/move: tags re-key through the move chokepoint (with #539's events and #541's versions); - **copy: tags carry over** — a copy of an "invoice · 2026" file is still an invoice from 2026, and the issue lists copy as a propagation target; - trash: tags re-key into trash with the file and back on restore; purge drops them. Tags die only when the file truly dies. - **UI**: chips row in the pane's overview (the mockup + OpenCloud pattern: chips + inline "+ tag" input), and the filter surfaced through the existing search box (`tag:foo` syntax) rather than a new control.
Author
Owner

Shipped in v0.6.222 (PR #620, tag on e7d7cd1), live on both dogfoods.

Built per the design note:

  • internal/tags: path → tags beside recents/favorites (tags.json). Normalization: lowercase, trimmed, single-spaced, letters/digits/dash/space, ≤40 chars — mirrored in the UI as pure:tag-norm so the input rejects instantly what the server would reject. Caps: 20 tags/item, 5000 tagged items per instance.
  • GET/PUT /api/v1/tags?path=, GET /api/v1/tags/find?tag=. Every path passes a Stat through the caller's scoped store — including each find result, so tag search never leaks paths the caller couldn't browse.
  • Rename/move re-keys (exact + directory-prefix carry, beside the #539 activity rekey); copy carries tags to the destination (existing destination tags win).
  • Pane: Tags section in the overview (chips with ×, pill input, Enter to add), editable behind menuGates(...).mutate; hidden on read-only items that have no tags. Typing tag:foo in the search box lists everything with that tag. i18n ×4.

Deliberate v1 edges — flag if they bite:

  1. Tagging permission = visibility (the OpenCloud stance): anyone who can see an item can tag it. Tags are shared, not per-user.
  2. Trash round-trip: tags stay keyed by path, so restore to the original path brings them back; a purge leaves a stale entry that the Stat gate hides and the 5000-path cap bounds. No purge-time drop (the purge handler only knows the trash id).
  3. WebDAV renames/moves don't re-key — same scope stance as the #539 feed.
Shipped in **v0.6.222** (PR #620, tag on e7d7cd1), live on both dogfoods. Built per the design note: - `internal/tags`: path → tags beside recents/favorites (`tags.json`). Normalization: lowercase, trimmed, single-spaced, letters/digits/dash/space, ≤40 chars — mirrored in the UI as `pure:tag-norm` so the input rejects instantly what the server would reject. Caps: 20 tags/item, 5000 tagged items per instance. - `GET/PUT /api/v1/tags?path=`, `GET /api/v1/tags/find?tag=`. Every path passes a Stat through the caller's scoped store — including each find result, so tag search never leaks paths the caller couldn't browse. - Rename/move re-keys (exact + directory-prefix carry, beside the #539 activity rekey); copy carries tags to the destination (existing destination tags win). - Pane: Tags section in the overview (chips with ×, pill input, Enter to add), editable behind `menuGates(...).mutate`; hidden on read-only items that have no tags. Typing `tag:foo` in the search box lists everything with that tag. i18n ×4. Deliberate v1 edges — flag if they bite: 1. **Tagging permission = visibility** (the OpenCloud stance): anyone who can see an item can tag it. Tags are shared, not per-user. 2. **Trash round-trip:** tags stay keyed by path, so restore to the original path brings them back; a purge leaves a stale entry that the Stat gate hides and the 5000-path cap bounds. No purge-time drop (the purge handler only knows the trash id). 3. **WebDAV renames/moves don't re-key** — same scope stance as the #539 feed.
Cordy closed this issue 2026-09-18 16:43:57 +00:00
Sign in to join this conversation.
No labels
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: Cordy/Cairn#540
No description provided.