* Allow dragging documents from list views into the sidebar
Previously the react-dnd provider was scoped to the sidebar, so only
sidebar rows could be dragged. This lifts the DndProvider up to the
authenticated layout so both the main content and the sidebar share a
single drag-and-drop context, and makes DocumentListItem a drag source.
Now any document in search results or paginated lists (Home, Drafts,
Collections, etc.) can be dragged into the sidebar to move it between
collections, reparent it under another document, star it, or archive it
— reusing the existing sidebar drop targets.
* Make the whole Starred section a drop target to star documents
Previously the only "create star" drop targets in the Starred section
were the thin cursors between items, so dragging a document onto the
section header or a starred row showed the drop cursor but did nothing.
Wrap the section in a catch-all drop target (mirroring the Archive
section) so dropping anywhere in Starred stars the document, while the
precise inter-item cursors still control ordering. A didDrop guard on
useDropToCreateStar prevents the catch-all from double-starring when a
nested cursor already handled the drop, and the hover highlight uses a
shallow isOver check so it only lights up when not over a nested target.
* Let document list drag ghost follow the cursor
The sidebar drag placeholder tethers the ghost near its starting x so it
stays aligned with the sidebar during reordering. When a drag starts out
in the main content (a document list item), that clamp pinned the ghost
to a narrow band, making it look stuck in a small area.
Thread a constrainToSidebar flag through the drag item (true for sidebar
drags, false for document list drags) and let the placeholder follow the
cursor freely when the drag originated outside the sidebar.
* Clarify constrainToSidebar JSDoc to match placeholder behavior
The placeholder treats an unset flag as tethered (constrainToSidebar
!== false), so external drags must set it explicitly to false rather
than leaving it unset. Update the comment to reflect that.
* css
* Render filter options as drawer popover on mobile
Filter options on the search page (and other FilterOptions consumers)
previously rendered as a Radix dropdown on all viewports. On mobile this
now renders as a bottom-sheet Drawer, matching the popover style already
used by context menus.
https://claude.ai/code/session_01MSjTD67PWfGbwgNA5FFoSH
* Fix filter drawer search input overlapping first option on mobile
The Input wrapper uses flex: 0 (a 0% basis), which collapsed the search
input inside the drawer's flex column so its content painted over the
first list item. Use flex: none to retain the input's natural height.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: Increase valid user-supplied URL length to 1024
* fix: Wrap URL length migration in a transaction
Wrap the multi-column changeColumn operations in a transaction so a
failure on any column rolls back the whole migration rather than leaving
the database partially migrated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Collection title/icon editing was gated by `isEditRoute && separateEditMode`,
which meant that in the default inline editing mode (separateEditMode off) the
title and icon were never editable inline — even though the collection
description was. This diverged from documents and from the collection
description editor.
Align the Header editing gate with documents (DataLoader) and the Overview
description editor: `isEditRoute || !separateEditMode`, so title and icon are
seamlessly editable inline whenever the user has update permission.
Co-authored-by: Claude <noreply@anthropic.com>
* fix: Access request logic for collection managers
* test: Exercise collection-manager path in access request regression tests
Grant the non-workspace-admin manager a collection-level Admin membership
instead of a direct document-level membership, so authorization flows
through the collection-manager path being tested for #12567.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Add tagging of outgoing emails
* Detect SES configured via well-known service key
The isSES check only matched "amazonaws" in the host, so SES configured
through SMTP_SERVICE (e.g. "SES" or "SES-US-EAST-1") was not detected and
tagging headers were not applied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(deps): bump zod from 4.3.6 to 4.4.3
Bumps [zod](https://github.com/colinhacks/zod) from 4.3.6 to 4.4.3.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.3.6...v4.4.3)
---
updated-dependencies:
- dependency-name: zod
dependency-version: 4.4.3
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
* fix: Make files.create file param optional in schema for zod 4.4
zod 4.4 changed z.custom() to reject undefined. Since validate runs
before multipart injects the file, validation failed with 400 on all
files.create requests. Mark the field optional and guard in the handler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(deps-dev): bump vite-plugin-babel from 1.6.0 to 1.7.3
Bumps [vite-plugin-babel](https://github.com/owlsdepartment/vite-plugin-babel) from 1.6.0 to 1.7.3.
- [Commits](https://github.com/owlsdepartment/vite-plugin-babel/commits)
---
updated-dependencies:
- dependency-name: vite-plugin-babel
dependency-version: 1.7.3
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
* fix: Use include option for vite-plugin-babel TS transform
vite-plugin-babel 1.7.0 added an `include` option defaulting to
`/\.jsx?$/` (JS only) that is applied before `filter`, so .ts/.tsx
files were no longer transformed by Babel and reached the parser
with types intact. Switch to the `include` option to match TS files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Forces transitive uuid copies (8.3.2 via sequelize/bull, 9.0.1 via
@hocuspocus/*) onto the patched 11.1.1, addressing GHSA-w5hq-g745-h8pq.
11.1.1 is the highest version that is both patched and ships a CommonJS
build, which the require()-based consumers depend on.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* perf: Lazy import mailparser, @fast-csv, and franc deps
Moves heavy dependencies off the startup path into the narrow async code
paths that actually use them, mirroring the mammoth lazy-import change:
- mailparser: only needed for Confluence Word imports (confluenceToHtml)
- @fast-csv/parse: only needed for CSV imports (csvToMarkdown)
- franc / iso-639-3: only needed by the DocumentUpdateText worker task
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* perf: Lazy import jsdom dep
jsdom is one of the heaviest server dependencies but is only needed for
HTML export (ProsemirrorHelper.toHTML) and HTML import
(DocumentConverter.htmlToProsemirror). Move it to a lazy `await import`
inside those methods so its dependency tree stays off the startup path.
Both methods become async; all callers were already in async contexts.
The type-only usage in patchGlobalEnv is now an `import type`.
* fix: Run single process when only the worker service is enabled
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* perf: Improve memory consumption through lazy service loading
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Re-frame the rendered SVG viewBox from a getBBox() measurement taken in
the visible editor rather than the hidden render element, where the
measurement is unreliable on high-DPI/RDP sessions. Bump the cache
namespace so previously mis-sized diagrams are re-rendered.
Returning the unfurl promises without awaiting them inside the try
block meant rejections (e.g. "Entity not found: Issue") escaped the
catch and were reported to error tracking. Await them so they are
caught and returned as a handled { error } result.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: Remove unused grid snapping from element resizing
Horizontal resizing snapped widths to a 5% grid, which is no longer
desired. Replace the only remaining use of the gridSnap prop (the
minimum-width clamp) with a named constant and drop the prop entirely.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: Remove resize lag by disabling size transition while dragging
The width/height CSS transition on resizable elements existed to smooth
the discrete jumps from grid snapping. With pixel-by-pixel resizing the
element perpetually animates toward a target ~150ms in the future, so it
visibly trails the cursor. Disable the transition while actively dragging
and restore it afterwards so snap-back and collaborative size changes
still animate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: Constrain image resizing to editor edge instead of snapping to natural size
When dragging an element past the editor bounds, the full-width sentinel
forced the width to the natural size. For images narrower than the editor
this snapped them back to their (smaller) natural width at the boundary.
Only use the natural-width sentinel when the image is genuinely wider than
the editor; otherwise constrain to the editor edge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* PR feedback
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The FileOperation import association was fetched for every non-public
document but only used when sourceMetadata is present. Move the lookup
inside that branch to eliminate an N+1 query for documents that are not
imports, benefiting every endpoint that presents documents.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The attachment cleanup loop used findAllInBatches, which advances an
OFFSET each iteration. Because the callback deletes each batch, the
remaining rows shift backwards and the advancing offset skips over them,
leaving attachments that still reference the team. team.destroy() then
failed with attachments_teamId_fkey.
Page from offset 0 until no attachments remain, and remove the now
redundant per-user attachment delete so the loop is the single
authoritative cleanup.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: Allow service worker to load on custom domains
Add explicit worker-src 'self' so the service worker can register on
team custom domains. Without it, browsers fall back to script-src which
only lists env.URL and env.CDN_URL, blocking /static/sw.js on hosts
like docs.getoutline.com.
* fix: Switch worker-src approach to script-src 'self' for type safety
The @types/koa-helmet definitions don't include workerSrc. Add 'self'
to script-src instead — worker-src falls back to script-src per spec,
and 'self' matches the document origin on custom domains.
* fix: Properly add worker-src directive without script-src widening
Extract the CSP directives to a local variable so workerSrc can be
included despite koa-helmet's outdated type definitions missing it
(the underlying helmet supports it). Also drop @types/koa-helmet
since the package now ships its own (equivalent) types.
When /oauthClients.info returns an AuthorizationError, ApiClient logs
the user out and clears auth.team. The subsequent re-render of the
Authorize component hit the strict useCurrentTeam() and threw before
the error UI could render. Make the inner hook tolerate a missing team
and fold it into the existing error branch.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>