53 lines
3.7 KiB
Markdown
53 lines
3.7 KiB
Markdown
# 08 — Collaboration: Comments, Attachments, Links, Wiki, Markdown, Watchers
|
|
|
|
## Comments
|
|
|
|
- [ ] Flat list per issue, chronological (no threading); markdown bodies
|
|
- [ ] Edit: author only; sets `updated_at`; UI shows "(edited)"
|
|
- [ ] Delete: soft; author or project owner; UI shows a "comment deleted" tombstone (author + timestamp, no body)
|
|
- [ ] **Resolvable**: any member can resolve/unresolve (`resolved_at`, `resolved_by`); resolved comments render collapsed with a "resolved by X" header; issue detail shows resolved count
|
|
- [ ] Emit `comment.created/updated/resolved/unresolved/deleted`; `comment.created` payload carries parsed mentions
|
|
|
|
## Markdown pipeline
|
|
|
|
- [ ] **Client**: markdown-it + DOMPurify sanitization; single shared `MarkdownView` component; supported: CommonMark + tables, strikethrough, task lists, fenced code with highlight.js, autolink
|
|
- [ ] **Server** (on save of any markdown field — issue/project/epic descriptions, comments, wiki bodies):
|
|
- [ ] Parse `@username` mentions (word-boundary, against existing usernames) → into the event payload for notification fan-out; mentions inside code blocks/spans are ignored
|
|
- [ ] Parse `#N` references (this project's issues) → validated list into event payload; used for cross-link rendering and future relation hints
|
|
- [ ] Server does NOT render HTML (client renders); server only extracts entities
|
|
- [ ] Client renders `@username` as a profile-ish chip and `#N` as a router-link to the issue (resolve via a lightweight `GET /projects/{id}/issues/{num}` title lookup, cached)
|
|
- [ ] Paste/drag an image into `MarkdownEditor` → uploads as attachment → inserts ``
|
|
|
|
## Attachments
|
|
|
|
- [ ] Upload targets: project, issue, comment (multipart; ≤ `MAX_UPLOAD_MB`; any mime type, but SVG served with `Content-Type: text/plain` nosniff to avoid stored XSS)
|
|
- [ ] Disk path: `UPLOAD_DIR/project_{pid}/{uploader_id}_{yyyymmddhhmmss}_{rand16}.{ext}` — path stored relative in DB; never trust client filenames for the path (original name kept separately for download)
|
|
- [ ] Serving: `GET /files/{path}` checks membership of the owning project (lookup by attachments row, not by path prefix), sets `Content-Disposition`
|
|
- [ ] Delete: soft; **file stays on disk until cull** ([12-retention.md](12-retention.md) deletes row + file together)
|
|
- [ ] Image attachments get a thumbnail treatment client-side only (no server thumbnailing in v1)
|
|
- [ ] Emit `attachment.uploaded/deleted`
|
|
|
|
## Links
|
|
|
|
- [ ] Simple URL + optional title on project/issue/comment; validated absolute http(s) URL
|
|
- [ ] Emit `link.added/removed`
|
|
|
|
## Wiki
|
|
|
|
- [ ] Per-project markdown pages; slug generated from title (kebab-case, deduplicated with `-2` suffix); slug is stable after creation (title edits don't re-slug)
|
|
- [ ] Page list + view + edit (single editor, last-write-wins with `updated_at` conflict warning: PATCH carries `expect_updated_at`, 409 on mismatch)
|
|
- [ ] `updated_by` tracked; no page history in v1 ([17-decisions.md](17-decisions.md))
|
|
- [ ] Wiki bodies participate in mentions/#refs parsing and search indexing
|
|
- [ ] Emit `wiki.created/updated/deleted`
|
|
|
|
## Watchers
|
|
|
|
- [ ] Any member can watch/unwatch any issue in their projects; watcher list shown on issue detail
|
|
- [ ] **Auto-watch** (insert if no row exists; never flip an existing `muted=true` row):
|
|
- [ ] creator on issue create
|
|
- [ ] assignee on assign
|
|
- [ ] commenter on comment
|
|
- [ ] mentioned user on mention (issue description or comment)
|
|
- [ ] **Unwatch = mute**: `DELETE /watch` sets `muted = true` (row kept) so no future auto-watch resurrects the subscription; `PUT /watch` clears muted
|
|
- [ ] Watchers feed notification fan-out ([09-notifications-realtime.md](09-notifications-realtime.md)); muted watchers receive nothing
|