Compare commits

...

870 Commits

Author SHA1 Message Date
Philip Okugbe 0e6944f52c New translations translation.json (Portuguese, Brazilian)
[ci skip]
2026-05-10 19:04:56 +01:00
Philip Okugbe 6abd1b6aec New translations translation.json (English)
[ci skip]
2026-05-10 19:04:54 +01:00
Philip Okugbe 964d60f520 New translations translation.json (Chinese Simplified)
[ci skip]
2026-05-10 19:04:53 +01:00
Philip Okugbe d856d95232 New translations translation.json (Ukrainian)
[ci skip]
2026-05-10 19:04:51 +01:00
Philip Okugbe 51f7017b58 New translations translation.json (Russian)
[ci skip]
2026-05-10 19:04:50 +01:00
Philip Okugbe c498b20ab1 New translations translation.json (Dutch)
[ci skip]
2026-05-10 19:04:48 +01:00
Philip Okugbe ddf05fb44a New translations translation.json (Korean)
[ci skip]
2026-05-10 19:04:47 +01:00
Philip Okugbe 129e2ce1e3 New translations translation.json (Japanese)
[ci skip]
2026-05-10 19:04:45 +01:00
Philip Okugbe 2f32f6e8b0 New translations translation.json (Italian)
[ci skip]
2026-05-10 19:04:44 +01:00
Philip Okugbe 7f6229ac8f New translations translation.json (Spanish)
[ci skip]
2026-05-10 19:04:43 +01:00
Philip Okugbe 23d852da1e New translations translation.json (French)
[ci skip]
2026-05-10 19:04:41 +01:00
Philip Okugbe 1a88b23394 New translations translation.json (German)
[ci skip]
2026-05-10 19:04:40 +01:00
Philip Okugbe 371c796afd New translations translation.json (Russian)
[ci skip]
2026-05-10 00:59:36 +01:00
Philip Okugbe 65f048e6c4 New translations translation.json (Russian)
[ci skip]
2026-05-09 19:03:28 +01:00
Philip Okugbe da61d464b2 New translations translation.json (Portuguese, Brazilian)
[ci skip]
2026-05-09 17:57:56 +01:00
Philip Okugbe 8eb8de6df7 New translations translation.json (English)
[ci skip]
2026-05-09 17:57:54 +01:00
Philip Okugbe 10d922eb71 New translations translation.json (Chinese Simplified)
[ci skip]
2026-05-09 17:57:53 +01:00
Philip Okugbe ab4605c4ad New translations translation.json (Ukrainian)
[ci skip]
2026-05-09 17:57:51 +01:00
Philip Okugbe a95f3d254d New translations translation.json (Russian)
[ci skip]
2026-05-09 17:57:50 +01:00
Philip Okugbe 5451efaf5d New translations translation.json (Dutch)
[ci skip]
2026-05-09 17:57:49 +01:00
Philip Okugbe 4ee65525c9 New translations translation.json (Korean)
[ci skip]
2026-05-09 17:57:47 +01:00
Philip Okugbe d39ba67834 New translations translation.json (Japanese)
[ci skip]
2026-05-09 17:57:46 +01:00
Philip Okugbe 253c9a5d88 New translations translation.json (Italian)
[ci skip]
2026-05-09 17:57:45 +01:00
Philip Okugbe cdf00a96a9 New translations translation.json (Spanish)
[ci skip]
2026-05-09 17:57:43 +01:00
Philip Okugbe 73b726c7a8 New translations translation.json (French)
[ci skip]
2026-05-09 17:57:42 +01:00
Philip Okugbe d9d6a8e2ae New translations translation.json (German)
[ci skip]
2026-05-09 17:57:41 +01:00
Philip Okugbe 3602860979 Merge branch 'main' into i10n_main 2026-05-09 17:23:02 +01:00
Philip Okugbe 537e45bc11 feat: page details section and backlinks (#2186)
* feat: page details section and backlinks
2026-05-09 17:03:08 +01:00
Philip Okugbe 79b79348ba New translations translation.json (Portuguese, Brazilian)
[ci skip]
2026-05-09 14:39:52 +01:00
Philip Okugbe b44d6e3138 New translations translation.json (Chinese Simplified)
[ci skip]
2026-05-09 14:39:51 +01:00
Philip Okugbe cdd9c5cf66 New translations translation.json (Ukrainian)
[ci skip]
2026-05-09 14:39:49 +01:00
Philip Okugbe c866c07219 New translations translation.json (Russian)
[ci skip]
2026-05-09 14:39:48 +01:00
Philip Okugbe 6638ca806c New translations translation.json (Dutch)
[ci skip]
2026-05-09 14:39:46 +01:00
Philip Okugbe adc2341029 New translations translation.json (Korean)
[ci skip]
2026-05-09 14:39:45 +01:00
Philip Okugbe 27ebcd248f New translations translation.json (Japanese)
[ci skip]
2026-05-09 14:39:43 +01:00
Philip Okugbe 366b8fbdc1 New translations translation.json (Italian)
[ci skip]
2026-05-09 14:39:42 +01:00
Philip Okugbe 44cba3c581 New translations translation.json (Spanish)
[ci skip]
2026-05-09 14:39:41 +01:00
Philip Okugbe 23c5bf6b66 New translations translation.json (French)
[ci skip]
2026-05-09 14:39:40 +01:00
Philip Okugbe a6b49f49bd New translations translation.json (German)
[ci skip]
2026-05-09 14:39:38 +01:00
Philip Okugbe 24a9403f09 New translations translation.json (Portuguese, Brazilian)
[ci skip]
2026-05-09 13:32:45 +01:00
Philip Okugbe e8323708d7 New translations translation.json (English)
[ci skip]
2026-05-09 13:32:44 +01:00
Philip Okugbe b9d28223d9 New translations translation.json (Chinese Simplified)
[ci skip]
2026-05-09 13:32:43 +01:00
Philip Okugbe 57ca56744d New translations translation.json (Ukrainian)
[ci skip]
2026-05-09 13:32:41 +01:00
Philip Okugbe f45c6bead3 New translations translation.json (Russian)
[ci skip]
2026-05-09 13:32:40 +01:00
Philip Okugbe e02cdf6d59 New translations translation.json (Dutch)
[ci skip]
2026-05-09 13:32:38 +01:00
Philip Okugbe 0f6d0fb440 New translations translation.json (Korean)
[ci skip]
2026-05-09 13:32:37 +01:00
Philip Okugbe 7025e3c947 New translations translation.json (Japanese)
[ci skip]
2026-05-09 13:32:36 +01:00
Philip Okugbe 53a803383b New translations translation.json (Italian)
[ci skip]
2026-05-09 13:32:35 +01:00
Philip Okugbe 1d5b5a3002 New translations translation.json (Spanish)
[ci skip]
2026-05-09 13:32:33 +01:00
Philip Okugbe 08a9df37ef New translations translation.json (French)
[ci skip]
2026-05-09 13:32:32 +01:00
Philip Okugbe 63be9a58c1 New translations translation.json (German)
[ci skip]
2026-05-09 13:32:31 +01:00
Philip Okugbe bdc369fce0 feat(editor): fixed toolbar preference (#2185)
* feat(editor): fixed toolbar preference

* remove key

* cleanup translation strings

* update axios
2026-05-09 13:27:03 +01:00
Philip Okugbe 2d8b470495 feat(editor): indentation (#2174)
* switch to default codeblock tab handling

* feat(editor): indentation
2026-05-08 21:40:37 +01:00
David Gallardo c66c08fa78 fix: ignore emoji when deriving avatar initials (#2167) 2026-05-08 21:36:10 +01:00
David Gallardo 6046d04375 feat(editor): replace emoji picker with browse + search (#2171)
* feat(editor): show emoji name in suggestion list

Replace the fixed-column emoji grid with a vertical list that displays
each emoji alongside its :shortcode: name. This makes the picker more
discoverable—users can see and learn shortcodes without prior knowledge.

Changes:
- EmojiList: switch from SimpleGrid/ActionIcon to UnstyledButton list
  rows showing emoji glyph + monospace 🆔 label
- Navigation simplified to ArrowUp/ArrowDown (list has no columns)
- Results capped at 8 items for a focused, scannable dropdown
- CSS module: rename menuBtn -> menuItem, tighten padding

* feat(editor): replace SearchIndex with name/id includes search

Port the exact search algorithm from the original extension:
- Build a flat index from @emoji-mart/data: { id, name (lowercase), native }
- Filter with name.includes(q) || id.includes(q) — predictable, no
  keyword indirection
- Results capped at 5 (same as extension)
- Frequently-used emojis (sorted by usage) shown when query is empty
- Remove emoji-mart init() / SearchIndex / getEmojiDataFromNative
  dependencies; index is built lazily and cached in memory
- Remove unused GRID_COLUMNS constant

* feat(editor): emoji picker with browse and search modes

When the query is empty the picker shows a category bar with 8 tabs
(people, nature, food…) and a scrollable emoji grid. Typing after ':'
switches to a compact list that shows the glyph and :shortcode: side by
side, making it easy to discover emoji names while you type.

- Category data is loaded lazily from @emoji-mart/data and cached, so
  opening the picker more than once has no overhead
- Grid keyboard nav: arrow keys move by cell/row, Enter picks
- List keyboard nav: up/down through results, Enter picks
- Mouse hover syncs the keyboard selection index in both modes
- incrementEmojiUsage tracks picks so frequently used ones bubble up
  in future sessions

* fix(editor): polish emoji picker copy and loading

* feat: add emoji to slash command

* Add keyboard support to emoji group navigation

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-05-08 21:33:43 +01:00
David Gallardo 5d8c11e741 fix: sync html lang with current user locale (#2165) 2026-05-08 21:15:04 +01:00
Philip Okugbe de60aa7e61 feat: synced blocks (transclusion) (#2163)
* feat: synced blocks (transclusion)

* fix:remove name

* make placeholders smaller

* feat: enforce strict transclusion schema

* fix: scope synced blocks to workspace, gate unsync on edit permission

* fix collab module error
2026-05-08 13:23:16 +01:00
Peter Tripp c9fa6e20b3 Add alias: /toc and /ol (#2161) 2026-05-08 01:20:27 +01:00
Philipinho ec51ca7815 fix request ip 2026-05-07 22:09:32 +01:00
Philipinho 2b63137217 mail 2026-05-07 18:13:24 +01:00
Philip Okugbe 696aa430f4 New translations translation.json (Portuguese, Brazilian)
[ci skip]
2026-05-04 23:55:59 +01:00
Philip Okugbe ed9d85ca95 New translations translation.json (English)
[ci skip]
2026-05-04 23:55:58 +01:00
Philip Okugbe d992abf8d8 New translations translation.json (Chinese Simplified)
[ci skip]
2026-05-04 23:55:57 +01:00
Philip Okugbe 149bcc14f1 New translations translation.json (Ukrainian)
[ci skip]
2026-05-04 23:55:55 +01:00
Philip Okugbe 8ed4f4b7aa New translations translation.json (Russian)
[ci skip]
2026-05-04 23:55:54 +01:00
Philip Okugbe fd0200331c New translations translation.json (Dutch)
[ci skip]
2026-05-04 23:55:53 +01:00
Philip Okugbe 44ab6e5485 New translations translation.json (Korean)
[ci skip]
2026-05-04 23:55:51 +01:00
Philip Okugbe 505d7923db New translations translation.json (Japanese)
[ci skip]
2026-05-04 23:55:50 +01:00
Philip Okugbe 3112709d03 New translations translation.json (Italian)
[ci skip]
2026-05-04 23:55:48 +01:00
Philip Okugbe 8c9d0389f4 New translations translation.json (Spanish)
[ci skip]
2026-05-04 23:55:47 +01:00
Philip Okugbe afafc8451f New translations translation.json (French)
[ci skip]
2026-05-04 23:55:46 +01:00
Philip Okugbe 0a4bbd5d30 New translations translation.json (German)
[ci skip]
2026-05-04 23:55:44 +01:00
Philipinho 3227bc6059 fix: a11y 2026-05-04 23:04:26 +01:00
Philip Okugbe 73dc62bca3 update react-email (#2149) 2026-05-04 22:26:53 +01:00
Philip Okugbe c802222562 New translations translation.json (Portuguese, Brazilian)
[ci skip]
2026-05-04 22:16:52 +01:00
Philip Okugbe be2d93877a New translations translation.json (English)
[ci skip]
2026-05-04 22:16:50 +01:00
Philip Okugbe 34da8d3fb4 New translations translation.json (Chinese Simplified)
[ci skip]
2026-05-04 22:16:49 +01:00
Philip Okugbe f55bd21b08 New translations translation.json (Ukrainian)
[ci skip]
2026-05-04 22:16:47 +01:00
Philip Okugbe f6f9cc14df New translations translation.json (Russian)
[ci skip]
2026-05-04 22:16:45 +01:00
Philip Okugbe 1790ee8f6f New translations translation.json (Dutch)
[ci skip]
2026-05-04 22:16:44 +01:00
Philip Okugbe 58840da7f4 New translations translation.json (Korean)
[ci skip]
2026-05-04 22:16:43 +01:00
Philip Okugbe db77b31782 New translations translation.json (Japanese)
[ci skip]
2026-05-04 22:16:41 +01:00
Philip Okugbe 77ba4facb1 New translations translation.json (Italian)
[ci skip]
2026-05-04 22:16:40 +01:00
Philip Okugbe 6fa08e487e New translations translation.json (Spanish)
[ci skip]
2026-05-04 22:16:38 +01:00
Philip Okugbe 979e8faeee New translations translation.json (French)
[ci skip]
2026-05-04 22:16:37 +01:00
Philip Okugbe 1839418430 New translations translation.json (German)
[ci skip]
2026-05-04 22:16:35 +01:00
Philipinho 3c74bb3dee update package 2026-05-04 22:09:19 +01:00
Philip Okugbe dbe6c2d6ba feat: A11y fixes (#2148) 2026-05-04 21:21:37 +01:00
Sarthak Chaturvedi fe18f22dc6 fix: prevent code block deletion when adding inline comments in read mode (#2146) 2026-05-04 21:14:21 +01:00
Philipinho fcef0c6b96 fix: S3 2026-05-04 20:57:35 +01:00
Philipinho 17f3158a3b update aws packages 2026-05-01 20:00:20 +01:00
Philip Okugbe 94461e90a3 New translations translation.json (Portuguese, Brazilian)
[ci skip]
2026-05-01 16:02:26 +01:00
Philip Okugbe 58aa02340e New translations translation.json (English)
[ci skip]
2026-05-01 16:02:24 +01:00
Philip Okugbe 592e6a39e8 New translations translation.json (Chinese Simplified)
[ci skip]
2026-05-01 16:02:23 +01:00
Philip Okugbe 56526c6c1c New translations translation.json (Ukrainian)
[ci skip]
2026-05-01 16:02:21 +01:00
Philip Okugbe 6f9387b8b4 New translations translation.json (Russian)
[ci skip]
2026-05-01 16:02:20 +01:00
Philip Okugbe aa2ca3ef91 New translations translation.json (Dutch)
[ci skip]
2026-05-01 16:02:18 +01:00
Philip Okugbe 21848b91bf New translations translation.json (Korean)
[ci skip]
2026-05-01 16:02:17 +01:00
Philip Okugbe 989231d818 New translations translation.json (Japanese)
[ci skip]
2026-05-01 16:02:15 +01:00
Philip Okugbe d50986453b New translations translation.json (Italian)
[ci skip]
2026-05-01 16:02:14 +01:00
Philip Okugbe 2c21af4e91 New translations translation.json (Spanish)
[ci skip]
2026-05-01 16:02:12 +01:00
Philip Okugbe 574f687335 New translations translation.json (French)
[ci skip]
2026-05-01 16:02:10 +01:00
Philip Okugbe 9956a98d1f New translations translation.json (German)
[ci skip]
2026-05-01 16:02:08 +01:00
Philipinho b74ca00bfd sync 2026-05-01 14:57:32 +01:00
Philip Okugbe c247d4c1e3 feat(ee): PDF import (#2142)
* feat: replace pdfjs-dist with firecrawl-pdf-inspector

* use modified firecrawl-pdf-inspector

* feat: pdf import

* increase single file upload size limit

* use npm package

* sync

* update package
2026-05-01 14:56:39 +01:00
Philip Okugbe 641ce142df feat(ee): SCIM (#1347)
* SCIM - init (EE)

* accept db transaction

* sync

* Content parser support for scim+json

* patch scimmy

* sync

* return early if userIds is empty

* sync

* SCIM db table

* fixes

* scim tokens

* backfill

* feat(audit): add scim token events

* rename scim migration

* fix

* fix translation

* cleanup
2026-05-01 14:53:30 +01:00
Sarthak Chaturvedi 1d2486455f fix: prevent browser tab fallback in editor (#2123) 2026-05-01 13:58:51 +01:00
Philipinho a0aea43e25 feat(saml): allow disabling RequestedAuthnContext via env var
Adds SAML_DISABLE_REQUESTED_AUTHN_CONTEXT env var, passed through
    to the SAML strategy's disableRequestedAuthnContext option.
    Defaults to existing behavior (element sent). Set to true to omit
    the element when the IdP authenticates the user with a method that
    does not match (e.g. MFA, FIDO, passwordless), which would
    otherwise cause AADSTS75011 with Microsoft Entra ID.
2026-05-01 11:47:03 +01:00
Philip Okugbe 09c69d7a0f feat: properly preserve table width (#2143) 2026-05-01 00:49:31 +01:00
Philip Okugbe 14fd3eb956 New translations translation.json (German)
[ci skip]
2026-05-01 00:49:11 +01:00
Sarthak Chaturvedi 9943e104a5 fix(i18n): Correct German column count label rendering (#2131) 2026-05-01 00:37:59 +01:00
Peter Tripp b16f1e5a55 fix: ctrl-k behavior on macOS (#2052)
* Improve cmd-k / ctrl-k behavior

Use cmd-k on macOS/iOS for search and keep ctrl-k everywhere else.

Fixes a bug where ctrl-k on macOS, which cuts to the end of the line,
was also triggering the search prompt.

* comment submit: cmd-enter (mac) / ctrl-enter (win/linux)
2026-05-01 00:36:40 +01:00
Philip Okugbe 24be90b95f fix: duplicate PDF uploads (#2139) 2026-04-29 10:01:47 +01:00
Olivier Lambert 3ecf27c6b0 fix(page-permission): make people-with-access list scroll past 4 entries (#2137)
The "People with access" list in the page share modal used
<ScrollArea mah={250}>, which caps the container height but does not
make the inner viewport scroll (no fixed height is given to the
viewport). Items beyond ~4 entries were rendered correctly but clipped
out of view.

Switches to <ScrollArea.Autosize mah={400}>, which is Mantine's
dedicated primitive for "grow with content up to a max, then scroll".

Closes #2135
2026-04-29 09:36:38 +01:00
Philipinho 980521f957 v0.80.1 2026-04-27 16:06:32 +01:00
Philipinho fe44dc92a9 sync 2026-04-27 15:51:23 +01:00
Philip Okugbe fad410ef23 chore: add undici for oidc proxy support (#2132) 2026-04-27 15:50:42 +01:00
Philipinho 15b8908b1a update postcss 2026-04-27 15:23:47 +01:00
Philipinho 8e15b22d8c package updates 2026-04-27 15:22:02 +01:00
Philipinho ec83fc82d5 fix: refactor sanitize 2026-04-27 15:16:26 +01:00
Philipinho a573acedd0 fix: local storage, and package overrides 2026-04-22 14:13:25 +01:00
Philipinho dba8e315ab override 2026-04-14 17:59:59 +01:00
Philipinho 81ae7a17a6 confirm dialog 2026-04-14 17:56:36 +01:00
Philipinho 271f855761 v0.80.0 2026-04-14 17:08:44 +01:00
Philipinho 3e6d915227 sync 2026-04-14 16:34:44 +01:00
Philip Okugbe a6a7e4370a feat(ee): PDF export api (#2112)
* feat(ee): server side PDF export

* feat: pdf export queue

* sync

* sync
2026-04-14 16:26:54 +01:00
Philip Okugbe cc00e77dfb fix: space overview favorites (#2110) 2026-04-14 02:58:24 +01:00
Philipinho 66c70c0e76 fix print 2026-04-14 00:40:17 +01:00
Philip Okugbe 0e8b3bbfb3 New Crowdin updates (#2109)
* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2026-04-14 00:05:51 +01:00
Philip Okugbe a3a9f35005 fix home flickers (#2108) 2026-04-13 23:54:03 +01:00
Philip Okugbe 4056bd0104 feat: enhancements (#2107)
* refactor
* fix
* update packages
2026-04-13 23:34:40 +01:00
Philip Okugbe bd68e47e03 feat(ee): page verification workflow (#2102)
* feat: page verification workflow

* feat: refactor page-verification

* sync

* fix type

* fix

* fix

* notification icon

* use full word

* accept .license file

* - update templates
- update migration and notification

* fix copy

* update audit labels

* sync

* add space name
2026-04-13 20:20:34 +01:00
Philip Okugbe d6068310b4 Merge commit from fork
Refactor link.ts to simplify HTML parsing and rendering logic.
2026-04-13 01:09:36 +01:00
Philipinho e02661974e sync 2026-04-13 00:13:18 +01:00
Philip Okugbe 1113f17a43 New Crowdin updates (#2104) 2026-04-12 22:46:39 +01:00
Philip Okugbe d42091ccb1 feat: favorites (#2103)
* feat: favorites and templates(ee)

* rename migrations

* fix sidebar

* cleanup tabs

* fix

* turn off templates

* cleanup

* uuid validation
2026-04-12 22:06:25 +01:00
Philip Okugbe 57efb91bd3 feat(ee): ai chat (#2098)
* feat: ai chat

* feat: ai chat

* sync

* cleanup

* view space button
2026-04-10 19:23:47 +01:00
Philip Okugbe da9b43681e feat: watch space (#2096) 2026-04-09 00:37:51 +01:00
Philipinho 4966f9b152 fix(deps): package updates 2026-04-07 10:24:46 +01:00
Philipinho e1bbceb9a6 fix: logs 2026-04-07 10:10:41 +01:00
Philip Okugbe 895c1817ae feat: bug fixes (#2084)
* handle enter in inline code

* fix: duplicate comment cache

* track link nodes (backlinks)

* fix en-US translation

* fix internal a-links

* overrides

* 0.71.1
2026-04-05 13:45:36 +01:00
Philip Okugbe 642024ba9d New Crowdin updates (#2078) 2026-03-31 21:14:41 +01:00
Philipinho 147d028036 v0.71.0 2026-03-31 20:42:37 +01:00
Philipinho 992691e6e0 fix module import 2026-03-31 20:41:09 +01:00
Philip Okugbe 9aaa6c731c feat: add AI_EMBEDDING_SUPPORTS_MRL env var to decouple pgvector dimensions from model API (#2079)
Some embedding models don't accept a `dimensions` parameter. This adds
an optional env var that controls whether the dimension is sent to the
model API, while always using it for pgvector indexing. Preset models
have this handled automatically; the env var allows explicit override
for custom models.
2026-03-31 19:39:49 +01:00
Philipinho fd91b11c6c pin version 2026-03-31 16:06:44 +01:00
Philipinho af8b0ddf3a sync 2026-03-31 16:05:09 +01:00
Philip Okugbe 879aa2c3d8 feat: page update notifications (#2074)
* feat: watchers notification and email preferences

* fix: email copy

* digests

* clean up

* fix

* clean up

* move backlinks queue-up to history processor

* fix

* fix keys

* feat: group notifications

* filter

* adjust email digest window
2026-03-31 16:03:59 +01:00
Philip Okugbe c180d0e487 feat: ratelimits (#2073)
* feat: rate limits

* ip
2026-03-30 15:38:44 +01:00
Philip Okugbe a062f7a165 fix: enhance confluence importer (#2072)
* fix placeholder

* min resize dimensions

* fix media links

* fix
2026-03-30 13:16:40 +01:00
Philip Okugbe cbd0dd4a0b feat: indexes (#2071) 2026-03-29 20:29:12 +01:00
Philip Okugbe 2d6d829581 New translations translation.json (English) (#2066) 2026-03-29 16:25:45 +01:00
Philipinho 5cea30cc5c fix markdown paste 2026-03-29 16:11:21 +01:00
Philipinho bca85a49d6 pin marked version 2026-03-29 03:03:35 +01:00
Philipinho c9cdfa0f17 fix 2026-03-29 02:20:56 +01:00
Philip Okugbe 412962204c fix: editor fixes (#2067)
* autojoiner

* fix marked

* return clipboardTextSerializer as markdown

* fix clipboardTextSerializer for single lines

* cleanup two preceeding spaces in ordered lists item

* fix extra paragraph in task list

* don't zip sinple page exports
2026-03-29 02:19:09 +01:00
Olivier Lambert a42ac3d450 fix: strip trailing whitespace-only paragraphs from pasted content (#2050) 2026-03-28 22:26:47 +00:00
Philipinho 642c92f779 fix select 2026-03-28 20:34:44 +00:00
Philipinho ccb35517bb sync 2026-03-28 20:29:31 +00:00
Philip Okugbe cbdb37ed0a New Crowdin updates (#2061) 2026-03-28 20:29:06 +00:00
Julien Fontanet aa27d57624 fix: notification items are now real links (#2039)
Replace UnstyledButton with UnstyledButton component={Link} so each
notification renders as a real anchor element. Regular left-clicks use
SPA navigation and close the popover; Ctrl/Cmd/middle-click open the
page in a new tab. All click types mark the notification as read.
2026-03-28 20:23:21 +00:00
Philip Okugbe 3829b6cbef feat(ee): viewer comments (#2060) 2026-03-28 19:32:52 +00:00
Philipinho 17da762984 overrides 2026-03-28 19:28:22 +00:00
Philipinho 859f16740b tooltip portal 2026-03-28 19:19:00 +00:00
Philip Okugbe 7981ef462e feat(editor): audio and PDF nodes (#2064)
* use local resizable

* feat: aduio

* support audio imports

* feat: use confluence real file names

* cleanup

* error handling

* hide notice

* add audio

* fix pulse

* Fix import and export

* unify pulse

* hide in readonly mode

* keywords

* keyword

* translations

* better sort

* feat: PDF embed

* cleanup

* remove audio menu

* open active

* hide focus on readonly mode

* increase iframe default dimension
2026-03-28 17:33:29 +00:00
Philip Okugbe 2d835da0e3 New Crowdin updates (#2059) 2026-03-27 22:11:19 +00:00
Philipinho a3559b7c33 sync 2026-03-26 20:01:02 +00:00
Philip Okugbe 803f1f0b81 feat: user session management (#2056)
* user session management

* WIP

* cleanup

* license

* cleanup

* don't cache index

* rename current device property

* fix
2026-03-26 20:00:04 +00:00
Philipinho 4e8f533b91 override 2026-03-26 16:48:33 +00:00
Philipinho 7b0d8fe140 override 2026-03-26 16:46:40 +00:00
Philipinho 2f92278a9d sync 2026-03-26 16:35:05 +00:00
Philipinho 53608eae35 clean up ws 2026-03-26 13:59:17 +00:00
Philipinho 0e4a1e7419 enum validation 2026-03-26 00:41:38 +00:00
Philipinho 9125996e97 sync 2026-03-25 10:08:36 +00:00
Philip Okugbe fa4872e89e fix(deps): package updates (#2041)
* update
* overrides
* override
* fix page update mutation
* fix
* cleanup
* loader
* fix excalidraw package
* override
* fix regex
2026-03-25 10:07:01 +00:00
Philipinho 6d6f3a8a8e merge commit 2026-03-24 10:52:09 +00:00
Philip Okugbe 975b4dcaab feat: auth pages layout (#2042)
* auth pages layout
* exclude home route from redirect
* fix margin
2026-03-22 16:40:50 +00:00
Philip Okugbe 6683c515cf fix: make codeblock language detection performant (#2032)
* fix: make codeblock language detection performant
* lint
2026-03-17 20:40:22 +00:00
Philipinho cc5c800238 0.70.3 2026-03-17 14:29:09 +00:00
Philipinho cfaee93af9 fix 2026-03-17 14:28:22 +00:00
Philipinho 74eddb0638 v0.70.2 2026-03-16 13:49:50 +00:00
Philipinho 7c83a9d4f0 update dompurify 2026-03-16 13:49:20 +00:00
Philipinho 2678c4e279 fix 2026-03-16 00:32:30 +00:00
Philipinho b0bde4b375 feat: replace link popover with dedicated bubble menu 2026-03-16 00:26:03 +00:00
Philipinho 724e37d5b7 revert 2026-03-15 23:03:32 +00:00
Philipinho 33184e9d8d sync 2026-03-15 22:07:26 +00:00
Philip Okugbe 7520c329d0 fix notion importer (#2027)
* fix notion importer

* fix link selector on mobile
2026-03-15 22:06:40 +00:00
Philip Okugbe d7a5fda53c feat: better feature flags (#2026)
* feat: feature flag upgrade

* fix translations

* refactor

* fix

* fix
2026-03-15 22:05:32 +00:00
Philipinho 236a63dadc sync 2026-03-15 17:09:29 +00:00
Philip Okugbe 89b94e5d19 feat: refactor link menu (#2025)
* link markview - WIP

* WIP

* feat: refactor links

* cleanup
2026-03-15 17:08:59 +00:00
Philip Okugbe 97c459be67 feat(cloud): add find-workspace and email verification endpoints (#2020)
* feat: add find-workspace and email verification endpoints
* sync
2026-03-14 13:36:30 +00:00
Philip Okugbe d0ed6865cb fix page level comment on mobile (#2018)
* add icon next to comment box
2026-03-14 01:01:24 +00:00
Philip Okugbe 65b89a1b24 fix email button (#2017) 2026-03-14 00:40:32 +00:00
Philip Okugbe 1fdee33206 feat(editor): add auto-save and unsaved changes protection for diagrams (#2011)
* feat(editor): add auto-save and unsaved changes protection for diagrams
* 30 seconds
2026-03-13 17:58:29 +00:00
Philip Okugbe 7b69727a30 fix shared page mention view for non-logged in users (#2008) 2026-03-11 19:25:27 +00:00
Philip Okugbe 66c26af34b noop audit module (#1994) 2026-03-05 09:29:39 +00:00
Philip Okugbe b4f009513e fix: resize handle clipping (#1990) 2026-03-04 12:24:46 +00:00
Philipinho fcffa3dfa0 fix media 2026-03-04 12:08:08 +00:00
Philipinho 1980b94825 0.70.1 2026-03-04 11:57:31 +00:00
Philip Okugbe bea1637519 fix: image fallback regression (#1989)
* fix: image fallback regression

* fix image preview on upload

* fix image loading
2026-03-04 11:51:43 +00:00
Philip Okugbe 37355452e1 update release workflow 2026-03-03 20:25:39 +00:00
Philipinho 057360c6be fix: validate import size 2026-03-03 20:00:05 +00:00
Philipinho f12bfc1ff7 fix menu positioning 2026-03-03 18:28:55 +00:00
Olivier Lambert f5d794220e fix: resolve keystroke input being swallowed after link in Firefox (#1922)
* fix: resolve keystroke input being swallowed after link in Firefox

In Firefox, when the cursor is at the right boundary of a link mark,
contenteditable inserts new text inside the <a> element. ProseMirror
then rejects the DOM mutation because the link mark has inclusive: false,
causing keystrokes to be silently swallowed. Unlike Chrome, Firefox also
does not fire ProseMirror's handleTextInput callback in this state.

This adds a ProseMirror plugin that intercepts printable character
keydowns at link mark boundaries and programmatically inserts the text
without the link mark, bypassing Firefox's native contenteditable
behavior entirely.

Fixes #1773

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve keystroke input being swallowed before a link in Firefox

Extend the linkBoundaryInput plugin to also handle the left boundary
of links, where the cursor is just before a link (e.g. at the start
of a line). Firefox inserts text inside the <a> element in this case
too, causing ProseMirror to reject the mutation.

Fixes #1748
2026-03-03 17:19:03 +00:00
faruk-agentiqus a3c1c6cccd fix(editor): disable slash and emoji menus inside code blocks (#1897)
The slash command menu (/) and emoji menu (:) were incorrectly
triggering when typing inside code blocks, breaking keyboard
navigation and confusing users who type paths like /work or
symbols like := in their code.

Added an `allow` function to both SlashCommand and EmojiCommand
extensions that checks if the cursor is inside a code block and
disables the menu accordingly.

Closes #1730
2026-03-03 16:51:00 +00:00
MATHEUS LUIS LORSCHEITER 4b105586a9 fix(client): ensure sidebar remains visible on shared subpages (#1887)
* fix(client): ensure sidebar remains visible on shared subpages

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-03 16:48:53 +00:00
Philip Okugbe d2641db895 New Crowdin updates (#1984)
* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)
2026-03-03 16:44:49 +00:00
Philipinho 1111df65cd fix type 2026-03-03 16:22:00 +00:00
Philipinho e455154b7d fix 2026-03-03 16:14:35 +00:00
Philipinho ef24b3c07d feat: API key restriction 2026-03-03 16:07:08 +00:00
Philipinho 2352f3c5d9 sync 2026-03-03 14:44:16 +00:00
Philipinho 568dd4c321 fix headings 2026-03-03 14:17:51 +00:00
Philipinho b6478fee84 fix imports 2026-03-03 13:57:10 +00:00
Philip Okugbe 5d2aad3668 New Crowdin updates (#1978)
* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)
2026-03-03 00:22:51 +00:00
Philipinho 9331ac2df8 v0.70.0 2026-03-03 00:13:01 +00:00
Philipinho 9f4728e279 fix 2026-03-03 00:08:20 +00:00
Philipinho 628b08339a UI tweaks 2026-03-02 22:56:05 +00:00
Philipinho 68842dbea2 comments view 2026-03-02 22:18:57 +00:00
Philipinho b1510cd6d7 fix 2026-03-02 22:09:57 +00:00
Philipinho af92224e10 github actions 2026-03-02 21:52:58 +00:00
Philipinho c24ff44e09 fix(deps): update dependencies 2026-03-02 21:44:24 +00:00
Philipinho 90c190df78 fix: space members view enhancement 2026-03-02 21:33:15 +00:00
Philipinho 17ec2f4ac5 lists sorting 2026-03-02 21:07:47 +00:00
Philipinho 9881c53f00 feat: spaces and groups search 2026-03-02 20:40:38 +00:00
Philipinho 721651e2e2 feat: user deactivation 2026-03-02 19:05:10 +00:00
Philip Okugbe a3fd79dae8 fix: spreadsheets paste (#1982) 2026-03-02 17:37:56 +00:00
Philipinho 616d9297eb sync 2026-03-02 04:08:59 +00:00
Philipinho ee6b98edaa * enhance ai menu
* remove api prefix from mcp
2026-03-02 03:31:52 +00:00
Philipinho cf43e2b4fe feat: enhance embed resizer 2026-03-02 02:45:13 +00:00
Philipinho 614baf153b fix: show resize handle if node is selected 2026-03-02 01:57:06 +00:00
Philip Okugbe 4f3577f009 feat: enhance comments (#1980)
* feat: non-inline comments support

* enhance comments

* fix types
2026-03-02 01:42:25 +00:00
Philipinho d5e4b8bb59 fix ui 2026-03-01 20:58:04 +00:00
Philipinho 1a897faaa2 exclude events 2026-03-01 19:13:56 +00:00
Philipinho 6f1a91cc05 sync 2026-03-01 18:38:43 +00:00
Philip Okugbe 60848ea903 feat(ee): mcp (#1976)
* feat: MCP
* sync
* sync
2026-03-01 18:37:39 +00:00
Philip Okugbe 2309d1434b feat: support cross-space page mentions (#1979) 2026-03-01 17:14:10 +00:00
Philipinho dcc2bacb22 sync 2026-03-01 01:31:10 +00:00
Philip Okugbe 69d7532c6c feat(ee): audit logs (#1977)
feat: clickhouse driver
* sync
* updates
2026-03-01 01:29:03 +00:00
Philip Okugbe 85ce0d32bf New Crowdin updates (#1960)
* New translations translation.json (Russian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Dutch)

* New translations translation.json (Dutch)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2026-02-28 01:24:57 +00:00
Philip Okugbe fc0997fd90 feat: editor attachment paste handling (#1975)
* reupload attachments if uploaded to a different page
* use image dimensions on paste/DnD

* tooltips withinPortal:false

* isolating attribute
2026-02-28 01:24:19 +00:00
Philipinho df64de5306 fix focus 2026-02-27 01:38:43 +00:00
Philip Okugbe ea44468fad feat: editor inline status node (#1973)
* inline status node

* fix alignment

* fix

* typed storage

* fix math block popup on select all
2026-02-27 01:34:03 +00:00
Philip Okugbe 59e945562d feat(ee): page-level access/permissions (#1971)
* Add page_hierarchy table

* feat(ee): page-level permissions

* pagination

* rename migration
fixes

* fix

* tabs

* fix theme

* cleanup

* sync

* page permissions notification
* other fixes

* sharing disbled

* fix column nodes

* toggle error handling
2026-02-26 19:49:10 +00:00
Philipinho 22f33bab7c cleanups 2026-02-25 22:41:54 +00:00
Philipinho e0a8521566 enhance columns 2026-02-25 22:31:01 +00:00
Philip Okugbe b5803f42da xwiki html import cleanup (#1969) 2026-02-24 15:53:38 +00:00
Olivier Lambert 5de1c8e3ed fix: inline code input rule deletes character before opening backtick (#1923)
The upstream TipTap Code extension input rule regex /(^|[^`])`([^`]+)`(?!`)$/
uses a capture group (^|[^`]) that includes the character preceding the
opening backtick in the full match. When markInputRule processes this,
it deletes everything from the match start to the code content, which
removes that preceding character along with the backtick delimiters.

For example, typing foo(`bar` would result in foo`bar` (formatted)
instead of the expected foo(`bar` (formatted) — the ( is lost.

Fix: disable the built-in Code extension from StarterKit and register it
separately with a corrected regex that uses a lookbehind assertion
(?:^|(?<=[^`])) instead of a capture group. The lookbehind asserts the
preceding character without including it in the match, so markInputRule
only deletes the backtick delimiters.

Functionally tested on Firefox and Chrome.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:51:24 +00:00
Philip Okugbe ef87210b3d feat: editor UI refresh and enhancements (#1968)
* feat: new image menu
* switch to resizable side handles
* use pixels

* refactor excalidraw and drawio menu

* support image resize undo

* video resize

* callout menu refresh

* refresh table menus

* fix color scheme

* fix: patch @tiptap/core ResizableNodeView to prevent resize sticking after mouseup

* feat: columns

* notes callout

* focus on first column

* capture tab key in column

* fix print

* hide columns menu when some nodes are focused

* fix print

* fix columns

* selective placeholder

* fix blockquote

* quote

* fix callout in columns
2026-02-24 15:22:37 +00:00
Philipinho c172d3bd5e fix 2026-02-21 00:43:49 +00:00
Philip Okugbe 53132acb0a fix: redirect to original page after re-authentication (#1959)
* fix: redirect to original page after re-authentication

When a session expires, the current URL is now preserved as a query
parameter on the login page. After successful login (including MFA
flows), the user is redirected back to their original page instead of
always landing on /home.

* secure

---------

Co-authored-by: Julien Fontanet <julien.fontanet@isonoe.net>
2026-02-21 00:02:23 +00:00
b4sh2 d6472f0876 Merge commit from fork
Co-authored-by: b4sh2 <b4sh2@users.noreply.github.com>
2026-02-20 16:59:44 +00:00
Philipinho 873c963043 fix db types duplication 2026-02-19 22:34:07 +00:00
Julien Fontanet 03a70d768a fix: allow deleting last character in headings (#1954)
The copy-link decoration widget (contentEditable="false") injected
inside headings prevented browsers from deleting the last remaining
character via Backspace or Delete keys. Only show the widget when the
heading has more than one character of content.
2026-02-18 13:48:15 +00:00
Philip Okugbe 0aeaa43112 feat: replace sharp with client-side icon resize (#1951) 2026-02-16 19:48:19 +00:00
Philip Okugbe 92d5d0b237 New Crowdin updates (#1950)
* New translations
2026-02-16 04:22:40 +00:00
Philipinho 0ce74d34de env validation 2026-02-16 04:11:19 +00:00
Philipinho 00b5328676 fix page error boundary 2026-02-16 04:06:41 +00:00
Philipinho 2ebdc2baea empty states 2026-02-16 00:33:16 +00:00
Philip Okugbe 621ef4f0cf New Crowdin updates (#1948)
* New translations
2026-02-15 23:10:32 +00:00
Philipinho 26b9338da5 sync 2026-02-15 23:04:18 +00:00
Philipinho 618f56577d turn into callout option 2026-02-15 22:51:23 +00:00
Philipinho 0a05ce6133 enhance editor bubble menu 2026-02-15 22:39:42 +00:00
Philipinho cb9d6be3b9 sync 2026-02-15 17:07:27 +00:00
Arek Nawo b76f5adaad feat(ee): AI menu (#1912)
* feat(ee): AI menu

* - Add insert below and copy option

* prebuild @editor-ext

* sanitize output

* clear existing output

* switch to menu component

* refactor directory

* separator

* refactor directory

* support more languages

* pass markdown to model

* fix: close AI menu on page change

* enhance text input and preview styling

* fix: Use absolute positioning for the AI menu

* make preview scrollable

* activation controls

* enhance bubble menu

* sync

* set width

* fix line break

* switch terminologies

* cloud

* buffer

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-02-14 20:58:08 -08:00
Philipinho 41fa77b29d sync 2026-02-14 20:03:35 -08:00
Philip Okugbe 05b3c65b0f feat: notifications (#1947)
* feat: notifications
* feat: watchers

* improvements

* handle page move for watchers

* make watchers non-blocking

* more
2026-02-14 20:00:38 -08:00
Philipinho e0ab9d9b5e override package 2026-02-14 10:37:11 -08:00
Philipinho 55280db672 dark color theme tweaks 2026-02-14 10:35:03 -08:00
Philipinho 32bbc6911f override qs 2026-02-12 11:46:28 -08:00
Philipinho 5814542128 update lock file 2026-02-12 11:41:24 -08:00
Philip Okugbe 18b5781522 feat(API): page content update and retrieval (#1937)
* feat: page content update and retrieval output

* import module

* refactor naming
* support prepend

* rename contentOperation -> operation

* dry

* add yjs utils
2026-02-12 11:13:47 -08:00
Philipinho 49ab9875ba fix tiptap version conflicts 2026-02-11 22:47:25 -08:00
Philipinho 25f4b8c2b4 fix 2026-02-11 17:47:30 -08:00
Philipinho 4d43f86c51 update deps 2026-02-11 17:43:13 -08:00
Philip Okugbe f170ede8da fix(deps): override packages (#1936)
* override packages
2026-02-11 16:48:26 -08:00
Philipinho 7861b5b186 fix: add RedisModule to CollabAppModule 2026-02-09 18:50:31 -08:00
Philipinho 3a9bdfbb06 fix(deps): update vite and nx 2026-02-09 18:32:09 -08:00
Philipinho ab7999a946 v0.25.3 2026-02-09 18:27:55 -08:00
Philip Okugbe 0f02261ee6 feat: page version history improvements (#1925)
* Refactor: use queue for page history

* feat: save multiple version contributors

* display contributor avatars in history list

* fix interval
2026-02-09 18:25:35 -08:00
Philip Okugbe aff8dba2cb fix: diagrams SVG content length (#1928) 2026-02-09 18:20:09 -08:00
Olivier Lambert f6a8247c48 fix: cursor jumps to end of text when editing a comment (#1924)
* fix: cursor jumps to end of text when editing a comment

When editing a comment mid-text, the cursor would jump to the end after
every keystroke, making it impossible to insert text at any position
other than the end.

Root cause: on each keystroke, the comment editor's onUpdate callback
updated parent state (setContent), which changed the defaultContent prop
passed back to CommentEditor. A useEffect watching defaultContent then
called commentEditor.commands.setContent(), which reset the entire
editor content and moved the cursor to the end.

Fix:
- Store in-progress edits in a ref instead of state to avoid triggering
  React re-renders and the prop->effect->setContent cascade
- Read from the ref when saving the comment
- Sync the ref back into state after a successful save so the read-only
  view updates immediately
- Guard the setContent useEffect to only run for read-only editors, so
  websocket-driven updates from other browsers still work

Fixes #1791

Functionally tested on Firefox and Chrome: mid-text editing, saving,
cross-browser live updates via websocket.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix stale content on edit cancel

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-02-09 15:16:40 -08:00
Philip Okugbe 7879e1f600 fix: add execCommand fallback for clipboard (#1927)
* fix: add execCommand fallback for clipboard
2026-02-09 14:44:27 -08:00
Philip Okugbe 3cb70f0696 New translations translation.json (German) (#1915) 2026-02-06 11:37:33 -08:00
Philipinho fbb44df548 v0.25.2 2026-02-06 11:32:00 -08:00
Philip Okugbe bc3ce893c4 New Crowdin updates (#1914)
* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2026-02-06 11:31:12 -08:00
Philipinho ae96352189 sync 2026-02-06 10:37:51 -08:00
Philip Okugbe 1ad53c2581 feat(ee): public sharing controls (#1910)
* feat(ee): public sharing controls
* lint
2026-02-06 10:35:36 -08:00
Philip Okugbe 2f97a3debc feat: DOCX import (#1913) 2026-02-06 10:34:51 -08:00
Philipinho 40b5346f9e cleanup redundant param 2026-02-06 10:28:52 -08:00
Philipinho d6b4573b79 update compose services versions 2026-02-06 10:27:34 -08:00
Philip Okugbe 4878850b25 fix: attachment bugs in safari(#1908)
* use widely available arrayBuffer
* fix stream fails in safari
* fix hasFocus bug
* fix safari upload bug
* feat: add HTTP range request support for file serving
2026-02-05 07:47:03 -08:00
Philip Okugbe 5c3942c159 fix safari print (#1907) 2026-02-04 08:26:03 -08:00
Philipinho e0809e7104 v0.25.1 2026-02-04 07:10:13 -08:00
Philipinho da6793ac87 downgrade tiptap version (fix menu) 2026-02-04 07:09:48 -08:00
Philip Okugbe 08e94eb3c1 update dependencies (#1902) 2026-02-03 15:15:23 -08:00
Philipinho 5a14186f1c fix global diff css 2026-02-03 13:47:56 -08:00
Philipinho 6a0bb8d4cb v0.25.0 2026-02-03 13:18:03 -08:00
Philip Okugbe fba9f4cb2b New Crowdin updates (#1896)
* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2026-02-03 13:16:27 -08:00
Philipinho d8f7c4a822 cleanup 2026-02-03 13:12:39 -08:00
Philipinho 202685b39f fix translation 2026-02-03 13:09:56 -08:00
Philip Okugbe fc4a428208 fix(deps): update dependencies (#1898) 2026-02-03 13:04:00 -08:00
Philip Okugbe 5506eb194b feat: page history diff (#1891)
* Show actual history changes
* V2 - WIP
* feat: page history diff
* fix: exclude content from history listing

---------

Co-authored-by: Jason Norwood-Young <jason@10layer.com>
2026-02-03 11:55:20 -08:00
Philipinho f32bb298e0 v0.25.0-beta.1 2026-01-30 23:09:01 +00:00
Pleasure1234 3178cad796 fix: handle empty replace term in search and replace functionality (#1562)
- Fix 'Empty text nodes are not allowed' error when replace field is empty
- Update both replace() and replaceAll() functions to check for empty replaceTerm
2026-01-30 22:37:22 +00:00
Philipinho 9d7f8c62c5 sync 2026-01-30 22:31:49 +00:00
Philip Okugbe 78b1c1a453 feat: switch to cursor pagination (#1884)
* add cursor pagination function

* support custom order modifier
* refactor returned object

* feat(db): migrate paginated endpoints to cursor-based pagination

* sync

* support hasPrevPage boolean

* feat(client): migrate pagination from offset to cursor-based

* support beforeCursor/prevCursor

* wrap search results in items array for API consistency
2026-01-30 19:28:54 +00:00
Philip Okugbe 96ed98619f feat: add IPv6 support via configurable HOST binding (#1885) 2026-01-30 00:33:10 +00:00
Philip Okugbe 60501de992 fix: missing logs on OnApplicationBootstrap hook (#1882)
* - fix: set default Nest logger and bufferLogs to false for pino compatibility
- handle redis error event

* fix collab server logging too
2026-01-29 09:25:23 +00:00
Philip Okugbe 74e915546b feat: collab redis extension with server affinity (#1873)
* feat(collab): better redis extension
* move types to own file
* debug logging
* fix: graceful collab shutdown
* rename default prefix
* pass wsAdapter to gateway
* expose event handler
* unique collab serverId generation
* uninstall @hocuspocus/extension-redis package
* expose more functions
* sync with latest
* cleanup
* fastify router options
* cleanup type
2026-01-27 17:05:05 +00:00
Philipinho 3523600f40 add timestamps 2026-01-27 16:49:22 +00:00
Philip Okugbe 6ccb2bb872 feat(export): add metadata file to preserve page icons and ordering on import (#1877)
* feat(export): add metadata file to preserve page icons and ordering on import
- Export includes `docmost-metadata.json`
- Import reads metadata to restore icons and sort siblings by original position

* cleanup

* bonus fixes

* handle unknown prosemirror nodes

* add docmost app  version
2026-01-27 16:39:39 +00:00
Philipinho 0245a183e1 sync 2026-01-26 02:08:54 +00:00
Philip Okugbe de5f71894a New Crowdin updates (#1869)
* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2026-01-25 12:39:19 +00:00
Philip Okugbe 351b075ebb fix(tree): update sidebar-pages cache directly instead of refetching on page move (#1870) 2026-01-25 12:38:44 +00:00
Philipinho 1ca7d42203 fix switch space toggle 2026-01-25 02:49:25 +00:00
Philipinho 1e441560f6 fix production logs filter 2026-01-25 02:15:10 +00:00
Philip Okugbe 54775f537d fix: handle malformed URLs gracefully during import/export (#1868)
* Handling malformed URLs gracefully

* Allow import of invalid URLs, but adding logging.

---------

Co-authored-by: gpapp <gergely.papp@itworks.hu>
2026-01-25 00:48:43 +00:00
Philipinho 5dbf0027bd Add isomorphic basename utility 2026-01-25 00:08:02 +00:00
Philip Okugbe 5588ec34fb New Crowdin updates (#1866)
* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)
2026-01-25 00:04:50 +00:00
Philipinho 55b8128829 Fix Google sheets regex 2026-01-24 23:35:04 +00:00
Philip Okugbe aa6a046aa6 feat(export): add export loading state and copy as markdown (#1867)
* feat: add loading state to export

* feat: copy as markdown

* preserve taskList comment
2026-01-24 23:30:17 +00:00
Philip Okugbe 657fdf8cb7 feat: Tiptap V3 migration (#1854)
* Tiptap3 migration - WIP

* fix collaboration

* remove unused code

* fix flicker

* disable duplicate extensions

* update tiptap version

* Switch to useEditorState
- Set shouldRerenderOnTransaction to false

* fix editable state

* add tippyoptions for reference

* merge main

* tiptap 3.6.1

* fix bubble menu

* fix converter

* fix menus

* fix collaboration caret css

* fix: Set `isInitialized` to force immediate react node view rendering

* feat: Migrate tippy.js menus to Floating UI

* feat: Update collaboration connection for HocusPocus v3

* fix: Connect/disconnect websocketProvider

* cleanup

* cleanup

* feat: Improved placeholder and upload handling for images

* feat: Improved placeholder and upload handling for videos

* refactor: Image node and view clean-up

* feat: Improved placeholder and upload handling for attachments

* fix: Video view styles

* fix: Transaction handling on asset upload

* fix: Use imageDimensionsFromStream

* feat: Multiple file upload, improved placeholders, local previews

* fix: Drag & drop, paste upload

* fix: Allow media as attachment

* * add skeleton pulse animation
* add translation strings
* fix attachment view responsiveness

* fix collab connection status display

* Tiptap v3.17.0

* fix suggestion menu exit bug

* fix search shortcut

* fix history editor css

* tiptap 3.17.1

---------

Co-authored-by: Arek Nawo <areknawo@areknawo.com>
2026-01-24 20:41:08 +00:00
Philip Okugbe 98f71c95fe feat: stream file serving (#1865) 2026-01-24 17:54:56 +00:00
Philip Okugbe efb0a9317b feat: allow upload of large files (#1862)
* Allow upload of large files

* feat: createByteCountingStream utility function.

---------

Co-authored-by: gpapp <gergely.papp@itworks.hu>
2026-01-22 20:00:58 +00:00
Philipinho 063ea99b66 sync 2026-01-21 18:17:48 +00:00
Philip Okugbe aa143ad79c refactor(db): migrate from node-postgres to postgres.js (#1846)
* refactor(db): migrate from node-postgres to postgres.js
* ignore schema param
2026-01-21 18:12:16 +00:00
Philip Okugbe 918f4508d2 feat: switch to pino for logs (#1855)
- switch to json logs in production
- add option to support http logging
2026-01-21 01:23:50 +00:00
Philipinho 5cd0ba6902 fix script 2026-01-20 22:36:19 +00:00
Philipinho a1260188ae fix: UI improvements 2026-01-19 21:05:34 +00:00
Philipinho bdf02f593d Merge branch 'feat/auto-tooltip' 2026-01-19 19:43:58 +00:00
Philipinho e24bf5ed57 feat: auto-tooltip component 2026-01-19 19:40:06 +00:00
Philip Okugbe f3f74c591f fix(share): escape page title in SEO meta tags (#1850) 2026-01-19 19:31:28 +00:00
Philipinho 5f966a2d89 chore: add clean up command 2026-01-18 16:50:51 +00:00
Philipinho bcb004af21 update lockfile 2026-01-16 13:22:41 +00:00
Philipinho ac675e7d74 update dockerfile 2026-01-16 13:21:42 +00:00
Philipinho bf89eff5e7 sync 2026-01-16 13:20:31 +00:00
Philip Okugbe 183787fa0c fix: update dependencies (#1843) 2026-01-14 16:36:47 +00:00
Philipinho 15aa04a5f7 sync 2026-01-14 11:49:39 +00:00
Philipinho 79343a5d52 fix: prevent text overflow in group and space list tables 2026-01-13 16:25:42 +00:00
Philipinho 61e252918e fix length 2026-01-13 16:13:52 +00:00
Philipinho e98fa7f69a sync
* fix form length
2026-01-13 16:13:04 +00:00
Philip Okugbe 6d148a35eb New Crowdin updates (#1830)
* New translations translation.json (Japanese)

* New translations translation.json (Japanese)
2026-01-13 16:01:08 +00:00
Philip Okugbe 0bbc1c35de fix: public sharing performance improvements (#1841) 2026-01-13 16:00:22 +00:00
Philip Okugbe 47097969a0 fix: use subquery (#1833)
- enhance file tasks list endpoint
2026-01-13 15:58:26 +00:00
Philip Okugbe 13f529e064 fix anchor scroll in same page (#1834) 2026-01-13 15:35:53 +00:00
Philip Okugbe 8fc8422fbc fix: increase max length for groups and spaces (#1840) 2026-01-13 15:31:03 +00:00
Philipinho 732951a322 v0.24.1 2025-12-14 13:24:09 +00:00
Philipinho 2544775266 fix: switch to node slim image 2025-12-14 13:16:40 +00:00
Philipinho d59539f197 fix ai streaming 2025-12-13 14:15:41 +00:00
Philipinho b061df7f7d Use new fastify router options 2025-12-13 14:15:06 +00:00
Philipinho 0fe1459864 fix: override jsonwebtoken version 2025-12-12 17:25:27 +00:00
Philipinho 6af7956889 v0.24.0 2025-12-12 17:15:59 +00:00
Philip Okugbe 3dbb957bd7 New Crowdin updates (#1541)
* New translations translation.json (Dutch)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Russian)

* New translations translation.json (German)

* New translations translation.json (German)

* New translations translation.json (German)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Russian)

* New translations translation.json (Spanish)

* New translations translation.json (Korean)

* New translations translation.json (Korean)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2025-12-12 17:15:19 +00:00
Philipinho f39a4cf2d5 fix space modal spacing 2025-12-12 14:08:30 +00:00
Philipinho 724e01bd55 fix default page share state (API) 2025-12-11 20:43:26 +00:00
Philip Okugbe 6e350f6746 fix nodeview dragging (#1775) 2025-12-11 19:32:18 +00:00
Philip Okugbe cb9f27da9a fix mermaid security (#1774) 2025-12-11 16:44:52 +00:00
Philip Okugbe d2629afff2 feat: anchor links (#1765)
* feat: add heading extension with unique ID support and scroll functionality
* Added unique id for heading
* remove baseUrl heading storage
* move heading to extensions package
* WIP
* support anchors in mentions
* enhance scrolling functionality
* nodeId function
* fix nanoid import
* Bring unique-id extension local
* fixes
* fix internal link scroll in public pages
* add unique id server side
* rename mention anchor to anchorId
* capture first anchorId on paste

---------

Co-authored-by: Romik <40670677+RomikMakavana@users.noreply.github.com>
2025-12-06 14:46:54 +00:00
Philip Okugbe 9139d393ef fix: update tiptap packages (#1755)
* update tiptap version

* create empty paragraph on enter

* feat: split title text into page content on Enter

* update hocuspocus
2025-12-02 13:15:19 +00:00
Philipinho ab96672ecd fix 2025-12-02 13:14:03 +00:00
Philipinho 2ea3c2da58 sync 2025-12-01 14:05:59 +00:00
Philip Okugbe 9fb16bc842 feat(EE): AI vector search (#1691)
* WIP

* AI module - init

* WIP

* sync

* WIP

* refactor naming

* new columns

* sync

* sync

* fix search bug

* stream response

* WIP

* feat embeddings sync

* refine

* Add workspaceId to page events

* refine

* WIP

* add translation string

* sync

* reset ai answer on query change

* hide AI search in cloud

* capture streaming error

* sync
2025-12-01 11:50:25 +00:00
Philip Okugbe c3b350d943 fix: zip extraction validation (#1753)
* fix: zip extraction validation

* fix
2025-12-01 11:37:59 +00:00
Philip Okugbe 8014ba3ab7 feat: Text background highlight (#1754)
* #1196/feat: add text background highlight

* unify text color

* dark mode support
* unify text color and highlight

* dark mode support for color selector trigger

* fix see through in color selector dark mode

* fix selection highlight in dark mode

* brown color

* clean up

---------

Co-authored-by: sanua356 <sanek.pankratov356@gmail.com>
2025-12-01 11:34:35 +00:00
Philipinho ec3a04f7c7 fix 2025-11-29 12:37:35 +00:00
Philip Okugbe 04a17c9b92 package security updates (#1744)
* package security updates

* package updates
2025-11-29 11:50:20 +00:00
Philip Okugbe 520c07a0bc fix: generic page import hierarchy (#1747)
* fix page hierarchy

* fix
2025-11-29 11:50:02 +00:00
Philipinho 60a8ed6826 sync 2025-10-25 02:08:29 +01:00
Philip Okugbe f5684b792e fix duplicated page parenting (#1692) 2025-10-23 15:00:11 +01:00
Philipinho 042836cb6d sync 2025-10-07 21:09:55 +01:00
Philipinho 4f1f0ba513 fix 2025-10-07 21:06:59 +01:00
Philip Okugbe 3164b6981c feat: api keys management (EE) (#1665)
* feat: api keys (EE)

* improvements

* fix table

* fix route

* remove token suffix

* api settings

* Fix

* fix

* fix

* fix
2025-10-07 21:05:13 +01:00
Philipinho 16c1e864af fix comment space 2025-10-07 18:44:37 +01:00
Philipinho c9b1cad982 sync 2025-10-07 18:39:30 +01:00
Philip Okugbe bf8cf6254f feat: Typesense search driver (EE) (#1664)
* feat: typesense driver (EE) - WIP

* feat: typesense driver (EE) - WIP

* feat: typesense

* sync

* fix
2025-10-07 17:34:32 +01:00
Philip Okugbe 3135030376 fix editor converter (#1647) 2025-09-30 16:07:19 +01:00
Philip Okugbe 3fae41a5ca fix: editor performance improvements (#1648)
* Switch to useEditorState
* change shouldRerenderOnTransaction to false
2025-09-30 14:04:01 +01:00
Philipinho b50e25600a sync 2025-09-28 16:44:33 +01:00
Philipinho 1f3b0c7276 cloud fix 2025-09-24 21:25:39 +01:00
Philipinho 3c4cab0d2a v0.23.2 2025-09-18 18:00:28 +01:00
Philipinho 4de25a8b94 invalidate queries on space deletion 2025-09-18 15:52:53 +01:00
Philipinho cf5bbb10df fix import html processing 2025-09-18 15:34:13 +01:00
Philipinho ac17521717 sync 2025-09-18 13:24:16 +01:00
Philip Okugbe 9ac180f719 fix: enhance page import (#1570)
* change import process

* fix processor

* fix page name in notion import

* preserve confluence table bg color

* sync
2025-09-17 23:50:27 +01:00
Philipinho 46669fea56 (cloud) disable page sharing in trial mode 2025-09-17 23:36:13 +01:00
Pleasure1234 fe6ecdf1f1 fix: update combobox props in SpaceSelect component (#1564)
Added 'keepMounted: false' and 'dropdownPadding: 0' to comboboxProps for improved dropdown behavior and appearance in the SpaceSelect sidebar component.
2025-09-17 13:36:12 +01:00
Philipinho 04ae1d7270 Allow lastColumnResizable in table 2025-09-15 22:34:29 +01:00
Philip Okugbe 1280f96f37 feat: implement space and workspace icons (#1558)
* feat: implement space and workspace icons
- Create reusable AvatarUploader component supporting avatars, space icons, and workspace icons
- Add Sharp package for server-side image resizing and optimization
- Create reusable AvatarUploader component supporting avatars, space icons, and workspace icons
- Support removing icons

* add workspace logo support
- add upload loader
- add white background to transparent image
- other fixes and enhancements

* dark mode

* fixes

* cleanup
2025-09-15 21:11:37 +01:00
Philipinho 61d1cf88a7 fix: reset file inputs after import 2025-09-15 12:52:31 +01:00
Philipinho f413720e15 - sync
- reinstantiate S3 client to fix file upload errors during import
- delete import zip file after use
2025-09-14 03:00:23 +01:00
Philipinho 8e16ad952a v0.23.1 2025-09-13 03:15:53 +01:00
Philip Okugbe 7ada3cb1f9 fix: page import task (#1551)
* fix import

* - fix notion importer
- support notion page icon import
- fix horizontal rule css
- rename service file

* sync

* 3 mins delay
2025-09-13 03:14:59 +01:00
Philipinho 47c54174b3 sync 2025-09-11 00:50:15 +01:00
Philipinho dc0650289d sync 2025-09-04 15:07:01 -07:00
Philipinho 091e790b83 fix attachment search in cloud 2025-09-04 14:22:40 -07:00
Philipinho ae24ea29ba v0.23.0 2025-09-04 13:42:59 -07:00
Philipinho 9df6061e1a lock file 2025-09-04 13:42:33 -07:00
Philipinho 31053e2b20 update mermaid 2025-09-04 13:41:55 -07:00
Philipinho eb8e8507ea use debug 2025-09-04 13:27:15 -07:00
Philipinho c99bfb8ef1 make print better 2025-09-04 13:22:43 -07:00
Philipinho 26ea04e2a3 sync 2025-09-04 12:25:53 -07:00
Philipinho 6cc58c57f5 sync 2025-09-04 12:16:30 -07:00
Philipinho 7d2ff346fa UI fixes 2025-09-04 11:35:04 -07:00
Philipinho b08d37fbf0 fix 2025-09-04 10:57:17 -07:00
Philipinho d43ee77617 remove debug log 2025-09-04 09:40:17 -07:00
Philipinho 5d91eb4f5f feat: queue imported attachments for indexing 2025-09-04 09:38:30 -07:00
Quinten Van Damme 3e9f6b11cc Remove version from docker-compose.yml [deprecated] (#1011) 2025-09-04 03:55:32 +01:00
Hoie Kim db55de9406 feat: progressive web app (#614)
* feat: progressive web app

* replace icons

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-09-04 01:33:52 +01:00
Philip Okugbe 1919eba340 New Crowdin updates (#1522)
* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2025-09-03 13:17:08 -07:00
Philip Okugbe 7951b2e0c6 New Crowdin updates (#1509)
* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)
2025-09-03 18:28:30 +01:00
Philipinho 73b78f625d more translations 2025-09-03 10:11:19 -07:00
Philipinho cf7534de3d fix version display 2025-09-03 09:37:29 -07:00
Philipinho adec36d544 fix: adjust margins
- use default browser highlight background
2025-09-02 21:45:38 -07:00
Philipinho f9e10805f0 sync 2025-09-02 21:38:14 -07:00
Eshwar Tangirala 00e499b3e5 Fixing extra page bug on print (#1478) 2025-09-03 05:25:48 +01:00
Sarthak Mittal 5ee6e46535 checkbox aligned to text (#1486) 2025-09-03 05:23:28 +01:00
Philip Okugbe 1f797c3d27 fix: confluence drawio import (#1518)
* POC

* WIP - working

* WIP

* WIP

* sync

* fix drawio preview image
2025-09-03 05:19:09 +01:00
Philip Okugbe f12866cf42 feat(EE): full-text search in attachments (#1502)
* feat(EE): fulltext search in attachments

* feat: global search
- search filters
- attachments search ui
- and more

* fix import

* fix import

* rename migration

* add GIN index

* fix table name

* sanitize
2025-09-02 05:27:01 +01:00
Philip Okugbe dcbb65d799 feat(EE): LDAP integration (#1515)
* LDAP - WIP

* WIP

* add hasGeneratedPassword

* fix jotai atom

* - don't require password confirmation for MFA is user has auto generated password (LDAP)
- cleanups

* fix

* reorder

* update migration

* update default

* fix type error
2025-09-02 04:59:01 +01:00
Finn Dittmar 5968764508 feat: emoji callout icon (#1323) 2025-08-31 21:16:52 +01:00
Alexander Schaber 242fb6bb57 fix: set mermaid theme based on computed color scheme (#1438) 2025-08-31 20:48:59 +01:00
Philip Okugbe 74cd890bdd feat(EE): implement SSO group sync for SAML and OIDC (#1452)
* feat: implement SSO group synchronization for SAML and OIDC

- Add group_sync column to auth_providers table
- Extract groups from SAML attributes (memberOf, groups, roles)
- Extract groups from OIDC claims (groups, roles)
- Implement case-insensitive group matching with auto-creation
- Sync user groups on each SSO login
- Ensure only one provider can have group sync enabled at a time
- Add group sync toggle to SAML and OIDC configuration forms

* rename column
2025-08-31 20:33:37 +01:00
Philipinho 509622af54 ignore type error 2025-08-31 12:20:40 -07:00
Philipinho 937386e42b fix: hide table handles in readonly mode 2025-08-31 12:08:02 -07:00
Philipinho 60a373f488 fix: readonly editor table responsiveness 2025-08-31 12:04:27 -07:00
Philip Okugbe 73ee6ee8c3 feat: subpages (child pages) list node (#1462)
* feat: subpages list node

* disable user-select

* support subpages node list in public pages
2025-08-31 18:54:52 +01:00
Mirone 7d1e5bce0d feat: table row/column drag and drop (#1467)
* chore: add dev container

* feat: add drag handle when hovering cell

* feat: add column drag and drop

* feat: add support for row drag and drop

* refactor: extract preview controllers

* fix: hover issue

* refactor: add handle controller

* chore: f

* chore: remove log

* chore: remove dev files

* feat: hide other drop indicators when table dnd working

* feat: add auto scroll and bug fix

* chore: f

* fix: firefox
2025-08-31 18:53:27 +01:00
Philip Okugbe aa58e272d6 fix: exclude deleted pages (#1494) 2025-08-31 09:11:33 +01:00
Philipinho 08135a2fba sync 2025-08-12 11:09:26 -07:00
Philipinho d92a94244f sync 2025-08-12 10:21:17 -07:00
Philipinho 5012a68d85 sync 2025-08-06 10:19:35 -07:00
Philip Okugbe 5a3377790e feat: debug mode env variable (#1450) 2025-08-06 18:16:30 +01:00
Philip Okugbe 3b85f4b616 fix: enforce C collation for page position ordering to ensure consistent behavior in Postgres 17+ (#1446)
- Add explicit C collation to position ordering queries to fix incorrect page placement in PostgreSQL 17+
- Ensures consistent ASCII-based ordering regardless of database locale settings
- Fixes issue where new pages were incorrectly placed at random positions instead of bottom
2025-08-04 09:49:29 +01:00
Philipinho cb2a0398c7 fix: invalidate trashed page from tree state 2025-08-04 00:42:13 -07:00
Philip Okugbe 95b7be61df fix: hide trash from can view permission (#1445) 2025-08-04 08:35:28 +01:00
Philip Okugbe b0c557272d fix nested taskList in markdown export (#1443) 2025-08-04 08:01:18 +01:00
Philip Okugbe dddfd48934 feat: add attachments support for single page exports (#1440)
* feat: add attachments support for single page exports
- Add includeAttachments option to page export modal and API
- Fix internal page url in single page exports in cloud

* remove redundant line

* preserve export state
2025-08-04 08:01:11 +01:00
Philipinho aa6eec754e fix: exclude trashed pages from position generation 2025-08-04 00:00:06 -07:00
Philip Okugbe 97a7701f5d fix local storage copy function (#1442) 2025-08-04 03:20:18 +01:00
Philipinho b97eb85d05 sync 2025-08-03 03:59:08 -07:00
Philipinho 1615e0f4ad v0.22.2 2025-08-01 16:15:02 -07:00
Philip Okugbe 1cb2535de3 fix trash in search (#1439)
- delete share if page is trashed
2025-08-02 00:14:00 +01:00
Philipinho 83bc273cb0 cleanup 2025-08-01 07:05:25 -07:00
Philipinho c7beaa3742 v0.22.1 2025-08-01 06:54:28 -07:00
Philipinho 4a228e5a51 fix comment replies 2025-08-01 06:51:56 -07:00
Philipinho edff375476 sync 2025-08-01 02:54:11 -07:00
Philipinho 95016b2bfc sync 2025-08-01 02:51:55 -07:00
Philipinho ca83712364 cleanup 2025-08-01 02:26:14 -07:00
Philip Okugbe 39550fe906 fix: duplicate page position bug (#1431) 2025-07-30 18:07:06 +01:00
Philipinho e74ecb2604 v0.22.0 2025-07-29 15:22:46 -07:00
Philipinho 992fb23160 update lock file 2025-07-29 15:04:38 -07:00
Philipinho d58a3bba9b update linkify 2025-07-29 14:59:50 -07:00
Philipinho 6ef47fc432 show button only if necessary 2025-07-29 14:59:23 -07:00
Philipinho 9e6765d83c fix 2025-07-29 14:51:55 -07:00
Philipinho ec0ed5c630 fix import 2025-07-29 14:50:59 -07:00
Philipinho 77b334ea37 reorder migration 2025-07-29 14:49:19 -07:00
Philip Okugbe 5da92a538a feat: add unaccent support for accent-insensitive search (#1402)
- Add PostgreSQL unaccent and pg_trgm extensions
- Create immutable f_unaccent wrapper function for performance
- Update all search queries to use f_unaccent for accent-insensitive matching
- Add 1MB limit to tsvector content to prevent errors on large documents
- Update full-text search trigger to use f_unaccent
- Fix MultiSelect client-side filtering to show server results properly
2025-07-29 22:47:13 +01:00
Philipinho f90c5a636b cleanup comment 2025-07-29 14:30:45 -07:00
Philipinho 6db93ef0c7 upsell 2025-07-29 14:28:40 -07:00
Philip Okugbe a3d058042f New Crowdin updates (#1342)
* New translations translation.json (German)

* New translations translation.json (Spanish)

* New translations translation.json (Russian)

* New translations translation.json (Spanish)

* New translations translation.json (Russian)

* New translations translation.json (French)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Spanish)

* New translations translation.json (Russian)

* New translations translation.json (French)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2025-07-29 21:53:16 +01:00
Philipinho 4ab9261cf5 sync 2025-07-29 13:41:07 -07:00
Philip Okugbe ca9558b246 feat(EE): resolve comments (#1420)
* feat: resolve comment (EE)

* Add resolve to comment mark in editor (EE)

* comment ui permissions

* sticky comment state tabs (EE)

* cleanup

* feat: add space_id to comments and allow space admins to delete any comment

- Add space_id column to comments table with data migration from pages
- Add last_edited_by_id, resolved_by_id, and updated_at columns to comments
- Update comment deletion permissions to allow space admins to delete any comment
- Backfill space_id on old comments

* fix foreign keys
2025-07-29 21:36:48 +01:00
Eddy Oyieko ec12e80423 feat: trash for deleted pages in space (#325)
* initial commit

* added recycle bin modal, updated api routes

* updated page service & controller, recycle bin modal

* updated page-query.ts, use-tree-mutation.ts, recycled-pages.ts

* removed quotes from openRestorePageModal prompt

* Updated page.repo.ts

* move button to space menu

* fix react issues

* opted to reload to enact changes in the client

* lint

* hide deleted pages in recents, handle restore child page

* fix null check

* WIP

* WIP

* feat: implement dedicated trash page
- Replace modal-based trash view with dedicated route `/s/:spaceSlug/trash`
- Add pagination support for deleted pages
- Other improvements

* fix translation

* trash cleanup cron

* cleanup

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-07-29 21:20:49 +01:00
Philip Okugbe 28fcb11cb4 update passport-saml (#1418) 2025-07-29 19:30:53 +01:00
Philip Okugbe 6b627d289c fix xss in generic iframe embed (#1419) 2025-07-29 19:28:48 +01:00
Philip Okugbe 78bce0e29d fix: validate public avatar path (#1416) 2025-07-28 18:17:06 +01:00
Philip Okugbe 0bd7ecb9b0 feat: enhance table cells with rich content support (#1409)
- Support multiple content types in table cells and headers: paragraphs, headings, lists (bullet/ordered/task), blockquotes, callouts, images, videos, attachments, math blocks, toggles, and code blocks
- Add custom table extension with smart Tab key handling for list indentation within tables
- Preserve default table navigation when not in lists
2025-07-28 08:22:22 +01:00
Philip Okugbe 1f815880a4 Revert "feat: set mermaid theme based on computed color scheme (#1397)" (#1412)
This reverts commit 32c7ecd9cf.
2025-07-26 01:34:15 +01:00
Philipinho 37b9056070 sync 2025-07-24 16:38:32 -07:00
Philip Okugbe ad5cf1e18b feat: add resizable embed component │ (#1401)
- Created reusable ResizableWrapper component
- Added drag-to-resize functionality for embeds
2025-07-25 00:23:14 +01:00
Alexander Schaber 32c7ecd9cf feat: set mermaid theme based on computed color scheme (#1397)
Use Mantine's `useComputedColorScheme` hook to dynamically configure mermaid's theme.
- When the computed color scheme is "light", the theme is set to "default".
- Otherwise, it is set to "dark".
2025-07-25 00:22:27 +01:00
Philip Okugbe b30bf61dc4 feat: home space list (#1400) 2025-07-25 00:21:40 +01:00
Philip Okugbe 662460252f feat(EE): MFA implementation (#1381)
* feat(EE): MFA implementation for enterprise edition
- Add TOTP-based two-factor authentication
- Add backup codes support
- Add MFA enforcement at workspace level
- Add MFA setup and challenge UI pages
- Support MFA for login and password reset flows
- Add MFA validation for secure pages
* fix types
* remove unused object
* sync
* remove unused type
* sync
* refactor: rename MFA enabled field to is_enabled
* sync
2025-07-25 00:18:53 +01:00
Philip Okugbe 8522844673 feat: duplicate page in same space (#1394)
* fix internal links in copies pages

* feat: duplicate page in same space

* fix children
2025-07-21 21:39:57 +01:00
Philip Okugbe f8dc9845a7 fix page tree api atom (#1391)
- The tree api atom state is not always set, which makes it impossble to create new pages since the buttons rely on it.
- this should fix it.
2025-07-21 05:02:40 +01:00
Philip Okugbe 4dfed2b2af queue import attachments upload (#1353) 2025-07-19 18:00:06 +01:00
Philip Okugbe 44e592763d feat: quick theme toggle and Mantine 8 upgrade (#1369)
* upgrade to mantine v8

* feat: quick theme toggle
2025-07-15 06:28:27 +01:00
Philip Okugbe 90488a95b1 feat: table background color, cell header and align (#1352)
* feat: add toggle header cell button to table cell menu

Added ability to toggle header cells directly from the table cell menu. This enhancement includes:
- New toggle header cell button with IconTableRow icon
- Consistent UI/UX with existing table menu patterns
- Proper internationalization support

* fix: typo in aria-label for toggle header cell button

* feat: add table cell background color picker

- Extended TableCell and TableHeader to support backgroundColor attribute
- Created TableBackgroundColor component with 21 color options
- Integrated color picker into table cell menu using Mantine UI
- Added support for both regular cells and header cells
- Updated imports to use custom TableHeader from @docmost/editor-ext

* feat: add text alignment to table cell menu

- Created TableTextAlignment component with left, center, and right alignment options
- Integrated alignment selector into table cell menu
- Shows current alignment icon in the button
- Displays checkmark next to active alignment in dropdown

* background colors

* table background color in dark mode

* add bg color name

* rename color attribute

* increase minimum table width
2025-07-15 06:27:48 +01:00
Philip Okugbe 9f39987404 fix: nested ordered-list style (#1351)
* feat: dynamic ordered-list style
* fix nested task list import
2025-07-15 02:43:59 +01:00
Philipinho 16ec218ba7 fix: deactivated user check 2025-07-14 10:28:42 -07:00
Philipinho 608783b5cf (cloud) billing copy 2025-07-14 03:56:26 -07:00
Philipinho 5f5f1484db throw early 2025-07-14 03:53:07 -07:00
Philip Okugbe f4082171ec feat: display user email below name in multi-member-select dropdown (#1355)
- Added email field to user items mapping
- Updated renderMultiSelectOption to show email in smaller, dimmed text
- Email only displays for user type options, not groups
2025-07-14 10:37:13 +01:00
fuscodev 6792a191b1 feat: Ctrl/Cmd+S: prevent 'Save As' dialog (#1272)
* init

* remove: force save

* switch from event.key to event.code by sanua356
2025-07-14 10:36:24 +01:00
Philip Okugbe e51a93221c more checks for collab auth token (#1345) 2025-07-14 10:35:03 +01:00
Philip Okugbe e856c8eb69 (cloud) fix: updates to billing (#1367)
* billing updates (cloud)

* old billing grace period
2025-07-14 10:34:18 +01:00
Philip Okugbe c2c165528b fix: seamlessly update editor collab token on expiration (#1366) 2025-07-14 07:19:06 +01:00
Philipinho 9fa2b9636c make sure editor is ready for editor search 2025-07-13 15:38:29 -07:00
fuscodev 29388636bf feat: find and replace in editor (#689)
* feat: page find and replace

* * Refactor search and replace directory

* bugfix scroll

* Fix search and replace functionality for macOS and improve UX

- Fixed cmd+f shortcut to work on macOS (using 'Mod' key instead of 'Control')
- Added search functionality to title editor
- Fixed "Not found" message showing when search term is empty
- Fixed tooltip error when clicking replace button
- Changed replace button from icon to text for consistency
- Reduced width of search input fields for better UI
- Fixed result index after replace operation to prevent out-of-bounds error
- Added missing translation strings for search and replace dialog
- Updated tooltip to show platform-specific shortcuts (⌘F on Mac, Ctrl-F on others)

* Hide replace functionality for users with view-only permissions

- Added editable prop to SearchAndReplaceDialog component
- Pass editable state from PageEditor to SearchAndReplaceDialog
- Conditionally render replace button based on edit permissions
- Hide replace input section for view-only users
- Disable Alt+R shortcut when user lacks edit permissions

* Fix search dialog not closing properly when navigating away

- Clear all state (search text, replace text) when closing dialog
- Reset replace button visibility state on close
- Clear editor search term to remove highlights
- Ensure dialog closes properly when route changes

* fix: preserve text marks (comments, etc.) when replacing text in search and replace

- Collect all marks that span the text being replaced using nodesBetween
- Apply collected marks to the replacement text to maintain formatting
- Fixes issue where comment marks were being removed during text replacement

* ignore type error

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-07-10 04:40:07 +01:00
Philipinho f80004817c sync 2025-07-08 16:05:34 -07:00
Finn Dittmar ac79a185de fix ctrl-a for codeblocks (#1336) 2025-07-08 22:13:21 +01:00
Philipinho 27a9c0ebe4 sync 2025-07-07 14:55:09 -07:00
Philipinho 81ffa6f459 sync 2025-07-03 04:12:24 -07:00
Whai 5364702b69 fix: comments block on edge and older browser (#1310)
* fix: overflow on edge and older browser
2025-07-01 05:14:08 +01:00
Philipinho 232cea8cc9 sync 2025-06-27 03:20:01 -07:00
Philipinho b9643d3584 sync 2025-06-27 03:07:51 -07:00
Philip Okugbe 9f144d35fb posthog integration (cloud) (#1304) 2025-06-27 10:58:36 +01:00
Philip Okugbe e44c170873 fix editor flickers on collab reconnection (#1295)
* fix editor flickers on reconnection

* cleanup

* adjust copy
2025-06-27 10:58:18 +01:00
Philipinho 1be39d4353 sync 2025-06-27 02:22:11 -07:00
Philipinho 36d028ef4d sync 2025-06-24 05:53:59 -07:00
Philip Okugbe f5a36c60e8 feat: tiered billing (cloud) (#1294)
* feat: tiered billing (cloud)

* custom tier
2025-06-24 13:22:38 +01:00
Finn Dittmar d5b84ae0b8 Only allow changing the email if the correct password is provided (#1288)
* fix

* fix overwriting password

* finalize

* BadRequestException

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-06-24 09:02:55 +01:00
Philip Okugbe e775e4dd8c fix(editor): prevent text color removal from other list items when setting color in lists (#1289)
Only unset color when 'Default' is selected. This ensures setting color on one list item does not remove it from others.
2025-06-23 19:31:30 +01:00
Philipinho 65b01038d7 v0.21.0 2025-06-18 14:28:14 -07:00
Philipinho e07cb57b01 sync 2025-06-18 14:25:40 -07:00
Philipinho 2b53e0a455 fix: add import size limit to static window config 2025-06-18 13:58:41 -07:00
Auxa b9b3406b28 Fix: Prevent premature focus change in TitleEditor when pressing Enter during IME composition (#730)
* fix: Prevents key events during text composition

Stops handling title key events when composing text,
ensuring proper input behavior during IME use.

* Refines IME composition event checks

Separates IME composition control from shift key logic and adds a Safari-specific keyCode check to prevent premature focus shifts during IME input.
2025-06-18 21:33:35 +01:00
Philip Okugbe 728cac0a34 fix word counter (#1269) 2025-06-18 21:32:11 +01:00
Philipinho d35e16010b handle empty invitation 2025-06-18 13:10:32 -07:00
Philipinho 15791d4e59 sync 2025-06-18 12:50:43 -07:00
Philip Okugbe 3318e13225 fix: use JWT expiry time for cookie duration (#1268)
* Set default jwt expiry to 90 days.
2025-06-18 20:50:11 +01:00
Philipinho 080900610d cleanup 2025-06-17 16:14:06 -07:00
fuscodev d1dc6977ab feat: edit mode preference (#666)
* lock/unlock pages

* remove using isLocked column - add default page edit state preference

* * Move state management to editors (avoids flickers on edit mode switch)
* Rename variables
* Add strings to translation file
* Memoize components in page component
* Fix title editor sending update request on editable state change

* fixed errors merging main

* Fix embed view in read-only mode

* remove unused line

* sync

* fix responsiveness on mobile

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-06-18 00:11:47 +01:00
Philip Okugbe 5f62448894 less create workspace form fields in cloud (#1265)
* sync

* less signup form fields in cloud

* min length
2025-06-17 23:56:07 +01:00
Philip Okugbe 44445fbf46 fix: enforce SSO in invitation signups (#1258) 2025-06-15 20:25:15 +01:00
Philip Okugbe 1c674efddd fix: revert tiptap version (#1255) 2025-06-13 21:38:49 +01:00
Philip Okugbe ccf7e34e99 feat: ukrainian language support (#1250) 2025-06-11 23:31:45 +01:00
Philip Okugbe f39d48d6ee New Crowdin updates (#1063)
* New translations translation.json
2025-06-11 23:21:01 +01:00
Philip Okugbe f584ea84b0 chore: upgrade packages (#1242)
* upgrade tiptap editor extensions

* upgrade packages

* fix type issue
2025-06-11 23:18:39 +01:00
Chai bc0c4d6258 fix: make link popup work on safari (#1243)
* fix: make link popup work on safari

* fix: second iteration

* chore: cleanup

* chore: format

* chore: undo unused stuff
2025-06-11 23:09:59 +01:00
Philip Okugbe d8da307a61 feat: enhance excalidraw (#1240)
* WIP

* use next excalidraw version

* support local persistence for excalidraw library.

Co-authored-by: Drauggy <n.fomenko@safe-tech.ru>

---------

Co-authored-by: Drauggy <n.fomenko@safe-tech.ru>
2025-06-09 23:25:36 +01:00
Philip Okugbe 50b3f9ddd9 generic iframe embed (#1234) 2025-06-09 22:32:23 +01:00
Philip Okugbe 0029f84d50 feat: toggle table header row and column (#1203)
* feat: toggle table header row and column
* switch position
2025-06-09 05:39:43 +01:00
Philip Okugbe 6d024fc3de feat: bulk page imports (#1219)
* refactor imports - WIP

* Add readstream

* WIP

* fix attachmentId render

* fix attachmentId render

* turndown video tag

* feat: add stream upload support and improve file handling

- Add stream upload functionality to storage drivers\n- Improve ZIP file extraction with better encoding handling\n- Fix attachment ID rendering issues\n- Add AWS S3 upload stream support\n- Update dependencies for better compatibility

* WIP

* notion formatter

* move embed parser to editor-ext package

* import embeds

* utility files

* cleanup

* Switch from happy-dom to cheerio
* Refine code

* WIP

* bug fixes and UI

* sync

* WIP

* sync

* keep import modal mounted

* Show modal during upload

* WIP

* WIP
2025-06-09 04:29:27 +01:00
fuscodev ce1503af85 fix: sidebar list when changing workspace (#1150)
* init

* navigate in overview if current page is in deleted node

* fix: implement pagination in sidebar-pages queries

* fix: appendNodeChildren()

Preserve deeper children if they exist and remove node if deleted
2025-06-08 03:27:09 +01:00
Philipinho 69447fc375 Merge branch 'main' of https://github.com/docmost/docmost 2025-05-21 08:43:56 -07:00
Philipinho 858ff9da06 sync 2025-05-20 09:27:30 -07:00
sanua356 343b2976c2 #1186/chore: add support language abap syntax highlight (#1188) 2025-05-19 20:05:31 +01:00
Philip Okugbe 7491224d0f hide shared page branding in EE (#1193)
* hide shared page branding in EE

* Hide branding in business plan
2025-05-17 19:17:34 +01:00
Philip Okugbe 4a0b4040ed Add second plan (#1187) 2025-05-17 19:03:01 +01:00
fuscodev e3ba817723 feat: comment editor emoji picker and ctrl+enter action (#1121)
* commenteditor-emoji-picker

* capture Mac command key
* remove tooltip

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-05-16 20:01:27 +01:00
fuscodev b0491d5da4 feat: create new page from mention (#1153)
* init

* create page in relative parent root
2025-05-16 19:15:11 +01:00
极地 1c200dbd0f fix(table-hover): adjust row height to prevent unexpected scrollbar on hover (#1124)
fix: Hover table style height error causing scrollbar to appear #1108
2025-05-16 16:26:05 +01:00
fuscodev fb7e4a7956 fix: copy/move select (#1174) 2025-05-16 16:24:31 +01:00
fuscodev 1413033568 feat: realtime comments (#1144)
* init

* fix: close bubblemenu after comment and wait before scroll

* scroll to comment when click

* highlight comment animation
2025-05-16 16:18:23 +01:00
fuscodev 00f4588c21 fix title update (#1154) 2025-05-16 16:11:29 +01:00
fuscodev 3a75251e75 fix alignment in shared page (#1123) 2025-05-16 16:00:47 +01:00
Philipinho c6bca6a602 fix deprecated kysely usage 2025-05-09 16:44:33 +01:00
edo0 55d1a2c932 Fix typo in enforce-sso.tsx (#1145) 2025-05-09 11:11:02 +01:00
Philipinho bc3cb2d63f fix: increase random subdomain suffix 2025-05-07 15:10:58 +01:00
Philipinho 7adbf85030 v0.20.4 2025-04-30 14:44:58 +01:00
Philip Okugbe de7982fe30 feat: copy page to different space (#1118)
* Add copy page to space endpoint
* copy storage function
* copy function
* feat: copy attachments too
* Copy page - WIP
* fix type
* sync
* cleanup
2025-04-30 14:43:16 +01:00
Philipinho 0402f7efb5 sync 2025-04-30 14:33:01 +01:00
Philipinho 8327251ab6 fix typo 2025-04-29 23:30:12 +01:00
Philip Okugbe e8847bd9cd fix: handle unhandled exceptions (#1116)
* Handle unhandled exceptions
* cleanup
2025-04-29 23:29:00 +01:00
Philipinho 9bbd62e0f0 v0.20.3 2025-04-24 23:22:53 +01:00
Philipinho 0289c5cb09 Reduce markdown checkbox space 2025-04-24 23:19:39 +01:00
Philip Okugbe 7993532111 fix page export (#1081) 2025-04-24 23:18:54 +01:00
Philipinho 31e5c0c660 v0.20.2 2025-04-24 17:57:14 +01:00
Philipinho 33c314d4e8 remove clickoutside hook 2025-04-24 17:56:54 +01:00
Philipinho 08f223899a cloud trial refactor 2025-04-23 16:07:58 +01:00
Philipinho c528f7e858 v0.20.1 2025-04-23 14:34:28 +01:00
Philip Okugbe c26a851d52 feat: enhance public sharing (#1057)
* fix tree nodes sort

* remove comment mark in shares

* remove clickoutside hook for now

* feat: search in shared pages

* fix user-select

* use Link

* render page icons
2025-04-23 14:32:35 +01:00
Philipinho de5f90309c v0.20.0 2025-04-22 22:49:45 +01:00
Philipinho 0ec3ff2965 Add empty placeholder text 2025-04-22 22:48:12 +01:00
Philipinho acffeacdbc fix TOC 2025-04-22 22:47:34 +01:00
Philip Okugbe 00d92a3690 New Crowdin updates (#1008)
* New translations translation.json (Russian)

* New translations translation.json (Russian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Spanish)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)
2025-04-22 20:57:07 +01:00
Diego Ochoa 3430f715ec feat: remember and restore previous route when exiting settings (#1046)
Improves user experience by allowing users to return to the previous
page after visiting the Settings section.

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-04-22 20:47:57 +01:00
Philip Okugbe 6c422011ac feat: public page sharing (#1012)
* Share - WIP

* - public attachment links
- WIP

* WIP

* WIP

* Share - WIP

* WIP

* WIP

* include userRole in space object

* WIP

* Server render shared page meta tags

* disable user select

* Close Navbar on outside click on mobile

* update shared page spaceId

* WIP

* fix

* close sidebar on click

* close sidebar

* defaults

* update copy

* Store share key in lowercase

* refactor page breadcrumbs

* Change copy

* add link ref

* open link button

* add meta og:title

* add twitter tags

* WIP

* make shares/info endpoint public

* fix

* * add /p/ segment to share urls
* minore fixes

* change mobile breadcrumb icon
2025-04-22 20:37:32 +01:00
Philipinho 3e8824435d update vite and axios 2025-04-22 20:28:27 +01:00
Philip Okugbe 37a1804db9 Revert "switch to vite rolldown (#1048)" (#1050)
This reverts commit 1a1b2c8682.
2025-04-22 20:00:36 +01:00
Philip Okugbe 882f3093bd search space members by email (#1049) 2025-04-22 19:37:06 +01:00
Philip Okugbe 1a1b2c8682 switch to vite rolldown (#1048)
* switch to vite rolldown

* update
2025-04-22 15:52:44 +01:00
Philip Okugbe 10b67929ea Update README.md 2025-04-21 21:50:21 +01:00
Philip Okugbe 5c957fda8d fix: nested tree open state 2025-04-21 19:24:25 +01:00
Philip Okugbe 862f6d4820 use non-esm nanoid version (#1040) 2025-04-19 19:45:09 +01:00
Philipinho de57d05199 0.10.2 2025-04-15 12:48:40 +01:00
Philipinho 89ec990232 sync ee 2025-04-15 12:46:28 +01:00
Philipinho 49d0f1cc9a Add click handler 2025-04-11 13:41:43 +01:00
Philipinho 268001ae26 v0.10.1 2025-04-11 13:23:42 +01:00
Philip Okugbe 27fa45a769 fix local attachment paths in exports (#1013) 2025-04-11 13:18:44 +01:00
Philipinho f9711918a3 fix comment editor padding 2025-04-11 12:32:54 +01:00
Philipinho 29bb52db0c v0.10.0 2025-04-09 19:14:51 +01:00
Philipinho f2241db5ee remove beta message 2025-04-09 19:14:33 +01:00
Philipinho 58d1855a36 fix hash check 2025-04-09 19:03:27 +01:00
Philipinho 7fe3c5f177 * time ago hook 2025-04-09 18:47:39 +01:00
Philipinho 5fd477d074 collapse by default in node-edit mode 2025-04-09 15:46:29 +01:00
Philip Okugbe 4aa5d7e326 hide history action menu for can-view role (#1001) 2025-04-09 15:42:29 +01:00
Philipinho 7f7f2bccd0 fix toggle node in non-edit mode 2025-04-09 15:37:18 +01:00
Philip Okugbe a9f370660b New Crowdin updates (#1005)
* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2025-04-08 17:28:33 +01:00
Philipinho 117c7049ff fix 2025-04-08 17:15:09 +01:00
Philipinho cd10365f71 new translations 2025-04-08 17:10:48 +01:00
Philip Okugbe ee30d9d0f2 New Crowdin updates (#1003)
* New translations translation.json (French)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Russian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)
2025-04-08 17:10:08 +01:00
Philipinho 276ececbf2 cleanup 2025-04-08 17:06:32 +01:00
Philipinho fa194a497c cleanup 2025-04-08 17:04:43 +01:00
Philip Okugbe 1eaba6e77f fix: bug fixes (#1000)
* sort by groups first

* add scroll area

* fix group members pagination

* move pagination to the right
2025-04-08 13:34:00 +01:00
Philipinho 651e5f6153 null check 2025-04-08 11:59:47 +01:00
Philip Okugbe 7431804a46 feat: delete workspace member (#987)
* add delete user endpoint (server)

* delete user (UI)

* prevent token generation

* more checks
2025-04-07 19:26:03 +01:00
Philipinho 3559358d14 fix pagination issue where user is not part of any space 2025-04-07 19:09:02 +01:00
Philipinho 06270ff747 - fixes
- allow mail from address override
- queue cloud emails
2025-04-07 19:07:10 +01:00
sanua356 233536314f feat: add Table of contents (#981)
* chore: add table of contents module

* refactor

* lint

* null check

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-04-05 19:03:42 +01:00
Philip Okugbe 17ce3bab8a feat: move page between spaces (#988)
* feat: Move the page to another space

- The ability to move a page to another space has been added

* feat: Move the page to another space
* feat: Move the page to another space

- Correction of the visibility attribute of elements that extend beyond the boundaries of the space selection modal window

* feat: Move the page to another space

- Added removal of query keys when moving pages

* feat: Move the page to another space

- Fix locales

* feat: Move the page to another space
* feat: Move the page to another space

- Fix docker compose

* feat: Move the page to another space

* feat: Move the page to another space

- Some refactor

* feat: Move the page to another space

- Attachments update

* feat: Move the page to another space

- The function of searching for attachments by page ID and updating attachments has been combined

* feat: Move the page to another space

- Fix variable name

* feat: Move the page to another space

- Move current space to parameter of component SpaceSelectionModal

* refactor ui

---------

Co-authored-by: plekhanov <astecom@mail.ru>
2025-04-04 23:44:18 +01:00
Philip Okugbe b27d1708b0 queue trial ended job (#992) 2025-04-04 23:35:08 +01:00
Philip Okugbe 64f0531093 feat: keep track of page contributors (#959)
* WIP

* feat: store and retrieve page contributors
2025-04-04 13:03:57 +01:00
fuscodev 8aa604637e feat: nested toggle block (#671)
* feat: nested toggle block

* fix: md export

* fix detailsButton icon alignment

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-04-04 13:01:39 +01:00
Philipinho 7ca2b437d4 sync 2025-04-03 14:08:06 +01:00
Philip Okugbe 595bd1dc81 Fix editor connection loop (#986)
* fix editor connection loop

* remove query refresh
2025-04-03 14:05:34 +01:00
Philipinho a74d3feae4 fix: make collab ready reliable on tab return 2025-03-27 14:39:43 +00:00
Philipinho e40faf97ec v0.9.0 2025-03-23 14:07:30 +00:00
Philipinho bbe4fe99f9 don't replace line breaks 2025-03-23 13:57:05 +00:00
Philipinho 8300c5b731 update env file 2025-03-23 13:14:20 +00:00
Philip Okugbe 13039cfacc telemetry module (#934)
* update lockfile

* fix color check

* telemetry

* complete

* Use interval
2025-03-23 13:12:41 +00:00
Hoie Kim 593f41a050 adds missing command for down migration (#908) 2025-03-22 15:30:37 +00:00
Philip Okugbe f8ce160906 feat: add version check (#922)
* Add version endpoint

* version indicator

* refetch

* * Translate strings
* Handle error
2025-03-22 15:29:10 +00:00
Philip Okugbe c824b5b570 fix collab token refresh which leads to collab editor reconnection loop (#933) 2025-03-22 15:15:50 +00:00
Philipinho 37e760d76c * fix color check
* update lock file
2025-03-22 12:31:01 +00:00
Philipinho 442fa23399 Refetch space list on mount 2025-03-17 11:49:42 +00:00
Philipinho 2e5990d057 Move suspense above popover dropdown 2025-03-17 11:23:57 +00:00
Philipinho 15bdbf74cd null check 2025-03-17 11:23:18 +00:00
Philip Okugbe 3d9a7d808b Revert "feat: auto focus emoji-picker search when opened (#894)" (#900)
This reverts commit 573457403e.
2025-03-17 11:17:44 +00:00
Philip Okugbe f45bdddb23 feat: billing sync (cloud) (#899)
* Set page history to 5 minutes interval

* * Configure default queue options

* sync

* * stripe seats sync (cloud)
2025-03-17 11:00:23 +00:00
Philip Okugbe 21c3ad0ecc feat: enhance editor uploads (#895)
* * multi-file paste support
* allow media files (image/videos) to be attachments
* insert trailing node if file placeholder is at the end of the editor

* fix video align
2025-03-15 18:27:26 +00:00
GlitchDev 573457403e feat: auto focus emoji-picker search when opened (#894)
Co-authored-by: JonasRingeis <jonas.ringeis@otto.de>
2025-03-15 18:25:01 +00:00
Philipinho d021d0a38f fix 2025-03-14 23:02:42 +00:00
Philip Okugbe 96dfe9f817 fix: page title editor bugs (#892)
* Fix page title

* compare empty page title

* Properly handle null tree node name and icon
2025-03-14 22:41:34 +00:00
Philipinho 598361992e fix trial days 2025-03-14 22:40:35 +00:00
Philip Okugbe 210d1474ea Add Dutch translation (#877) 2025-03-13 15:26:23 +00:00
Philipinho 5f520689ed prevent overflow 2025-03-13 15:23:35 +00:00
Philip Okugbe 2a535de29d New Crowdin updates (#840) 2025-03-13 15:10:28 +00:00
Philip Okugbe f45d9dc5a0 feat: add page stats to page menu (#876) 2025-03-13 14:54:18 +00:00
Philip Okugbe f7a14e23cd fix editor flickers (#875) 2025-03-13 08:58:21 +00:00
Philip Okugbe 1f40e9b960 fix drag handle visibility (#868) 2025-03-12 13:17:59 +00:00
Philip Okugbe fea6518352 fix: VSCode markdown pasting (#857)
* fix vscode markdown pasting

* fix markdown -> html formatting
2025-03-10 02:38:22 +00:00
Philipinho 061a02ce51 Make codeblock comment more legible in light mode 2025-03-10 02:15:15 +00:00
Philipinho 2205ce0c3b prevent slider flickers 2025-03-10 01:15:21 +00:00
Philipinho a812cdcf15 enable shouldRerenderOnTransaction 2025-03-09 22:49:58 +00:00
Philipinho 30acc6676a exclude billing webhook endpoint 2025-03-08 19:08:02 +00:00
Philipinho 5c9e0a2630 * prefetch sso providers in settings
* hide sso enforcement in standard plan
2025-03-08 18:26:34 +00:00
Philip Okugbe fd36076ae7 feat: disconnect collab websocket on idle tabs (#848)
* disconnect real-time collab if user is idle
* log yjs document disconnect and unload in dev mode
* no longer set editor to read-only mode on collab websocket disconnection
* treat delayed collab websocket "connecting" state as disconnected
* increase maxDebounce to 45 seconds
* add reset handle to useIdle hook
2025-03-08 18:16:23 +00:00
fuscodev dd52eb15ca fix: table header in exported markdown (#769) 2025-03-07 12:16:49 +00:00
Iago Angelim Costa Cavalcante 6776e073b6 feat: adding family 6 in uri to configure for both 4 and 6 (#807)
* feat: adding family 6 in uri to configure for both 4 and 6
* feat: adding redis family in websocket config
2025-03-07 12:12:19 +00:00
Philipinho 7a47da9273 Add emoji command to title editor 2025-03-07 11:57:28 +00:00
fuscodev e62bc6c250 feat: editor emoji picker (#775)
* feat: emoji picker

* fix: lazy load emoji data

* loading animation (for slow connection)

* parsing :shortcode: and replace with emoji + add extension to title-editor

* fix

* Remove title editor support
* Remove shortcuts support
* Cleanup

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-03-07 11:53:06 +00:00
Philipinho 4f9e588494 sort workspace list 2025-03-07 11:51:04 +00:00
Philip Okugbe 05a3dfa26d Option to log db queries in dev mode (#827) 2025-03-07 00:06:25 +00:00
Philip Okugbe 8826cca539 fix space translations (#826) 2025-03-07 00:03:57 +00:00
Philipinho 1988feb9ce exclude /health/live endpoint 2025-03-06 23:45:41 +00:00
Philipinho e9b7273489 remove cloud env check 2025-03-06 22:30:24 +00:00
Philipinho 315afd6818 fix cookie name 2025-03-06 21:44:53 +00:00
Philipinho 93ea31feb0 sync 2025-03-06 21:09:05 +00:00
Philipinho 3b4e414c97 * configurable trial days
* hide create sso provider in cloud
2025-03-06 21:06:24 +00:00
Philipinho d925c95fc9 add pnpm to packageManager for consistency 2025-03-06 18:54:33 +00:00
Philipinho 4511db1526 fix 2025-03-06 18:32:25 +00:00
Philipinho 56d9e46fd3 * Upgrade Dockerfile to node 22
* Pin pnpm to pnpm@10.4.0
2025-03-06 18:29:15 +00:00
Philipinho cdea149ce7 * Update EE license fil
* State license in Readme file
2025-03-06 17:59:22 +00:00
Philipinho 16254802e3 Add api prefix to attachment nodes 2025-03-06 14:19:29 +00:00
Philipinho a7dd9b9198 Hide version in cloud 2025-03-06 14:17:20 +00:00
Philip Okugbe b81c9ee10c feat: cloud and ee (#805)
* stripe init
git submodules for enterprise modules

* * Cloud billing UI - WIP
* Proxy websockets in dev mode
* Separate workspace login and creation for cloud
* Other fixes

* feat: billing (cloud)

* * add domain service
* prepare links from workspace hostname

* WIP

* Add exchange token generation
* Validate JWT token type during verification

* domain service

* add SkipTransform decorator

* * updates (server)
* add new packages
* new sso migration file

* WIP

* Fix hostname generation

* WIP

* WIP

* Reduce input error font-size
* set max password length

* jwt package

* license page - WIP

* * License management UI
* Move license key store to db

* add reflector

* SSO enforcement

* * Add default plan
* Add usePlan hook

* * Fix auth container margin in mobile
* Redirect login and home to select page in cloud

* update .gitignore

* Default to yearly

* * Trial messaging
* Handle ended trials

* Don't set to readonly on collab disconnect (Cloud)

* Refine trial (UI)
* Fix bug caused by using jotai optics atom in AppHeader component

* configurable database maximum pool

* Close SSO form on save

* wip

* sync

* Only show sign-in in cloud

* exclude base api part from workspaceId check

* close db connection beforeApplicationShutdown

* Add health/live endpoint

* clear cookie on hostname change

* reset currentUser atom

* Change text

* return 401 if workspace does not match

* feat: show user workspace list in cloud login page

* sync

* Add home path

* Prefetch to speed up queries

* * Add robots.txt
* Disallow login and forgot password routes

* wildcard user-agent

* Fix space query cache

* fix

* fix

* use space uuid for recent pages

* prefetch billing plans

* enhance license page

* sync
2025-03-06 13:38:37 +00:00
Zero King 91596be70e fix: add missing awaits (#814) 2025-03-06 10:14:30 +00:00
Philip Okugbe 72f64e7b10 revert sentry (#808)
* revert sentry
* remove sentry env
2025-02-27 15:58:32 +00:00
Philipinho 3cfb17bb62 fix sentry 2025-02-27 14:44:28 +00:00
Philipinho fe5066c7b5 v0.8.4 2025-02-27 14:34:38 +00:00
Philipinho e13be904cd cleanup 2025-02-27 14:18:25 +00:00
Philip Okugbe fda5c7d60f push files left (#360) (#804) 2025-02-26 18:33:50 +00:00
Peter Shcherbakov 7fc1a782a7 feat: add copy invite link to invitation action menu (#360)
* +copy invite link to clipboard from invite action menu

* -remove log to console for copy link action

* Refactor copy invite link feature

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-02-26 18:28:44 +00:00
Philipinho 54d27af76a * Add SENTRY_DNS env variable
* Commit lock file
2025-02-26 17:38:25 +00:00
Philip Okugbe 0065f29634 feat: sentry (#802) 2025-02-26 15:42:19 +00:00
Philipinho 7d034e8a8b enable trustProxy 2025-02-26 13:16:11 +00:00
Philipinho 81b6c7ef69 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-02-26 13:14:45 +00:00
Philip Okugbe 89f6b0a8c2 feat: add stats to standalone collab server (#798)
* Log APP_URL on startup

* add stats endpoint to standalone collab server
2025-02-26 13:00:01 +00:00
Philipinho ad1571b902 Log APP_URL on startup 2025-02-26 11:49:58 +00:00
Philip Okugbe 4b9ab4f63c feat: standalone collab server (#767)
* feat: standalone collab server

* * custom collab server port env
* fix collab start script command

* * API prefix
* Log startup PORT

* Tweak collab debounce
2025-02-25 13:15:51 +00:00
Philipinho 08829ea721 v0.8.3 2025-02-22 12:25:49 +00:00
Philip Okugbe 6c502b4749 pin react-email version (#779) 2025-02-22 12:16:02 +00:00
Philipinho 6b41538b60 v0.8.2 2025-02-21 13:16:16 +00:00
Philipinho 496f5d7384 pin s3 package to 3.701.0 2025-02-21 13:15:19 +00:00
Alexander 32c7a16d06 fix: accept invitation password hashing (#773) 2025-02-21 12:48:25 +00:00
Philip Okugbe 64ecef09bc upgrade to NestJS 11 (#766)
* upgrade to nest 11

* update dependencies
2025-02-20 21:17:03 +00:00
Philipinho 3e5cb92621 v0.8.1 2025-02-18 16:59:27 +00:00
Philipinho fd5ad2f576 fix signup email 2025-02-18 16:26:16 +00:00
Philipinho 74a5360561 v0.8.0 2025-02-18 11:14:16 +00:00
Philipinho 7580e8d1fe fix pagination limit 2025-02-15 14:15:39 +00:00
Philip Okugbe f92d63261d Implement space member search (#731)
* Hide pagination buttons if there is nothing to paginate
* Create reusable hook for search and pagination
2025-02-15 14:14:30 +00:00
Philip Okugbe 4d51986250 update dependences (#729) 2025-02-14 16:59:19 +00:00
Philip Okugbe e209aaa272 feat: internal page links and mentions (#604)
* Work on mentions

* fix: properly parse page slug

* fix editor suggestion bugs

* mentions must start with whitespace

* add icon to page mention render

* feat: backlinks - WIP

* UI - WIP

* permissions check
* use FTS for page suggestion

* cleanup

* WIP

* page title fallback

* feat: handle internal link paste

* link styling

* WIP

* Switch back to LIKE operator for search suggestion

* WIP
* scope to workspaceId
* still create link for pages not found

* select necessary columns

* cleanups
2025-02-14 15:36:44 +00:00
Philip Okugbe 0ef6b1978a feat: UI pagination and members search (#724)
* feat: pagination (UI)

* Fixes

* feat: add search to member list page

* responsiveness
2025-02-13 23:28:00 +00:00
Auxa ae842f94d0 * fix: popover does not close when clicking outside in SwitchSpace. (#720) 2025-02-12 16:14:21 +00:00
Philipinho 7121771f92 fix workspace setup 2025-02-12 15:33:08 +00:00
Philip Okugbe 040d6625df fix: enforce 32-character minimum length for APP_SECRET (#702)
* Enforce 32 characters minimum APP_SECRET length

* update APP_SECRET comment
2025-02-06 17:46:32 +00:00
Philip Okugbe 33ddd92198 * fix codeblock tab-size (#703)
* hide codeblock menu group during printing
2025-02-06 17:43:31 +00:00
Philip Okugbe 54e8d60840 New language options (es-ES, it-IT, ja-JP, ko-KR, ru-RU) (#701) 2025-02-06 16:47:41 +00:00
Philip Okugbe db986038c2 New Crowdin updates (#659)
New translations
2025-02-06 16:44:48 +00:00
fuscodev de0b5f0046 feat: add text alignment (#667)
* feat: text alignment

* fix text case

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-02-06 16:24:36 +00:00
Philipinho 638b811857 fix import 2025-02-03 21:51:09 +00:00
Philip Okugbe d775a61c95 fix client side env variable refresh (#695) 2025-02-03 21:47:38 +00:00
Philipinho 0f74f03264 fix name extraction from email 2025-01-30 22:13:23 +00:00
fuscodev f8b93ce93f fix: switch space bug (#692) 2025-01-30 21:14:11 +00:00
Philip Okugbe 85d18b8cc8 Set default language on invitation signup (#691)
* Default language selection to en-US if locale is undefined (Client)
2025-01-30 13:25:10 +00:00
Philip Okugbe 4d9fe6f804 Fix invitation signup redirect (#690)
* Fix invitation signup redirect
2025-01-30 12:54:02 +00:00
Philip Okugbe 85159a2c95 * fix 401 redirect in auth routes (#674)
* fix config getter
2025-01-26 14:01:08 +00:00
Philip Okugbe 990612793f refactor: switch to HttpOnly cookie (#660)
* Switch to httpOnly cookie
* create endpoint to retrieve temporary collaboration token

* cleanups
2025-01-22 22:11:11 +00:00
Philip Okugbe f2235fd2a2 update katex (#658) 2025-01-22 18:08:31 +00:00
Philipinho 2044cbb21c fix translation
* fix filesize formatting
2025-01-16 15:29:09 +00:00
Philipinho 3d52b82cd4 v0.7.0 2025-01-16 13:15:39 +00:00
Philip Okugbe 89a2dd602b fix punycode DeprecationWarning error (#631) 2025-01-16 13:13:14 +00:00
Philip Okugbe 3cb954db69 fix: editor improvements (#583)
* delete unused component

* return page prosemirror content

* prefetch pages

* use prosemirro json content on editor

* cache page query with id and slug as key

* Show notice on collaboration disconnection

* enable scroll while typing

* enable immediatelyRender

* avoid image break in PDF print

* Comment editor rendering props
2025-01-16 12:48:35 +00:00
Naifer 71cfe3cd8e fix: add cancel button for editing comments (#580)
* fix: add cancel button for editing comments

* cleanup

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-01-15 16:37:57 +00:00
Naifer f7efb6c2c9 Fix: Ensure only one emoji list appears (#572)
* Fix: Ensure only one emoji list appears

* fix: refactor logic

* remove unused file node-id-atoms

* small fix

* align with Mantine UI

* close emoji picker on escape

* translate string

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-01-15 16:07:26 +00:00
Philip Okugbe 59b514fa26 New Crowdin updates (#630)
* New translations translation.json
2025-01-15 14:18:05 +00:00
Philip Okugbe 0c1f9304f4 New Crowdin updates (#627)
New translations translation.json
2025-01-15 14:12:48 +00:00
Philipinho e876214eeb fix: embed provider name in error message 2025-01-11 22:25:51 +00:00
Hoie Kim 5fece5fc68 feat: google sheets embed (#615) 2025-01-11 22:22:06 +00:00
Philip Okugbe f3dbf7cc5d feat: add new languages to selection (#626)
* Add new languages to selection

* more translations
2025-01-11 22:11:31 +00:00
Philip Okugbe f7ac6bb4bb New Crowdin updates (#605)
* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2025-01-11 15:38:32 +00:00
Philip Okugbe 1f5ffe7f9d Fix empty translation value 2025-01-11 15:34:27 +00:00
Philipinho 95715421c6 fix: move markdown clipboard extension to client app 2025-01-04 21:25:05 +00:00
Philip Okugbe f5bc99b449 fix: link paste handler (#609)
* feat: support pasting markdown

* fix link paste handler
2025-01-04 20:47:49 +00:00
Philip Okugbe 287b833838 feat: support pasting markdown (#606) 2025-01-04 16:57:36 +00:00
Philip Okugbe 0cbbcb8eb1 Update Crowdin configuration file 2025-01-04 13:22:31 +00:00
lleohao 670ee64179 Support I18n (#243)
* feat: support i18n

* feat: wip support i18n

* feat: complete space translation

* feat: complete page translation

* feat: update space translation

* feat: update workspace translation

* feat: update group translation

* feat: update workspace translation

* feat: update page translation

* feat: update user translation

* chore: update pnpm-lock

* feat: add query translation

* refactor: merge to single file

* chore: remove necessary code

* feat: save language to BE

* fix: only load current language

* feat: save language to locale column

* fix: cleanups

* add language menu to preferences page

* new translations

* translate editor

* Translate editor placeholders

* translate space selection component

---------

Co-authored-by: Philip Okugbe <phil@docmost.com>
Co-authored-by: Philip Okugbe <16838612+Philipinho@users.noreply.github.com>
2025-01-04 13:17:17 +00:00
Philipinho 290b7d9d94 v0.6.2 2024-12-14 20:39:19 +00:00
Philip Okugbe 2503bfd3a2 fix: prevent CDNs from caching attachments (#562) 2024-12-14 19:55:49 +00:00
Philipinho f48d6dd60b fix: don't throw error while parsing auth tokens 2024-12-12 14:29:25 +00:00
Philipinho 1302b1b602 v0.6.1 2024-12-11 14:55:06 +00:00
Philipinho 89a3f4cfc2 v0.6.1 2024-12-11 14:54:19 +00:00
Philip Okugbe e48b1c0dae fix: markdown math import (#529)
* fix: markdown math block import

* fix: block and inline math import

* cleanup
2024-12-09 15:08:25 +00:00
Philip Okugbe 4a2a5a7a4d fix: postgres and redis url validation (#548) 2024-12-09 14:56:15 +00:00
Philip Okugbe 532001fd82 chore: fix linting (#544)
* fix: eslint (server)

* fix: eslint (client)

* commit package lock file

* fix linting
2024-12-09 14:51:31 +00:00
Philip Okugbe e6bf4cdd6c fix: fix markdown import file button (#542) 2024-12-06 12:26:55 +00:00
Philipinho a9a4a26db5 fix export controller reference in module 2024-11-30 20:42:31 +00:00
Philipinho ede5633415 fix export fileName 2024-11-30 20:40:53 +00:00
Philipinho a25cf84671 fix: add spaceId 2024-11-30 20:29:13 +00:00
Philipinho a37d558bac v0.6.0 2024-11-30 20:06:44 +00:00
Philip Okugbe ddb0f9225f fix: uuid7 for commentId (#524) 2024-11-30 20:04:50 +00:00
Philip Okugbe c717847ca8 chore: update packages (#507) 2024-11-30 19:54:04 +00:00
Philip Okugbe fe83557767 feat: space export (#506)
* wip

* Space export
* option to export pages with children
* include attachments in exports
* unified export UI

* cleanup

* fix: change export icon

* add export button to space settings

* cleanups

* export name
2024-11-30 19:47:22 +00:00
Philip Okugbe 9fa432dba9 feat: support tab key in code block (#523) 2024-11-30 14:40:05 +00:00
Philip Okugbe c6aaefecbd fix: clear local cache on logout (#519) 2024-11-28 20:35:53 +00:00
Philip Okugbe 311d81bc71 fix wrong tree sync bug (#514) 2024-11-28 19:39:38 +00:00
Philip Okugbe f178e6654f fix: properly support redis db (#517) 2024-11-28 18:54:28 +00:00
Philip Okugbe ca186f3c0e fix: return direct embed urls if present (#516) 2024-11-28 18:53:49 +00:00
Philip Okugbe a16d5d1bf4 feat: websocket rooms (#515) 2024-11-28 18:53:29 +00:00
Philip Okugbe d97baf5824 add env variable (#513) 2024-11-28 18:48:25 +00:00
Philip Okugbe 8349d8271c fix: allow space in inline math (#508) 2024-11-28 18:48:08 +00:00
Philip Okugbe 2e6d16dbc3 fix: full width bug on smaller screens (#518) 2024-11-28 18:44:42 +00:00
Philipinho 4107793e73 fix: disable user-select 2024-11-28 15:55:10 +00:00
Philip Okugbe a1b6ac7f3e fix: close space selection popover onClickOutside (#485) 2024-11-27 02:32:12 +00:00
Philip Okugbe dd0319a14d fix: index imported content (#495) 2024-11-20 13:36:36 +00:00
Philip Okugbe 8194c7d42d fix: focus editor on bottom click (#484) 2024-11-13 20:00:25 +00:00
Philipinho d01ced078b * Reduce code block font-size
* Make inline code more distinctive
2024-11-13 11:36:55 -08:00
ftibi93 da9c971050 fix breadcrumb clipping (#457) 2024-11-13 19:15:37 +00:00
Philipinho 4e7af507c6 fix tree dnd 2024-11-06 19:29:12 -08:00
Philipinho f7426a0b45 fix: use clsx 2024-11-01 10:09:52 +00:00
Philip Okugbe b85b34d6b1 feat: resizable sidebar (#452)
* feat: resizable sidebar

* only expand space sidebar
2024-11-01 10:05:03 +00:00
ftibi93 e064e58f79 Fix sidebar responsivity (#453)
* navbar height fix. has to be cleaned up
* use parent height for tree
* cleanups
2024-11-01 09:41:23 +00:00
Philip Okugbe 4f1a97ceb9 Revert "fix: prevent default browser save behavior (#450)" (#451)
This reverts commit d07338861b.
2024-10-30 12:23:31 +00:00
Philip Okugbe d07338861b fix: prevent default browser save behavior (#450) 2024-10-30 11:41:23 +00:00
Philipinho 95159625aa v0.5.0 2024-10-29 19:50:44 +00:00
Philipinho 9e0fbae1de fix: save excalidraw diagram in light mode only 2024-10-29 19:39:08 +00:00
Philipinho a52c86a180 fix: add drawio dark mode support 2024-10-29 19:37:49 +00:00
Philipinho 31feb38def fix: sync color scheme with excalidraw 2024-10-29 19:33:08 +00:00
Philipinho ba32e42ece fix: filter out redundant group 2024-10-29 19:15:26 +00:00
Philipinho a574d13f43 fix: email overflow 2024-10-29 18:44:59 +00:00
Philip Okugbe ab70cee278 feat: third-party embeds (#423)
* wip

* Add more providers

* icons

* unify embed providers (Youtube)

* fix case

* YT music

* remove redundant code
2024-10-29 18:13:20 +00:00
Philip Okugbe 978fadd6b9 fix: improve sidebar page tree syncing (#407)
* sync node deletion

* tree sync improvements

* fix cache bug

* fix debounced page title

* fix
2024-10-26 15:48:40 +01:00
Philipinho b57be9c736 fix: rename edit -> save 2024-10-14 12:29:11 +01:00
James Choi d4b219d608 add COPY patches to Dockerfile (#400) 2024-10-14 09:13:36 +01:00
Philip Okugbe 36e720920b fix: bug fixes (#397)
* Add more html page titles

* Make tables responsive

* fix react query keys

* Add tooltip to sidebar toggle

* fix: trim inputs

* fix inputs
2024-10-13 17:09:45 +01:00
Philip Okugbe fa3c8a03e1 fix: remove space tree delete shortcut key (#394) 2024-10-12 13:14:29 +01:00
Philipinho 46d92fbabc v0.4.1 2024-10-11 11:39:12 +01:00
Philip Okugbe e17b975aaa Revert "Add alignment styles for task list items (#378)" (#390)
This reverts commit 2721ab6a29.
2024-10-11 11:37:39 +01:00
Philipinho 038d21b438 v0.4.0 2024-10-10 22:03:16 +01:00
Philipinho 078361b367 add local editor-ext to client package.json
* update vite
2024-10-10 21:57:36 +01:00
Philip Okugbe 384f11f2b7 make file upload size limit configurable (#386) 2024-10-10 21:28:28 +01:00
servostar e333eee08b fix: base64 encoded drawio image decoded to Latin-1 instead of UTF-8 (#369) 2024-10-10 15:39:04 +01:00
servostar 7ec6a36515 fix: removed font overwrite for KaTeX elements (#377) 2024-10-10 15:35:20 +01:00
ja49619 2721ab6a29 Add alignment styles for task list items (#378) 2024-10-09 18:52:20 +01:00
servostar a2bc374f47 fix: horizontal scrollbar always shown on math block (#353) 2024-09-30 02:39:57 +01:00
ceroma eaa80a5546 fix: disconnect Redis health checker (#351) 2024-09-29 10:00:24 +01:00
Orel Lazri e9e668bd39 fix: use environment service for refresh token's expiration (#337) 2024-09-21 10:41:26 +01:00
Orion 9390b39e35 Implement nodemailer ignore tls property (#299) 2024-09-20 17:57:50 +01:00
ceroma 2ae3816324 fix: send "invitation accepted" email to inviter (#331)
The email says "${invitedUserName} has accepted your invitation ...", so it makes more sense to send it to the inviter instead of the invitee.
2024-09-19 22:19:04 +01:00
Philipinho e96330afbf fix: text casing 2024-09-19 15:59:56 +01:00
Philip Okugbe e56f7933f4 fix: refactor forgot password system (#329)
* refactor forgot password system

* ready
2024-09-19 15:51:51 +01:00
Philip Okugbe b152c858b4 fix: add user tokens repo to database module 2024-09-18 20:28:39 +01:00
Sahariar Alam Khandoker e43ea66442 add forgot-password ui (#273) 2024-09-17 15:53:05 +01:00
Reinaldy Rafli f34812653e feat(backend): forgot password (#250)
* feat(backend): forgot password

* feat: apply feedback from code review

* chore(auth): validate the minimum length of 'newPassword'

* chore(auth): make token has an expiry of 1 hour

* chore: rename all occurrences of 'code' to 'token'

* chore(backend): provide value on nanoIdGen method
2024-09-17 15:52:47 +01:00
Philip Okugbe 6a3a7721be features and bug fixes (#322)
* fix page import title bug

* fix youtube embed in markdown export

* add link to rendered file html

* fix: markdown callout import

* update local generateJSON

* feat: switch spaces from sidebar

* remove unused package

* feat: editor date menu command

* fix date description

* update default locale code

* feat: add more code highlight languages
2024-09-17 15:40:49 +01:00
Philip Okugbe fb27282886 feat: delete space and edit space slug (#307)
* feat: make space slug editable

* feat: delete space

* client
2024-09-16 17:43:40 +01:00
Philip Okugbe dea9f4c063 remove unnecessary log 2024-09-13 22:37:38 +01:00
Philip Okugbe 0b6730c06f fix page export failure when title contains non-ASCII characters (#309) 2024-09-13 17:40:24 +01:00
Philipinho be0d97661a update README 2024-09-04 18:56:14 +01:00
Philipinho 4e2b23c97e v0.3.1 2024-09-03 10:49:38 +01:00
Philipinho dc3ce27762 fix collaboration websocket 2024-09-03 10:48:47 +01:00
Philipinho 8af2d4e8cf file content-disposition 2024-09-02 16:39:07 +01:00
Philipinho 73ddec4ca7 v0.3.0 2024-09-02 15:56:24 +01:00
Philip Okugbe 2b9765fb35 lazy load (#237) 2024-09-02 15:51:28 +01:00
Philipinho 7fdd355cc3 Reduce version text size 2024-09-02 13:08:01 +01:00
Philipinho 6c6b47599a update dependencies 2024-09-02 12:43:33 +01:00
Philipinho 7e6a71fa2d add HR divider to slash menu 2024-09-02 01:28:15 +01:00
Philipinho 1141796f24 Show version
* Add default mermaid content
2024-09-01 17:30:34 +01:00
Philipinho 11dbc079be Add home link to logo 2024-09-01 16:24:20 +01:00
Philip Okugbe 87b99f8646 feat: draw.io (diagrams.net) integration (#215)
* draw.io init

* updates
2024-09-01 12:26:20 +01:00
Philip Okugbe 38e9eef2dc feat: excalidraw integration (#214)
* update tiptap version

* excalidraw init

* cleanup

* better file handling and other fixes

* use different modal to fix excalidraw cursor position issue
* see https://github.com/excalidraw/excalidraw/issues/7312
* fix websocket in vite dev mode

* WIP

* add align attribute

* fix table

* menu icons

* Render image in excalidraw html
* add size to custom SVG components

* rewrite undefined font urls
2024-08-31 19:11:07 +01:00
Philip Okugbe 77b541ec71 Fix mime attribute 2024-08-26 17:12:59 +01:00
Philip Okugbe 7dc37b933f feat: editor file attachments (#194)
* fix current slider value

* WIP

* changes to extension attributes

* update command title
2024-08-26 12:38:47 +01:00
Philip Okugbe 7e80797e3f feat: mermaid diagram integration (#202) 2024-08-24 18:30:07 +01:00
Philip Okugbe 17475bf123 feat: code block language selection (#198)
* code block language selection

* cleanup

* Add copy button
2024-08-24 18:12:19 +01:00
Marc 4433d5174d Add Source Label to Dockerfile (#157) 2024-08-20 13:09:36 +01:00
sidnelui-krystal c810d0b314 fix: added env variable for support for forcepathstyle on s3 (#181) 2024-08-20 13:05:59 +01:00
Philipinho 463480ae67 v0.2.10 (fix) 2024-08-07 23:03:00 +02:00
Philipinho 2449d69fab v2.9.10 2024-08-07 22:22:49 +02:00
Philip Okugbe e0d74fcb0e Fix: editor formatting (#137)
* reduce space between list items

* reduce spacing

* Make inline code readable in dark mode
* Disable spellcheck in code

* fix numbered list in toggle block
2024-08-04 10:06:22 +02:00
Philipinho 4967849e3a add SMTP_SECURE 2024-08-02 11:19:12 +02:00
Philipinho 0a447e91bb fix markdown import 2024-07-22 18:39:44 +01:00
Philipinho 48e76aa9f4 v0.2.8 2024-07-22 16:36:06 +01:00
Philipinho 2bd6422a35 Show new workspace role on change 2024-07-22 16:35:00 +01:00
olivierIllogika 407a1aff3b only owner can assign owner role (#108)
* backend fix: https://github.com/docmost/docmost/commit/b4bc184cb3749a3faa5a00d5a1240faacd4b1035
2024-07-22 16:18:09 +01:00
Philipinho b4bc184cb3 prevent admin role from managing owner role (backend) 2024-07-22 16:16:33 +01:00
Philipinho 109dbdbe02 cleanup log 2024-07-22 15:59:43 +01:00
Philipinho 2df7de5828 fix table commands type error 2024-07-22 15:43:43 +01:00
Philipinho 373fc86e47 preserve details tag in markdown export 2024-07-22 14:09:52 +01:00
Philipinho 5052a9ea40 Support math export in Markdown 2024-07-22 13:20:00 +01:00
Philipinho cd47c79d86 Make math node handling better 2024-07-22 13:05:07 +01:00
Philipinho 78746938b7 fix export format state 2024-07-22 13:02:13 +01:00
Philipinho 4d2936627c fix: generate ydoc state during page import to prevent duplicate nodes on the editor 2024-07-22 11:02:43 +01:00
Philipinho d2ecd28047 fix: localize attachment type
* fixes #86
2024-07-22 10:58:32 +01:00
Philipinho bb92ca75e9 use logger 2024-07-21 21:57:31 +01:00
Philipinho 8f3e2ff663 fix editor placeholder bug 2024-07-21 20:50:08 +01:00
Philipinho 89f6311e46 * Make page import handling better 2024-07-21 20:48:33 +01:00
Philipinho e5a97d2a26 v0.2.7 2024-07-20 18:21:12 +01:00
Philipinho 7f0fd45f3a singular & plural count 2024-07-20 18:20:10 +01:00
Philipinho 078959dfa0 v0.2.6 2024-07-20 18:05:56 +01:00
Philip Okugbe 937a07059a feat: implement Markdown and HTML page imports (#85)
* page import feature
* make file interceptor common

* replace @tiptap/html
* update tiptap version

* reduce table margin

* update tiptap version

* switch to upstream drag handle lib (fixes table dragging)

* WIP

* Page import module and other fixes

* working page imports

* extract page title from h1 heading

* finalize page imports

* cleanup unused imports

* add menu arrow
2024-07-20 17:59:04 +01:00
olivierIllogika 227ac30d5e add STMP_SECURE and changed auth config (#81)
* add STMP_SECURE and changed auth config
* relocated logic to mail.provider.ts
2024-07-19 12:54:55 +01:00
Philipinho a2ae341934 v0.2.5 2024-07-12 18:05:59 +01:00
Philipinho 3c70e40d16 add table header by default 2024-07-12 16:53:33 +01:00
Philipinho 14197d7365 add cookie duration 2024-07-12 14:52:09 +01:00
Philip Okugbe f388540293 feat: Individual page export in Markdown and HTML formats (#80)
* fix maths node

* render default html width

* Add page export module
* with support for html and markdown exports

* Page export UI
* Add PDF print too

* remove unused import
2024-07-12 14:45:09 +01:00
Philipinho b43de81013 cleanup 2024-07-07 16:36:14 +01:00
Philipinho 6659adc7fe v0.2.4 2024-07-07 16:31:28 +01:00
Philip Okugbe 24adff9679 Merge pull request #74 from docmost/remove-redundant-page-slug_id-index
remove redundant page slug_id index
2024-07-07 16:29:21 +01:00
Philipinho e960b8c1a9 create migration 2024-07-07 16:27:43 +01:00
Philipinho 1958067110 Revert "remove redundant slug_id index"
This reverts commit 3e519ebcd8.
2024-07-07 16:16:13 +01:00
Philipinho 3e519ebcd8 remove redundant slug_id index 2024-07-07 16:07:43 +01:00
Philip Okugbe 07cd650205 Merge pull request #73 from docmost/fix/case-insensitive-email
make emails case-insensitive
2024-07-07 15:51:14 +01:00
Philipinho 949d782a28 make emails case-insensitive 2024-07-07 15:49:43 +01:00
Philipinho 295d4325bf update tiptap hocuspocus 2024-07-07 15:36:36 +01:00
Philip Okugbe 40a40bb3c7 Merge pull request #71 from docmost/aws_env_option
Add support for AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables
2024-07-07 09:49:31 +01:00
Philipinho bc1579b022 fix condition check 2024-07-07 09:48:43 +01:00
Philip Okugbe 85b3073681 Merge pull request #57 from will2hew/will/url-setup
CORS setup and Vite devServer proxy
2024-07-07 09:42:51 +01:00
Philipinho 4af3a54649 Allow AWS SDK use AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY 2024-07-07 09:24:19 +01:00
Philip Okugbe c4c169b17a Merge pull request #64 from docmost/feat/health
Add health check
2024-07-06 01:06:17 +01:00
Philipinho a0536d852f cleanup indicators 2024-07-05 23:05:19 +01:00
Philipinho f12f93b373 increase startup db retry limit 2024-07-05 19:00:55 +01:00
Philipinho 35dcd5f254 refactor health module 2024-07-05 18:59:26 +01:00
Philipinho 9496ec9b57 prevent database error from crashing server 2024-07-05 18:59:16 +01:00
Will H 5ace7616d0 cors setup and dev server proxy 2024-07-05 16:25:00 +12:00
Will H ce6a05ab66 cr - add redis check and logging 2024-07-05 13:40:51 +12:00
Philipinho 7383673636 v0.2.3 2024-07-05 00:49:08 +01:00
Philip Okugbe 3e7b2495c5 Merge pull request #51 from docmost/private-attachments
make page attachments private
2024-07-05 00:47:51 +01:00
Philip Okugbe 0fc8edeb52 Merge pull request #55 from docmost/fix/bug-fixes
Fix: missing tree, editor font and responsive recent pages
2024-07-05 00:45:32 +01:00
Philipinho bbf865b2f6 cleanup debug log 2024-07-05 00:41:30 +01:00
Philipinho 0c622a0dc1 use sane font-weight 2024-07-05 00:33:59 +01:00
Philipinho f52cd011a4 remove unused imports 2024-07-05 00:33:12 +01:00
Philipinho a4d53468c3 fix tree state 2024-07-05 00:30:56 +01:00
Will H 66773dfaca Add health check and dev script 2024-07-05 10:10:08 +12:00
Philipinho cc93abfb7e make pages table responsive 2024-07-04 21:13:43 +01:00
Philipinho 13f26f9c31 make page attachments private 2024-07-04 16:01:35 +01:00
Philipinho a4ec2dac6c v0.2.2 2024-07-03 13:01:00 +01:00
Philip Okugbe 681d7c789c merge: fix/media-in-firefox
fix media visibility in firefox
2024-07-03 11:57:03 +01:00
Philipinho 9f583174a9 fix media visibility in firefox 2024-07-03 11:53:09 +01:00
Philip Okugbe 05633082c5 feat/fuzzy-match-menu
added fuzzy matching logic for menu search
2024-07-03 11:25:21 +01:00
Philip Okugbe 491fbad4ac feat/full-width
add full page width user preference
2024-07-03 11:24:32 +01:00
Philipinho e824aeced7 Add width option to page menu 2024-07-03 11:23:42 +01:00
Philipinho 8f056d1071 add full page width preference 2024-07-03 11:00:42 +01:00
SurajJadhav7 99cf6dab62 added fuzzy matching logic for menu search 2024-07-01 16:49:36 +05:30
Philip Okugbe d1ae117f76 v0.2.1
client: fix APP_URL usage
2024-07-01 10:25:36 +01:00
Philipinho eea4e62c2e v0.2.1 2024-07-01 10:09:28 +01:00
Philipinho d429384d22 client: fix APP_URL 2024-07-01 10:08:12 +01:00
1142 changed files with 116417 additions and 13832 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
node_modules
.git
.gitignore
dist
data
/data
.env*
.nx
+23 -1
View File
@@ -2,7 +2,7 @@
APP_URL=http://localhost:3000
PORT=3000
# make sure to replace this.
# minimum of 32 characters. Generate one with: openssl rand -hex 32
APP_SECRET=REPLACE_WITH_LONG_SECRET
JWT_TOKEN_EXPIRES_IN=30d
@@ -19,6 +19,10 @@ AWS_S3_SECRET_ACCESS_KEY=
AWS_S3_REGION=
AWS_S3_BUCKET=
AWS_S3_ENDPOINT=
AWS_S3_FORCE_PATH_STYLE=
# default: 50mb
FILE_UPLOAD_SIZE_LIMIT=
# options: smtp | postmark
MAIL_DRIVER=smtp
@@ -30,7 +34,25 @@ SMTP_HOST=127.0.0.1
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_SECURE=false
SMTP_IGNORETLS=false
# Postmark driver config
POSTMARK_TOKEN=
# for custom drawio server
DRAWIO_URL=
# Gotenberg URL for server-side PDF export
GOTENBERG_URL=
DISABLE_TELEMETRY=false
# Enable debug logging in production (default: false)
DEBUG_MODE=false
# Log database queries
DEBUG_DB=false
# Log http requests
LOG_HTTP=false
+154
View File
@@ -0,0 +1,154 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g. v0.25.3)'
required: true
permissions:
contents: write
env:
VERSION: ${{ inputs.version || github.ref_name }}
jobs:
build:
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Generate token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.BUILD_APP_ID }}
private-key: ${{ secrets.BUILD_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Checkout with submodules
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
outputs: type=image,name=docmost/docmost,push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.suffix }}
cache-to: type=gha,scope=${{ matrix.suffix }},mode=max
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digest-${{ matrix.suffix }}
path: /tmp/digests/*
if-no-files-found: error
- name: Strip v prefix
id: strip-v
run: echo "version=${VERSION#v}" >> "$GITHUB_OUTPUT"
- name: Export Docker image
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
push: false
tags: |
docmost/docmost:latest
docmost/docmost:${{ steps.strip-v.outputs.version }}
outputs: type=docker,dest=docmost-${{ matrix.suffix }}.docker.tar
cache-from: type=gha,scope=${{ matrix.suffix }}
- name: Compress image
run: gzip docmost-${{ matrix.suffix }}.docker.tar
- name: Upload image archive
uses: actions/upload-artifact@v4
with:
name: docker-image-${{ matrix.suffix }}
path: docmost-${{ matrix.suffix }}.docker.tar.gz
if-no-files-found: error
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
pattern: digest-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata for tags
id: meta
uses: docker/metadata-action@v5
with:
images: docmost/docmost
tags: |
type=semver,pattern={{version}},value=${{ env.VERSION }}
type=semver,pattern={{major}}.{{minor}},value=${{ env.VERSION }},enable=${{ !contains(env.VERSION, '-') }}
type=raw,value=latest,enable=${{ !contains(env.VERSION, '-') }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'docmost/docmost@sha256:%s ' *)
- name: Download image archives
uses: actions/download-artifact@v4
with:
pattern: docker-image-*
path: /tmp/images
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.VERSION }}
files: |
/tmp/images/docmost-amd64.docker.tar.gz
/tmp/images/docmost-arm64.docker.tar.gz
draft: true
+2
View File
@@ -1,4 +1,6 @@
.env
.env.dev
.env.prod
data
# compiled output
/dist
+3
View File
@@ -0,0 +1,3 @@
[submodule "apps/server/src/ee"]
path = apps/server/src/ee
url = https://github.com/docmost/ee
+10 -4
View File
@@ -1,4 +1,7 @@
FROM node:21-alpine AS base
FROM node:22-slim AS base
LABEL org.opencontainers.image.source="https://github.com/docmost/docmost"
RUN npm install -g pnpm@10.4.0
FROM base AS builder
@@ -6,13 +9,14 @@ WORKDIR /app
COPY . .
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile
RUN pnpm build
FROM base AS installer
RUN apk add --no-cache curl bash
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl bash \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
@@ -28,8 +32,10 @@ COPY --from=builder /app/packages/editor-ext/package.json /app/packages/editor-e
# Copy root package files
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/pnpm*.yaml /app/
COPY --from=builder /app/.npmrc /app/.npmrc
RUN npm install -g pnpm
# Copy patches
COPY --from=builder /app/patches /app/patches
RUN chown -R node:node /app
+36 -9
View File
@@ -4,32 +4,59 @@
Open-source collaborative wiki and documentation software.
<br />
<a href="https://docmost.com"><strong>Website</strong></a> |
<a href="https://docmost.com/docs"><strong>Documentation</strong></a>
<a href="https://docmost.com/docs"><strong>Documentation</strong></a> |
<a href="https://twitter.com/DocmostHQ"><strong>Twitter / X</strong></a>
</p>
</div>
<br />
> [!NOTE]
> Docmost is currently in **beta**. We value your feedback as we progress towards a stable release.
## Getting started
To get started with Docmost, please refer to our [documentation](https://docmost.com/docs).
To get started with Docmost, please refer to our [documentation](https://docmost.com/docs) or try our [cloud version](https://docmost.com/pricing) .
## Features
- Real-time collaboration
- Diagrams (Draw.io, Excalidraw and Mermaid)
- Spaces
- Permissions management
- Groups
- Comments
- Page history
- Search
- File attachment
- File attachments
- Embeds (Airtable, Loom, Miro and more)
- Translations (10+ languages)
### Screenshots
#### Screenshots
<p align="center">
<img alt="home" src="https://docmost.com/screenshots/home.png" width="70%">
<img alt="editor" src="https://docmost.com/screenshots/editor.png" width="70%">
</p>
### Contributing
See the [development doc](https://docmost.com/docs/self-hosting/development)
### License
Docmost core is licensed under the open-source AGPL 3.0 license.
Enterprise features are available under an enterprise license (Enterprise Edition).
All files in the following directories are licensed under the Docmost Enterprise license defined in `packages/ee/License`.
- apps/server/src/ee
- apps/client/src/ee
- packages/ee
### Contributing
See the [development documentation](https://docmost.com/docs/self-hosting/development)
## Thanks
Special thanks to;
<img width="100" alt="Crowdin" src="https://github.com/user-attachments/assets/a6c3d352-e41b-448d-b6cd-3fbca3109f07" />
[Crowdin](https://crowdin.com/) for providing access to their localization platform.
<img width="48" alt="Algolia-mark-square-white" src="https://github.com/user-attachments/assets/6ccad04a-9589-4965-b6a1-d5cb1f4f9e94" />
[Algolia](https://www.algolia.com/) for providing full-text search to the docs.
-22
View File
@@ -1,22 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:@tanstack/eslint-plugin-query/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
}
+36
View File
@@ -0,0 +1,36 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import pluginQuery from "@tanstack/eslint-plugin-query";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
"@tanstack/query": pluginQuery,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": "off",
"react-hooks/exhaustive-deps": "off",
"@typescript-eslint/no-unused-expressions": "off",
"no-useless-escape": "off",
},
},
);
+12 -3
View File
@@ -2,10 +2,19 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no" />
<title>Docmost</title>
<meta name="theme-color" content="#1f1f1f" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#f6f7f9" media="(prefers-color-scheme: light)" />
<link rel="manifest" href="/manifest.json" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-title" content="Docmost" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<!--meta-tags-->
</head>
<body>
<div id="root"></div>
+60 -43
View File
@@ -1,67 +1,84 @@
{
"name": "client",
"private": true,
"version": "0.2.0",
"version": "0.80.1",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"lint": "eslint .",
"preview": "vite preview",
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\""
},
"dependencies": {
"@casl/ability": "^6.7.1",
"@casl/react": "^4.0.0",
"@casl/react": "^5.0.1",
"@docmost/editor-ext": "workspace:*",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@mantine/core": "^7.11.0",
"@mantine/form": "^7.11.0",
"@mantine/hooks": "^7.11.0",
"@mantine/modals": "^7.11.0",
"@mantine/notifications": "^7.11.0",
"@mantine/spotlight": "^7.11.0",
"@tabler/icons-react": "^3.7.0",
"@tanstack/react-query": "^5.48.0",
"@tiptap/extension-code-block-lowlight": "^2.4.0",
"axios": "^1.7.2",
"@excalidraw/excalidraw": "0.18.0-3a5ef40",
"@mantine/core": "^8.3.18",
"@mantine/dates": "^8.3.18",
"@mantine/form": "^8.3.18",
"@mantine/hooks": "^8.3.18",
"@mantine/modals": "^8.3.18",
"@mantine/notifications": "^8.3.18",
"@mantine/spotlight": "^8.3.18",
"@tabler/icons-react": "^3.40.0",
"@tanstack/react-query": "5.90.17",
"alfaaz": "^1.1.0",
"axios": "1.16.0",
"blueimp-load-image": "^5.16.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"emoji-mart": "^5.6.0",
"jotai": "^2.8.3",
"file-saver": "^2.0.5",
"highlightjs-sap-abap": "^0.3.0",
"i18next": "25.10.1",
"i18next-http-backend": "3.0.6",
"jotai": "^2.18.1",
"jotai-optics": "^0.4.0",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"katex": "^0.16.10",
"lowlight": "^3.1.0",
"katex": "0.16.40",
"lowlight": "^3.3.0",
"mantine-form-zod-resolver": "^1.3.0",
"mermaid": "^11.13.0",
"mitt": "^3.0.1",
"posthog-js": "1.372.2",
"react": "^18.3.1",
"react-arborist": "^3.4.0",
"react-arborist": "3.4.0",
"react-clear-modal": "^2.0.18",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
"react-helmet-async": "^2.0.5",
"react-moveable": "^0.56.0",
"react-router-dom": "^6.24.0",
"socket.io-client": "^4.7.5",
"tippy.js": "^6.3.7",
"zod": "^3.23.8"
"react-drawio": "^1.0.7",
"react-error-boundary": "^6.1.1",
"react-helmet-async": "^3.0.0",
"react-i18next": "16.5.8",
"react-router-dom": "^7.13.1",
"semver": "^7.7.4",
"socket.io-client": "^4.8.3",
"zod": "^4.3.6"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.47.0",
"@eslint/js": "^9.28.0",
"@tanstack/eslint-plugin-query": "^5.94.4",
"@types/blueimp-load-image": "^5.16.6",
"@types/file-saver": "^2.0.7",
"@types/js-cookie": "^3.0.6",
"@types/katex": "^0.16.7",
"@types/node": "20.14.9",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^9.5.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"@types/katex": "^0.16.8",
"@types/node": "22.19.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.28.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^15.13.0",
"optics-ts": "^2.4.1",
"postcss": "^8.4.38",
"postcss-preset-mantine": "^1.15.0",
"postcss": "^8.5.12",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.3.2",
"typescript": "^5.5.2",
"vite": "^5.3.1"
"prettier": "^3.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.1",
"vite": "8.0.5"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
{
"name": "Docmost",
"short_name": "Docmost",
"start_url": "/",
"display": "standalone",
"background_color": "#222",
"theme_color": "#222",
"icons": [
{
"src": "icons/favicon-16x16.png",
"type": "image/png",
"sizes": "16x16"
},
{
"src": "icons/favicon-32x32.png",
"type": "image/png",
"sizes": "32x32"
},
{
"src": "icons/app-icon-192x192.png",
"type": "image/png",
"sizes": "180x180 192x192"
},
{
"src": "icons/app-icon-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
+80 -47
View File
@@ -10,51 +10,46 @@ import Groups from "@/pages/settings/group/groups";
import GroupInfo from "./pages/settings/group/group-info";
import Spaces from "@/pages/settings/space/spaces.tsx";
import { Error404 } from "@/components/ui/error-404.tsx";
import { useQuerySubscription } from "@/features/websocket/use-query-subscription.ts";
import { useAtom, useAtomValue } from "jotai";
import { socketAtom } from "@/features/websocket/atoms/socket-atom.ts";
import { useTreeSocket } from "@/features/websocket/use-tree-socket.ts";
import { useEffect } from "react";
import { io } from "socket.io-client";
import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom.ts";
import { SOCKET_URL } from "@/features/websocket/types";
import AccountPreferences from "@/pages/settings/account/account-preferences.tsx";
import SpaceHome from "@/pages/space/space-home.tsx";
import PageRedirect from "@/pages/page/page-redirect.tsx";
import Layout from "@/components/layouts/global/layout.tsx";
import { ErrorBoundary } from "react-error-boundary";
import InviteSignup from "@/pages/auth/invite-signup.tsx";
import ForgotPassword from "@/pages/auth/forgot-password.tsx";
import PasswordReset from "./pages/auth/password-reset";
import Billing from "@/ee/billing/pages/billing.tsx";
import CloudLogin from "@/ee/pages/cloud-login.tsx";
import CreateWorkspace from "@/ee/pages/create-workspace.tsx";
import { isCloud } from "@/lib/config.ts";
import { useTranslation } from "react-i18next";
import Security from "@/ee/security/pages/security.tsx";
import License from "@/ee/licence/pages/license.tsx";
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
import SharedPage from "@/pages/share/shared-page.tsx";
import PdfRenderPage from "@/ee/pdf-export/pdf-render-page.tsx";
import Shares from "@/pages/settings/shares/shares.tsx";
import ShareLayout from "@/features/share/components/share-layout.tsx";
import ShareRedirect from "@/pages/share/share-redirect.tsx";
import { useTrackOrigin } from "@/hooks/use-track-origin";
import SpacesPage from "@/pages/spaces/spaces.tsx";
import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page";
import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page";
import SpaceTrash from "@/pages/space/space-trash.tsx";
import UserApiKeys from "@/ee/api-key/pages/user-api-keys";
import WorkspaceApiKeys from "@/ee/api-key/pages/workspace-api-keys";
import AiSettings from "@/ee/ai/pages/ai-settings.tsx";
import AuditLogs from "@/ee/audit/pages/audit-logs.tsx";
import VerifiedPages from "@/ee/page-verification/pages/verified-pages.tsx";
import TemplateList from "@/ee/template/pages/template-list";
import TemplateEditor from "@/ee/template/pages/template-editor";
import FavoritesPage from "@/pages/favorites/favorites-page";
import AiChat from "@/ee/ai-chat/pages/ai-chat.tsx";
import VerifyEmail from "@/ee/pages/verify-email.tsx";
export default function App() {
const [, setSocket] = useAtom(socketAtom);
const authToken = useAtomValue(authTokensAtom);
useEffect(() => {
if (!authToken?.accessToken) {
return;
}
const newSocket = io(SOCKET_URL, {
transports: ["websocket"],
auth: {
token: authToken.accessToken,
},
});
// @ts-ignore
setSocket(newSocket);
newSocket.on("connect", () => {
console.log("ws connected");
});
return () => {
console.log("ws disconnected");
newSocket.disconnect();
};
}, [authToken?.accessToken]);
useQuerySubscription();
useTreeSocket();
const { t } = useTranslation();
useRedirectToCloudSelect();
useTrackOrigin();
return (
<>
@@ -62,23 +57,51 @@ export default function App() {
<Route index element={<Navigate to="/home" />} />
<Route path={"/login"} element={<LoginPage />} />
<Route path={"/invites/:invitationId"} element={<InviteSignup />} />
<Route path={"/setup/register"} element={<SetupWorkspace />} />
<Route path={"/forgot-password"} element={<ForgotPassword />} />
<Route path={"/password-reset"} element={<PasswordReset />} />
<Route path={"/login/mfa"} element={<MfaChallengePage />} />
<Route path={"/login/mfa/setup"} element={<MfaSetupRequiredPage />} />
{!isCloud() && (
<Route path={"/setup/register"} element={<SetupWorkspace />} />
)}
{isCloud() && (
<>
<Route path={"/create"} element={<CreateWorkspace />} />
<Route path={"/select"} element={<CloudLogin />} />
<Route path={"/verify-email"} element={<VerifyEmail />} />
</>
)}
<Route element={<ShareLayout />}>
<Route
path={"/share/:shareId/p/:pageSlug"}
element={<SharedPage />}
/>
<Route path={"/share/p/:pageSlug"} element={<SharedPage />} />
</Route>
<Route path={"/pdf-render/:pageId"} element={<PdfRenderPage />} />
<Route path={"/share/:shareId"} element={<ShareRedirect />} />
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
<Route element={<Layout />}>
<Route path={"/home"} element={<Home />} />
<Route path={"/ai"} element={<AiChat />} />
<Route path={"/ai/chat/:chatId"} element={<AiChat />} />
<Route path={"/spaces"} element={<SpacesPage />} />
<Route path={"/favorites"} element={<FavoritesPage />} />
<Route path={"/templates"} element={<TemplateList />} />
<Route
path={"/templates/:templateId"}
element={<TemplateEditor />}
/>
<Route path={"/s/:spaceSlug"} element={<SpaceHome />} />
<Route path={"/s/:spaceSlug/trash"} element={<SpaceTrash />} />
<Route
path={"/s/:spaceSlug/p/:pageSlug"}
element={
<ErrorBoundary
fallback={<>Failed to load page. An error occurred.</>}
>
<Page />
</ErrorBoundary>
}
element={<Page />}
/>
<Route path={"/settings"}>
@@ -87,11 +110,21 @@ export default function App() {
path={"account/preferences"}
element={<AccountPreferences />}
/>
<Route path={"account/api-keys"} element={<UserApiKeys />} />
<Route path={"workspace"} element={<WorkspaceSettings />} />
<Route path={"members"} element={<WorkspaceMembers />} />
<Route path={"api-keys"} element={<WorkspaceApiKeys />} />
<Route path={"groups"} element={<Groups />} />
<Route path={"groups/:groupId"} element={<GroupInfo />} />
<Route path={"spaces"} element={<Spaces />} />
<Route path={"sharing"} element={<Shares />} />
<Route path={"security"} element={<Security />} />
<Route path={"ai"} element={<AiSettings />} />
<Route path={"ai/mcp"} element={<AiSettings />} />
<Route path={"audit"} element={<AuditLogs />} />
<Route path={"verifications"} element={<VerifiedPages />} />
{!isCloud() && <Route path={"license"} element={<License />} />}
{isCloud() && <Route path={"billing"} element={<Billing />} />}
</Route>
</Route>
@@ -0,0 +1,175 @@
import React, { useRef } from "react";
import { Menu, Box, Loader } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { IconTrash, IconUpload } from "@tabler/icons-react";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
import { notifications } from "@mantine/notifications";
interface AvatarUploaderProps {
currentImageUrl?: string | null;
fallbackName?: string;
radius?: string | number;
size?: string | number;
variant?: string;
type: AvatarIconType;
onUpload: (file: File) => Promise<void>;
onRemove: () => Promise<void>;
isLoading?: boolean;
disabled?: boolean;
}
export default function AvatarUploader({
currentImageUrl,
fallbackName,
radius,
variant,
size,
type,
onUpload,
onRemove,
isLoading = false,
disabled = false,
}: AvatarUploaderProps) {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileInputChange = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0];
if (!file || disabled) {
return;
}
// Validate file size (max 10MB)
const maxSizeInBytes = 10 * 1024 * 1024;
if (file.size > maxSizeInBytes) {
notifications.show({
message: t("Image exceeds 10MB limit."),
color: "red",
});
// Reset the input
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
return;
}
try {
await onUpload(file);
} catch (error) {
console.error(error);
notifications.show({
message: t("Failed to upload image"),
color: "red",
});
}
// Reset the input so the same file can be selected again
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleUploadClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
} else {
console.error("File input ref is null!");
}
};
const ariaLabel = {
[AvatarIconType.AVATAR]: t("Change avatar"),
[AvatarIconType.SPACE_ICON]: t("Change space icon"),
[AvatarIconType.WORKSPACE_ICON]: t("Change workspace icon"),
}[type];
const handleRemove = async () => {
if (disabled) return;
try {
await onRemove();
notifications.show({
message: t("Image removed successfully"),
});
} catch (error) {
console.error(error);
notifications.show({
message: t("Failed to remove image"),
color: "red",
});
}
};
return (
<Box>
<input
type="file"
ref={fileInputRef}
onChange={handleFileInputChange}
accept="image/png,image/jpeg,image/jpg"
aria-label={ariaLabel}
tabIndex={-1}
style={{ display: "none" }}
/>
<Menu shadow="md" width={200} withArrow disabled={disabled || isLoading}>
<Menu.Target>
<Box style={{ position: "relative", display: "inline-block" }}>
<CustomAvatar
component="button"
size={size}
avatarUrl={currentImageUrl}
name={fallbackName}
aria-label={ariaLabel}
aria-haspopup="menu"
style={{
cursor: disabled || isLoading ? "default" : "pointer",
opacity: isLoading ? 0.6 : 1,
}}
radius={radius}
variant={variant}
type={type}
/>
{isLoading && (
<Box
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 200,
}}
>
<Loader size="sm" />
</Box>
)}
</Box>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconUpload size={16} />}
disabled={isLoading || disabled}
onClick={handleUploadClick}
>
{t("Upload image")}
</Menu.Item>
{currentImageUrl && (
<Menu.Item
leftSection={<IconTrash size={16} />}
color="red"
onClick={handleRemove}
disabled={isLoading || disabled}
>
{t("Remove image")}
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
</Box>
);
}
@@ -0,0 +1,33 @@
// Source: https://github.com/mantinedev/mantine/blob/master/packages/@mantine/core/src/components/CopyButton/CopyButton.tsx - MIT
// modified to use the polyfilled clipboard api
import React from "react";
import { useClipboard } from "@/hooks/use-clipboard";
import { useProps } from "@mantine/core";
interface CopyButtonProps {
/** Children callback, provides current status and copy function as an argument */
children: (payload: { copied: boolean; copy: () => void }) => React.ReactNode;
/** Value that is copied to the clipboard when the button is clicked */
value: string;
/** Copied status timeout in ms @default `1000` */
timeout?: number;
}
const defaultProps = {
timeout: 1000,
} satisfies Partial<CopyButtonProps>;
export function CopyButton(props: CopyButtonProps) {
const { children, timeout, value, ...others } = useProps(
"CopyButton",
defaultProps,
props,
);
const clipboard = useClipboard({ timeout });
const copy = () => clipboard.copy(value);
return <>{children({ copy, copied: clipboard.copied, ...others })}</>;
}
CopyButton.displayName = "@mantine/core/CopyButton";
@@ -0,0 +1,36 @@
import { ActionIcon, MantineColor, MantineSize, Tooltip } from "@mantine/core";
import { CopyButton } from "@/components/common/copy-button";
import { IconCheck, IconCopy } from "@tabler/icons-react";
import React from "react";
import { useTranslation } from "react-i18next";
interface CopyProps {
text: string;
size?: MantineSize;
color?: MantineColor;
}
export default function CopyTextButton({ text, size }: CopyProps) {
const { t } = useTranslation();
return (
<CopyButton value={text} timeout={2000}>
{({ copied, copy }) => (
<Tooltip
label={copied ? t("Copied") : t("Copy")}
withArrow
position="right"
>
<ActionIcon
color={copied ? "teal" : "gray"}
variant="subtle"
onClick={copy}
size={size}
aria-label={copied ? t("Copied") : t("Copy")}
>
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
);
}
@@ -0,0 +1,176 @@
import {
Modal,
Button,
Group,
Text,
Select,
Switch,
Divider,
} from "@mantine/core";
import { exportPage } from "@/features/page/services/page-service.ts";
import { useState } from "react";
import { ExportFormat } from "@/features/page/types/page.types.ts";
import { notifications } from "@mantine/notifications";
import { exportSpace } from "@/features/space/services/space-service";
import { useTranslation } from "react-i18next";
interface ExportModalProps {
id: string;
type: "space" | "page";
open: boolean;
onClose: () => void;
}
export default function ExportModal({
id,
type,
open,
onClose,
}: ExportModalProps) {
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
const [includeChildren, setIncludeChildren] = useState<boolean>(false);
const [includeAttachments, setIncludeAttachments] = useState<boolean>(false);
const [isExporting, setIsExporting] = useState<boolean>(false);
const { t } = useTranslation();
const handleExport = async () => {
setIsExporting(true);
try {
if (type === "page") {
await exportPage({
pageId: id,
format,
includeChildren,
includeAttachments,
});
}
if (type === "space") {
await exportSpace({ spaceId: id, format, includeAttachments });
}
notifications.show({
message: t("Export successful"),
});
onClose();
} catch (err) {
notifications.show({
message: "Export failed:" + err.response?.data.message,
color: "red",
});
console.error("export error", err);
} finally {
setIsExporting(false);
}
};
const handleChange = (format: ExportFormat) => {
setFormat(format);
};
return (
<Modal.Root
opened={open}
onClose={onClose}
size={500}
padding="xl"
yOffset="10vh"
xOffset={0}
mah={400}
onClick={(e) => e.stopPropagation()}
>
<Modal.Overlay />
<Modal.Content style={{ overflow: "hidden" }}>
<Modal.Header py={0}>
<Modal.Title fw={500}>{t(`Export ${type}`)}</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
<Group justify="space-between" wrap="nowrap">
<div>
<Text size="md">{t("Format")}</Text>
</div>
<ExportFormatSelection format={format} onChange={handleChange} />
</Group>
{type === "page" && (
<>
<Divider my="sm" />
<Group justify="space-between" wrap="nowrap">
<div>
<Text size="md">{t("Include subpages")}</Text>
</div>
<Switch
onChange={(event) =>
setIncludeChildren(event.currentTarget.checked)
}
checked={includeChildren}
/>
</Group>
<Group justify="space-between" wrap="nowrap" mt="md">
<div>
<Text size="md">{t("Include attachments")}</Text>
</div>
<Switch
onChange={(event) =>
setIncludeAttachments(event.currentTarget.checked)
}
checked={includeAttachments}
/>
</Group>
</>
)}
{type === "space" && (
<>
<Divider my="sm" />
<Group justify="space-between" wrap="nowrap">
<div>
<Text size="md">{t("Include attachments")}</Text>
</div>
<Switch
onChange={(event) =>
setIncludeAttachments(event.currentTarget.checked)
}
checked={includeAttachments}
/>
</Group>
</>
)}
<Group justify="center" mt="md">
<Button onClick={onClose} variant="default">
{t("Cancel")}
</Button>
<Button onClick={handleExport} loading={isExporting}>{t("Export")}</Button>
</Group>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}
interface ExportFormatSelection {
format: ExportFormat;
onChange: (value: string) => void;
}
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
const { t } = useTranslation();
return (
<Select
data={[
{ value: "markdown", label: "Markdown" },
{ value: "html", label: "HTML" },
]}
defaultValue={format}
onChange={onChange}
styles={{ wrapper: { maxWidth: 120 } }}
comboboxProps={{ width: "120" }}
allowDeselect={false}
withCheckIcon={false}
aria-label={t("Select export format")}
/>
);
}
@@ -0,0 +1,20 @@
import { Table, Text } from "@mantine/core";
import React from "react";
import { useTranslation } from "react-i18next";
interface NoTableResultsProps {
colSpan: number;
text?: string;
}
export default function NoTableResults({ colSpan, text }: NoTableResultsProps) {
const { t } = useTranslation();
return (
<Table.Tr>
<Table.Td colSpan={colSpan}>
<Text fw={500} c="dimmed" ta="center">
{text || t("No results found...")}
</Text>
</Table.Td>
</Table.Tr>
);
}
@@ -0,0 +1,44 @@
import { Button, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
export interface PagePaginationProps {
hasPrevPage: boolean;
hasNextPage: boolean;
onPrev: () => void;
onNext: () => void;
}
export default function Paginate({
hasPrevPage,
hasNextPage,
onPrev,
onNext,
}: PagePaginationProps) {
const { t } = useTranslation();
if (!hasPrevPage && !hasNextPage) {
return null;
}
return (
<Group mt="md" justify="flex-end">
<Button
variant="default"
size="compact-sm"
onClick={onPrev}
disabled={!hasPrevPage}
>
{t("Prev")}
</Button>
<Button
variant="default"
size="compact-sm"
onClick={onNext}
disabled={!hasNextPage}
>
{t("Next")}
</Button>
</Group>
);
}
@@ -1,70 +1,111 @@
import { Text, Group, UnstyledButton, Badge, Table } from "@mantine/core";
import {
Text,
Group,
UnstyledButton,
Badge,
Table,
ThemeIcon,
Button,
} from "@mantine/core";
import { Link } from "react-router-dom";
import PageListSkeleton from "@/components/ui/page-list-skeleton.tsx";
import { buildPageUrl } from "@/features/page/page.utils.ts";
import { formattedDate } from "@/lib/time.ts";
import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts";
import { IconFileDescription } from "@tabler/icons-react";
import { IconFileDescription, IconFiles } from "@tabler/icons-react";
import { EmptyState } from "@/components/ui/empty-state.tsx";
import { getSpaceUrl } from "@/lib/config.ts";
import { useTranslation } from "react-i18next";
import { getInitialsColor } from "@/lib/get-initials-color.ts";
interface Props {
spaceId?: string;
}
export default function RecentChanges({ spaceId }: Props) {
const { data: pages, isLoading, isError } = useRecentChangesQuery(spaceId);
const { t } = useTranslation();
const { data, isLoading, isError, hasNextPage, fetchNextPage, isFetchingNextPage } = useRecentChangesQuery(spaceId);
const pages = data?.pages.flatMap((p) => p.items) ?? [];
if (isLoading) {
return <PageListSkeleton />;
}
if (isError) {
return <Text>Failed to fetch recent pages</Text>;
return <Text>{t("Failed to fetch recent pages")}</Text>;
}
return pages && pages.items.length > 0 ? (
<Table highlightOnHover verticalSpacing="sm">
<Table.Tbody>
{pages.items.map((page) => (
<Table.Tr key={page.id}>
<Table.Td>
<UnstyledButton
component={Link}
to={buildPageUrl(page?.space.slug, page.slugId, page.title)}
>
<Group wrap="nowrap">
{page.icon || <IconFileDescription size={18} />}
return pages.length > 0 ? (
<>
<Table.ScrollContainer minWidth={500}>
<Table highlightOnHover verticalSpacing="sm">
<Table.Tbody>
{pages.map((page) => (
<Table.Tr key={page.id}>
<Table.Td>
<UnstyledButton
component={Link}
to={buildPageUrl(page?.space.slug, page.slugId, page.title)}
>
<Group wrap="nowrap">
{page.icon || (
<ThemeIcon variant="transparent" color="gray" size={18}>
<IconFileDescription size={18} />
</ThemeIcon>
)}
<Text fw={500} size="md" lineClamp={1}>
{page.title || "Untitled"}
<Text fw={500} size="md" lineClamp={1}>
{page.title || t("Untitled")}
</Text>
</Group>
</UnstyledButton>
</Table.Td>
{!spaceId && (
<Table.Td>
<Badge
color={getInitialsColor(page?.space.name)}
variant="light"
component={Link}
to={getSpaceUrl(page?.space.slug)}
style={{ cursor: "pointer" }}
>
{page?.space.name}
</Badge>
</Table.Td>
)}
<Table.Td>
<Text
c="dimmed"
style={{ whiteSpace: "nowrap" }}
size="xs"
fw={500}
>
{formattedDate(page.updatedAt)}
</Text>
</Group>
</UnstyledButton>
</Table.Td>
{!spaceId && (
<Table.Td>
<Badge
color="blue"
variant="light"
component={Link}
to={getSpaceUrl(page?.space.slug)}
style={{ cursor: "pointer" }}
>
{page?.space.name}
</Badge>
</Table.Td>
)}
<Table.Td>
<Text c="dimmed" size="xs" fw={500}>
{formattedDate(page.updatedAt)}
</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
{hasNextPage && (
<Button
variant="subtle"
fullWidth
mt="sm"
mb="xl"
onClick={() => fetchNextPage()}
loading={isFetchingNextPage}
>
{t("Load more")}
</Button>
)}
</>
) : (
<Text size="md" ta="center">
No pages yet
</Text>
<EmptyState
icon={IconFiles}
title={t("No pages yet")}
description={t("Pages you create will show up here.")}
/>
);
}
@@ -0,0 +1,40 @@
import React, { useState, useEffect } from "react";
import { TextInput, Group } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import { IconSearch } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
export interface SearchInputProps {
placeholder?: string;
ariaLabel?: string;
debounceDelay?: number;
onSearch: (value: string) => void;
}
export function SearchInput({
placeholder,
ariaLabel,
debounceDelay = 500,
onSearch,
}: SearchInputProps) {
const { t } = useTranslation();
const [value, setValue] = useState("");
const [debouncedValue] = useDebouncedValue(value, debounceDelay);
useEffect(() => {
onSearch(debouncedValue);
}, [debouncedValue, onSearch]);
return (
<Group mb="sm">
<TextInput
size="sm"
placeholder={placeholder || t("Search...")}
aria-label={ariaLabel || placeholder || t("Search")}
leftSection={<IconSearch size={16} />}
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
/>
</Group>
);
}
@@ -0,0 +1,24 @@
import { Group, Text } from "@mantine/core";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import React from "react";
import { IUser } from '@/features/user/types/user.types.ts';
interface UserInfoProps {
user: Partial<IUser>;
size?: string;
}
export function UserInfo({ user, size }: UserInfoProps) {
return (
<Group gap="sm" wrap="nowrap">
<CustomAvatar avatarUrl={user?.avatarUrl} name={user?.name} size={size} />
<div>
<Text fz="sm" fw={500} lineClamp={1}>
{user?.name}
</Text>
<Text fz="xs" c="dimmed">
{user?.email}
</Text>
</div>
</Group>
);
}
@@ -0,0 +1,32 @@
import { rem } from '@mantine/core';
interface Props {
size?: number | string;
}
export function AirtableIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 215"
style={{ width: rem(size), height: rem(size) }}
>
<path
fill="#ffbf00"
d="M114.259 2.701 18.86 42.176c-5.305 2.195-5.25 9.73.089 11.847l95.797 37.989a35.544 35.544 0 0 0 26.208 0l95.799-37.99c5.337-2.115 5.393-9.65.086-11.846L141.442 2.7a35.549 35.549 0 0 0-27.183 0"
/>
<path
fill="#26b5f8"
d="M136.35 112.757v94.902c0 4.514 4.55 7.605 8.746 5.942l106.748-41.435a6.39 6.39 0 0 0 4.035-5.941V71.322c0-4.514-4.551-7.604-8.747-5.941l-106.748 41.434a6.392 6.392 0 0 0-4.035 5.942"
/>
<path
fill="#ed3049"
d="m111.423 117.654-31.68 15.296-3.217 1.555L9.65 166.548C5.411 168.593 0 165.504 0 160.795V71.72c0-1.704.874-3.175 2.046-4.283a7.266 7.266 0 0 1 1.618-1.213c1.598-.959 3.878-1.215 5.816-.448l101.41 40.18c5.155 2.045 5.56 9.268.533 11.697"
/>
<path
fillOpacity={0.25}
d="m111.423 117.654-31.68 15.296L2.045 67.438a7.266 7.266 0 0 1 1.618-1.213c1.598-.959 3.878-1.215 5.816-.448l101.41 40.18c5.155 2.045 5.56 9.268.533 11.697"
/>
</svg>
);
}
@@ -0,0 +1,20 @@
import { rem } from "@mantine/core";
interface Props {
size?: number | string;
}
export function ConfluenceIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
style={{ width: rem(size), height: rem(size) }}
>
<path d="M.87 18.257c-.248.382-.53.875-.763 1.245a.764.764 0 0 0 .255 1.04l4.965 3.054a.764.764 0 0 0 1.058-.26c.199-.332.454-.763.733-1.221 1.967-3.247 3.945-2.853 7.508-1.146l4.957 2.337a.764.764 0 0 0 1.028-.382l2.364-5.346a.764.764 0 0 0-.382-1 599.851 599.851 0 0 1-4.965-2.361C10.911 10.97 5.224 11.185.87 18.257zM23.131 5.743c.249-.405.531-.875.764-1.25a.764.764 0 0 0-.256-1.034L18.675.404a.764.764 0 0 0-1.058.26c-.195.335-.451.763-.734 1.225-1.966 3.246-3.945 2.85-7.508 1.146L4.437.694a.764.764 0 0 0-1.027.382L1.046 6.422a.764.764 0 0 0 .382 1c1.039.49 3.105 1.467 4.965 2.361 6.698 3.246 12.392 3.029 16.738-4.04z" />
</svg>
);
}
@@ -0,0 +1,23 @@
import { rem } from '@mantine/core';
interface Props {
size?: number | string;
}
export function FigmaIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
style={{ width: rem(size), height: rem(size) }}
>
<g fill="none" fillRule="evenodd" transform="translate(4)">
<circle cx={12} cy={12} r={4} fill="#19bcfe" />
<path fill="#09cf83" d="M4 24a4 4 0 0 0 4-4v-4H4a4 4 0 1 0 0 8z" />
<path fill="#a259ff" d="M4 16h4V8H4a4 4 0 1 0 0 8z" />
<path fill="#f24e1e" d="M4 8h4V0H4a4 4 0 1 0 0 8z" />
<path fill="#ff7262" d="M12 8H8V0h4a4 4 0 1 1 0 8z" />
</g>
</svg>
);
}
@@ -0,0 +1,17 @@
import { rem } from '@mantine/core';
interface Props {
size?: number | string;
}
export function FramerIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
style={{ width: rem(size), height: rem(size) }}
>
<path d="M4 0h16v8h-8zm0 8h8l8 8H4zm0 8h8v8z" />
</svg>
);
}
@@ -0,0 +1,24 @@
import { rem } from '@mantine/core';
interface Props {
size?: number | string;
}
export function GoogleDriveIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 87.3 78"
style={{ width: rem(size), height: rem(size) }}
>
<path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da" />
<path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47" />
<path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
fill="#ea4335" />
<path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d" />
<path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc" />
<path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
fill="#ffba00" />
</svg>
);
}
@@ -0,0 +1,33 @@
import { rem } from "@mantine/core";
interface Props {
size?: number | string;
}
export function GoogleIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
viewBox="0 0 256 262"
style={{ width: rem(size), height: rem(size) }}
>
<path
fill="#4285F4"
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
/>
<path
fill="#34A853"
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
/>
<path
fill="#FBBC05"
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782"
/>
<path
fill="#EB4335"
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
/>
</svg>
);
}
@@ -0,0 +1,23 @@
import { rem } from '@mantine/core';
interface Props {
size?: number | string;
}
export function GoogleSheetsIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
style={{ width: rem(size), height: rem(size) }}
>
<path fill="#43a047" d="M37,45H11c-1.657,0-3-1.343-3-3V6c0-1.657,1.343-3,3-3h19l10,10v29C40,43.657,38.657,45,37,45z"/>
<path fill="#c8e6c9" d="M40 13L30 13 30 3z"/>
<path fill="#2e7d32" d="M30 13L40 23 40 13z"/>
<path
fill="#e8f5e9"
d="M31,23H17h-2v2v2v2v2v2v2v2h18v-2v-2v-2v-2v-2v-2v-2H31z M17,25h4v2h-4V25z M17,29h4v2h-4V29z M17,33h4v2h-4V33z M31,35h-8v-2h8V35z M31,31h-8v-2h8V31z M31,27h-8v-2h8V27z"
/>
</svg>
);
}
@@ -0,0 +1,27 @@
import { rem } from "@mantine/core";
type Props = {
size?: number | string;
stroke?: number;
};
export function IconColumns4({ size = 24, stroke = 2 }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={rem(size)}
height={rem(size)}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={stroke}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 4a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v16a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-16" />
<path d="M7.5 3v18" />
<path d="M12 3v18" />
<path d="M16.5 3v18" />
</svg>
);
}
@@ -0,0 +1,28 @@
import { rem } from "@mantine/core";
type Props = {
size?: number | string;
stroke?: number;
};
export function IconColumns5({ size = 24, stroke = 2 }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={rem(size)}
height={rem(size)}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={stroke}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 4a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v16a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-16" />
<path d="M6.6 3v18" />
<path d="M10.2 3v18" />
<path d="M13.8 3v18" />
<path d="M17.4 3v18" />
</svg>
);
}
@@ -0,0 +1,20 @@
import { rem } from '@mantine/core';
interface Props {
size?: number | string;
}
function IconDrawio({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="#F08705"
style={{ width: rem(size), height: rem(size) }}
>
<path d="M19.69 13.419h-2.527l-2.667-4.555a1.292 1.292 0 001.035-1.28V4.16c0-.725-.576-1.312-1.302-1.312H9.771c-.726 0-1.312.576-1.312 1.301v3.435c0 .619.426 1.152 1.034 1.28l-2.666 4.555H4.309c-.725 0-1.312.576-1.312 1.301v3.435c0 .725.576 1.312 1.302 1.312h4.458c.726 0 1.312-.576 1.312-1.302v-3.434c0-.726-.576-1.312-1.301-1.312h-.437l2.645-4.523h2.059l2.656 4.523h-.438c-.725 0-1.312.576-1.312 1.301v3.435c0 .725.576 1.312 1.302 1.312H19.7c.726 0 1.312-.576 1.312-1.302v-3.434c0-.726-.576-1.312-1.301-1.312zM24 22.976c0 .565-.459 1.024-1.013 1.024H1.024A1.022 1.022 0 010 22.987V1.024C0 .459.459 0 1.013 0h21.963C23.541 0 24 .459 24 1.013z"></path>
</svg>
);
}
export default IconDrawio;
@@ -0,0 +1,20 @@
import { rem } from "@mantine/core";
interface Props {
size?: number | string;
}
function IconExcalidraw({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="#6965DB"
viewBox="0 0 24 24"
style={{ width: rem(size), height: rem(size) }}
>
<path d="M23.943 19.806a.196.196 0 00-.168-.034c-1.26-1.855-2.873-3.61-4.419-5.315l-.252-.284c-.001-.073-.067-.12-.134-.15l-.084-.084c-.05-.1-.169-.167-.286-.1-.47.234-.907.585-1.327.919-.554.434-1.109.87-1.63 1.354a5.058 5.058 0 00-.588.618c-.084.117-.017.217.084.267-.37.368-.74.736-1.109 1.12a.19.19 0 00-.05.134c0 .05.033.1.067.117l.655.502v.016c.924.92 2.554 2.173 4.285 3.527.251.201.52.402.773.602.117.134.234.285.335.418.05.066.169.084.236.033.033.034.084.067.118.1a.24.24 0 00.1.034.153.153 0 00.135-.066.237.237 0 00.033-.1c.017 0 .017.016.034.016a.192.192 0 00.134-.05l3.058-3.327c.12-.116.014-.267 0-.267zm-7.628-.134l-1.546-1.17-.15-.1c-.035-.017-.068-.05-.102-.067l-.117-.1c.66-.66 1.33-1.308 2-1.956-.488.484-1.463 1.906-1.261 2.373.002 0 .018.042.067.084l1.11.936zm4.1 3.126l-1.277-.97a26.906 26.906 0 00-1.58-1.504c.69.518 1.277.97 1.361 1.053.673.585.638.485 1.093.87l.554.4c-.074.103-.151.148-.151.151zm.336.25l-.034-.016a.913.913 0 00.152-.117zM.587 3.476c.034.217.085.435.118.636.201 1.103.403 2.106.772 2.858l.152.568c.05.217.134.485.219.552a66.769 66.769 0 003.578 2.942.177.177 0 00.219 0s0 .016.016.016a.153.153 0 00.118.05.191.191 0 00.134-.05c1.798-1.989 3.142-3.627 4.1-4.998.068-.066.084-.167.084-.25.067-.067.118-.151.185-.201.067-.067.067-.184 0-.235l-.017-.016c0-.033-.017-.084-.05-.1-.42-.401-.722-.685-1.042-.986a93.555 93.555 0 01-2.352-2.273c-.017-.017-.034-.034-.067-.034-.336-.117-1.025-.234-1.882-.385-1.277-.216-3.008-.517-4.57-.986 0 0-.101 0-.118.017l-.05.05C.05.714.022.707 0 .718c.017.1.017.167.05.284 0 .033.068.301.068.334zm7.191 4.78l-.033.034a.036.036 0 01.033-.034zM6.553 2.238c.101.1.521.502.622.585-.437-.2-1.529-.702-2.034-.869.505.1 1.194.201 1.412.284zM.79 1.403c.252.434.454 1.939.655 3.41-.118-.469-.201-.936-.302-1.372C.992 2.673.84 1.988.638 1.386c.124 0 .152.021.152.017zm-.286-.369c0-.016 0-.033-.017-.033.085 0 .135.017.202.05 0 .006-.145-.017-.185-.017zm23.17-.217c.017-.066-.336-.367-.219-.384.253-.017.253-.401 0-.401-.335.017-.688.1-1.008.15-.587.117-1.192.234-1.78.367a79.696 79.696 0 00-3.949.937c-.403.117-.857.2-1.243.401-.135.067-.118.2-.05.284-.034.017-.051.017-.085.034-.117.017-.218.034-.335.05-.102.017-.152.1-.135.2 0 .017.017.05.017.067-.706.936-1.496 1.923-2.353 2.976-.84.969-1.73 1.989-2.62 3.042-2.84 3.31-6.05 7.07-9.594 10.38a.161.161 0 000 .234c.016.016.033.033.05.033-.05.05-.101.085-.152.134-.033.034-.05.067-.05.1a.364.364 0 00-.067.084c-.067.067-.067.184.017.234.067.066.185.066.235-.017.017-.017.017-.033.033-.033a.265.265 0 01.37 0c.202.217.404.435.588.618l-.42-.35c-.067-.067-.184-.05-.234.016-.068.066-.051.184.016.234l4.469 3.727c.034.034.067.034.118.034a.15.15 0 00.117-.05l.101-.1c.017.016.05.016.067.016.05 0 .084-.016.118-.05 6.049-6.05 10.922-10.614 16.5-14.693.05-.033.067-.1.067-.15.067 0 .118-.05.15-.117 1.026-3.125 1.228-5.9 1.295-7.27 0-.059.016-.038.016-.068.017-.033.017-.05.017-.05a.978.978 0 00-.067-.619zm-10.82 4.915c.268-.301.537-.619.806-.903-1.73 2.273-4.603 5.766-8.67 9.929 2.773-3.059 5.562-6.218 7.864-9.026zM5.14 23.466c-.016-.017-.016-.017 0-.017zm2.504-2.156c.135-.15.27-.284.42-.434 0 0 0 .016.017.016-.224.198-.433.418-.437.418zm.69-.668c.099-.1.14-.173.284-.318.992-1.02 2.017-2.04 3.059-3.076l.016-.016c.252-.2.555-.418.824-.619a228.063 228.063 0 00-4.184 4.029zM14.852 3.91c-.554.719-1.176 1.671-1.697 2.423-1.646 2.374-6.94 8.174-7.057 8.274a1189.647 1189.647 0 01-4.839 4.597l-.1.1c-.085-.1-.085-.25.016-.334C8.652 11.966 13.19 6.133 15.021 3.576c-.05.116-.084.216-.168.334zm2.906 3.427c-.671-.386-.99-.987-.806-1.572l.05-.2a.775.775 0 01.085-.167 1.9 1.9 0 01.756-.703c.016 0 .033 0 .05-.016-.017-.034-.017-.084-.017-.134.017-.1.085-.167.202-.167.202 0 .824.184 1.059.384.067.05.134.117.202.184.084.1.218.268.285.401.034.017.067.184.118.268.033.134.067.284.05.418-.017.016 0 .116-.017.116a1.605 1.605 0 01-.218.619c-.03.03.006.012-.05.067a1.22 1.22 0 01-.32.334 1.49 1.49 0 01-1.26.234 2.191 2.191 0 00-.169-.066zm4.37 1.403c0 .017-.017.05 0 .067-.034 0-.05.017-.085.034a109.886 109.886 0 00-3.915 3.025c1.11-.986 2.218-1.989 3.378-2.975.336-.301.571-.686.638-1.12l.168-1.003v-.033c.085-.201.404-.118.353.1-.004-.001-.173.795-.537 1.905z"></path>
</svg>
);
}
export default IconExcalidraw;
@@ -0,0 +1,20 @@
import { rem } from "@mantine/core";
interface Props {
size?: number | string;
}
function IconMermaid({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="#FF3670"
viewBox="0 0 24 24"
style={{ width: rem(size), height: rem(size) }}
>
<path d="M23.99 2.115A12.223 12.223 0 0012 10.149 12.223 12.223 0 00.01 2.115a12.23 12.23 0 005.32 10.604 6.562 6.562 0 012.845 5.423v3.754h7.65v-3.754a6.561 6.561 0 012.844-5.423 12.223 12.223 0 005.32-10.604z"></path>
</svg>
);
}
export default IconMermaid;
@@ -1,11 +1,11 @@
import { ActionIcon, rem } from "@mantine/core";
import { ThemeIcon } from "@mantine/core";
import React from "react";
import { IconUsersGroup } from "@tabler/icons-react";
export function IconGroupCircle() {
return (
<ActionIcon variant="light" size="lg" color="gray" radius="xl">
<ThemeIcon variant="light" size="lg" color="gray" radius="xl">
<IconUsersGroup stroke={1.5} />
</ActionIcon>
</ThemeIcon>
);
}
+11
View File
@@ -0,0 +1,11 @@
export { AirtableIcon } from "./airtable-icon.tsx";
export { FigmaIcon } from "./figma-icon.tsx";
export { TypeformIcon } from "./typeform-icon.tsx";
export { VimeoIcon } from "./vimeo-icon.tsx";
export { MiroIcon } from "./miro-icon.tsx";
export { GoogleDriveIcon } from "./google-drive-icon.tsx";
export { GoogleSheetsIcon } from "./google-sheets-icon.tsx";
export { FramerIcon } from "./framer-icon.tsx";
export { LoomIcon } from "./loom-icon.tsx";
export { YoutubeIcon } from "./youtube-icon.tsx";
@@ -0,0 +1,19 @@
import { rem } from '@mantine/core';
interface Props {
size?: number | string;
}
export function LoomIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="#625DF5"
style={{ width: rem(size), height: rem(size) }}
>
<path
d="M24 10.665h-7.018l6.078-3.509-1.335-2.312-6.078 3.509 3.508-6.077L16.843.94l-3.508 6.077V0h-2.67v7.018L7.156.94 4.844 2.275l3.509 6.077-6.078-3.508L.94 7.156l6.078 3.509H0v2.67h7.017L.94 16.844l1.335 2.313 6.077-3.508-3.509 6.077 2.312 1.335 3.509-6.078V24h2.67v-7.017l3.508 6.077 2.312-1.335-3.509-6.078 6.078 3.509 1.335-2.313-6.077-3.508h7.017v-2.67H24zm-12 4.966a3.645 3.645 0 1 1 0-7.29 3.645 3.645 0 0 1 0 7.29z" />
</svg>
);
}
@@ -0,0 +1,18 @@
import { rem } from '@mantine/core';
interface Props {
size?: number | string;
}
export function MiroIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
style={{ width: rem(size), height: rem(size) }}
>
<path
d="M17.392 0H13.9L17 4.808 10.444 0H6.949l3.102 6.3L3.494 0H0l3.05 8.131L0 24h3.494L10.05 6.985 6.949 24h3.494L17 5.494 13.899 24h3.493L24 3.672 17.392 0z" />
</svg>
);
}
@@ -0,0 +1,20 @@
import { rem } from "@mantine/core";
interface Props {
size?: number | string;
}
export function OpenIdIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
style={{ width: rem(size), height: rem(size) }}
>
<path d="M14.54.889l-3.63 1.773v18.17c-4.15-.52-7.27-2.78-7.27-5.5 0-2.58 2.8-4.75 6.63-5.41v-2.31C4.42 8.322 0 11.502 0 15.332c0 3.96 4.74 7.24 10.91 7.78l3.63-1.71V.888m.64 6.724v2.31c1.43.25 2.71.7 3.76 1.31l-1.97 1.11 7.03 1.53-.5-5.21-1.87 1.06c-1.74-1.06-3.96-1.81-6.45-2.11z" />
</svg>
);
}
@@ -0,0 +1,18 @@
import { rem } from '@mantine/core';
interface Props {
size?: number | string;
}
export function TypeformIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
style={{ width: rem(size), height: rem(size) }}
>
<path
d="M15.502 13.035c-.5 0-.756-.411-.756-.917 0-.505.252-.894.756-.894.513 0 .756.407.756.894-.004.515-.261.917-.756.917Zm-4.888-1.81c.292 0 .414.17.414.317 0 .357-.365.514-1.126.536 0-.442.253-.854.712-.854Zm-3.241 1.81c-.473 0-.67-.384-.67-.917 0-.527.202-.894.67-.894.477 0 .702.38.702.894 0 .537-.234.917-.702.917Zm-3.997-2.334h-.738l1.224 2.808c-.234.519-.36.648-.522.648-.171 0-.333-.138-.45-.259l-.324.43c.22.232.522.366.832.366.387 0 .685-.224.856-.626l1.413-3.371h-.725l-.738 2.012-.828-2.008Zm19.553.523c.36 0 .432.246.432.823v1.516H24v-1.914c0-.689-.473-.988-.91-.988-.386 0-.742.241-.94.688a.901.901 0 0 0-.891-.688c-.365 0-.73.232-.927.666v-.626h-.64v2.857h.64v-1.22c0-.617.324-1.114.765-1.114.36 0 .427.246.427.823v1.516h.64l-.005-1.225c0-.617.329-1.114.77-1.114Zm-5.1-.523h-.324v2.857h.639v-1.095c0-.693.306-1.163.76-1.163.118 0 .217.005.325.05l.099-.676c-.081-.009-.153-.018-.225-.018-.45 0-.774.309-.964.707V10.7h-.31Zm-2.327-.045c-.846 0-1.418.644-1.418 1.458 0 .845.58 1.475 1.418 1.475.85 0 1.431-.648 1.431-1.475-.004-.818-.594-1.458-1.431-1.458Zm-4.852 2.38c-.333 0-.581-.17-.685-.515.847-.036 1.675-.242 1.675-.988 0-.43-.423-.872-1.03-.872-.82 0-1.374.666-1.374 1.457 0 .828.545 1.476 1.36 1.476.567 0 .927-.228 1.21-.559l-.31-.42c-.329.335-.531.42-.846.42Zm-3.151-2.38c-.324 0-.648.188-.774.483v-.438h-.64v3.98h.64v-1.422c.135.205.445.34.72.34.85 0 1.3-.631 1.3-1.48-.004-.841-.445-1.463-1.246-1.463Zm-4.483-1.1H0v.622h1.18v3.38h.67v-3.38h1.166v-.622Zm9.502 1.145h-.383v.572h.383v2.285h.639v-2.285h.621v-.572h-.621v-.447c0-.286.117-.385.382-.385.1 0 .19.027.311.068l.144-.537c-.117-.067-.351-.094-.504-.094-.612 0-.972.367-.972 1.002v.393Z" />
</svg>
);
}
@@ -0,0 +1,19 @@
import { rem } from '@mantine/core';
interface Props {
size?: number | string;
}
export function VimeoIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="#1AB7EA"
style={{ width: rem(size), height: rem(size) }}
>
<path
d="M23.9765 6.4168c-.105 2.338-1.739 5.5429-4.894 9.6088-3.2679 4.247-6.0258 6.3699-8.2898 6.3699-1.409 0-2.578-1.294-3.553-3.881l-1.9179-7.1138c-.719-2.584-1.488-3.878-2.312-3.878-.179 0-.806.378-1.8809 1.132l-1.129-1.457a315.06 315.06 0 003.501-3.1279c1.579-1.368 2.765-2.085 3.5539-2.159 1.867-.18 3.016 1.1 3.447 3.838.465 2.953.789 4.789.971 5.5069.5389 2.45 1.1309 3.674 1.7759 3.674.502 0 1.256-.796 2.265-2.385 1.004-1.589 1.54-2.797 1.612-3.628.144-1.371-.395-2.061-1.614-2.061-.574 0-1.167.121-1.777.391 1.186-3.8679 3.434-5.7568 6.7619-5.6368 2.4729.06 3.6279 1.664 3.4929 4.7969z" />
</svg>
);
}
@@ -0,0 +1,19 @@
import { rem } from '@mantine/core';
interface Props {
size?: number | string;
}
export function YoutubeIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="#FF0000"
style={{ width: rem(size), height: rem(size) }}
>
<path
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</svg>
);
}
@@ -7,6 +7,19 @@
padding-right: var(--mantine-spacing-md);
}
.brand {
display: flex;
align-items: center;
text-decoration: none;
color: inherit;
cursor: pointer;
}
.brandIcon {
display: flex;
align-items: center;
}
.link {
display: block;
line-height: 1;
@@ -16,6 +29,9 @@
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
font-size: var(--mantine-font-size-sm);
font-weight: 500;
user-select: none;
white-space: nowrap;
flex-shrink: 0;
@mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
@@ -1,31 +1,62 @@
import { Group, Text } from "@mantine/core";
import {
ActionIcon,
Badge,
Box,
Group,
Text,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import classes from "./app-header.module.css";
import React from "react";
import TopMenu from "@/components/layouts/global/top-menu.tsx";
import { Link } from "react-router-dom";
import { Link, useLocation } from "react-router-dom";
import { IconSparkles } from "@tabler/icons-react";
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
import APP_ROUTE from "@/lib/app-route.ts";
import { useAtom } from "jotai/index";
import { useAtom } from "jotai";
import {
desktopSidebarAtom,
mobileSidebarAtom,
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
import { useTranslation } from "react-i18next";
import useTrial from "@/ee/hooks/use-trial.tsx";
import { isCloud } from "@/lib/config.ts";
import {
SearchControl,
SearchMobileControl,
} from "@/features/search/components/search-control.tsx";
import {
searchSpotlight,
shareSearchSpotlight,
} from "@/features/search/constants.ts";
import { NotificationPopover } from "@/features/notification/components/notification-popover.tsx";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
const links = [{ link: APP_ROUTE.HOME, label: "Home" }];
const links = [
{ link: APP_ROUTE.HOME, label: "Home" },
];
export function AppHeader() {
const { t } = useTranslation();
const [mobileOpened] = useAtom(mobileSidebarAtom);
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
const [desktopOpened] = useAtom(desktopSidebarAtom);
const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
const { isTrial, trialDaysLeft } = useTrial();
const location = useLocation();
const toggleAside = useToggleAside();
const [workspace] = useAtom(workspaceAtom);
const aiChatEnabled = workspace?.settings?.ai?.chat === true;
const isHomeRoute = location.pathname.startsWith("/home");
const isPageRoute = location.pathname.includes("/p/");
const items = links.map((link) => (
<Link key={link.label} to={link.link} className={classes.link}>
{link.label}
{t(link.label)}
</Link>
));
@@ -33,40 +64,117 @@ export function AppHeader() {
<>
<Group h="100%" px="md" justify="space-between" wrap={"nowrap"}>
<Group wrap="nowrap">
{!isHomeRoute && (
<>
<SidebarToggle
aria-label="sidebar toggle"
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Tooltip label={t("Sidebar toggle")}>
<SidebarToggle
aria-label={t("Sidebar toggle")}
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
</Tooltip>
<SidebarToggle
aria-label="sidebar toggle"
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
/>
</>
)}
<Tooltip label={t("Sidebar toggle")}>
<SidebarToggle
aria-label={t("Sidebar toggle")}
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
/>
</Tooltip>
<Text
size="lg"
fw={600}
style={{ cursor: "pointer", userSelect: "none" }}
>
Docmost
</Text>
<Link to="/home" className={classes.brand} aria-label="Docmost">
<Box hiddenFrom="sm" className={classes.brandIcon}>
<img
src="/icons/favicon-32x32.png"
alt="Docmost"
width={22}
height={22}
/>
</Box>
<Text
size="lg"
fw={600}
style={{ userSelect: "none" }}
visibleFrom="sm"
>
Docmost
</Text>
</Link>
<Group ml={50} gap={5} className={classes.links} visibleFrom="sm">
{items}
</Group>
</Group>
<Group px={"xl"}>
<div>
<Group visibleFrom="sm">
<SearchControl onClick={searchSpotlight.open} />
</Group>
<Group hiddenFrom="sm">
<SearchMobileControl onSearch={searchSpotlight.open} />
</Group>
</div>
<Group px={"xl"} wrap="nowrap">
{aiChatEnabled && (
<>
<UnstyledButton
component={Link}
to="/ai"
className={classes.link}
visibleFrom="sm"
onClick={(e: React.MouseEvent) => {
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) {
return;
}
if (isPageRoute) {
e.preventDefault();
toggleAside("chat");
}
}}
>
{t("AI Chat")}
</UnstyledButton>
<Tooltip label={t("AI Chat")} openDelay={250} withArrow>
<ActionIcon
component={Link}
to="/ai"
variant="subtle"
color="dark"
size="sm"
hiddenFrom="sm"
aria-label={t("AI Chat")}
onClick={(e: React.MouseEvent) => {
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) {
return;
}
if (isPageRoute) {
e.preventDefault();
toggleAside("chat");
}
}}
>
<IconSparkles size={20} stroke={2} />
</ActionIcon>
</Tooltip>
</>
)}
<NotificationPopover />
{isCloud() && isTrial && trialDaysLeft !== 0 && (
<Badge
variant="light"
style={{ cursor: "pointer" }}
component={Link}
to={APP_ROUTE.SETTINGS.WORKSPACE.BILLING}
visibleFrom="xs"
>
{trialDaysLeft === 1
? "1 day left"
: `${trialDaysLeft} days left`}
</Badge>
)}
<TopMenu />
</Group>
</Group>
@@ -14,3 +14,16 @@
}
}
.resizeHandle {
width: 3px;
cursor: col-resize;
position: absolute;
right: 0;
top: 0;
bottom: 0;
&:hover, &:active {
width: 5px;
background: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-5))
}
}
@@ -1,40 +1,66 @@
import { Box, ScrollArea, Text } from "@mantine/core";
import CommentList from "@/features/comment/components/comment-list.tsx";
import CommentListWithTabs from "@/features/comment/components/comment-list-with-tabs.tsx";
import { useAtom } from "jotai";
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import React, { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx";
import { useAtomValue } from "jotai";
import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
import AsideChatPanel from "@/ee/ai-chat/components/aside-chat-panel";
import { PageDetailsAside } from "@/features/page-details/components/page-details-aside.tsx";
export default function Aside() {
const [{ tab }] = useAtom(asideStateAtom);
const { t } = useTranslation();
const pageEditor = useAtomValue(pageEditorAtom);
let title: string;
let component: ReactNode;
switch (tab) {
case "comments":
component = <CommentList />;
component = <CommentListWithTabs />;
title = "Comments";
break;
case "toc":
component = <TableOfContents editor={pageEditor} />;
title = "Table of contents";
break;
case "chat":
component = <AsideChatPanel />;
title = "AI Chat";
break;
case "details":
component = <PageDetailsAside />;
title = "Details";
break;
default:
component = null;
title = null;
}
return (
<Box p="md">
<Box p="md" style={{ height: "100%", display: "flex", flexDirection: "column" }}>
{component && (
<>
<Text mb="md" fw={500}>
{title}
</Text>
{tab !== "chat" && (
<Text mb="md" fw={500}>
{t(title)}
</Text>
)}
<ScrollArea
style={{ height: "85vh" }}
scrollbarSize={5}
type="scroll"
>
<div style={{ paddingBottom: "200px" }}>{component}</div>
</ScrollArea>
{tab === "comments" || tab === "chat" ? (
component
) : (
<ScrollArea
style={{ height: "85vh" }}
scrollbarSize={5}
type="scroll"
>
<div style={{ paddingBottom: "200px" }}>{component}</div>
</ScrollArea>
)}
</>
)}
</Box>
@@ -1,46 +1,96 @@
import { AppShell, Container } from "@mantine/core";
import React from "react";
import React, { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import SettingsSidebar from "@/components/settings/settings-sidebar.tsx";
import { useAtom } from "jotai";
import {
asideStateAtom,
desktopSidebarAtom,
mobileSidebarAtom,
sidebarWidthAtom,
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
import AiChatSidebar from "@/ee/ai-chat/components/ai-chat-sidebar.tsx";
import { AppHeader } from "@/components/layouts/global/app-header.tsx";
import Aside from "@/components/layouts/global/aside.tsx";
import classes from "./app-shell.module.css";
import { useTrialEndAction } from "@/ee/hooks/use-trial-end-action.tsx";
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
import GlobalSidebar from "@/components/layouts/global/global-sidebar.tsx";
export default function GlobalAppShell({
children,
}: {
children: React.ReactNode;
}) {
const { t } = useTranslation();
useTrialEndAction();
const [mobileOpened] = useAtom(mobileSidebarAtom);
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
const [desktopOpened] = useAtom(desktopSidebarAtom);
const [{ isAsideOpen }] = useAtom(asideStateAtom);
const [{ isAsideOpen, tab: asideTab }] = useAtom(asideStateAtom);
const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom);
const [isResizing, setIsResizing] = useState(false);
const sidebarRef = useRef(null);
const startResizing = React.useCallback((mouseDownEvent) => {
mouseDownEvent.preventDefault();
setIsResizing(true);
}, []);
const stopResizing = React.useCallback(() => {
setIsResizing(false);
}, []);
const resize = React.useCallback(
(mouseMoveEvent) => {
if (isResizing) {
const newWidth =
mouseMoveEvent.clientX -
sidebarRef.current.getBoundingClientRect().left;
if (newWidth < 220) {
setSidebarWidth(220);
return;
}
if (newWidth > 600) {
setSidebarWidth(600);
return;
}
setSidebarWidth(newWidth);
}
},
[isResizing],
);
useEffect(() => {
//https://codesandbox.io/p/sandbox/kz9de
window.addEventListener("mousemove", resize);
window.addEventListener("mouseup", stopResizing);
return () => {
window.removeEventListener("mousemove", resize);
window.removeEventListener("mouseup", stopResizing);
};
}, [resize, stopResizing]);
const location = useLocation();
const isSettingsRoute = location.pathname.startsWith("/settings");
const isSpaceRoute = location.pathname.startsWith("/s/");
const isHomeRoute = location.pathname.startsWith("/home");
const isAiRoute = location.pathname.startsWith("/ai");
const isPageRoute = location.pathname.includes("/p/");
const showGlobalSidebar = !isSpaceRoute && !isSettingsRoute && !isAiRoute;
return (
<AppShell
header={{ height: 45 }}
navbar={
!isHomeRoute && {
width: 300,
breakpoint: "sm",
collapsed: {
mobile: !mobileOpened,
desktop: !desktopOpened,
},
}
}
navbar={{
width: isSpaceRoute ? sidebarWidth : 300,
breakpoint: "sm",
collapsed: {
mobile: !mobileOpened,
desktop: !desktopOpened,
},
}}
aside={
isPageRoute && {
width: 350,
@@ -53,22 +103,55 @@ export default function GlobalAppShell({
<AppShell.Header px="md" className={classes.header}>
<AppHeader />
</AppShell.Header>
{!isHomeRoute && (
<AppShell.Navbar className={classes.navbar} withBorder={false}>
{isSpaceRoute && <SpaceSidebar />}
{isSettingsRoute && <SettingsSidebar />}
</AppShell.Navbar>
)}
<AppShell.Main>
<AppShell.Navbar
className={classes.navbar}
withBorder={false}
ref={sidebarRef}
aria-label={
isSpaceRoute
? t("Space navigation")
: isSettingsRoute
? t("Settings navigation")
: isAiRoute
? t("AI navigation")
: t("Main navigation")
}
>
{isSpaceRoute && (
<div className={classes.resizeHandle} onMouseDown={startResizing} />
)}
{isSpaceRoute && <SpaceSidebar />}
{isSettingsRoute && <SettingsSidebar />}
{isAiRoute && <AiChatSidebar />}
{showGlobalSidebar && <GlobalSidebar />}
</AppShell.Navbar>
<AppShell.Main id="main-content">
{isSettingsRoute ? (
<Container size={800}>{children}</Container>
<Container size={900} pb={80}>
{children}
</Container>
) : (
children
)}
</AppShell.Main>
{isPageRoute && (
<AppShell.Aside className={classes.aside} p="md" withBorder={false}>
<AppShell.Aside
className={classes.aside}
p="md"
withBorder={false}
aria-label={
asideTab === "comments"
? t("Comments")
: asideTab === "toc"
? t("Table of contents")
: asideTab === "chat"
? t("AI Chat")
: asideTab === "details"
? t("Details")
: undefined
}
>
<Aside />
</AppShell.Aside>
)}
@@ -0,0 +1,89 @@
.navbar {
height: 100%;
width: 100%;
padding: var(--mantine-spacing-md);
display: flex;
flex-direction: column;
}
.section {
padding-bottom: var(--mantine-spacing-xs);
}
.link {
cursor: pointer;
display: flex;
align-items: center;
text-decoration: none;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
padding-left: var(--mantine-spacing-xs);
min-height: 30px;
border-radius: var(--mantine-radius-sm);
font-weight: 500;
user-select: none;
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-dark-6)
);
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
}
&[data-active] {
&,
& :hover {
background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
}
}
}
.linkIcon {
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
margin-right: var(--mantine-spacing-sm);
width: rem(16px);
height: rem(16px);
}
.sectionHeader {
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
font-size: var(--mantine-font-size-xs);
color: var(--mantine-color-dimmed);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.spacer {
flex: 1;
}
.bottomSection {
padding-top: var(--mantine-spacing-xs);
border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.spaceItem {
cursor: pointer;
display: flex;
align-items: center;
gap: var(--mantine-spacing-sm);
text-decoration: none;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
padding-left: var(--mantine-spacing-xs);
min-height: 30px;
border-radius: var(--mantine-radius-sm);
font-weight: 500;
user-select: none;
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-dark-6)
);
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
}
}
@@ -0,0 +1,154 @@
import { useEffect, useState } from "react";
import { ScrollArea, Text, Divider, Modal, UnstyledButton } from "@mantine/core";
import {
IconHome,
IconClock,
IconStar,
IconLayoutGrid,
IconSettings,
IconUserPlus,
} from "@tabler/icons-react";
import { Link, useLocation } from "react-router-dom";
import classes from "./global-sidebar.module.css";
import { useTranslation } from "react-i18next";
import { useAtom } from "jotai";
import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar";
import { useFavoritesQuery } from "@/features/favorite/queries/favorite-query";
import { getSpaceUrl } from "@/lib/config";
import { useDisclosure } from "@mantine/hooks";
import { WorkspaceInviteForm } from "@/features/workspace/components/members/components/workspace-invite-form";
import { CustomAvatar } from "@/components/ui/custom-avatar";
import { AvatarIconType } from "@/features/attachments/types/attachment.types";
const mainNavItems = [
{ label: "Home", icon: IconHome, path: "/home" },
{ label: "Favorites", icon: IconStar, path: "/favorites" },
{ label: "Spaces", icon: IconLayoutGrid, path: "/spaces" },
];
export default function GlobalSidebar() {
const { t } = useTranslation();
const location = useLocation();
const [active, setActive] = useState(location.pathname);
const [mobileSidebarOpened] = useAtom(mobileSidebarAtom);
const toggleMobileSidebar = useToggleSidebar(mobileSidebarAtom);
const { data: favoriteSpacesData, isPending: isFavoritesPending } = useFavoritesQuery("space");
const favoriteSpaces = favoriteSpacesData?.pages.flatMap((p) => p.items) ?? [];
const sortedFavoriteSpaces = [...favoriteSpaces]
.filter((fav) => fav.space)
.sort((a, b) => {
const cmp = (a.space!.name ?? "").localeCompare(b.space!.name ?? "", undefined, { sensitivity: "base" });
return cmp !== 0 ? cmp : a.id.localeCompare(b.id);
});
const [inviteOpened, { open: openInvite, close: closeInvite }] =
useDisclosure(false);
useEffect(() => {
setActive(location.pathname);
}, [location.pathname]);
const handleNavClick = () => {
if (mobileSidebarOpened) {
toggleMobileSidebar();
}
};
return (
<div className={classes.navbar}>
<ScrollArea w="100%" style={{ flex: 1 }}>
<div className={classes.section}>
{mainNavItems.map((item) => (
<Link
key={item.label}
className={classes.link}
data-active={active === item.path || undefined}
to={item.path}
onClick={handleNavClick}
>
<item.icon className={classes.linkIcon} stroke={2} />
<span>{t(item.label)}</span>
</Link>
))}
</div>
<Divider my="xs" />
<div className={classes.section}>
<Text className={classes.sectionHeader}>{t("Favorite spaces")}</Text>
{!isFavoritesPending && sortedFavoriteSpaces.length === 0 ? (
<Text size="xs" c="dimmed" pl="xs" py={4}>
{t("Favorite spaces appear here")}
</Text>
) : (
<>
{sortedFavoriteSpaces.slice(0, 10).map((fav) => (
<Link
key={fav.id}
className={classes.spaceItem}
to={getSpaceUrl(fav.space!.slug)}
onClick={handleNavClick}
>
<CustomAvatar
name={fav.space!.name}
avatarUrl={fav.space!.logo}
type={AvatarIconType.SPACE_ICON}
color="initials"
variant="filled"
size={20}
/>
<Text size="sm" fw={500} lineClamp={1}>
{fav.space!.name}
</Text>
</Link>
))}
{sortedFavoriteSpaces.length > 10 && (
<Link
className={classes.spaceItem}
to="/spaces"
onClick={handleNavClick}
>
<Text size="xs" c="dimmed">
{t("View all")}
</Text>
</Link>
)}
</>
)}
</div>
</ScrollArea>
<div className={classes.bottomSection}>
<UnstyledButton
className={classes.link}
onClick={openInvite}
>
<IconUserPlus className={classes.linkIcon} stroke={2} />
<span>{t("Invite People")}</span>
</UnstyledButton>
<Link
className={classes.link}
data-active={active.startsWith("/settings") || undefined}
to="/settings/account/profile"
onClick={handleNavClick}
>
<IconSettings className={classes.linkIcon} stroke={2} />
<span>{t("Settings")}</span>
</Link>
</div>
<Modal
size="550"
opened={inviteOpened}
onClose={closeInvite}
title={t("Invite new members")}
centered
>
<Divider size="xs" mb="xs" />
<ScrollArea h="80%">
<WorkspaceInviteForm onClose={closeInvite} />
</ScrollArea>
</Modal>
</div>
);
}
@@ -10,6 +10,7 @@ export const desktopSidebarAtom = atomWithWebStorage<boolean>(
export const desktopAsideAtom = atom<boolean>(false);
// Valid `tab` values: "" | "comments" | "toc" | "chat" | "details"
type AsideStateType = {
tab: string;
isAsideOpen: boolean;
@@ -19,3 +20,5 @@ export const asideStateAtom = atom<AsideStateType>({
tab: "",
isAsideOpen: false,
});
export const sidebarWidthAtom = atomWithWebStorage<number>('sidebarWidth', 300);
@@ -1,13 +1,23 @@
import { UserProvider } from "@/features/user/user-provider.tsx";
import { Outlet } from "react-router-dom";
import { Outlet, useParams } from "react-router-dom";
import GlobalAppShell from "@/components/layouts/global/global-app-shell.tsx";
import { PosthogUser } from "@/ee/components/posthog-user.tsx";
import { isCloud } from "@/lib/config.ts";
import { SearchSpotlight } from "@/features/search/components/search-spotlight.tsx";
import React from "react";
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
export default function Layout() {
const { spaceSlug } = useParams();
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
return (
<UserProvider>
<GlobalAppShell>
<Outlet />
</GlobalAppShell>
{isCloud() && <PosthogUser />}
<SearchSpotlight spaceId={space?.id} />
</UserProvider>
);
}
@@ -1,8 +1,20 @@
import { Avatar, Group, Menu, rem, UnstyledButton, Text } from "@mantine/core";
import {
Group,
Menu,
Text,
UnstyledButton,
useMantineColorScheme,
} from "@mantine/core";
import {
IconBrightnessFilled,
IconBrush,
IconCheck,
IconChevronDown,
IconDeviceDesktop,
IconLogout,
IconMoon,
IconSettings,
IconSun,
IconUserCircle,
IconUsers,
} from "@tabler/icons-react";
@@ -12,10 +24,14 @@ import { Link } from "react-router-dom";
import APP_ROUTE from "@/lib/app-route.ts";
import useAuth from "@/features/auth/hooks/use-auth.ts";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { useTranslation } from "react-i18next";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
export default function TopMenu() {
const { t } = useTranslation();
const [currentUser] = useAtom(currentUserAtom);
const { logout } = useAuth();
const { colorScheme, setColorScheme } = useMantineColorScheme();
const user = currentUser?.user;
const workspace = currentUser?.workspace;
@@ -30,53 +46,41 @@ export default function TopMenu() {
<UnstyledButton>
<Group gap={7} wrap={"nowrap"}>
<CustomAvatar
avatarUrl={workspace.logo}
name={workspace.name}
avatarUrl={workspace?.logo}
name={workspace?.name}
variant="filled"
size="sm"
type={AvatarIconType.WORKSPACE_ICON}
/>
<Text fw={500} size="sm" lh={1} mr={3}>
{workspace.name}
<Text fw={500} size="sm" lh={1} mr={3} lineClamp={1}>
{workspace?.name}
</Text>
<IconChevronDown
style={{ width: rem(12), height: rem(12) }}
stroke={1.5}
/>
<IconChevronDown size={16} />
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Workspace</Menu.Label>
<Menu.Label>{t("Workspace")}</Menu.Label>
<Menu.Item
component={Link}
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
leftSection={
<IconSettings
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
leftSection={<IconSettings size={16} />}
>
Workspace settings
{t("Workspace settings")}
</Menu.Item>
<Menu.Item
component={Link}
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
leftSection={
<IconUsers
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
leftSection={<IconUsers size={16} />}
>
Manage members
{t("Manage members")}
</Menu.Item>
<Menu.Divider />
<Menu.Label>Account</Menu.Label>
<Menu.Label>{t("Account")}</Menu.Label>
<Menu.Item component={Link} to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}>
<Group wrap={"nowrap"}>
<CustomAvatar
@@ -85,11 +89,11 @@ export default function TopMenu() {
name={user.name}
/>
<div>
<div style={{ width: 190 }}>
<Text size="sm" fw={500} lineClamp={1}>
{user.name}
</Text>
<Text size="xs" c="dimmed">
<Text size="xs" c="dimmed" truncate="end">
{user.email}
</Text>
</div>
@@ -98,28 +102,61 @@ export default function TopMenu() {
<Menu.Item
component={Link}
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
leftSection={
<IconUserCircle
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
leftSection={<IconUserCircle size={16} />}
>
My profile
{t("My profile")}
</Menu.Item>
<Menu.Item
component={Link}
to={APP_ROUTE.SETTINGS.ACCOUNT.PREFERENCES}
leftSection={<IconBrush size={16} />}
>
{t("My preferences")}
</Menu.Item>
<Menu.Sub>
<Menu.Sub.Target>
<Menu.Sub.Item leftSection={<IconBrightnessFilled size={16} />}>
{t("Theme")}
</Menu.Sub.Item>
</Menu.Sub.Target>
<Menu.Sub.Dropdown>
<Menu.Item
onClick={() => setColorScheme("light")}
leftSection={<IconSun size={16} />}
rightSection={
colorScheme === "light" ? <IconCheck size={16} /> : null
}
>
{t("Light")}
</Menu.Item>
<Menu.Item
onClick={() => setColorScheme("dark")}
leftSection={<IconMoon size={16} />}
rightSection={
colorScheme === "dark" ? <IconCheck size={16} /> : null
}
>
{t("Dark")}
</Menu.Item>
<Menu.Item
onClick={() => setColorScheme("auto")}
leftSection={<IconDeviceDesktop size={16} />}
rightSection={
colorScheme === "auto" ? <IconCheck size={16} /> : null
}
>
{t("System settings")}
</Menu.Item>
</Menu.Sub.Dropdown>
</Menu.Sub>
<Menu.Divider />
<Menu.Item
onClick={logout}
leftSection={
<IconLogout
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
>
Logout
<Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}>
{t("Logout")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
@@ -0,0 +1,59 @@
import { useAppVersion } from "@/features/workspace/queries/workspace-query.ts";
import { isCloud } from "@/lib/config.ts";
import classes from "@/components/settings/settings.module.css";
import { Indicator, Text, Tooltip } from "@mantine/core";
import React from "react";
import semverGt from "semver/functions/gt";
import { useTranslation } from "react-i18next";
export default function AppVersion() {
const { t } = useTranslation();
const { data: appVersion } = useAppVersion(!isCloud());
let hasUpdate = false;
try {
hasUpdate =
appVersion &&
parseFloat(appVersion.latestVersion) > 0 &&
semverGt(appVersion.latestVersion, appVersion.currentVersion);
} catch (err) {
console.error(err);
}
return (
<div className={classes.text}>
<Tooltip
label={t("{{latestVersion}} is available", {
latestVersion: `v${appVersion?.latestVersion}`,
})}
disabled={!hasUpdate}
>
<Indicator
label={t("New update")}
color="gray"
inline
size={16}
position="middle-end"
style={{ cursor: "pointer" }}
disabled={!hasUpdate}
onClick={() => {
window.open(
"https://github.com/docmost/docmost/releases",
"_blank",
);
}}
>
<Text
size="sm"
c="dimmed"
component="a"
mr={45}
href="https://github.com/docmost/docmost/releases"
target="_blank"
>
{appVersion?.currentVersion && <>v{appVersion?.currentVersion}</>}
</Text>
</Indicator>
</Tooltip>
</div>
);
}
@@ -0,0 +1,10 @@
import { atom, WritableAtom } from "jotai";
export const settingsOriginAtom: WritableAtom<string | null, [string | null], void> = atom(
null,
(get, set, newValue) => {
if (get(settingsOriginAtom) !== newValue) {
set(settingsOriginAtom, newValue);
}
}
);
@@ -0,0 +1,108 @@
import { queryClient } from "@/main.tsx";
import {
getBilling,
getBillingPlans,
} from "@/ee/billing/services/billing-service.ts";
import { getSpaces } from "@/features/space/services/space-service.ts";
import { getGroups } from "@/features/group/services/group-service.ts";
import { QueryParams } from "@/lib/types.ts";
import { getWorkspaceMembers } from "@/features/workspace/services/workspace-service.ts";
import { getLicenseInfo } from "@/ee/licence/services/license-service.ts";
import { getSsoProviders } from "@/ee/security/services/security-service.ts";
import { getShares } from "@/features/share/services/share-service.ts";
import { getApiKeys } from "@/ee/api-key";
import { getAuditLogs } from "@/ee/audit/services/audit-service";
import { getVerificationList } from "@/ee/page-verification/services/page-verification-service";
import { getScimTokens } from "@/ee/scim/services/scim-token-service";
export const prefetchWorkspaceMembers = () => {
const params: QueryParams = { limit: 100, query: "" };
queryClient.prefetchQuery({
queryKey: ["workspaceMembers", params],
queryFn: () => getWorkspaceMembers(params),
});
};
export const prefetchSpaces = () => {
queryClient.prefetchQuery({
queryKey: ["spaces", {}],
queryFn: () => getSpaces({}),
});
};
export const prefetchGroups = () => {
queryClient.prefetchQuery({
queryKey: ["groups", {}],
queryFn: () => getGroups({}),
});
};
export const prefetchBilling = () => {
queryClient.prefetchQuery({
queryKey: ["billing"],
queryFn: () => getBilling(),
});
queryClient.prefetchQuery({
queryKey: ["billing-plans"],
queryFn: () => getBillingPlans(),
});
};
export const prefetchLicense = () => {
queryClient.prefetchQuery({
queryKey: ["license"],
queryFn: () => getLicenseInfo(),
});
};
export const prefetchSsoProviders = () => {
queryClient.prefetchQuery({
queryKey: ["sso-providers"],
queryFn: () => getSsoProviders(),
});
};
export const prefetchShares = () => {
queryClient.prefetchQuery({
queryKey: ["share-list", {}],
queryFn: () => getShares({}),
});
};
export const prefetchApiKeys = () => {
queryClient.prefetchQuery({
queryKey: ["api-key-list", {}],
queryFn: () => getApiKeys({}),
});
};
export const prefetchApiKeyManagement = () => {
queryClient.prefetchQuery({
queryKey: ["api-key-list", { adminView: true }],
queryFn: () => getApiKeys({ adminView: true }),
});
};
export const prefetchAuditLogs = () => {
const params = { limit: 50 };
queryClient.prefetchQuery({
queryKey: ["audit-logs", params],
queryFn: () => getAuditLogs(params),
});
};
export const prefetchVerifiedPages = () => {
const params = { limit: 50 };
queryClient.prefetchQuery({
queryKey: ["verification-list", params],
queryFn: () => getVerificationList(params),
});
};
export const prefetchScimTokens = () => {
queryClient.prefetchQuery({
queryKey: ["scim-token-list", { cursor: undefined }],
queryFn: () => getScimTokens({}),
});
};
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { Group, Text, ScrollArea, ActionIcon, rem } from "@mantine/core";
import { Group, Text, ScrollArea, ActionIcon, Tooltip } from "@mantine/core";
import {
IconUser,
IconSettings,
@@ -8,20 +8,55 @@ import {
IconUsersGroup,
IconSpaces,
IconBrush,
IconCoin,
IconLock,
IconKey,
IconWorld,
IconSparkles,
IconHistory,
IconShieldCheck,
} from "@tabler/icons-react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Link, useLocation } from "react-router-dom";
import classes from "./settings.module.css";
import { useTranslation } from "react-i18next";
import { isCloud } from "@/lib/config.ts";
import useUserRole from "@/hooks/use-user-role.tsx";
import { useAtom } from "jotai";
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
import { Feature } from "@/ee/features";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
import {
prefetchApiKeyManagement,
prefetchApiKeys,
prefetchBilling,
prefetchGroups,
prefetchLicense,
prefetchScimTokens,
prefetchShares,
prefetchSpaces,
prefetchSsoProviders,
prefetchWorkspaceMembers,
prefetchAuditLogs,
prefetchVerifiedPages,
} from "@/components/settings/settings-queries.tsx";
import AppVersion from "@/components/settings/app-version.tsx";
import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
import { useSettingsNavigation } from "@/hooks/use-settings-navigation";
interface DataItem {
type DataItem = {
label: string;
icon: React.ElementType;
path: string;
}
feature?: string;
role?: "admin" | "owner";
env?: "cloud" | "selfhosted";
};
interface DataGroup {
type DataGroup = {
heading: string;
items: DataItem[];
}
};
const groupedData: DataGroup[] = [
{
@@ -33,66 +68,252 @@ const groupedData: DataGroup[] = [
icon: IconBrush,
path: "/settings/account/preferences",
},
{
label: "API keys",
icon: IconKey,
path: "/settings/account/api-keys",
feature: Feature.API_KEYS,
},
],
},
{
heading: "Workspace",
items: [
{ label: "General", icon: IconSettings, path: "/settings/workspace" },
{ label: "Members", icon: IconUsers, path: "/settings/members" },
{
label: "Members",
icon: IconUsers,
path: "/settings/members",
label: "Billing",
icon: IconCoin,
path: "/settings/billing",
role: "admin",
env: "cloud",
},
{
label: "Security & SSO",
icon: IconLock,
path: "/settings/security",
feature: Feature.SECURITY_SETTINGS,
role: "admin",
},
{ label: "Groups", icon: IconUsersGroup, path: "/settings/groups" },
{ label: "Spaces", icon: IconSpaces, path: "/settings/spaces" },
{ label: "Public sharing", icon: IconWorld, path: "/settings/sharing" },
{
label: "Verified pages",
icon: IconShieldCheck,
path: "/settings/verifications",
feature: Feature.PAGE_VERIFICATION,
},
{
label: "API management",
icon: IconKey,
path: "/settings/api-keys",
feature: Feature.API_KEYS,
role: "admin",
},
{
label: "AI settings",
icon: IconSparkles,
path: "/settings/ai",
role: "admin",
},
{
label: "Audit log",
icon: IconHistory,
path: "/settings/audit",
feature: Feature.AUDIT_LOGS,
role: "owner",
env: "selfhosted",
},
],
},
{
heading: "System",
items: [
{
label: "License & Edition",
icon: IconKey,
path: "/settings/license",
},
],
},
];
export default function SettingsSidebar() {
const { t } = useTranslation();
const location = useLocation();
const [active, setActive] = useState(location.pathname);
const navigate = useNavigate();
const { goBack } = useSettingsNavigation();
const { isAdmin, isOwner } = useUserRole();
const [entitlements] = useAtom(entitlementAtom);
const upgradeLabel = useUpgradeLabel();
const [mobileSidebarOpened] = useAtom(mobileSidebarAtom);
const toggleMobileSidebar = useToggleSidebar(mobileSidebarAtom);
useEffect(() => {
setActive(location.pathname);
}, [location.pathname]);
const menuItems = groupedData.map((group) => (
<div key={group.heading}>
<Text c="dimmed" className={classes.linkHeader}>
{group.heading}
</Text>
{group.items.map((item) => (
<Link
className={classes.link}
data-active={active.startsWith(item.path) || undefined}
key={item.label}
to={item.path}
>
<item.icon className={classes.linkIcon} stroke={2} />
<span>{item.label}</span>
</Link>
))}
</div>
));
const hasFeature = (f: string) =>
entitlements?.features?.includes(f) ?? false;
const canShowItem = (item: DataItem) => {
if (item.env === "cloud" && !isCloud()) return false;
if (item.env === "selfhosted" && isCloud()) return false;
if (item.role === "admin" && !isAdmin) return false;
if (item.role === "owner" && !isOwner) return false;
return true;
};
const isItemDisabled = (item: DataItem) => {
if (!item.feature) return false;
return !hasFeature(item.feature);
};
const menuItems = groupedData.map((group) => {
if (group.heading === "System" && (!isAdmin || isCloud())) {
return null;
}
return (
<div key={group.heading}>
<Text c="dimmed" className={classes.linkHeader}>
{t(group.heading)}
</Text>
{group.items.map((item) => {
if (!canShowItem(item)) {
return null;
}
let prefetchHandler: any;
switch (item.label) {
case "Members":
prefetchHandler = prefetchWorkspaceMembers;
break;
case "Spaces":
prefetchHandler = prefetchSpaces;
break;
case "Groups":
prefetchHandler = prefetchGroups;
break;
case "Billing":
prefetchHandler = prefetchBilling;
break;
case "License & Edition":
if (entitlements?.tier !== "free") {
prefetchHandler = prefetchLicense;
}
break;
case "Security & SSO":
prefetchHandler = () => {
prefetchSsoProviders();
prefetchScimTokens();
};
break;
case "Public sharing":
prefetchHandler = prefetchShares;
break;
case "API keys":
prefetchHandler = prefetchApiKeys;
break;
case "API management":
prefetchHandler = prefetchApiKeyManagement;
break;
case "Audit log":
prefetchHandler = prefetchAuditLogs;
break;
case "Verified pages":
prefetchHandler = prefetchVerifiedPages;
break;
default:
break;
}
const isDisabled = isItemDisabled(item);
if (isDisabled) {
return (
<Tooltip
key={item.label}
label={upgradeLabel}
position="right"
withArrow
>
<span
className={classes.link}
data-disabled
role="link"
aria-disabled="true"
tabIndex={0}
style={{
opacity: 0.5,
cursor: "not-allowed",
}}
>
<item.icon className={classes.linkIcon} stroke={2} />
<span>{t(item.label)}</span>
</span>
</Tooltip>
);
}
return (
<Link
onMouseEnter={prefetchHandler}
className={classes.link}
data-active={active.startsWith(item.path) || undefined}
key={item.label}
to={item.path}
onClick={() => {
if (mobileSidebarOpened) {
toggleMobileSidebar();
}
}}
>
<item.icon className={classes.linkIcon} stroke={2} />
<span>{t(item.label)}</span>
</Link>
);
})}
</div>
);
});
return (
<div className={classes.navbar}>
<Group className={classes.title} justify="flex-start">
<ActionIcon
onClick={() => navigate(-1)}
onClick={() => {
goBack();
if (mobileSidebarOpened) {
toggleMobileSidebar();
}
}}
variant="transparent"
c="gray"
aria-label="Back"
aria-label={t("Back")}
>
<IconArrowLeft stroke={2} />
</ActionIcon>
<Text fw={500}>Settings</Text>
<Text fw={500}>{t("Settings")}</Text>
</Group>
<ScrollArea w="100%">{menuItems}</ScrollArea>
{!isCloud() && <AppVersion />}
{isCloud() && (
<div className={classes.text}>
<Text
size="sm"
c="dimmed"
component="a"
href="mailto:help@docmost.com"
>
help@docmost.com
</Text>
</div>
)}
</div>
);
}
@@ -57,3 +57,8 @@
display: flex;
align-items: center;
}
.text {
padding-left: var(--mantine-spacing-xs) ;
padding-top: 10px;
}
@@ -0,0 +1,19 @@
.dark {
@mixin dark {
display: none;
}
@mixin light {
display: block;
}
}
.light {
@mixin light {
display: none;
}
@mixin dark {
display: block;
}
}
+24 -9
View File
@@ -1,13 +1,28 @@
import { Button, Group, useMantineColorScheme } from '@mantine/core';
import {
ActionIcon,
Tooltip,
useComputedColorScheme,
useMantineColorScheme,
} from "@mantine/core";
import { IconMoon, IconSun } from "@tabler/icons-react";
import classes from "./theme-toggle.module.css";
export function ThemeToggle() {
const { setColorScheme } = useMantineColorScheme();
const { setColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme();
return (
<Group justify="center" mt="xl">
<Button onClick={() => setColorScheme('light')}>Light</Button>
<Button onClick={() => setColorScheme('dark')}>Dark</Button>
<Button onClick={() => setColorScheme('auto')}>Auto</Button>
</Group>
);
return (
<Tooltip label="Toggle Color Scheme">
<ActionIcon
variant="default"
onClick={() => {
setColorScheme(computedColorScheme === "light" ? "dark" : "light");
}}
aria-label="Toggle color scheme"
>
<IconSun className={classes.light} size={18} stroke={1.5} />
<IconMoon className={classes.dark} size={18} stroke={1.5} />
</ActionIcon>
</Tooltip>
);
}
@@ -0,0 +1,50 @@
import { useRef, useState, ReactNode } from "react";
import { Text, TextProps, Tooltip } from "@mantine/core";
type AutoTooltipTextProps = TextProps & {
children: ReactNode;
tooltipLabel?: string;
tooltipProps?: Omit<
React.ComponentProps<typeof Tooltip>,
"children" | "label"
>;
};
export function AutoTooltipText({
children,
tooltipLabel,
tooltipProps,
...textProps
}: AutoTooltipTextProps) {
const textRef = useRef<HTMLParagraphElement>(null);
const [isTruncated, setIsTruncated] = useState(false);
const handleMouseEnter = () => {
const element = textRef.current;
if (element) {
setIsTruncated(element.scrollWidth > element.clientWidth);
}
};
const label = tooltipLabel ?? (typeof children === "string" ? children : "");
return (
<Tooltip
label={label}
disabled={!isTruncated || !label}
multiline
withArrow
withinPortal={false}
{...tooltipProps}
>
<Text
ref={textRef}
truncate
onMouseEnter={handleMouseEnter}
{...textProps}
>
{children}
</Text>
</Tooltip>
);
}
@@ -0,0 +1,68 @@
.root {
position: relative;
}
.track {
display: flex;
gap: var(--mantine-spacing-md);
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
-ms-overflow-style: none;
padding: 2px;
margin: -2px;
}
.track::-webkit-scrollbar {
display: none;
}
.track > * {
scroll-snap-align: start;
flex: 0 0 auto;
}
.arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
cursor: pointer;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease, background-color 120ms ease, transform 120ms ease;
z-index: 2;
}
.root:hover .arrow.visible,
.arrow.visible:focus-visible {
opacity: 1;
pointer-events: auto;
}
.arrow:hover {
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
}
.arrow:active {
transform: translateY(-50%) scale(0.95);
}
.arrowLeft {
left: -14px;
}
.arrowRight {
right: -14px;
}
@@ -0,0 +1,77 @@
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import classes from "./card-carousel.module.css";
type Props = {
children: ReactNode;
ariaLabel?: string;
};
export default function CardCarousel({ children, ariaLabel }: Props) {
const { t } = useTranslation();
const trackRef = useRef<HTMLDivElement>(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
const updateScrollState = useCallback(() => {
const el = trackRef.current;
if (!el) return;
const maxScroll = el.scrollWidth - el.clientWidth;
setCanScrollLeft(el.scrollLeft > 1);
setCanScrollRight(el.scrollLeft < maxScroll - 1);
}, []);
useEffect(() => {
updateScrollState();
const el = trackRef.current;
if (!el) return;
const observer = new ResizeObserver(updateScrollState);
observer.observe(el);
for (const child of Array.from(el.children)) {
observer.observe(child);
}
return () => observer.disconnect();
}, [updateScrollState, children]);
const scrollBy = (direction: 1 | -1) => {
const el = trackRef.current;
if (!el) return;
el.scrollBy({ left: direction * el.clientWidth * 0.85, behavior: "smooth" });
};
return (
<div className={classes.root}>
<div
ref={trackRef}
className={classes.track}
onScroll={updateScrollState}
{...(ariaLabel ? { role: "region", "aria-label": ariaLabel } : {})}
>
{children}
</div>
<button
type="button"
className={`${classes.arrow} ${classes.arrowLeft} ${canScrollLeft ? classes.visible : ""}`}
onClick={() => scrollBy(-1)}
aria-label={t("Scroll left")}
tabIndex={canScrollLeft ? 0 : -1}
>
<IconChevronLeft size={18} />
</button>
<button
type="button"
className={`${classes.arrow} ${classes.arrowRight} ${canScrollRight ? classes.visible : ""}`}
onClick={() => scrollBy(1)}
aria-label={t("Scroll right")}
tabIndex={canScrollRight ? 0 : -1}
>
<IconChevronRight size={18} />
</button>
</div>
);
}
@@ -1,9 +1,10 @@
import React from "react";
import { Avatar } from "@mantine/core";
import { Avatar, MantineColor } from "@mantine/core";
import { getAvatarUrl } from "@/lib/config.ts";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
interface CustomAvatarProps {
avatarUrl: string;
avatarUrl?: string;
name: string;
color?: string;
size?: string | number;
@@ -11,21 +12,57 @@ interface CustomAvatarProps {
variant?: string;
style?: any;
component?: any;
type?: AvatarIconType;
mt?: string | number;
}
// `color.shade` pairs whose filled background meets WCAG AA (4.5:1) against
// white text. Avoids lime/yellow/green/orange — even their dark shades have
// weak white-text contrast.
const SAFE_INITIALS_COLORS: MantineColor[] = [
"blue.8",
"cyan.9",
"grape.7",
"indigo.7",
"pink.8",
"red.8",
"violet.7",
];
function hashName(input: string) {
let hash = 0;
for (let i = 0; i < input.length; i += 1) {
hash = (hash << 5) - hash + input.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
function pickInitialsColor(name: string) {
return SAFE_INITIALS_COLORS[hashName(name) % SAFE_INITIALS_COLORS.length];
}
function sanitizeInitialsSource(name: string) {
const sanitized = name.replace(/[^\p{L}\p{N}\s]/gu, " ").trim();
return sanitized || name;
}
export const CustomAvatar = React.forwardRef<
HTMLInputElement,
CustomAvatarProps
>(({ avatarUrl, name, ...props }: CustomAvatarProps, ref) => {
const avatarLink = getAvatarUrl(avatarUrl);
>(({ avatarUrl, name, type, color, ...props }: CustomAvatarProps, ref) => {
const avatarLink = getAvatarUrl(avatarUrl, type);
const resolvedColor =
!color || color === "initials" ? pickInitialsColor(name ?? "") : color;
const initialsSource = sanitizeInitialsSource(name ?? "");
return (
<Avatar
ref={ref}
src={avatarLink}
name={name}
name={initialsSource}
alt={name}
color="initials"
color={resolvedColor}
{...props}
/>
);
@@ -0,0 +1,67 @@
import { useState, useEffect } from "react";
import { Modal, Button, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { DestinationPicker } from "./destination-picker";
import {
DestinationPickerModalProps,
DestinationSelection,
} from "./destination-picker.types";
export function DestinationPickerModal({
opened,
onClose,
title,
actionLabel,
onSelect,
loading,
excludePageId,
pageLimit,
}: DestinationPickerModalProps) {
const { t } = useTranslation();
const [selection, setSelection] = useState<DestinationSelection | null>(null);
useEffect(() => {
if (!opened) {
setSelection(null);
}
}, [opened]);
return (
<Modal.Root
opened={opened}
onClose={onClose}
size={550}
padding="lg"
yOffset="10vh"
onClick={(e) => e.stopPropagation()}
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header py={0}>
<Modal.Title fw={500}>{title}</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
<DestinationPicker
onSelectionChange={setSelection}
excludePageId={excludePageId}
pageLimit={pageLimit}
/>
<Group justify="flex-end" mt="md">
<Button variant="default" onClick={onClose}>
{t("Close")}
</Button>
<Button
onClick={() => selection && onSelect(selection)}
disabled={!selection}
loading={loading}
>
{actionLabel}
</Button>
</Group>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}
@@ -0,0 +1,128 @@
.searchInput {
margin-bottom: var(--mantine-spacing-sm);
}
.scrollArea {
max-height: 50vh;
}
.row {
padding: 6px 12px;
border-radius: var(--mantine-radius-sm);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: background-color 150ms ease;
user-select: none;
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-0),
var(--mantine-color-dark-6)
);
}
}
.selected {
background-color: light-dark(
var(--mantine-color-blue-0),
var(--mantine-color-dark-5)
);
border-left: 2px solid var(--mantine-primary-color-filled);
}
.spaceRow {
composes: row;
font-weight: 500;
}
.pageRow {
composes: row;
font-weight: 400;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.chevron {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--mantine-radius-sm);
flex-shrink: 0;
transition: transform 150ms ease;
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-dark-5)
);
}
}
.chevronExpanded {
transform: rotate(90deg);
}
.loadMore {
text-align: center;
padding: 6px;
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
font-size: var(--mantine-font-size-sm);
cursor: pointer;
@mixin hover {
text-decoration: underline;
}
}
.selectedIndicator {
padding: 8px 12px;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
margin-top: var(--mantine-spacing-xs);
}
.emptyState {
padding: 12px;
text-align: center;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
}
.searchResult {
composes: row;
}
.pageTitle {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--mantine-font-size-sm);
}
.spaceName {
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
font-size: var(--mantine-font-size-xs);
flex-shrink: 0;
}
.iconWrapper {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 16px;
line-height: 1;
}
@@ -0,0 +1,168 @@
import { useState, useCallback } from "react";
import { TextInput, ScrollArea, Loader } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import { IconSearch, IconFile } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { useGetSpacesQuery } from "@/features/space/queries/space-query";
import { useSearchSuggestionsQuery } from "@/features/search/queries/search-query";
import { ISpace } from "@/features/space/types/space.types";
import { IPage } from "@/features/page/types/page.types";
import { DestinationSelection } from "./destination-picker.types";
import { SpaceRow } from "./space-row";
import classes from "./destination-picker.module.css";
type DestinationPickerProps = {
onSelectionChange: (selection: DestinationSelection | null) => void;
excludePageId?: string;
pageLimit?: number;
};
export function DestinationPicker({
onSelectionChange,
excludePageId,
pageLimit = 15,
}: DestinationPickerProps) {
const { t } = useTranslation();
const [searchQuery, setSearchQuery] = useState("");
const [selection, setSelection] = useState<DestinationSelection | null>(null);
const [debouncedQuery] = useDebouncedValue(searchQuery, 300);
const { data: spacesData, isLoading: spacesLoading } = useGetSpacesQuery({
limit: 100,
});
const searchEnabled = debouncedQuery && debouncedQuery.length >= 2;
const { data: searchData, isLoading: searchLoading } =
useSearchSuggestionsQuery({
query: searchEnabled ? debouncedQuery : "",
includePages: true,
limit: 20,
});
const isSearching = !!searchEnabled;
const selectedId =
selection?.type === "space" ? selection.spaceId : selection?.pageId ?? null;
const updateSelection = useCallback(
(next: DestinationSelection | null) => {
setSelection(next);
onSelectionChange(next);
},
[onSelectionChange],
);
const handleSearchResultClick = (page: Partial<IPage>) => {
if (!page.space || !page.id) return;
updateSelection({
type: "page",
spaceId: page.space.id,
pageId: page.id,
page,
space: page.space,
});
setSearchQuery("");
};
const handleSelectSpace = useCallback(
(space: ISpace) => {
updateSelection({ type: "space", spaceId: space.id, space });
},
[updateSelection],
);
const handleSelectPage = useCallback(
(page: Partial<IPage>, space: ISpace) => {
if (!page.id) return;
updateSelection({
type: "page",
spaceId: page.spaceId ?? space.id,
pageId: page.id,
page,
space,
});
},
[updateSelection],
);
return (
<>
<TextInput
leftSection={<IconSearch size={16} />}
placeholder={t("Search pages and spaces...")}
variant="filled"
value={searchQuery}
onChange={(e) => setSearchQuery(e.currentTarget.value)}
className={classes.searchInput}
/>
<ScrollArea h="50vh" offsetScrollbars className={classes.scrollArea}>
{isSearching ? (
searchLoading ? (
<div className={classes.emptyState}>
<Loader size="xs" />
</div>
) : searchData?.pages && searchData.pages.length > 0 ? (
searchData.pages.map(
(page) =>
page && (
<div
key={page.id}
className={classes.searchResult}
onClick={() => handleSearchResultClick(page)}
>
<div className={classes.iconWrapper}>
{page.icon ? (
page.icon
) : (
<IconFile
size={16}
color="var(--mantine-color-gray-5)"
/>
)}
</div>
<div className={classes.pageTitle}>
{page.title || t("Untitled")}
</div>
{page.space && (
<div className={classes.spaceName}>
{page.space.name}
</div>
)}
</div>
),
)
) : (
<div className={classes.emptyState}>{t("No results found")}</div>
)
) : spacesLoading ? (
<div className={classes.emptyState}>
<Loader size="xs" />
</div>
) : (
spacesData?.items?.map((space) => (
<SpaceRow
key={space.id}
space={space}
limit={pageLimit}
selectedId={selectedId}
excludePageId={excludePageId}
onSelectSpace={handleSelectSpace}
onSelectPage={handleSelectPage}
/>
))
)}
</ScrollArea>
{selection && (
<div className={classes.selectedIndicator}>
{selection.type === "space"
? selection.space.name
: `${selection.space.name} / ${selection.page.title || t("Untitled")}`}
</div>
)}
</>
);
}
@@ -0,0 +1,23 @@
import { ISpace } from "@/features/space/types/space.types";
import { IPage } from "@/features/page/types/page.types";
export type DestinationSelection =
| { type: "space"; spaceId: string; space: ISpace }
| {
type: "page";
spaceId: string;
pageId: string;
page: Partial<IPage>;
space: Partial<ISpace>;
};
export type DestinationPickerModalProps = {
opened: boolean;
onClose: () => void;
title: string;
actionLabel: string;
onSelect: (selection: DestinationSelection) => void | Promise<void>;
loading?: boolean;
excludePageId?: string;
pageLimit?: number;
};
@@ -0,0 +1,94 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { Loader } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { getSidebarPages } from "@/features/page/services/page-service";
import { IPage } from "@/features/page/types/page.types";
import { IPagination } from "@/lib/types";
import { PageRow } from "./page-row";
import classes from "./destination-picker.module.css";
type PageChildrenProps = {
spaceId: string;
pageId?: string;
depth: number;
limit: number;
selectedId: string | null;
excludePageId?: string;
onSelectPage: (page: Partial<IPage>) => void;
};
export function PageChildren({
spaceId,
pageId,
depth,
limit,
selectedId,
excludePageId,
onSelectPage,
}: PageChildrenProps) {
const { t } = useTranslation();
const { data, isLoading, hasNextPage, fetchNextPage } = useInfiniteQuery({
queryKey: ["destination-pages", spaceId, pageId ?? "root"],
queryFn: ({ pageParam }) =>
getSidebarPages({
spaceId,
pageId,
limit,
cursor: pageParam,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage: IPagination<IPage>) =>
lastPage.meta?.nextCursor ?? undefined,
});
const pages = data?.pages.flatMap((page) => page.items) ?? [];
if (isLoading) {
return (
<div className={classes.emptyState}>
<Loader size="xs" />
</div>
);
}
if (pages.length === 0) {
return (
<div className={classes.emptyState}>
{pageId ? t("No pages inside") : t("No pages in this space")}
</div>
);
}
return (
<>
{pages.map((page) => (
<PageRow
key={page.id}
page={page}
depth={depth}
limit={limit}
selectedId={selectedId}
excludePageId={excludePageId}
onSelect={onSelectPage}
/>
))}
{hasNextPage && (
<div
className={classes.loadMore}
onClick={() => fetchNextPage()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
fetchNextPage();
}
}}
role="button"
tabIndex={0}
>
{t("Load more")}
</div>
)}
</>
);
}
@@ -0,0 +1,89 @@
import { useState } from "react";
import { IconChevronRight, IconFile } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { IPage } from "@/features/page/types/page.types";
import { PageChildren } from "./page-children";
import classes from "./destination-picker.module.css";
type PageRowProps = {
page: Partial<IPage>;
depth: number;
limit: number;
selectedId: string | null;
excludePageId?: string;
onSelect: (page: Partial<IPage>) => void;
};
export function PageRow({
page,
depth,
limit,
selectedId,
excludePageId,
onSelect,
}: PageRowProps) {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
const isExcluded = page.id === excludePageId;
const isSelected = page.id === selectedId;
const rowClasses = [
classes.pageRow,
isSelected && classes.selected,
isExcluded && classes.disabled,
]
.filter(Boolean)
.join(" ");
return (
<>
<div
className={rowClasses}
style={{ paddingLeft: depth * 20 + 12 }}
onClick={() => !isExcluded && onSelect(page)}
>
{page.hasChildren ? (
<div
className={`${classes.chevron} ${expanded ? classes.chevronExpanded : ""}`}
onClick={(e) => {
e.stopPropagation();
setExpanded(!expanded);
}}
>
<IconChevronRight size={14} />
</div>
) : (
<div style={{ width: 20, flexShrink: 0 }} />
)}
<div className={classes.iconWrapper}>
{page.icon ? (
page.icon
) : (
<IconFile
size={16}
color="var(--mantine-color-gray-5)"
/>
)}
</div>
<div className={classes.pageTitle}>
{page.title || t("Untitled")}
</div>
</div>
{expanded && page.hasChildren && (
<PageChildren
spaceId={page.spaceId}
pageId={page.id}
depth={depth + 1}
limit={limit}
selectedId={selectedId}
excludePageId={excludePageId}
onSelectPage={onSelect}
/>
)}
</>
);
}
@@ -0,0 +1,108 @@
import { useState } from "react";
import { Tooltip } from "@mantine/core";
import { IconChevronRight, IconLock } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { ISpace } from "@/features/space/types/space.types";
import { IPage } from "@/features/page/types/page.types";
import { SpaceRole } from "@/lib/types";
import { CustomAvatar } from "@/components/ui/custom-avatar";
import { AvatarIconType } from "@/features/attachments/types/attachment.types";
import { PageChildren } from "./page-children";
import classes from "./destination-picker.module.css";
type SpaceRowProps = {
space: ISpace;
limit: number;
selectedId: string | null;
excludePageId?: string;
onSelectSpace: (space: ISpace) => void;
onSelectPage: (page: Partial<IPage>, space: ISpace) => void;
};
export function SpaceRow({
space,
limit,
selectedId,
excludePageId,
onSelectSpace,
onSelectPage,
}: SpaceRowProps) {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
const writable =
!!space.membership?.role && space.membership.role !== SpaceRole.READER;
const isSelected = space.id === selectedId;
const rowClasses = [
classes.spaceRow,
isSelected && classes.selected,
!writable && classes.disabled,
]
.filter(Boolean)
.join(" ");
const rowContent = (
<div
className={rowClasses}
onClick={() => writable && onSelectSpace(space)}
>
{writable ? (
<div
className={`${classes.chevron} ${expanded ? classes.chevronExpanded : ""}`}
onClick={(e) => {
e.stopPropagation();
setExpanded(!expanded);
}}
>
<IconChevronRight size={14} />
</div>
) : (
<div style={{ width: 20, flexShrink: 0 }} />
)}
<CustomAvatar
name={space.name}
avatarUrl={space.logo}
type={AvatarIconType.SPACE_ICON}
size={22}
/>
<div className={classes.pageTitle}>{space.name}</div>
{!writable && (
<IconLock
size={14}
color="var(--mantine-color-gray-5)"
/>
)}
</div>
);
return (
<>
{writable ? (
rowContent
) : (
<Tooltip
label={t("You don't have permission to create pages here")}
position="right"
withArrow
>
<div>{rowContent}</div>
</Tooltip>
)}
{expanded && writable && (
<PageChildren
spaceId={space.id}
depth={1}
limit={limit}
selectedId={selectedId}
excludePageId={excludePageId}
onSelectPage={(page) => onSelectPage(page, space)}
/>
)}
</>
);
}
+65 -29
View File
@@ -1,19 +1,25 @@
import React, { ReactNode } from "react";
import data from "@emoji-mart/data";
import Picker from "@emoji-mart/react";
import React, { ReactNode, useState } from "react";
import {
ActionIcon,
Popover,
Button,
useMantineColorScheme,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { useClickOutside, useDisclosure, useWindowEvent } from "@mantine/hooks";
import { Suspense } from "react";
const Picker = React.lazy(() => import("@emoji-mart/react"));
import { useTranslation } from "react-i18next";
export interface EmojiPickerInterface {
onEmojiSelect: (emoji: any) => void;
icon: ReactNode;
removeEmojiAction: () => void;
readOnly: boolean;
actionIconProps?: {
size?: string;
variant?: string;
c?: string;
};
}
function EmojiPicker({
@@ -21,9 +27,28 @@ function EmojiPicker({
icon,
removeEmojiAction,
readOnly,
actionIconProps,
}: EmojiPickerInterface) {
const { t } = useTranslation();
const [opened, handlers] = useDisclosure(false);
const { colorScheme } = useMantineColorScheme();
const [target, setTarget] = useState<HTMLElement | null>(null);
const [dropdown, setDropdown] = useState<HTMLDivElement | null>(null);
useClickOutside(
() => handlers.close(),
["mousedown", "touchstart"],
[dropdown, target],
);
// We need this because the default Mantine popover closeOnEscape does not work
useWindowEvent("keydown", (event) => {
if (opened && event.key === "Escape") {
event.stopPropagation();
event.preventDefault();
handlers.close();
}
});
const handleEmojiSelect = (emoji) => {
onEmojiSelect(emoji);
@@ -42,35 +67,46 @@ function EmojiPicker({
width={332}
position="bottom"
disabled={readOnly}
closeOnEscape={true}
>
<Popover.Target>
<ActionIcon c="gray" variant="transparent" onClick={handlers.toggle}>
<Popover.Target ref={setTarget}>
<ActionIcon
c={actionIconProps?.c || "gray"}
variant={actionIconProps?.variant || "transparent"}
size={actionIconProps?.size}
onClick={handlers.toggle}
aria-label={t("Pick emoji")}
aria-haspopup="dialog"
aria-expanded={opened}
>
{icon}
</ActionIcon>
</Popover.Target>
<Popover.Dropdown bg="000" style={{ border: "none" }}>
<Picker
data={data}
onEmojiSelect={handleEmojiSelect}
perLine={8}
skinTonePosition="search"
theme={colorScheme}
/>
<Button
variant="default"
c="gray"
size="xs"
style={{
position: "absolute",
zIndex: 2,
bottom: "1rem",
right: "1rem",
}}
onClick={handleRemoveEmoji}
>
Remove
</Button>
</Popover.Dropdown>
<Suspense fallback={null}>
<Popover.Dropdown bg="000" style={{ border: "none" }} ref={setDropdown}>
<Picker
data={async () => (await import("@emoji-mart/data")).default}
onEmojiSelect={handleEmojiSelect}
perLine={8}
skinTonePosition="search"
theme={colorScheme}
/>
<Button
variant="default"
c="gray"
size="xs"
style={{
position: "absolute",
zIndex: 2,
bottom: "1rem",
right: "1rem",
}}
onClick={handleRemoveEmoji}
>
{t("Remove")}
</Button>
</Popover.Dropdown>
</Suspense>
</Popover>
);
}
@@ -0,0 +1,8 @@
.root {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
@@ -0,0 +1,30 @@
import { Stack, Text } from "@mantine/core";
import { type TablerIcon } from "@tabler/icons-react";
import { ReactNode } from "react";
import classes from "./empty-state.module.css";
type EmptyStateProps = {
icon: TablerIcon;
title: string;
description?: string;
action?: ReactNode;
};
export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
return (
<div className={classes.root}>
<Stack align="center" gap="xs">
<Icon size={40} stroke={1.5} color="var(--mantine-color-dimmed)" />
<Text size="lg" fw={500}>
{title}
</Text>
{description && (
<Text size="sm" c="dimmed" maw={350}>
{description}
</Text>
)}
{action}
</Stack>
</div>
);
}
+20 -11
View File
@@ -1,19 +1,28 @@
import { Title, Text, Button, Container, Group } from "@mantine/core";
import classes from "./error-404.module.css";
import { Link } from "react-router-dom";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
export function Error404() {
const { t } = useTranslation();
return (
<Container className={classes.root}>
<Title className={classes.title}>404 Page Not Found</Title>
<Text c="dimmed" size="lg" ta="center" className={classes.description}>
Sorry, we can't find the page you are looking for.
</Text>
<Group justify="center">
<Button component={Link} to={"/home"} variant="subtle" size="md">
Take me back to homepage
</Button>
</Group>
</Container>
<>
<Helmet>
<title>{t("404 page not found")} - Docmost</title>
</Helmet>
<Container className={classes.root}>
<Title className={classes.title}>{t("404 page not found")}</Title>
<Text c="dimmed" size="lg" ta="center" className={classes.description}>
{t("Sorry, we can't find the page you are looking for.")}
</Text>
<Group justify="center">
<Button component={Link} to={"/home"} variant="subtle" size="md">
{t("Take me back to homepage")}
</Button>
</Group>
</Container>
</>
);
}
@@ -0,0 +1,47 @@
import { Box } from "@mantine/core";
import React from "react";
interface ResponsiveSettingsRowProps {
children: React.ReactNode;
}
export function ResponsiveSettingsRow({ children }: ResponsiveSettingsRowProps) {
return (
<Box
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
gap: "1rem",
flexWrap: "wrap",
}}
>
{children}
</Box>
);
}
interface ResponsiveSettingsContentProps {
children: React.ReactNode;
}
export function ResponsiveSettingsContent({ children }: ResponsiveSettingsContentProps) {
return (
<Box style={{ flex: "1 1 300px", minWidth: 0 }}>
{children}
</Box>
);
}
interface ResponsiveSettingsControlProps {
children: React.ReactNode;
}
export function ResponsiveSettingsControl({ children }: ResponsiveSettingsControlProps) {
return (
<Box style={{ flex: "0 0 auto" }}>
{children}
</Box>
);
}
@@ -2,6 +2,7 @@ import React, { forwardRef } from "react";
import { IconCheck, IconChevronDown } from "@tabler/icons-react";
import { Group, Text, Menu, Button } from "@mantine/core";
import { IRoleData } from "@/lib/types.ts";
import { useTranslation } from "react-i18next";
interface RoleButtonProps extends React.ComponentPropsWithoutRef<"button"> {
name: string;
@@ -23,7 +24,7 @@ const RoleButton = forwardRef<HTMLButtonElement, RoleButtonProps>(
),
);
interface SpaceRoleMenuProps {
interface RoleMenuProps {
roles: IRoleData[];
roleName: string;
onChange?: (value: string) => void;
@@ -35,11 +36,13 @@ export default function RoleSelectMenu({
roleName,
onChange,
disabled,
}: SpaceRoleMenuProps) {
}: RoleMenuProps) {
const { t } = useTranslation();
return (
<Menu withArrow>
<Menu.Target>
<RoleButton name={roleName} disabled={disabled} />
<RoleButton name={t(roleName)} disabled={disabled} />
</Menu.Target>
<Menu.Dropdown>
@@ -50,9 +53,9 @@ export default function RoleSelectMenu({
>
<Group flex="1" gap="xs">
<div>
<Text size="sm">{item.label}</Text>
<Text size="sm">{t(item.label)}</Text>
<Text size="xs" opacity={0.65}>
{item.description}
{t(item.description)}
</Text>
</div>
{item.label === roleName && <IconCheck size={20} />}
@@ -1,15 +1,9 @@
import React from "react";
import {
IconLayoutSidebarRightCollapse,
IconLayoutSidebarRightExpand,
IconLayoutSidebarRightExpand
} from "@tabler/icons-react";
import {
ActionIcon,
BoxProps,
ElementProps,
MantineColor,
MantineSize,
} from "@mantine/core";
import React from "react";
import { ActionIcon, BoxProps, ElementProps, MantineColor, MantineSize } from "@mantine/core";
export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
size?: MantineSize | `compact-${MantineSize}` | (string & {});
@@ -17,18 +11,18 @@ export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
opened?: boolean;
}
export default function SidebarToggle({
opened,
size = "sm",
...others
}: SidebarToggleProps) {
return (
<ActionIcon size={size} {...others} variant="subtle" color="gray">
{opened ? (
<IconLayoutSidebarRightExpand />
) : (
<IconLayoutSidebarRightCollapse />
)}
</ActionIcon>
);
}
const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
({ opened, size = "sm", ...others }, ref) => {
return (
<ActionIcon size={size} {...others} variant="subtle" color="gray" ref={ref}>
{opened ? (
<IconLayoutSidebarRightExpand />
) : (
<IconLayoutSidebarRightCollapse />
)}
</ActionIcon>
);
}
);
export default SidebarToggle;
+1
View File
@@ -0,0 +1 @@
Files in this directory are subject to the Docmost Enterprise Edition license.
@@ -0,0 +1,105 @@
import { useEffect, useRef } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { useChatInfoQuery } from "../queries/ai-chat-query";
import { useChatStream } from "../hooks/use-chat-stream";
import ChatMessageList from "./chat-message-list";
import ChatEmptyState from "./chat-empty-state";
import ChatInput from "./chat-input";
import type { HomeAiPromptInitialState } from "@/features/home/components/home-ai-prompt";
import classes from "../styles/ai-chat.module.css";
export default function AiChatLayout() {
const { chatId } = useParams<{ chatId: string }>();
const location = useLocation();
const navigate = useNavigate();
const chatInfoQuery = useChatInfoQuery(chatId);
// If the URL points at a chat the user does not own, the info fetch 404s.
// Bounce them back to /ai so they cannot interact with any chat UI (including
// kicking off orphan uploads) tied to a chat they have no access to.
useEffect(() => {
if (chatId && chatInfoQuery.isError) {
navigate("/ai", { replace: true });
}
}, [chatId, chatInfoQuery.isError, navigate]);
const {
messages,
streamingContent,
streamingToolCalls,
isStreaming,
error,
sendMessage,
stopGeneration,
hydrateFromServer,
} = useChatStream(chatId);
const autoSentRef = useRef(false);
useEffect(() => {
if (chatInfoQuery.data?.messages) {
hydrateFromServer(chatInfoQuery.data.messages);
}
}, [chatInfoQuery.data, hydrateFromServer]);
useEffect(() => {
if (autoSentRef.current || chatId) return;
const state = location.state as HomeAiPromptInitialState | null;
if (!state?.initialContent && !state?.initialAttachments?.length) return;
autoSentRef.current = true;
sendMessage(
state.initialContent ?? "",
state.initialMentions ?? [],
state.initialAttachments ?? [],
);
navigate(location.pathname, { replace: true, state: null });
}, [chatId, location, navigate, sendMessage]);
const hasMessages = messages.length > 0 || isStreaming || !!chatId;
// While the redirect effect is running (or if the user is still on this
// component for any reason) never render the chat UI for a forbidden chat.
if (chatId && chatInfoQuery.isError) {
return null;
}
return (
<div className={classes.main}>
{hasMessages ? (
<>
<ChatMessageList
messages={messages}
isStreaming={isStreaming}
streamingContent={streamingContent}
streamingToolCalls={streamingToolCalls}
/>
{error && (
<div
style={{
padding: "var(--mantine-spacing-sm) var(--mantine-spacing-lg)",
color: "var(--mantine-color-red-6)",
fontSize: "var(--mantine-font-size-sm)",
}}
>
{error}
</div>
)}
<div className={classes.inputArea}>
<ChatInput
isStreaming={isStreaming}
onSend={sendMessage}
onStop={stopGeneration}
chatId={chatId}
/>
</div>
</>
) : (
<ChatEmptyState
isStreaming={isStreaming}
onSend={sendMessage}
onStop={stopGeneration}
/>
)}
</div>
);
}
@@ -0,0 +1,167 @@
import { useState, useRef, useEffect, useMemo, useCallback } from "react";
import { ActionIcon, Menu, TextInput } from "@mantine/core";
import { IconDots, IconTrash, IconEdit } from "@tabler/icons-react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import type { AiChat } from "../types/ai-chat.types";
import classes from "../styles/chat-sidebar.module.css";
type Props = {
chat: AiChat;
isActive: boolean;
onDelete: (chatId: string, title: string | null) => void;
onRename: (chatId: string, title: string) => void;
};
function formatChatDate(
isoString: string | Date,
locale: string | undefined,
): string {
const date = new Date(isoString);
if (Number.isNaN(date.getTime())) return "";
const now = new Date();
const startOfToday = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
).getTime();
const ts = date.getTime();
const sameYear = date.getFullYear() === now.getFullYear();
if (ts >= startOfToday) {
return date.toLocaleTimeString(locale, {
hour: "numeric",
minute: "2-digit",
});
}
if (sameYear) {
return date.toLocaleDateString(locale, {
month: "short",
day: "numeric",
});
}
return date.toLocaleDateString(locale, {
month: "short",
day: "numeric",
year: "numeric",
});
}
export default function AiChatSidebarItem({
chat,
isActive,
onDelete,
onRename,
}: Props) {
const { t, i18n } = useTranslation();
const [renaming, setRenaming] = useState(false);
const [renameValue, setRenameValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const formattedDate = useMemo(
() => formatChatDate(chat.updatedAt, i18n.language),
[chat.updatedAt, i18n.language],
);
useEffect(() => {
if (renaming) {
// Wait for the input to be mounted before selecting.
const id = window.setTimeout(() => inputRef.current?.select(), 0);
return () => window.clearTimeout(id);
}
}, [renaming]);
const startRename = useCallback(() => {
setRenameValue(chat.title || "");
setRenaming(true);
}, [chat.title]);
const submitRename = useCallback(() => {
const trimmed = renameValue.trim();
if (trimmed && trimmed !== chat.title) {
onRename(chat.id, trimmed);
}
setRenaming(false);
}, [renameValue, chat.id, chat.title, onRename]);
if (renaming) {
return (
<div className={classes.chatItem} data-active={isActive || undefined}>
<TextInput
ref={inputRef}
size="xs"
variant="unstyled"
placeholder={t("Chat name")}
value={renameValue}
onChange={(e) => setRenameValue(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
submitRename();
} else if (e.key === "Escape") {
e.preventDefault();
setRenaming(false);
}
}}
onBlur={submitRename}
classNames={{ input: classes.chatItemRenameInput }}
style={{ flex: 1 }}
/>
</div>
);
}
return (
<Link
to={`/ai/chat/${chat.id}`}
className={classes.chatItem}
data-active={isActive || undefined}
>
<span className={classes.chatItemTitle}>
{chat.title || t("Untitled chat")}
</span>
<span className={classes.chatItemDate}>{formattedDate}</span>
<div className={classes.chatItemActions}>
<Menu position="bottom-end" withinPortal>
<Menu.Target>
<ActionIcon
variant="subtle"
size="xs"
color="gray"
onClick={(e) => e.preventDefault()}
aria-label={t("Chat menu")}
>
<IconDots size={14} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconEdit size={14} />}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
startRename();
}}
>
{t("Rename")}
</Menu.Item>
<Menu.Item
leftSection={<IconTrash size={14} />}
color="red"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelete(chat.id, chat.title);
}}
>
{t("Delete")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</div>
</Link>
);
}
@@ -0,0 +1,204 @@
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import {
ActionIcon,
Center,
Text,
TextInput,
Loader,
Tooltip,
} from "@mantine/core";
import { modals } from "@mantine/modals";
import { useDebouncedValue } from "@mantine/hooks";
import { IconPlus, IconSearch, IconMessageCircle2 } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import {
useChatsQuery,
useDeleteChatMutation,
useUpdateChatTitleMutation,
useSearchChatsQuery,
} from "../queries/ai-chat-query";
import AiChatSidebarItem from "./ai-chat-sidebar-item";
import { groupChatsByAge } from "../utils/group-chats-by-age";
import classes from "../styles/chat-sidebar.module.css";
export default function AiChatSidebar() {
const { t } = useTranslation();
const navigate = useNavigate();
const { chatId } = useParams<{ chatId: string }>();
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 300);
const chatsQuery = useChatsQuery();
const searchQuery = useSearchChatsQuery(debouncedSearch);
const deleteMutation = useDeleteChatMutation();
const renameMutation = useUpdateChatTitleMutation();
const chats = useMemo(() => {
if (debouncedSearch) {
return searchQuery.data || [];
}
return chatsQuery.data?.pages.flatMap((p) => p.items) || [];
}, [debouncedSearch, searchQuery.data, chatsQuery.data]);
const groupedChats = useMemo(() => groupChatsByAge(chats, t), [chats, t]);
const sentinelRef = useRef<HTMLDivElement>(null);
const { hasNextPage, fetchNextPage, isFetchingNextPage } = chatsQuery;
const isSearching = Boolean(debouncedSearch);
useEffect(() => {
if (isSearching) return;
const sentinel = sentinelRef.current;
if (!sentinel) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
{ threshold: 0.1 },
);
observer.observe(sentinel);
return () => observer.disconnect();
}, [isSearching, hasNextPage, isFetchingNextPage, fetchNextPage]);
const handleNewChat = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
if (
event.button !== 0 ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey
) {
return;
}
event.preventDefault();
navigate("/ai");
},
[navigate],
);
const handleDelete = useCallback(
(id: string, title: string | null) => {
modals.openConfirmModal({
title: t("Delete chat"),
centered: true,
children: (
<Text size="sm">
{t("Are you sure you want to delete '{{title}}'? This action cannot be undone.", {
title: title || t("Untitled"),
})}
</Text>
),
labels: { confirm: t("Delete"), cancel: t("Cancel") },
confirmProps: { color: "red" },
onConfirm: () => {
deleteMutation.mutate(id, {
onSuccess: () => {
if (chatId === id) {
navigate("/ai");
}
},
});
},
});
},
[deleteMutation, chatId, navigate, t],
);
const handleRename = useCallback(
(chatId: string, title: string) => {
renameMutation.mutate({ chatId, title });
},
[renameMutation],
);
const isLoading = chatsQuery.isLoading || searchQuery.isLoading;
return (
<div className={classes.sidebar}>
<div className={classes.header}>
<span className={classes.title}>{t("AI Chat")}</span>
<Tooltip label={t("New chat")} openDelay={250} withArrow>
<ActionIcon
component={Link}
to="/ai"
variant="subtle"
color="gray"
onClick={handleNewChat}
aria-label={t("New chat")}
>
<IconPlus size={18} />
</ActionIcon>
</Tooltip>
</div>
<TextInput
className={classes.searchInput}
placeholder={t("Search chats...")}
aria-label={t("Search chats")}
leftSection={<IconSearch size={14} />}
size="xs"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<div className={classes.chatList}>
{isLoading && <Loader size="xs" mx="auto" mt="md" />}
{!isLoading && chats.length === 0 && (
<div className={classes.chatListEmpty}>
<IconMessageCircle2
size={28}
stroke={1.5}
className={classes.chatListEmptyIcon}
/>
<div className={classes.chatListEmptyTitle}>
{isSearching ? t("No chats found") : t("No conversations yet")}
</div>
<div className={classes.chatListEmptyHint}>
{isSearching
? t("Try a different search term.")
: t("Start a new chat to see it here.")}
</div>
</div>
)}
{isSearching
? chats.map((chat) => (
<AiChatSidebarItem
key={chat.id}
chat={chat}
isActive={chat.id === chatId}
onDelete={handleDelete}
onRename={handleRename}
/>
))
: groupedChats.map((group) => (
<div key={group.key} className={classes.chatGroup}>
<div className={classes.chatGroupLabel}>{group.label}</div>
{group.chats.map((chat) => (
<AiChatSidebarItem
key={chat.id}
chat={chat}
isActive={chat.id === chatId}
onDelete={handleDelete}
onRename={handleRename}
/>
))}
</div>
))}
{!isSearching && (
<>
<div ref={sentinelRef} style={{ height: 1 }} />
{isFetchingNextPage && (
<Center py="xs">
<Loader size="xs" />
</Center>
)}
</>
)}
</div>
</div>
);
}
@@ -0,0 +1,67 @@
import { useState } from "react";
import { TextInput, Loader, Text, ScrollArea } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { useChatsQuery, useSearchChatsQuery } from "../queries/ai-chat-query";
import { useDebouncedValue } from "@mantine/hooks";
import { useTranslation } from "react-i18next";
import classes from "../styles/aside-chat-panel.module.css";
type Props = {
activeChatId: string | undefined;
onSelect: (chatId: string) => void;
};
export default function AsideChatHistory({ activeChatId, onSelect }: Props) {
const { t } = useTranslation();
const [searchValue, setSearchValue] = useState("");
const [debouncedSearch] = useDebouncedValue(searchValue, 300);
const chatsQuery = useChatsQuery();
const searchQuery = useSearchChatsQuery(debouncedSearch);
const isSearching = debouncedSearch.length > 0;
const chats = isSearching
? (searchQuery.data ?? [])
: (chatsQuery.data?.pages.flatMap((p) => p.items) ?? []);
const isLoading = isSearching ? searchQuery.isLoading : chatsQuery.isLoading;
return (
<div>
<TextInput
placeholder={t("Search chats...")}
leftSection={<IconSearch size={14} />}
size="xs"
mb="xs"
value={searchValue}
onChange={(e) => setSearchValue(e.currentTarget.value)}
/>
{isLoading ? (
<div style={{ display: "flex", justifyContent: "center", padding: 16 }}>
<Loader size="sm" />
</div>
) : chats.length === 0 ? (
<Text size="sm" c="dimmed" ta="center" py="md">
{isSearching ? t("No chats found") : t("No chat history")}
</Text>
) : (
<ScrollArea.Autosize mah={300} scrollbars="y">
<div className={classes.historyList}>
{chats.map((chat) => (
<div
key={chat.id}
className={classes.historyItem}
data-active={chat.id === activeChatId || undefined}
onClick={() => onSelect(chat.id)}
>
<span className={classes.historyItemTitle}>
{chat.title || t("Untitled chat")}
</span>
</div>
))}
</div>
</ScrollArea.Autosize>
)}
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More