Skip to content

feat: Document tagging system#11882

Open
dmorel wants to merge 30 commits into
outline:mainfrom
dmorel:feature/tags
Open

feat: Document tagging system#11882
dmorel wants to merge 30 commits into
outline:mainfrom
dmorel:feature/tags

Conversation

@dmorel

@dmorel dmorel commented Mar 27, 2026

Copy link
Copy Markdown

Summary

Implements a full document tagging feature for Outline, as requested in #765.

  • Database: Two new tables — tags (name + team scope, unique index) and document_tags (join table with cascade deletes)
  • API: Six new endpoints — tags.create (upsert by name), tags.list (with document counts), tags.update, tags.delete (admin-only), tags.add, tags.remove
  • Search: #tag tokens in the search bar are parsed client-side, resolved to tag IDs, and passed as tagIds filters to documents.search and documents.search_titles (AND semantics)
  • UI: TagInput component with typeahead autocomplete on document pages, dismissible tag pills, /tags index page in sidebar, tag cards link to search

Details

  • Tags are normalized to lowercase; create is idempotent (upsert)
  • Authorization: members can create/update tags and tag documents, only admins can delete tags, viewers are blocked
  • Team isolation enforced on all endpoints — cross-team tag operations return 403
  • documents.info eagerly loads tags; list endpoints skip tags to avoid N+1
  • Webhook integration: tag events added to the exhaustive switch-case (currently ignored, ready for future webhook support)
  • SearchHelper uses parameterized EXISTS subqueries with sequelize.escape() for tag filtering

Test plan

  • 18 server API tests covering CRUD, authorization, team isolation, cascade delete, idempotency, documentCount accuracy
  • 9 parseSearchQuery unit tests (extraction, normalization, deduplication, edge cases)
  • 6 TagsStore unit tests (name cache, case-insensitive lookup, clear, deduplication)
  • 1 documents.search tagIds filter integration test
  • Full test suite: 2554 passed, 0 regressions (2 pre-existing failures on main)
  • TypeScript: yarn tsc --noEmit passes with 0 errors
  • Lint: 0 new warnings or errors
  • Browser-tested: tag CRUD, autocomplete, search with #tag tokens, persistence across reload

Closes #765

Copilot AI review requested due to automatic review settings March 27, 2026 08:25
@auto-assign auto-assign Bot requested a review from tommoor March 27, 2026 08:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements a new end-to-end document tagging feature across the database, server API, search filtering, and client UI/navigation for Outline.

Changes:

  • Add tags and document_tags models + migration, and wire up policies/presenters/events.
  • Introduce tags.* API endpoints and integrate tag filters into documents.search / documents.search_titles via tagIds.
  • Add client-side tag UX (document tag editor, /tags page, sidebar link) and #tag search token parsing.

Reviewed changes

Copilot reviewed 39 out of 40 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
server/types.ts Adds TagEvent to the event union for tag-related activity.
server/routes/api/tags/tags.ts Implements tag CRUD + tag/document association endpoints.
server/routes/api/tags/tags.test.ts Adds server API tests for tags endpoints and authorization/isolation.
server/routes/api/tags/schema.ts Adds zod schemas for all tag endpoints.
server/routes/api/tags/index.ts Exposes the tags router.
server/routes/api/index.ts Registers tags routes in the main API router.
server/routes/api/documents/schema.ts Adds tagIds filters and relaxes search_titles query validation.
server/routes/api/documents/documents.ts Injects tags into documents.info and passes tagIds into search helpers.
server/routes/api/documents/documents.test.ts Adds tests for documents.info tags and documents.search tag filtering.
server/presenters/tag.ts Adds a presenter for tag API payloads (incl. documentCount).
server/presenters/index.ts Exports presentTag.
server/policies/tag.ts Adds tag policies (read/update/delete/createTag).
server/policies/index.ts Registers tag policy module.
server/models/index.ts Exports Tag and DocumentTag models.
server/models/helpers/SearchHelper.ts Adds AND-semantics tag filtering via EXISTS subqueries.
server/models/Tag.ts Introduces the Tag model with name normalization hook and associations.
server/models/DocumentTag.ts Introduces the join model between documents and tags.
server/models/Document.ts Adds documentTags association on Document.
server/migrations/20260317213539-create-tags.js Creates tags + document_tags tables and relevant indexes/FKs.
plugins/webhooks/server/tasks/DeliverWebhookTask.ts Adds tag event names to the ignored webhook switch-case.
app/utils/routeHelpers.ts Adds tagsPath() route helper.
app/utils/parseSearchQuery.ts Adds #tag token extraction/normalization for search queries.
app/utils/parseSearchQuery.test.ts Unit tests for tag token parsing behavior.
app/stores/TagsStore.ts Adds TagsStore with name cache and tag CRUD/association RPCs.
app/stores/TagsStore.test.ts Unit tests for name cache, ordering, and clear behavior.
app/stores/RootStore.ts Registers TagsStore in the root store.
app/stores/DocumentsStore.ts Hydrates document.tags from API payload into Tag models.
app/scenes/Tags/index.tsx Adds /tags scene listing workspace tags.
app/scenes/Search/components/SearchInput.tsx Disables browser autocomplete for search input.
app/scenes/Search/Search.tsx Parses #tag tokens and applies tagIds filters to search requests.
app/scenes/Document/components/Editor.tsx Adds TagInput under the title for non-shared, non-template docs.
app/routes/authenticated.tsx Adds authenticated route for /tags.
app/models/Tag.ts Introduces Tag client model.
app/models/Document.ts Adds tags observable array to Document client model.
app/components/TagList.tsx Adds tag pill list component with optional dismiss action.
app/components/TagInput.tsx Adds inline tag editor with autocomplete and create/add/remove behaviors.
app/components/Sidebar/components/TagsLink.tsx Adds sidebar link to the tags index.
app/components/Sidebar/App.tsx Renders the new Tags link in the sidebar.
.gitignore Ignores .worktrees/.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/scenes/Tags/index.tsx
Comment thread server/routes/api/documents/schema.ts
Comment thread server/types.ts Outdated
Comment thread server/routes/api/tags/tags.ts Outdated
Comment thread app/scenes/Search/Search.tsx
Comment thread server/routes/api/tags/tags.ts
Comment thread server/routes/api/tags/schema.ts
Comment thread server/routes/api/tags/schema.ts
Comment thread app/scenes/Search/Search.tsx
Comment thread app/scenes/Tags/index.tsx

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/routes/api/tags/tags.ts Outdated
Comment thread server/routes/api/tags/tags.ts Outdated
Comment thread app/utils/parseSearchQuery.ts Outdated
Comment thread app/scenes/Search/Search.tsx Outdated
Comment thread server/routes/api/tags/tags.ts
Comment thread server/routes/api/tags/tags.ts
Comment thread app/scenes/Search/Search.tsx
Comment thread app/components/TagInput.tsx Outdated
Comment thread app/stores/TagsStore.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/routes/api/tags/schema.ts
Comment thread server/routes/api/documents/schema.ts Outdated
Comment thread app/components/TagInput.tsx
Comment thread app/stores/TagsStore.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/components/TagInput.tsx
Comment thread server/routes/api/documents/schema.ts
Comment thread server/routes/api/tags/tags.ts
Comment thread app/stores/TagsStore.ts Outdated
Comment thread app/components/TagInput.tsx Outdated
dmorel added 16 commits March 28, 2026 11:47
- TagInput force-fetches document when tags not populated (cache hit fix)
- TagInput updates document.tags observable immediately after add/remove
- Search page fetches all tags on mount so #tag queries resolve tag IDs
- Made query optional (was required non-empty) to allow pure tag searches
- Added tagIds field to DocumentsSearchTitlesSchema
- Pass tagIds through to SearchHelper.searchTitlesForUser in route handler
- SearchHelper.buildWhere already handled tagIds, no changes needed there
With 'search titles only' on and a pure #tag query (no text),
cleanQuery is empty — consistent with how an empty plain-text
search returns nothing.
- Arrow keys navigate suggestions and update the input with the full tag name
- Mouse hover highlights suggestion and fills the input
- Leaving hover restores the typed text
- Enter selects the keyboard-highlighted item, then the first suggestion,
  then creates a new tag (only if no suggestions match)
- Blur restores the typed text if the dropdown closes without selection
Partial tag tokens like #eng that don't resolve to a tag ID were being
treated as an empty filter, returning all documents. Now returns an
empty result set immediately.

Also disables browser autocomplete on the search input to prevent stale
partial tag tokens from appearing as native suggestions.
When clicking a dropdown suggestion, onBlur fired before handleAddTag's
async operations completed, causing the onBlur timeout (150ms) to
restore typedValue to the input. Prevent default on onMouseDown so the
input retains focus and no blur/restore cycle occurs.
- tags API: cross-team isolation, cascade delete, documentCount
  accuracy, viewer auth on add/remove, cross-team boundary on add
- parseSearchQuery: lone hash, hyphens in names, deduplication
- TagsStore: clear() empties name cache, duplicate add doesn't corrupt cache
- documents.info: response includes tags array

Also fixes two bugs found by the new tests:
- parseSearchQuery regex now matches tag names containing hyphens
- parseSearchQuery deduplicates repeated tag tokens
- Handle UniqueConstraintError in tags.update with proper ValidationError
- Only fire tags.remove event when a row is actually destroyed
- Use instance destroy instead of static destroy in tags.remove
- Clean up documentCount type cast in tags.list
- Apply Prettier formatting to all tags feature files

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/components/TagInput.tsx Outdated
Comment thread server/routes/api/tags/tags.ts
ArrowDown was capped at suggestions.length-1, making the rendered
"Create" row unreachable by keyboard. Enter with no highlighted item
always selected suggestions[0] instead of using the typed value.

Now ArrowDown reaches index suggestions.length (the Create row),
ArrowUp navigates back from it, and Enter prefers the typed value
unless an existing suggestion was explicitly highlighted.
@pro-ts

pro-ts commented Apr 15, 2026

Copy link
Copy Markdown

Basically the most important (at least for me) feature for Outline right now. I hope it will be merged to main soon!

@Howardzi-nn

Copy link
Copy Markdown
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Document tags

4 participants