Compare commits

...

15 Commits

Author SHA1 Message Date
Anthony LC
bd662d72bf 🐛(frontend) fix loading comments transaction
When we load the comments we have to notify the
subscribers of the DocsThreadStore. This generates
a Yjs transaction that is currently treated as a
user-initiated content change that will trigger
a patch request when the doc will try to save.
We now update the transaction origin when we notify
the subscribers so that we can reliably identify
and ignore those transactions in the useSaveDoc
hook.
2026-05-06 15:55:47 +02:00
Anthony LC
3701fe5a45 🐛(frontend) interlinking are exported correctly in print mode
Interlinking are now exported correctly in print
mode. The interlinking was not a link in print
mode, but now it is.
2026-05-06 15:12:24 +02:00
Anthony LC
0f527a789a 🔒️(frontend) sanitize color during collaboration
To improve security we sanitize the color used
for collaboration presence to ensure it's a valid
hex color. If the color is not valid, we generate
a random color instead. This prevents potential
issues with invalid color values being used in the UI.
2026-05-05 15:21:16 +02:00
renovate[bot]
85128c7b11 ⬆️(dependencies) update axios to v1.15.2 [SECURITY] 2026-05-05 12:25:22 +00:00
Anthony LC
5f700ed6c4 💬(frontend) add missing link in onboarding description
We added a missing link in the onboarding step
description to direct users to ready-made templates f
or common use cases. This enhancement aims to improve
the user experience by providing easy access to
resources that can help users get started quickly
and customize their workflow efficiently.
2026-05-05 13:23:19 +02:00
Anthony LC
b0d9ed15c0 ️(frontend) add skeleton on content loading
Content is longer to load than other parts of the
editor because of the connection with websocket
to the collaboration server. To improve the user
experience, we add a skeleton on the content part
of the editor while the others parts are displayed.
2026-05-05 12:12:11 +02:00
Anthony LC
d41e44dcd5 🐛(frontend) fix uikit dnd tree
The last version of UIKit has a bug that causes
the dnd tree to break. It is due to some
pointers event that are not properly handled.
We remove the pointer event in waiting for the
fix to be released.
2026-05-05 10:56:59 +02:00
Anthony LC
07e7b3feb6 🏷️(mail) adapt to mjml v5
We upgraded to mjml v5, which has some breaking changes.
By default the ubuntu font was loaded, with google
fonts, that is not GDPR compliant.
We switched to Inter, and uses fonts.bunny.net to
load the font, which is GDPR compliant.
2026-05-05 10:56:59 +02:00
Anthony LC
aa71cfdfc0 ️(frontend) remove listener on hover with react-dropzone
React-dropzone was rerendering components on hover,
which was unnecessary. This commit removes the
rerendering by adapting its settings.
2026-05-05 10:53:39 +02:00
Anthony LC
7afa17a181 💄(frontend) adapt css to Blocknote v0.49
We updated Blocknote to v0.49, which includes
some breaking changes in the CSS. This commit adapts
our custom styles to the new version of Blocknote.
2026-05-05 10:53:39 +02:00
Anthony LC
af2b381097 🐛(frontend) fix scroll table of content
The scroll of the table of content was calculated
on mount of the component, so when the editor height change,
the scroll of the table of content was not updated.
We added a observer to observe the height of the
editor and update the scroll of the table of
content when the height change.
2026-05-05 10:53:39 +02:00
Anthony LC
5015d42677 🏷️(frontend) adapt types to i18next v26
We updated i18next to v26, which includes some
breaking changes. This commit adapts our types
to the new version, ensuring compatibility and
proper type checking throughout our codebase.
2026-05-05 10:53:39 +02:00
Anthony LC
738ff90fc7 🏷️(frontend) adapt types to upgrade Cunningham and ui-kit
We upgraded Cunningham and ui-kit dependencies, which
introduced some breaking changes. This commit adapts
our code to these changes, ensuring compatibility
with the new versions of these libraries.
2026-05-05 10:53:38 +02:00
Anthony LC
0e8094c733 🏷️(frontend) adapt types to typescript v6
We updated typescript to v6, which includes some
breaking changes. This commit adapts our code to
be compatible with the new version of typescript.
2026-05-05 10:53:38 +02:00
renovate[bot]
9231730bf0 ⬆️(dependencies) update js dependencies 2026-05-05 10:50:49 +02:00
72 changed files with 3252 additions and 3267 deletions

View File

@@ -6,9 +6,24 @@ and this project adheres to
## [Unreleased]
### Changed
### Added
- ⚡️(frontend) add skeleton on content loading #2254
### Fixed
- 🐛(frontend) fix patch and comments #2273
- 🐛(frontend) interlinking are exported correctly in print mode #2269
- 💬(frontend) add missing link in onboarding description #2233
- 🐛(frontend) sanitize pasted and dropped content in document title #2210
- 🐛(frontend) Emoji menu doesn't display above comment box #2229
- 🐛(frontend) Block menu doesn't stay open on 1st line #2229
- 🐛(frontend) The "+" on the first line of a new doc doesn't work #2229
### Security
- 🔒️(frontend) sanitize color during collaboration #2270
## [v5.0.0] - 2026-04-08

View File

@@ -161,7 +161,8 @@
},
"onboarding": {
"enabled": true,
"learn_more_url": ""
"learn_more_url": "",
"ready_template_url": ""
},
"help": {
"documentation_url": ""

File diff suppressed because one or more lines are too long

View File

@@ -192,10 +192,10 @@ endobj
(react-pdf)
endobj
55 0 obj
(D:20260403132357Z)
(D:20260505110445Z)
endobj
56 0 obj
(chromium-8651-0-doc-export-override-content)
(chromium-4903-0-doc-export-override-content)
endobj
52 0 obj
<<
@@ -216,7 +216,7 @@ endobj
58 0 obj
<<
/Type /FontDescriptor
/FontName /VIBRRZ+Inter18pt-Regular
/FontName /HRJUFI+Inter18pt-Regular
/Flags 4
/FontBBox [-742.1875 -323.242187 2579.589844 1109.375]
/ItalicAngle 0
@@ -232,7 +232,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /VIBRRZ+Inter18pt-Regular
/BaseFont /HRJUFI+Inter18pt-Regular
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -247,7 +247,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /VIBRRZ+Inter18pt-Regular
/BaseFont /HRJUFI+Inter18pt-Regular
/Encoding /Identity-H
/DescendantFonts [59 0 R]
/ToUnicode 60 0 R
@@ -256,7 +256,7 @@ endobj
62 0 obj
<<
/Type /FontDescriptor
/FontName /TDKMKH+Inter18pt-Bold
/FontName /XKLDZR+Inter18pt-Bold
/Flags 4
/FontBBox [-790.527344 -334.472656 2580.566406 1114.746094]
/ItalicAngle 0
@@ -272,7 +272,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /TDKMKH+Inter18pt-Bold
/BaseFont /XKLDZR+Inter18pt-Bold
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -287,7 +287,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /TDKMKH+Inter18pt-Bold
/BaseFont /XKLDZR+Inter18pt-Bold
/Encoding /Identity-H
/DescendantFonts [63 0 R]
/ToUnicode 64 0 R
@@ -296,7 +296,7 @@ endobj
66 0 obj
<<
/Type /FontDescriptor
/FontName /JYBWBW+Inter18pt-Italic
/FontName /QHBJWW+Inter18pt-Italic
/Flags 68
/FontBBox [-747.558594 -323.242187 2595.703125 1109.375]
/ItalicAngle -9.398804
@@ -312,7 +312,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /JYBWBW+Inter18pt-Italic
/BaseFont /QHBJWW+Inter18pt-Italic
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -327,7 +327,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /JYBWBW+Inter18pt-Italic
/BaseFont /QHBJWW+Inter18pt-Italic
/Encoding /Identity-H
/DescendantFonts [67 0 R]
/ToUnicode 68 0 R
@@ -336,7 +336,7 @@ endobj
70 0 obj
<<
/Type /FontDescriptor
/FontName /DLRHPN+GeistMono-Regular
/FontName /NBHLIK+GeistMono-Regular
/Flags 5
/FontBBox [-1738 -247 654 1012]
/ItalicAngle 0
@@ -352,7 +352,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /DLRHPN+GeistMono-Regular
/BaseFont /NBHLIK+GeistMono-Regular
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -367,7 +367,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /DLRHPN+GeistMono-Regular
/BaseFont /NBHLIK+GeistMono-Regular
/Encoding /Identity-H
/DescendantFonts [71 0 R]
/ToUnicode 72 0 R
@@ -376,7 +376,7 @@ endobj
74 0 obj
<<
/Type /FontDescriptor
/FontName /LHWXUO+Inter18pt-BoldItalic
/FontName /VMRKYJ+Inter18pt-BoldItalic
/Flags 68
/FontBBox [-795.898437 -334.472656 2596.191406 1114.746094]
/ItalicAngle -9.398804
@@ -392,7 +392,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /LHWXUO+Inter18pt-BoldItalic
/BaseFont /VMRKYJ+Inter18pt-BoldItalic
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -407,7 +407,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /LHWXUO+Inter18pt-BoldItalic
/BaseFont /VMRKYJ+Inter18pt-BoldItalic
/Encoding /Identity-H
/DescendantFonts [75 0 R]
/ToUnicode 76 0 R
@@ -713,30 +713,21 @@ endobj
/Filter /FlateDecode
>>
stream
xœí]ݎ㸱¾ï§ð ´Â?°èdO=6;@.\Ìhfr0}‚Í¢DŠÿ”e¯»·¶×cWY*ŠUÉ*Êö‡/Hý=cõ<63>dx<64>Bx¼Ì¯O?Û?lÀ:êÿÕ?ª¿ä?{ëèBbßu:Œ¾h÷ý—ÿÿßùË_þôûËüË2Gü2ÿßÓïþô ¾üã<75>Û™‰üýϪ÷Ö<65>äÉåÃ×§¿}‡RzÄÔƒª÷…z¨—ø³úgTÏxyFüå"ù0a†8½è“ÈË…Ða"£<>Ø(Ä‹º
9Œ#ãR<18> ¿_>ü÷Ó}P—º^Œ<> ¢$㲟<C2B2>ÛÜŸï#,Ä€&J©\Ž~“>$§ûP¢aDxiéÈ,9ü ûðË-|ÈbLù0ð¶Qüp^“½£™Èħ@ÖNªéI¢ËëÓÈÑ@¤rç¤åo<C3A5>\fÿX…uåªo±ª®˜õµ¬ço/ã³½æoöÙÿw~úŸ§¿^œ“ƒ “dSAF6©á£ÆÃ0<C383>db Î¥T“Ò õ3uz¶ÅþHÆõ¢™¾}yúêů—‡ŠÓJõ¢‡·!gíˆ^åg¥x^—(ó&2ºÍòøòý?mì<6D>AÏ4j G<C2A0>h%°·^C<E28093>ßo"/½qs·ÑsY¦>ó`vºÓÇÝëõÔ˜[Z<>¾óÕDÉŽîw3ˆÝcô<77>åØ-ÏzŠÒ áë4ªäo<C3A4>\fÿXµýo±ª®˜õµ¬ço/ã³½æoöÙÿ¿ºØ¸UŬ&ziÙ­5Ô,@ËR„„]šL„—F"¶«6®ÜÕ$Yp²KÍÒï¹HÌ ³è7 ÓåÏxmÎ+9p2pfð齌ûåYZÃW6°žPpÐÞ<C390>¹ä×ä¥^ãdøI¡C%äþâÒ¦úé~"—?ýôô·¿«ëülà°oëõ´ØÙq*”QÕóa<C3B3>yb-¬Õ<C2AC>ô!/áÔŸ #<03>bc‰ˆì<CB86>½X*„ônSä<53> »4÷Ú3#¯ÐCÑ«ðy>±<¶sŽ£ûÁ³÷•‡¬ïŒ0F \ƒtÓµö€Ûšy-®Fhy+µ–µ€ e"”ïs{Ør+YÛúWŽÿ¶ôEƒ±7'º] ý:Û¶¶O8Ù F<>µ)5fl¹lºSnMýÜZo\}µYØ>«JS»±Å²ù·Ž"WFùí«9*rÅ({¹L2föÒÌœÞe³ïK]¤ê·a)V¾ý.qÒGõø¤³÷ú³ó<C2B3>©rWÿèý<4˜ÍM»Ÿ§[z¹ÈA5g÷ø´ó0K:MW"z—ÇßÖK­»õÙ-?ìÕ.<2E>nënˆ“Òí½¡÷•n¯}TuÅ.SqvœŠgTuÅO·Wg”@øLOfL‰ˆÜ.ݾ*üwìÒÜ«ïÍíeüÂÌ)+ôPô*|žOD,<2C>íœãè~ðì}•K·}#Þ `Œ@¸éPÿZ#{ãtÛÚ±t»6”‰P¾Ïía»:Ý.Ä[ú¢Á˜<19>o(Ýþ5v°õ½¨q £Édœæž‰½eŒ?ÚtV½&Øfžt½<74>be¶Üj&ëq*ád
Ì&¡Ú†>UknIì5L{•1¬ra:0,ô¾ùz!a#™ÌU<C38C>a2MÞ!ß÷|‡µ<E280A1>¸­RDúNÆiK©ïÔ´Ÿîw‡Û1Íÿ¸3eJ„éÞŤ)’¤-”& íq)¨°~ÌËÃÀþóRlmŸœ°Çlï­èW{»Q83ÎäRµé¡°<C2A1>X0¼#8.5ä†IM1“¸ #æMJ2,Ñ…‰_c¢RŒ`)—ZÙÎTÌ»ÃKí„¡B@Ò{jiðŠÜß̈Üí¬|Zæk<1F>zÞ&D<>MtqïÇä³i<C2B3>~‡­<C2AD>ÿ¸Z¦ØÀõ‡Ž¾Cч<C391>¶UJØgW<1B><˜÷>-Ãt¹]tÈ2˜ã¿X[VÖÓñÞÛlØ…h[õyiœá‰<58>œK´<4B>.”¯Kÿ¿•×05£I%­+8­æ[˜¥:åø)ù™$×<C397>Ca<06>jù¥„ªdò£½\fÿXdNEY1/\¹ #F…TŽUÊâ^ŽòììwkÓ§¯‡œÓ|¿óß²¸Ø·õšê[¤ª+f?ìÎŽS¡Œª®˜Ìka­†¤u —ø<ªúǦÑHl¾ÂÙ3ޝ
ý<EFBFBD>àzã{<7B>ç¡ÇvÎqt/tžsw:3&¼ãp С¾µFõ·{Ÿ³´ÜúÕ²æ•cÿžïqÞkÓe¿q j2p½B·|V.·ìð×t©@L‰ÄÉ/.ì³¼ýÊ÷úD<C3BA>º2NÒf/—¤Ù?™SQVÌ ‰¤ÑóBiñ·PG»./­Ã >P1a9^Så5O¸¨ÐÐ~Pdì…çBØó;Ú)wðš0ßr^Ý·õšê{¤ª+f|ÎŽS¡Œª®˜ÃÜka¯†¤ ¯ Ÿ¶@ö™ xŠM&Â’vC?ßžéå*,Ü ¿.Ç);8LÓŠLÖYL Q˜ó‰CÇvÎ Z/Ë^÷³Ò´ÉÄù  <>àPŸÏBÎm«® ¹x=ëY—
rg.pÎÈH7çD¥~µ4¡+Çhƒw”fdç´Î ¬÷©V¶&@æFQz+Yl“Dì÷<>ü+j½%€Ù€&½¯^¯L¶õÅ~Xˆ0183ÿ<E280B9>\fÿX“£1žó·¾M6Õóļ0û]Ù¥ˆÓ:}¹$mŸ¬Pc3®<©½N šd.Ê7´â9{áùùöü‡vʼ&Ì7Íìvm½¦ú©êŠÙŸ³ãT(£ª+æã0wb-ìÕ<C3AC>ôá5áÓÈ>“‰Ød",ÉQaíåãÛ3½\……;á×%¥o#U]±›Þ¢´¿ ª+懂ˆíœ´n8½î#2c2q~ôÈ7š´NBÎmKñ ¹ÄzÖ³.9pîÓïî\à¤á©š<C2A9>Õ•nøUÒ„¾£ ÞQš<C5A1>Ó:'°n\ܧ2 Z}ÜÊäs©2È|ŠóÍW&v}…MôÇwФ*G Ÿ<>\fÿX3<58>L$+æ·¾<C2B7>¦zž˜f¿+~Џô,”KûÌ÷4)'Æö£<tQ¾¡`ˆgì…ççCØó;Ú)wðš0ßt¡Ûµõšê{¤ª+f|ÎŽS¡Œª®˜<C2AE>Ã܉µ°WC҇ׄO »[8aIŽ
k/ßžéå*,Ü ¿.)<29>|©êŠÝô¦ý%U]1Ÿ8hl眠uñèu“‰óë <C3AB><>àФurn[ŠÍ%Ö³žuÉ<75>s—~÷ç'<27> OÕ„¬ö¨tï&ôåMðŽÒŒìœÖ9<C396>uãâN•ɾՇ­LËW&|@rÒ_Èyó•‰]_ˆÝĤTª`[ÖúRîõìŽ2?þIERH¿t«±[ξ˜fÿÂý„<C3BD>¬[¶;¹$­sÕ7Â(š”»ý1½wH¡™ýpÎX Ïϸ;˜ü®†6Ê];Î[.d~K¯qEYœ¼Üù«%eqîï&”Ytvö"oµ€Ï_îòîNbÛÚJG¬mB8×» Ð%<25>#U]±<C2B1>´¼¤ª+æ`-Ãó¯ PÌÊö±6˜8¿åºk¨;{y2n[
ÍÅkLÏzá<7A>o—þö¯Îç ßW5¡©=*½<>+/Ý]«~  £¥?;_uNNݘ¸SU°oõa«åûüÙORQ:No¿*°+ˆœì"•úW7¨ä¶¨ÜË%iö<69>5Y•¤Y1/lk˜¾$äÆË"æ…ÙÔÙž…rIÚf ŽÕ<AôÇýT-ôP¾<50> WK  0çw7´SîÞ5A¾iƶkë5Õ÷HUWÌ>ôœ§BU]1¹ka¯†¤ ­ —Ö»KúŠAI k.Ýž©å*$Ü ½. <0A>\©êŠÝÔ$öEU]1Ÿ8Xl眠õ¢±èt<1F>i‰Óë<C393><C3AB> ¾AàЄunn[^Í%Ö²ž5ÉAÓOºd'<27> ¬ö¨ô¢¯’!teMàŽŒì|Ö9yu£â>IÐêÃV$æÇ–òŸ ¢’,ßɺ¶,Y#“‚³ ëm¹½\fÿXdN•Y1/xKMñœ”{í-«û´pÁž˜Ö™éßÇähd\<5C>©I!Zò„g²<67>ì‡uÆZpvÁíÁ,áuôµ½gׄõ«Ú¾­×T¿#U]1û`svœ
eTuÅ|ÖžX y5$]@Mø´¬Ïx˜ˆJj4XsùèöL&W!áNèuùgèÙPS“ýÉ,Jí³šš<Ÿˆÿ)¶sN¬zAXr¶ôÁøìœGÀ^¨Ãg å¶5ö¾µhÝj^€÷Iu×ý ð4 j A'Ì*kOÞЄá0sÈÍU=3S_ôïTWì}ܲb|Ñ?0MÌÏ™æê 6Œìþ?1{ƒúb]AìÇ ˜`
ÌÁ£ÿN.I³¬É¼ÍŠyÁ-`ÌæoNʽž½^ø9[°'æ…mžä˜$d¿wy&×H<C397>²e¬gçݘó:úÚÞ³kÂzÓ m×Ökªßª®˜}°9;N…2ªºb>k'ÖB^ IP>­ƒÕß…+G%5¬¹|t{&“«<E2809C>p'ôº43ôl¨©Éþd&ðyMMžOÄ?‹íœ«nœíÃ0c0>»ŽóØ[àMS' å¶ô¾µxÝj^€6<îrê¾5þ„Aà4 j A/ÌÊkWÞÐá0sÈÍU=3S_ôïS_ì}Üú‚o[0†ÙÛ¿m±.ÈþXîÊ9…liº—KÒìës¦Ä¼à.$mâæ‰yaö»â'|H®Ã×—KÒ:QŒ ˜(oºB<Žtš.*4´Ù{áù…3…ßáÐN¹ƒ×„ùˆGp\û©êŠÙŸ³ãT(£ª+æÃ0÷ÄZØ«!éÂk§-<2D>}Vƒ Å&aIŽŠ=Sa"¾=ÓËUX¸~]Âù6RÕ»é-ÌçKªºb>q(ðØÎ9Aë…cÙë>"Ó&ç7€>ù
C}> 9·-­ƒæâõ¬g]òÀ¹Ë¹ûs<C3BB>sF†¯jBV{TzáWKºrŒ6xGiFvNëœÀºqq§ÂdßêãV&â]~ *¦q&ítàq[Ÿ÷xÜ$ð¸<01>ð¸<01>ð¸m`Ûš·Ür ÂÌÈË:xÜÌ7<C38C>±¥þê(ˆRå4õ4¸t4õ@á†#Þªc0
·èڀ (Ü2A
·ãÈ
·Ã#(Ü€Âí1Êâ—_ÔCÍ¿dJ¦ÿDÖŠ<ii9€'­fàIž4àIž4àIž4àI;àIž´ƒé?ù¨ŸJéÿH(£²!ý2²H22 #³ <02><19>µ #22 #22 #»iú?Û Ÿþ3.ÅØ<C385>þã0~ã
ã0~ã0~<7E> `ü:Œ|`üƯK½Í®{)õ¦BõƒÕSo ÕŠ% Õ*6! ÕZ- ÕZ- ÕZ- ÕZ­[¦ýŸí®{6í'¸¨«ÃPW¥Â
ÔU@]ÔU@]µÙê* ®ê* ®êªc_•ÅöQøº,C„6ìÚ5”c0j¨TX<54>
¨¡€
¨¡6;@ ÔP@ ¨¡²ÑÿÍSCEù;)ß•!±þ;p0áH&à`¦°¸&à`ºj(p0p0Ó­Kó}×Âw^ÅÀõ'©HªˆÉŽ0Ùú TGþ<>êHÕPÕPÕѶ­ :Ê-wÑ ÌŒ¼¬ó€êÈd{òåÂUmÀ0S¹ùw ï¢*õ“–óÈç?¢Ét<C389>£<EFBFBD>sÑTßþ£Xþ#à?²_Êþ#à?þ£ö¡üGÀüGÀüG·å?z¹0U#,ߺÔ_Ð$/Êà0<C3A0>£À$Y`1P$Ç–hf¤Xf$`F²É0#30#µ`Ff$`FjŠ
0#3Òaf¤— W9¾$Óµ0 Ó€øR<16>Oé²€Ó$ e0&E0&c](€1 “€1©}(c0&c0&cÒm“TY@&øÈmY°Ô
é[£ù÷†Zè“€> è“P`è“€> è“€>éd}Òaä}Ð'=XžÞ~*ÚŒà†ŒX“b X“
AÖ¤€,X“€5 X“€5 X“€5 X“€5馬I/— BÚÏäëÏâìä9÷a<>´¡:¥íWÿ<57>N)V S:% S:¥ÍÐ)<01>Ð)<01>Ð)]E§Ôú…[…]eG_$p+·Ò¸•öøÛ’àVʸ•ºÐër:àV:+àVn%àVŒ¾en¥¦/ɪà#I'ÙB“
4K4K@³´¨€f hŽâ×%y@³4K@³4Kû¨ÍÐ,]E³ý~Nö²ª%A4I»šã^¢ª·oýõ{L hLvV…h"*йÚÇï¥:¨mý#×·úÅlÔ²l[ŽY˜‡éãd¯Vµ?Œk? —ïÿ¹½ÝÁ[wz¹<s”úiíÉC¹õ¤á/ɇI³g˜lè2¾<77>\84ˆz<03>ƒ-üõ¡˜ªÇd´˜r<CB9C>'gÃ>>/”øs²×ø=^Ê£väÉY §&g²*oKŠBØÖ?lƒ@íƒÙøM:&¸Œ3ɘ™³QmêXS4<53>žº¡5èÚÔGû0fn䢡p×ó$ä}þ{ƒó;£c)òWëŸOŸ“E
êF
Ks댃:<3A>Š&(pþ¾¡ §f(Lz “Ê6ËhPÝ^¢ß {No<0F>t<EFBFBD>HPÝöü6bå!}uäýbcdh™>¦ð!ôÏÝr@ÁºbOë<1B> Y—[
ÿ1ªyZ ÷|ªBä=× £š<ήÊ©¹­>ÍKn¾Yþ<59>>Vf~¼#ì½td|/áï¥#â½tD>VGÞaN¾*ý×¥œ6ó§e]4%6ZÖX³/š®¢õœþ ºucòÀ•«&m7<6D>UY£ÊúçÖSûOÕÜZ\rüe9êYåÔÂ$f;Cû1È6ÄU=&[¶ŽÎ¡æ½Uþl_ +g62('*[¼ÍeeÕþÙxóõI,Ë4OîŸY+ĸ¼ü¯ pÍ[þ Vê.ûè.^æh¬í,=Ò'7ÒÃmÙÿ§os2
xœí][<5B>㸱~ï_á?Ð
XôC²'Áž‡ Af439Ÿ`³ÀÉßIQÅ;eÙÛÝ[Û뱫,Ū<C385>deûäÿž±þG2<H…/Óõég÷‡íøBGó¿~ãGý—|ãgïoy]È@Ü»›ã<>ÏÚß}ÿåÿÿwúò—?ýþ2ýò„ì¿Lÿ÷ô»?ý‚/ÿøEŸõãzfâ"ÿÁ³ê½õA[þ#¹`rùðõéoß!„´ ú}¡ú%þ¬ÿõ3žŸ¹H>(̧s:(2
­B¼è«<EFBFBD>Ã82.…ÕÈ— úûåÃ?ý×}©ËÅ9 Á± Z².ûÙºmûó}„…<18>¢”Êùè7éCrº%F„ÕH[|HG6`ɹào؇_îáC.°cʇ<C38A>ȰŽâWçµ1Ù;:EW9€,<2C>ÔÓ“D—ëÓÈÑ@¤v§2ò·@.I“¬ÆºvÕ·XUWLæZó×—ñÉ]ó7÷ìÿ;=ýÏÓ_/›“ƒ J2Š© #Szøèñ0¨(†±àL QJ=) Ò<ÓMoÃ6Ûɸ\43×ò¯/O_½8âå2òPÙ´Rÿ…èámÈYgG7¢ùY+ž—%ʾ‰¬nµü¾|ÿOûcг<C390>¤G ì-—µ¡Îï7ŒÞȸyØÅ˜¹ŽÌSŸ}07Ý™ã¿îõfjÌ-­“RH…|õǦ  Qr£ûÝ̲a÷<>ãc9vóß³™¢ÌbCø2<C3B8>jù[ —¤É?`cÿ[¬ª+&s-ËùëËøÅä®ù›{öÿ¯.6ÛªbW³´ìÖj y)BÂ-M6Âs#ëU[W®Ë<C2AE>nÌ8Ù¥véß¹HÌ è7ÓùÏz­Î+9p2pfñ齌ûåYZÂW6°œPpÐÞ<C390>½äkòRoq²‡Ž?ü¤Ñ¡rÿ?qiSýô‡??‘Ë¿Ÿ~zúÛßõu~¶pØ·u=mŒxÞìl*”QÕÓa<C393>yb-¬Õ<C2AC>ô!/áÔŸ #U±±DDvÈž-Bú°)òA<C3B2><41>»öÌÈ ôPô*|žND,<2C>íœãè~ðì}åá&kÄ;£Œ— Ýu­½'àÖf®ÅÕÍo¥Ö²°¡L„ò}n[n%k[ÿÊñ_—¾h0öæD÷¡_g»Öö 'Äh²Ö ¥ÆŒ #‚©åÖÔÏ­ÍÆÕW·™…ݳ®ô0u[,(rm”ß¿š£"WŒ²—ˆ”1»—f÷àÌ.{_š"Õì¸ j¤Xûö»ÄIõã“~LÞëÏol•»øÇìç¡Ánnºý<ÓÒËEº9·Çgœ‡YÒi¦1»<þ¶^­Ïnùa¯v<C2AF>tÛtCœ”nï ½¯t{é[¤ª+v™ÊfgSñŒª®xãéö⌟ âÉŒ)û¥Û7…ÿA<C3BF><41>»úÞ\_Æ/윲@E¯ÂçéDÄòØÎ9ŽîÏÞW¹tÛ7â<37>ÑÆ„K<E2809E>õ¯5²wN·= K·ë`C™åûÜÓíBü×¥/Œ™ø†Òí_cÛÜ‹*0R6ã´÷LÜ-cüÑ¥³ú5Á.ó¤Ë}'³ùV3YŽÓ 'Ó¨`. 56Ì©æX{KÚf¯aÚ«<C39A>a<EFBFBD> Ó<>aaöÍ— Éd® Ê6ù€|ßó6>â®Jé;xÕšR?,¨i?=îvcÿqgÊêÑŤ-’¤+””ƒö8TØ<¦ùaaÿy.¶ÖON¸cÖ÷ô«»Ý(-œgr®ÚÌPXO,œ rƒÒSŒ÷#aļII†å º0ñkLTÚ!,å\+»™Šywx©0tHzo@/ ^‘û›»<E28098>•Oó|íãÑÌÛ„h#Eg÷~L^1Sƒ$„ðl­ìüÇõ2Ån>tôŠ>ì´®RÂ<52><·Ú˜äÁ¾÷i¦j|änÑ!óT`<60>ÿâl9ÙLÄ{oµá¢ue4ç¥q†°0;9—h;](_þk¯ajG“NZp:Í·0K±LÊñSò3I[B…M<êå—ªGÍ<E28099>örIšüc=eżpã&Œ5R9Ö)Ëö*î•gg¿[>}9¼àœæû<C3A6>·8øžÅž­kªoª®˜ü°ov6ʨêŠé0À<±ÖjHzP—pi€Ï£®ßql*<11>ÄæK!œ=ãø¦Ð?®w¾×yZqlçG÷Bç9w§3cÂ;¾ˆê[kT»÷9ëAË­_-k^9öïùç£6]öº–!7; tÍgå|ËM—
dÀ”HœüâÂ>ËÛ¯|×'ô¥<C3B4>QI˜½\&ÿXdOEY1/$FOÌ ¥ÅßAíV¸¼´ #,ø@…Âr¼60¦ËkžpQ¡¡ý ÈØ Ï/„ °çw8´Sîà-a¾ç¼ºoëšê{¤ª+&|<>M…2ªºb: sO¬…½.¼&|ÚÙg6`L„%9*܆~>¾=ÓËMXx§Xîà09L+2Yg1<67>Da•VL'Û9'h½p,{ÝÏJÓ&ç7€>ùC}> ­ºæâõ¬g]*È<>¹À9##YÜœ•^øÕÒ„®£ ÞQš<C5A1>Ó:'°n\<¦Z}µ5²7ŠÒ[ɺ`S±Üò¯¨õ–fRf_½^™¬ë‹û°ab°pfÿ;¹$Mþ±6Gc<+æo}S.Õóļ0ù]Ù¥ˆjɾ\ÖOVè±<C3A8>מ4^§) Ù„‹ò í‡xÎ^x~><04>=¿Ã¡<C383>ro ó]3»][×Tß#U]1ùàÛìl*”QÕÓq˜ob-ìÕ<C3AC>ôá5áÓÈ>“‰Ød",ÉQáìåãÛ3½Ü„…áwKJ#ßFªºb7½EiAUWL'Û9'hÝp,zÝGdÆdâü:è#<23>¯ 84i<34>„œûâAs‰õ¬g]ÚÀ¹O¿»s<C2BB>“F†§jBV{TºáWIúrŒ&xGiFvNëœÀºqñ˜Ê$hõõV&ŸK•‰@öSœo¾2qëË(\ª`>¾ƒ”®|vrIšücí4¢HVÌ Ûú6
—êyb^˜ü®ø)âܳP.IÛg¾Õ ¤TŒíGyè¢|CÁÏØ Ïχ °çw8´Sîà-a¾ëB·këšê{¤ª+&|<>M…2ªºb:óM¬…½>¼&|ÚÙÝÂY KrT8{ùøöL/7aáAøÝÒÈ·ª®ØMoaÚ_RÕÓ‰C<E280B0>ÆvÎ Z7™1™8¿úä+MZ'!ç¾¥xÐ\b=ëY—6pîÒïþ\à¤á©š<C2A9>Õ•nøUÒ„¾£ ÞQš<C5A1>Ó:'°n\<¨2Ù·új+Âò• <1F> 9o¾2që q˜”JìÊZ_ʽž¶£ì<C2A3>RÒ/·ÕŒ¸-g_Ì “á~BH<>\9ša)ínRhf?œ3ÖÂó3«¡<C2AB>r׎„óž ™ßÒ5îo (‹Ó¯íüE<C3BC>в8uƒwJÁ,:»{·ZÀç/wyw'±íl¥#Ö6!ŒëC¸%<25>#U]±<C2B1>´¼¤ª+¦`-Ãóo PÌÊö±6˜8¿åºK¨;{y2î[
ÍÅkLÏzá<7A>o—þö¯Îç ßW5¡©=*½<>+/Ý]«~  £¥?;_uNNݘxPU°oõÕVó÷ù³Ÿ¤¢tTo¿*p+ˆTnJó«TrWTîå4ùÇÚ¬JÒ¬˜Ö5Ì\ÚÆË,æ…ÉԹž…rIZg Žõ<AÌÇýT-ôP¾<50> WK  0çw7´SîÞ-A¾kƶkëšê{¤ª+&z<7A>M…2ªºb:òM¬…½.´&\Zì.é+%9$œ¹|t{¦<E28093>ð ônihäÚHUW즶 ±/ªêŠéÄ<C3A9>Àb;ç­<17>E§û€L[Lœ^‡|ñ‡&¬“psßò:h.±õ¬I4ý¤û@¸ðTMÀj<C380>J/ú*BWvÑî(ÁÈÎg<C38E>“W7*S­¾ÚŠÄþØRþTTùû£o»"YÖ9K`Di8 ²Ü–ÛË%iò<69>EöT™ó‚·´¹o“r¯½euŸ.#ØóÂ2S0óû˜<1C>Œ1¥4¢%Ox&ÛÈ~Xg¬gÜÌ^G¯í=»%¬÷\Õöm]SýŽTuÅäƒm³³©PFUWL‡a퉵<E280B0>WCÒÔ„OÀúŒ<07>c‰¨¤Fƒ3—<33>nÏdr„Þ-ÿ =jj²?™E©}VS“§ñ¯b;çĪ„%gû0LŒÏnÀyì%ð‡:|Zî[cï[‹Ö­æhÃã>©îZãož¦C­!è„YeíïÉš0f¹¹ªgfêþƒêŠ]£¯·¬Lûs¦¹ú #{üOÌÞ¡¾XV÷1(&˜³"xtàßÉ%iò<69>µ™— Y1/l sùÛ&å^O^/üœ<C3BC>-ØóÂ:OŒrL²ß;ˆ<“k$HÙ2Ö³ónÌy½¶÷ì°Þ5CÛµuMõ;RÕ¶ÍΦBU]1‡õ&ÖB^ IP>­ƒÕß…+G%5œ¹|t{&“<E2809C>ð ônifèÙPS“ýÉ,Làóšš<<3C>ˆÛ9'VÝ ,8Û‡aÆ`|vç°×Àš¦N@Ë} è}kñºÕ¼­xÜåÔ}kü ƒ`Ó4`¨5½0+¯ý]yC †ÃÌ!7WõÌL}ÑL}±oôõÖ¼xÛ1ÌÞþmeá@îÇrÎ)äJÓ½\&ÿXŸ+0%æ…máBÒ%nž˜&¿+~‡ä2|}¹$-ňЀ‰ö¶¸è+ÄãH•J¸¨ÐÐ~dgì…çBÌ~‡C;åÞæGÐ "Áqé{¤ª+&|<>M…2ªºb: sO¬…½.¼&|ÚÙg=ØPl2䍨3&âÛ3½Ü„…áwK8#ßFªºb7½…ù|IUWL'Û9'h½p,{ÝGdÚdâüÐG _@p¨Ïg!ç¾¥uÐ\¼žõ¬K8w9w.pÎÈðUMÈj<C388>J/üjiBWŽÑï(ÍÈÎi<C38E>X7.T˜ì[}½•‰x—¨ŠiÜ”2N·åy?0<>ÇM<02>ð¸<01>ð¸<01>Û
¶µàqË-wÑ ÌŒ¼¬ó€ÇÍ~Ó;Jà¯ñLªœ¦žFM=P¸áH
·Ãa
·€·êÌ<>Â-º6 p
·LЀÂí8r€ÂíðÈ
7 p{åñË<C3B1>/ú¡ç_¢é?1Ÿµ"„OÚAZàI+„xÒ€' xÒ€' xÒ€' xÒGxÒ€'í`úO>êǧRú?ʨlHÿ<48>Œ,’€Œ ÈÈÜ‚dd@FddíCÈÈ€Œ ÈÈ€Œ ÈÈîšþO®ȧÿŒK16¤ÿÀøŒ_Àø…ƒÀøŒ_ÀøŒ_§ ¿#¿€ñë•¥Þv×½”zS¡ûÁê©7ÐjÅÐj ´ZÐj­Ðj­Ðj­Ðj­Ö=ÓþÏn×=›öÜ´áÔUÃPW¥Â
ÔU@]ÔU@]µÚê* ®ê* ®êªc_•ÅîQøº,C„6ìÚ5ÔÆ`ÔP©°5PC5PC­v€
¨¡€*úè PCe£ÿ§†ŠòwRþ¾+$Bbù!và`‘ LÀÁLaqLÀÁtÓP&à`&à`¦{—öû®…*<C2BC>ORT a²ö¨ŽüÕ‘ª# :ª# :ª£lk3@u”[î¢A˜yYçÕÍöäË…ëÚ€a¦sóïfÞ#Duê'çÏD“é Gç#Ußþ£Xþ#à?r_Êþ#à?þ£ö¡üGÀüGÀüG÷å?z¹0]#Ìߺ4_Ð$/Úà FЉU`, °(´3R,30#¹ä˜‘€ ˜‘Ú‡0#30#5E˜‘€é03ÒË…ë_‰éRP5 >—äSº,àƒR1ÑPcR$c0&¹…“€1 “Ú‡0&c0&c0&Ý—1I—t`<>Ü•s­<73>¾E0Úqo¨€> è“€> <06>> è“€> è“NAÐ'F>Ð'}Ò+ËÃÓÛïBGÜ<>qkR,kR!ÈÀš<14>Åk°&k°&k°&k°&Ý•5éå2¢AH÷™|óYœ<59><å>Œcй6”@§´þê?Ð)¥Â
tJ@§tJ@§´Ú:% S:% Sè”Z¿p«±«í˜n%àVº·ÒkÜJY°·Rz·œ¸•Ç
¸•€[ ¸•€[©£o™[©éK²:øHR%[hR<68>f)€f hfÐ,ÍÒQünIÐ,ÍÐ,ÍÒ>*@³4K7Ñ,E¿Ÿ“ý¢¬®FIMҮ渗¨î-Æký“Wá4&;«C¤ˆŽb®öñûG© j[ÿÈ«ë[ýbÖj9¶­<C2B6>Y؇í£rW«ÛÆ¥Ÿ?<3F>Ë÷ÿÜ <0A>Þîàµ;½\ž9J}€Œ´öäU¹õ¤á/ÉeØ3ì¶t_»uHÎD¿<44>ÇÁþæPLõCY-¦\`µÙp<C399>Ï3%þœìµ”î~¿áÜ>…Œ<9EâÔJAå}Z[û‡"¨{0&e€eÆ8“ŒÙ…Уaó“Â4´ ИúèöÀÌ]e4î){žd˜´OÆop±atL"Eþjýó¹|²HAÝHai¢ŸqЧSÑÎß7¤j†2;úâXP:õ-£Aw{Ž~$Üi8½W=ÒA"AYv¯ÚóÛˆµ‡ÌÕ÷<C3B7>¡Adú˜Â‡0¿‰ôëŠ l<e,o4d9|œïo$üǨ!<21>-ÜÐ𨫢÷\´Œzò8»h)× ®¶oÎ…ÂjùúºÊ„ãaï¥#ã{é/ï¥#òuuä¤ä«Ö<C396>k{[¾š×E[ï£y<C2A3>µ´éÞlšÄkY'9&¯¸r5 ò¶³:kÔYâÜqŒš¯"êšÛˆs<0E>¿ÌG=ëœZØDÒî­?ÙÆœ¸ê‡reë¸9Ô¾·ÈŸÝkáäÌ®ª`¥£¢²ÅÛ\VVퟭ7¯ObþŒ¨}Úþ™ŒBŒóÁÿ†<C3BF>×¾åŸàô1»ï¼©¿ÅË­ƒµžeFºÚFz¸=`
endstream
endobj
77 0 obj
@@ -1405,7 +1396,7 @@ trailer
/Size 87
/Root 3 0 R
/Info 52 0 R
/ID [<7800bd1e70bdb9114e48fc6d480ec696> <7800bd1e70bdb9114e48fc6d480ec696>]
/ID [<f859d5aa137a1b00be187261c0c2b77d> <f859d5aa137a1b00be187261c0c2b77d>]
>>
startxref
101711

View File

@@ -688,25 +688,23 @@ test.describe('Doc Editor', () => {
test('it checks interlink feature', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
await verifyDocName(page, randomDoc);
const { name: docChild1 } = await createRootSubPage(
page,
browserName,
'doc-interlink-child-1',
);
await verifyDocName(page, docChild1);
const { name: docChild2 } = await createRootSubPage(
page,
browserName,
'doc-interlink-child-2',
);
await verifyDocName(page, docChild2);
const treeRow = await getTreeRow(page, docChild2);
// To let the time for the emoji-picker to load
await page.waitForTimeout(500);
await treeRow.locator('.--docs--doc-icon').click();
await page.getByRole('button', { name: '😀' }).first().click();

View File

@@ -104,6 +104,9 @@ test.describe('Doc Header', () => {
browserName,
1,
);
await writeInEditor({ page, text: 'Hello Content' });
await page.getByRole('button', { name: 'Share' }).click();
await updateShareLink(page, 'Public', 'Editing');
@@ -116,7 +119,9 @@ test.describe('Doc Header', () => {
docTitle,
});
// Wait for other page to sync
await expect(otherPage.getByText('Hello Content')).toBeVisible();
// Wait for other page to broadcast sync
await page.waitForTimeout(1000);
await page.keyboard.press('Escape');
@@ -124,9 +129,8 @@ test.describe('Doc Header', () => {
await expect(elTitle).toBeVisible();
await elTitle.fill('Hello World');
await elTitle.blur();
await verifyDocName(page, 'Hello World');
// Wait for other page to sync
// Wait for other page to broadcast sync
await page.waitForTimeout(1000);
// Check other user page
@@ -531,7 +535,7 @@ test.describe('Doc Header', () => {
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
);
const uuid = await mockedDocument(page, {
await mockedDocument(page, {
abilities: {
destroy: false, // Means owner
link_configuration: true,
@@ -552,7 +556,6 @@ test.describe('Doc Header', () => {
name: 'Share',
exact: true,
});
await expect(shareButton).toBeVisible();
await shareButton.click();
await page.getByRole('button', { name: 'Copy link' }).click();
@@ -563,8 +566,8 @@ test.describe('Doc Header', () => {
);
const clipboardContent = await handle.jsonValue();
const origin = await page.evaluate(() => window.location.origin);
expect(clipboardContent.trim()).toMatch(`${origin}/docs/${uuid}/`);
const url = page.url();
expect(clipboardContent.trim()).toMatch(url);
});
test('it pins a document', async ({ page, browserName }) => {

View File

@@ -31,6 +31,8 @@ test.describe('Inherited share accesses', () => {
.getByRole('link')
.click();
await page.getByRole('button', { name: 'close' }).first().click();
await verifyDocName(page, parentTitle);
});

View File

@@ -185,23 +185,23 @@ test.describe('Doc Version', () => {
await page.getByLabel('Restore', { exact: true }).click();
await page.waitForTimeout(500);
const mainEditor = page.getByLabel('Document editor');
await expect(editor.getByText('Hello')).toBeVisible();
await expect(editor.getByText('World')).toBeHidden();
await expect(mainEditor.getByText('Hello')).toBeVisible();
await expect(mainEditor.getByText('World')).toBeHidden();
// The old comment is not restored
await expect(editor.getByText('Hello')).toHaveCSS(
await expect(mainEditor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
// We can add a new comment
await editor.getByText('Hello').selectText();
await mainEditor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Add comment' }).click();
await thread.getByRole('paragraph').first().fill('This is a comment');
await thread.locator('[data-test="save"]').click();
await expect(editor.getByText('Hello')).toHaveClass('bn-thread-mark');
await expect(mainEditor.getByText('Hello')).toHaveClass('bn-thread-mark');
});
});

View File

@@ -153,7 +153,8 @@ test.describe('Help feature', () => {
theme_customization: {
onboarding: {
enabled: true,
learn_more_url: 'https://example.com/learn-more',
learn_more_url: 'http://localhost:3000/learn-more',
ready_template_url: 'http://localhost:3000/ready-template',
},
},
});
@@ -184,18 +185,19 @@ test.describe('Help feature', () => {
'0',
);
await page.getByTestId('onboarding-step-3').click();
await expect(page.getByTestId('onboarding-step-3')).toHaveAttribute(
'tabindex',
'0',
);
const step3 = page.getByTestId('onboarding-step-3');
await step3.click();
await expect(step3).toHaveAttribute('tabindex', '0');
await expect(
step3.getByRole('link', { name: 'ready-made template' }),
).toHaveAttribute('href', 'http://localhost:3000/ready-template');
const learnMoreLink = page.getByRole('link', {
name: 'Learn more docs features',
});
await expect(learnMoreLink).toHaveAttribute(
'href',
'https://example.com/learn-more',
'http://localhost:3000/learn-more',
);
await learnMoreLink.click();
@@ -241,6 +243,16 @@ test.describe('Help feature', () => {
await expect(
modal.getByRole('button', { name: /Suivant/i }),
).toBeVisible();
await modal
.getByText(/Tirez parti de la bibliothèque de contenu/)
.first()
.click();
await expect(
modal.getByText(/Commencez à partir de/).first(),
).toBeVisible();
await expect(modal.getByRole('link')).toHaveText(
"modèles prêts à l'emploi",
);
});
test('Modal is displayed automatically on first connection', async ({

View File

@@ -131,42 +131,64 @@ export const createDoc = async (
await openHeaderMenu(page);
}
const responsePromiseCreateDoc = page.waitForResponse(
(response) =>
response.url().includes('/api/v1.0/documents/') &&
response.status() === 201 &&
response.request().method() === 'POST',
);
await page
.getByRole('button', {
name: 'New doc',
})
.click();
await page.waitForURL('**/docs/**', {
timeout: 10000,
waitUntil: 'networkidle',
});
const responseCreateDoc = await responsePromiseCreateDoc;
expect(responseCreateDoc.ok()).toBeTruthy();
const { id: docId } = (await responseCreateDoc.json()) as { id: string };
const responsePromiseUpdateDoc = page.waitForResponse(
(response) =>
response.url().includes(`/api/v1.0/documents/${docId}`) &&
response.status() === 200 &&
response.request().method() === 'PATCH',
);
const input = page.getByLabel('Document title');
await expect(input).toBeVisible({
timeout: 10000,
});
await expect(input).toHaveText('');
await expect(input).toHaveText('', {
timeout: 10000,
});
await input.fill(randomDocs[i]);
await input.blur();
void input.blur();
const responseUpdateDoc = await responsePromiseUpdateDoc;
expect(responseUpdateDoc.ok()).toBeTruthy();
}
return randomDocs;
};
export const verifyDocName = async (page: Page, docName: string) => {
await expect(
page.getByLabel('It is the card information about the document.'),
).toBeVisible({
const card = page.getByLabel(
'It is the card information about the document.',
);
await expect(card).toBeVisible({
timeout: 10000,
});
/*replace toHaveText with toContainText to handle cases where emojis or other characters might be added*/
try {
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toContainText(docName, {
timeout: 3000,
});
} catch {
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
}
await expect(card).toHaveText(new RegExp(docName), {
timeout: 10000,
});
};
export const getGridRow = async (page: Page, title: string) => {
@@ -228,11 +250,9 @@ export const updateDocTitle = async (page: Page, title: string) => {
const input = page.getByRole('textbox', { name: 'Document title' });
await expect(input).toHaveText('');
await expect(input).toBeVisible();
await input.click();
await input.fill(title, {
force: true,
});
await input.click();
await input.blur();
await verifyDocName(page, title);
};
@@ -248,10 +268,11 @@ export const waitForResponseCreateDoc = (page: Page) => {
export const mockedDocument = async (page: Page, data: object) => {
// document/[ID]/ or document/[ID]/tree/ routes
const uuid = crypto.randomUUID();
let uuid: string | undefined;
await page.route(/.*\/documents\/[^/]+\/(?:$|tree\/.*)/, async (route) => {
const request = route.request();
if (request.method().includes('GET') && !request.url().includes('page=')) {
uuid = request.url().match(/\/documents\/([^/]+)\//)?.[1];
const { abilities, ...doc } = data as unknown as {
abilities?: Record<string, unknown>;
};

View File

@@ -15,7 +15,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.58.2",
"@playwright/test": "1.59.1",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"eslint-plugin-docs": "*",
@@ -24,7 +24,7 @@
"dependencies": {
"@types/pngjs": "6.0.5",
"convert-stream": "1.0.2",
"dotenv": "17.3.1",
"dotenv": "17.4.2",
"pdf-parse": "2.4.5",
"pixelmatch": "7.1.0",
"pngjs": "7.0.0"

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

View File

@@ -23,59 +23,59 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@ai-sdk/openai": "3.0.47",
"@blocknote/code-block": "0.47.3",
"@blocknote/core": "0.47.3",
"@blocknote/mantine": "0.47.3",
"@blocknote/react": "0.47.3",
"@blocknote/xl-ai": "0.47.3",
"@blocknote/xl-docx-exporter": "0.47.3",
"@blocknote/xl-multi-column": "0.47.3",
"@blocknote/xl-odt-exporter": "0.47.3",
"@blocknote/xl-pdf-exporter": "0.47.3",
"@ai-sdk/openai": "3.0.53",
"@blocknote/code-block": "0.49.0",
"@blocknote/core": "0.49.0",
"@blocknote/mantine": "0.49.0",
"@blocknote/react": "0.49.0",
"@blocknote/xl-ai": "0.49.0",
"@blocknote/xl-docx-exporter": "0.49.0",
"@blocknote/xl-multi-column": "0.49.0",
"@blocknote/xl-odt-exporter": "0.49.0",
"@blocknote/xl-pdf-exporter": "0.49.0",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@emoji-mart/data": "1.2.1",
"@emoji-mart/react": "1.1.1",
"@fontsource-variable/inter": "5.2.8",
"@fontsource-variable/material-symbols-outlined": "5.2.38",
"@fontsource-variable/material-symbols-outlined": "5.2.42",
"@fontsource/material-icons": "5.2.7",
"@gouvfr-lasuite/cunningham-react": "4.2.0",
"@gouvfr-lasuite/cunningham-react": "4.3.0",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.19.10",
"@gouvfr-lasuite/ui-kit": "0.20.1",
"@hocuspocus/provider": "3.4.4",
"@mantine/core": "8.3.18",
"@mantine/hooks": "8.3.18",
"@react-aria/live-announcer": "3.4.4",
"@mantine/core": "9.0.2",
"@mantine/hooks": "9.0.2",
"@react-aria/live-announcer": "3.5.0",
"@react-pdf/renderer": "4.3.1",
"@sentry/nextjs": "10.45.0",
"@tanstack/react-query": "5.95.0",
"@sentry/nextjs": "10.49.0",
"@tanstack/react-query": "5.99.2",
"@tiptap/extensions": "*",
"ai": "6.0.134",
"ai": "6.0.168",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"crisp-sdk-web": "1.0.27",
"crisp-sdk-web": "1.1.1",
"emoji-datasource-apple": "16.0.0",
"emoji-mart": "5.6.0",
"emoji-regex": "10.6.0",
"i18next": "25.10.4",
"i18next": "26.0.6",
"i18next-browser-languagedetector": "8.2.1",
"idb": "8.0.3",
"lodash": "4.18.1",
"luxon": "3.7.2",
"next": "16.2.3",
"posthog-js": "1.363.1",
"next": "16.2.4",
"posthog-js": "1.369.4",
"react": "*",
"react-aria-components": "1.16.0",
"react-aria-components": "1.17.0",
"react-dom": "*",
"react-dropzone": "15.0.0",
"react-i18next": "16.6.1",
"react-i18next": "17.0.4",
"react-intersection-observer": "10.0.3",
"react-resizable-panels": "3.0.6",
"react-select": "5.10.2",
"styled-components": "6.3.12",
"use-debounce": "10.1.0",
"styled-components": "6.4.0",
"use-debounce": "10.1.1",
"uuid": "14.0.0",
"y-protocols": "1.0.7",
"yjs": "*",
@@ -84,7 +84,7 @@
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.95.0",
"@tanstack/react-query-devtools": "5.99.2",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
@@ -96,18 +96,18 @@
"@types/react-dom": "*",
"@vitejs/plugin-react": "6.0.1",
"cross-env": "10.1.0",
"dotenv": "17.3.1",
"dotenv": "17.4.2",
"eslint-plugin-docs": "*",
"fetch-mock": "9.11.0",
"jsdom": "29.0.1",
"jsdom": "29.0.2",
"node-fetch": "2.7.0",
"prettier": "3.8.1",
"prettier": "3.8.3",
"stylelint": "16.26.1",
"stylelint-config-standard": "39.0.1",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"vitest": "4.1.0",
"webpack": "5.105.4",
"vitest": "4.1.4",
"webpack": "5.106.2",
"workbox-webpack-plugin": "7.1.0"
},
"packageManager": "yarn@1.22.22"

View File

@@ -1,12 +1,13 @@
import { Ref, forwardRef } from 'react';
import { ComponentPropsWithRef, Ref, forwardRef } from 'react';
import { css } from 'styled-components';
import { Box, BoxType } from './Box';
import { Box, BoxProps } from './Box';
export type BoxButtonType = Omit<BoxType, 'ref'> & {
disabled?: boolean;
ref?: Ref<HTMLButtonElement>;
};
export type BoxButtonType = BoxProps &
Omit<ComponentPropsWithRef<'button'>, keyof BoxProps | 'ref'> & {
disabled?: boolean;
ref?: Ref<HTMLButtonElement>;
};
/**
* Styleless button that extends the Box component.
@@ -59,7 +60,7 @@ const BoxButton = forwardRef<HTMLButtonElement, BoxButtonType>(
if (disabled) {
return;
}
props.onClick?.(event as unknown as React.MouseEvent<HTMLDivElement>);
props.onClick?.(event);
}}
/>
);

View File

@@ -34,9 +34,7 @@ export const TextStyled = styled(Box)<TextProps>`
const Text = forwardRef<HTMLElement, ComponentPropsWithRef<typeof TextStyled>>(
(props, ref) => {
return (
<TextStyled ref={ref as React.Ref<HTMLDivElement>} as="span" {...props} />
);
return <TextStyled ref={ref} as="span" {...props} />;
},
);

View File

@@ -2,7 +2,7 @@ import {
Button,
ButtonProps,
Modal,
ModalProps,
ModalDefaultVariantProps,
ModalSize,
} from '@gouvfr-lasuite/cunningham-react';
import { ReactNode, useEffect } from 'react';
@@ -20,7 +20,7 @@ export type AlertModalProps = {
title: string;
cancelLabel?: string;
confirmLabel?: string;
} & Partial<ModalProps>;
} & Partial<ModalDefaultVariantProps>;
export const AlertModal = ({
cancelLabel,

View File

@@ -49,7 +49,7 @@ export const SideModal = ({
return (
<>
<SideModalStyle width={width} side={side} $css={$css} />
<Modal {...modalProps} size={ModalSize.FULL}>
<Modal {...modalProps} size={ModalSize.FULL} variant="default">
{children}
</Modal>
</>

View File

@@ -28,6 +28,7 @@ interface ThemeCustomization {
onboarding?: {
enabled: true;
learn_more_url?: string;
ready_template_url?: string;
};
translations?: Resource;
waffle?: WaffleType;

View File

@@ -70,6 +70,7 @@
.c__modal__title {
padding: 0;
display: block;
}
.c__modal__footer {

View File

@@ -361,7 +361,7 @@
--c--globals--font--weights--medium: 500;
--c--globals--font--weights--bold: 600;
--c--globals--font--weights--extrabold: 800;
--c--globals--font--weights--black: 900;
--c--globals--font--weights--black: 800;
--c--globals--font--families--base:
inter variable, roboto flex variable, sans-serif;
--c--globals--font--families--accent:
@@ -849,6 +849,18 @@
--c--components--forms-checkbox--font-size: var(
--c--globals--font--sizes--sm
);
--c--components--forms-input--border-radius: 4px;
--c--components--forms-input--border-radius--hover: 4px;
--c--components--forms-input--border-radius--focus: 4px;
--c--components--forms-select--border-radius: 4px;
--c--components--forms-select--border-radius--hover: 4px;
--c--components--forms-select--border-radius--focus: 4px;
--c--components--forms-textarea--border-radius: 4px;
--c--components--forms-textarea--border-radius--hover: 4px;
--c--components--forms-textarea--border-radius--focus: 4px;
--c--components--forms-datepicker--border-radius: 4px;
--c--components--forms-datepicker--border-radius--hover: 4px;
--c--components--forms-datepicker--border-radius--focus: 4px;
--c--components--badge--font-size: var(--c--globals--font--sizes--xs);
--c--components--badge--border-radius: 12px;
--c--components--badge--padding-inline: var(--c--globals--spacings--xs);
@@ -1731,7 +1743,6 @@
--c--globals--font--sizes--xs-alt: 3rem;
--c--globals--font--weights--thin: 100;
--c--globals--font--weights--extrabold: 800;
--c--globals--font--weights--black: 900;
--c--globals--font--families--accent:
marianne, inter variable, roboto flex variable, sans-serif;
--c--globals--font--families--base:
@@ -2539,6 +2550,18 @@
--c--components--forms-checkbox--font-size: var(
--c--globals--font--sizes--sm
);
--c--components--forms-input--border-radius: 4px;
--c--components--forms-input--border-radius--hover: 4px;
--c--components--forms-input--border-radius--focus: 4px;
--c--components--forms-select--border-radius: 4px;
--c--components--forms-select--border-radius--hover: 4px;
--c--components--forms-select--border-radius--focus: 4px;
--c--components--forms-textarea--border-radius: 4px;
--c--components--forms-textarea--border-radius--hover: 4px;
--c--components--forms-textarea--border-radius--focus: 4px;
--c--components--forms-datepicker--border-radius: 4px;
--c--components--forms-datepicker--border-radius--hover: 4px;
--c--components--forms-datepicker--border-radius--focus: 4px;
--c--components--badge--font-size: var(--c--globals--font--sizes--xs);
--c--components--badge--border-radius: 12px;
--c--components--badge--padding-inline: var(--c--globals--spacings--xs);

View File

@@ -372,7 +372,7 @@ export const tokens = {
medium: 500,
bold: 600,
extrabold: 800,
black: 900,
black: 800,
},
families: {
base: 'Inter Variable, Roboto Flex Variable, sans-serif',
@@ -664,6 +664,26 @@ export const tokens = {
'body--background-color-hover': '#F0F0F3',
},
'forms-checkbox': { 'font-size': '0.875rem' },
'forms-input': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-select': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-textarea': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-datepicker': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
badge: {
'font-size': '0.75rem',
'border-radius': '12px',
@@ -1334,7 +1354,7 @@ export const tokens = {
'sm-alt': '3.5rem',
'xs-alt': '3rem',
},
weights: { thin: 100, extrabold: 800, black: 900 },
weights: { thin: 100, extrabold: 800 },
families: {
accent:
'Marianne, Inter Variable, Roboto Flex Variable, sans-serif',
@@ -1948,6 +1968,26 @@ export const tokens = {
'body--background-color-hover': '#F0F0F3',
},
'forms-checkbox': { 'font-size': '0.875rem' },
'forms-input': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-select': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-textarea': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-datepicker': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
badge: {
'font-size': '0.75rem',
'border-radius': '12px',

View File

@@ -35,7 +35,7 @@ const initialState: ThemeStore = {
colorsTokens: defaultTokens.globals.colors,
componentTokens: defaultTokens.components,
contextualTokens: defaultTokens.contextuals,
currentTokens: tokens.themes[DEFAULT_THEME] as Partial<Tokens>,
currentTokens: tokens.themes[DEFAULT_THEME],
fontSizesTokens: defaultTokens.globals.font.sizes,
setTheme: () => {},
spacingsTokens: defaultTokens.globals.spacings,

View File

@@ -1,12 +1,13 @@
import React from 'react';
import React, { ComponentPropsWithRef } from 'react';
import { Box, BoxType } from '@/components';
import { Box, BoxProps } from '@/components';
type AvatarSvgProps = {
initials: string;
background: string;
fontFamily?: string;
} & BoxType;
type AvatarSvgProps = BoxProps &
Omit<ComponentPropsWithRef<'svg'>, keyof BoxProps> & {
initials: string;
background: string;
fontFamily?: string;
};
export const AvatarSvg: React.FC<AvatarSvgProps> = ({
initials,

View File

@@ -11,11 +11,15 @@ vi.mock('@/stores', () => ({
useResponsiveStore: () => ({ isDesktop: false }),
}));
vi.mock('@/features/skeletons', () => ({
useSkeletonStore: () => ({
setIsSkeletonVisible: vi.fn(),
}),
}));
vi.mock('@/features/skeletons', async () => {
const actual = await vi.importActual<any>('../../../skeletons');
return {
...actual,
useSkeletonStore: () => ({
setIsSkeletonVisible: vi.fn(),
}),
};
});
vi.mock('../../doc-management', async () => {
const actual = await vi.importActual<any>('../../doc-management');

View File

@@ -0,0 +1,33 @@
import { sanitizeColor } from '../utils';
const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
describe('sanitizeColor', () => {
it('accepts valid 6-digit hex colors', () => {
expect(sanitizeColor('#1a2b3c')).toBe('#1a2b3c');
expect(sanitizeColor('#AABBCC')).toBe('#AABBCC');
expect(sanitizeColor('#000000')).toBe('#000000');
expect(sanitizeColor('#ffffff')).toBe('#ffffff');
});
it('rejects 3-digit hex colors and returns a valid random hex color', () => {
expect(sanitizeColor('#abc')).toMatch(HEX_COLOR_RE);
});
it('rejects named colors and returns a valid random hex color', () => {
expect(sanitizeColor('red')).toMatch(HEX_COLOR_RE);
expect(sanitizeColor('blue')).toMatch(HEX_COLOR_RE);
});
it('rejects CSS injection attempts and returns a valid random hex color', () => {
expect(sanitizeColor('red; behavior: expression(alert(1))')).toMatch(
HEX_COLOR_RE,
);
expect(sanitizeColor('#fff; color: red')).toMatch(HEX_COLOR_RE);
expect(sanitizeColor('javascript:alert(1)')).toMatch(HEX_COLOR_RE);
});
it('rejects empty string and returns a valid random hex color', () => {
expect(sanitizeColor('')).toMatch(HEX_COLOR_RE);
});
});

View File

@@ -15,7 +15,6 @@ import { useCreateBlockNote } from '@blocknote/react';
import { HocuspocusProvider } from '@hocuspocus/provider';
import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import type { Awareness } from 'y-protocols/awareness';
import * as Y from 'yjs';
@@ -35,14 +34,14 @@ import {
useUploadStatus,
} from '../hook';
import { useEditorStore } from '../stores';
import { cssEditor } from '../styles';
import { DocsEditorStyle } from '../styles';
import { DocsBlockNoteEditor } from '../types';
import { randomColor } from '../utils';
import { randomColor, sanitizeColor } from '../utils';
import BlockNoteAI from './AI';
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
import { cssComments, useComments } from './comments/';
import { DocsCommentsStyle, useComments } from './comments/';
import {
AccessibleImageBlock,
CalloutBlock,
@@ -153,12 +152,13 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
*/
renderCursor: (user: { color: string; name: string }) => {
const cursorElement = document.createElement('span');
const safeColor = sanitizeColor(user.color);
cursorElement.classList.add('collaboration-cursor-custom__base');
const caretElement = document.createElement('span');
caretElement.classList.add('collaboration-cursor-custom__caret');
caretElement.setAttribute('spellcheck', `false`);
caretElement.setAttribute('style', `background-color: ${user.color}`);
caretElement.setAttribute('style', `background-color: ${safeColor}`);
if (showCursorLabels === 'always') {
cursorElement.setAttribute('data-active', '');
@@ -170,7 +170,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
labelElement.setAttribute('spellcheck', `false`);
labelElement.setAttribute(
'style',
`background-color: ${user.color};border: 1px solid ${user.color};`,
`background-color: ${safeColor};border: 1px solid ${safeColor};`,
);
labelElement.insertBefore(document.createTextNode(user.name), null);
@@ -260,13 +260,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
}, [setEditor, editor]);
return (
<Box
ref={refEditorContainer}
$css={css`
${cssEditor};
${cssComments(showComments, currentUserAvatarUrl)}
`}
>
<Box ref={refEditorContainer} $height="100%">
<DocsEditorStyle />
<DocsCommentsStyle
canSeeComment={canSeeComment}
currentUserAvatarUrl={currentUserAvatarUrl}
/>
{errorAttachment && (
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
<TextErrors
@@ -350,12 +349,9 @@ export const BlockNoteReader = ({
useHeadings(editor);
return (
<Box
$css={css`
${cssEditor};
${cssComments(false)}
`}
>
<Box>
<DocsEditorStyle />
<DocsCommentsStyle canSeeComment={false} />
<BlockNoteView
className="--docs--main-editor"
editor={editor}

View File

@@ -1,37 +1,41 @@
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { PropsWithChildren, useEffect, useState } from 'react';
import { css } from 'styled-components';
import { Box, Loading } from '@/components';
import { Box } from '@/components';
import { DocHeader } from '@/docs/doc-header/';
import {
Doc,
LinkReach,
getDocLinkReach,
useCollaboration,
useIsCollaborativeEditable,
useProviderStore,
} from '@/docs/doc-management';
import { TableContent } from '@/docs/doc-table-content/';
import { useAuth } from '@/features/auth/';
import { useSkeletonStore } from '@/features/skeletons';
import { SkeletonEditorCore, useSkeletonStore } from '@/features/skeletons';
import { useSkeletonFadeOut } from '@/features/skeletons/hooks/useFadeOut';
import { useAnalytics } from '@/libs';
import { useResponsiveStore } from '@/stores';
import { useCollaboration } from '../hook/useCollaboration';
import { BlockNoteEditor, BlockNoteReader } from './BlockNoteEditor';
const DOCS_EDITOR_CLASS = '--docs--doc-editor';
interface DocEditorContainerProps {
docHeader: React.ReactNode;
docEditor: React.ReactNode;
isDeletedDoc: boolean;
readOnly: boolean;
}
export const DocEditorContainer = ({
children,
docHeader,
docEditor,
isDeletedDoc,
readOnly,
}: DocEditorContainerProps) => {
}: PropsWithChildren<DocEditorContainerProps>) => {
const { isDesktop } = useResponsiveStore();
return (
@@ -39,8 +43,8 @@ export const DocEditorContainer = ({
<Box
$maxWidth="868px"
$width="100%"
$height="100%"
className="--docs--doc-editor"
$flex="1"
className={DOCS_EDITOR_CLASS}
>
<Box
$padding={{ horizontal: isDesktop ? '54px' : 'base' }}
@@ -66,7 +70,7 @@ export const DocEditorContainer = ({
})}
$height="100%"
>
{docEditor}
{children}
</Box>
</Box>
</Box>
@@ -82,23 +86,19 @@ interface DocEditorProps {
export const DocEditor = ({ doc }: DocEditorProps) => {
useCollaboration(doc.id);
const { isDesktop } = useResponsiveStore();
const { provider, isReady } = useProviderStore();
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
const isDeletedDoc = !!doc.deleted_at;
const readOnly =
!doc.abilities.partial_update || !isEditable || isLoading || isDeletedDoc;
const { setIsSkeletonVisible } = useSkeletonStore();
const isProviderReady = isReady && provider;
const { trackEvent } = useAnalytics();
const [hasTracked, setHasTracked] = useState(false);
const { authenticated } = useAuth();
const isPublicDoc = getDocLinkReach(doc) === LinkReach.PUBLIC;
const { setIsSkeletonVisible } = useSkeletonStore();
useEffect(() => {
if (isProviderReady) {
setIsSkeletonVisible(false);
}
}, [isProviderReady, setIsSkeletonVisible]);
setIsSkeletonVisible(false);
}, [setIsSkeletonVisible, doc.id]);
/**
* Track doc view event only once per doc change
@@ -124,30 +124,56 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
});
}, [authenticated, hasTracked, isPublicDoc, trackEvent]);
if (!isProviderReady || provider?.configuration.name !== doc.id) {
return <Loading />;
}
return (
<>
{isDesktop && <TableContent />}
{isDesktop && <TableContent selector={`.${DOCS_EDITOR_CLASS}`} />}
<DocEditorContainer
docHeader={<DocHeader doc={doc} />}
docEditor={
readOnly ? (
<BlockNoteReader
initialContent={provider.document.getXmlFragment(
'document-store',
)}
docId={doc.id}
/>
) : (
<BlockNoteEditor doc={doc} provider={provider} />
)
}
isDeletedDoc={isDeletedDoc}
readOnly={readOnly}
/>
>
<DocCoreEditor doc={doc} readOnly={readOnly} />
</DocEditorContainer>
</>
);
};
interface DocCoreEditorProps {
doc: Doc;
readOnly: boolean;
}
export const DocCoreEditor = ({ doc, readOnly }: DocCoreEditorProps) => {
const { provider, isReady } = useProviderStore();
const isProviderReady = isReady && provider;
const showContent = !!(
isProviderReady && provider?.configuration.name === doc.id
);
const { skeletonVisible, isFadingOut } = useSkeletonFadeOut(showContent);
if (
skeletonVisible ||
!isProviderReady ||
provider?.configuration.name !== doc.id
) {
return (
<SkeletonEditorCore
isFadingOut={isFadingOut}
$css={css`
padding-top: 0px;
`}
/>
);
}
if (readOnly) {
return (
<BlockNoteReader
initialContent={provider.document.getXmlFragment('document-store')}
docId={doc.id}
/>
);
}
return <BlockNoteEditor doc={doc} provider={provider} />;
};

View File

@@ -1,5 +1,6 @@
import { CommentBody, ThreadStore } from '@blocknote/core/comments';
import type { Awareness } from 'y-protocols/awareness';
import * as Y from 'yjs';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Doc } from '@/features/docs/doc-management';
@@ -17,6 +18,13 @@ import {
type ServerThreadListResponse = ServerThread[];
/**
* notifySubscribers generate a transaction, to distinguish
* the origin of the update, we use a specific origin "commentMarkUpdate"
* for the updates coming from the comment mark changes.
*/
export const COMMENT_UPDATE_ORIGIN = 'commentMarkUpdate';
export class DocsThreadStore extends ThreadStore {
protected static COMMENTS_PING = 'commentsPing';
protected threads: Map<string, ClientThreadData> = new Map();
@@ -24,6 +32,7 @@ export class DocsThreadStore extends ThreadStore {
(threads: Map<string, ClientThreadData>) => void
>();
private awareness?: Awareness;
private yDoc?: Y.Doc;
private lastPingAt = 0;
private pingTimer?: ReturnType<typeof setTimeout>;
@@ -31,11 +40,13 @@ export class DocsThreadStore extends ThreadStore {
protected docId: Doc['id'],
awareness: Awareness | undefined,
protected docAuth: DocsThreadStoreAuth,
yDoc?: Y.Doc,
) {
super(docAuth);
if (docAuth.canSee) {
this.awareness = awareness;
this.yDoc = yDoc;
this.awareness?.on('update', this.onAwarenessUpdate);
this.refreshThreads();
@@ -67,7 +78,7 @@ export class DocsThreadStore extends ThreadStore {
continue;
}
const state = states.get(clientId) as
const state:
| {
[DocsThreadStore.COMMENTS_PING]?: {
at: number;
@@ -76,7 +87,7 @@ export class DocsThreadStore extends ThreadStore {
threadId: string;
};
}
| undefined;
| undefined = states.get(clientId);
const ping = state?.commentsPing;
@@ -134,18 +145,30 @@ export class DocsThreadStore extends ThreadStore {
}
/**
* Notifies all subscribers about the current thread state
* Notifies all subscribers about the current thread state.
* We trigger the transaction with a specific origin so we will be able
* to flag that the update comes from a comment update.
* The inner ydoc.transact calls from y-prosemirror will see there's already
* an active transaction and reuse it.
*/
private notifySubscribers() {
// Always emit a new Map reference to help consumers detect changes
const threads = new Map(this.threads);
this.subscribers.forEach((cb) => {
try {
cb(threads);
} catch (e) {
console.warn('DocsThreadStore subscriber threw', e);
}
});
const notify = () => {
this.subscribers.forEach((cb) => {
try {
cb(threads);
} catch (e) {
console.warn('DocsThreadStore subscriber threw', e);
}
});
};
if (this.yDoc) {
this.yDoc.transact(notify, COMMENT_UPDATE_ORIGIN);
} else {
notify();
}
}
private upsertClientThreadData(thread: ClientThreadData) {

View File

@@ -1,11 +1,11 @@
import { css } from 'styled-components';
import { createGlobalStyle, css } from 'styled-components';
export const cssComments = (
canSeeComment: boolean,
currentUserAvatarUrl?: string,
) => css`
& .--docs--main-editor,
& .--docs--main-editor .ProseMirror {
export const DocsCommentsStyle = createGlobalStyle<{
canSeeComment: boolean;
currentUserAvatarUrl?: string;
}>`
.--docs--main-editor.bn-root,
.--docs--main-editor.bn-root .ProseMirror {
// Comments marks in the editor
.bn-editor {
// Resets blocknote comments styles
@@ -14,30 +14,31 @@ export const cssComments = (
background-color: transparent;
}
${canSeeComment &&
css`
.bn-thread-mark:not([data-orphan='true']) {
background-color: color-mix(
in srgb,
var(--c--contextuals--background--palette--yellow--tertiary) 40%,
transparent
);
border-bottom: 2px solid
var(--c--contextuals--background--palette--yellow--secondary);
mix-blend-mode: multiply;
transition:
background-color var(--c--globals--transitions--duration),
border-bottom-color var(--c--globals--transitions--duration);
&:has(.bn-thread-mark-selected) {
background-color: var(
--c--contextuals--background--palette--yellow--tertiary
${({ canSeeComment }) =>
canSeeComment &&
css`
.bn-thread-mark:not([data-orphan='true']) {
background-color: color-mix(
in srgb,
var(--c--contextuals--background--palette--yellow--tertiary) 40%,
transparent
);
border-bottom: 2px solid
var(--c--contextuals--background--palette--yellow--secondary);
mix-blend-mode: multiply;
transition:
background-color var(--c--globals--transitions--duration),
border-bottom-color var(--c--globals--transitions--duration);
&:has(.bn-thread-mark-selected) {
background-color: var(
--c--contextuals--background--palette--yellow--tertiary
);
}
}
}
`}
`}
[data-show-selection] {
color: HighlightText;
@@ -82,6 +83,8 @@ export const cssComments = (
.bn-thread-comment {
padding: 8px;
flex-wrap: nowrap;
gap: 0px;
& .bn-editor {
padding-left: var(--c--globals--spacings--lg);
@@ -105,10 +108,14 @@ export const cssComments = (
// Top bar (Name / Date / Actions) when actions displayed
&:has(.bn-comment-actions) {
& > .mantine-Group-root {
max-width: 70%;
& > .mantine-Group-root:first-child {
right: 0.3rem !important;
top: 0.3rem !important;
background: linear-gradient(
to left,
#fff 90%,
rgba(255, 255, 255, 0) 100%
);
}
.bn-menu-dropdown {
@@ -124,7 +131,6 @@ export const cssComments = (
// Date
span.mantine-focus-auto {
display: inline-block;
}
.bn-comment-actions {
@@ -150,7 +156,8 @@ export const cssComments = (
}
// Actions button edit comment
.bn-container + .bn-comment-actions-wrapper {
.bn-root + .bn-comment-actions-wrapper {
margin-top: var(--c--globals--spacings--2xs);
.bn-comment-actions {
flex-direction: row-reverse;
background: none;
@@ -201,9 +208,8 @@ export const cssComments = (
width: 26px;
height: 26px;
flex: 0 0 26px;
background-image: ${currentUserAvatarUrl
? `url("${currentUserAvatarUrl}")`
: 'none'};
background-image: ${({ currentUserAvatarUrl }) =>
currentUserAvatarUrl ? `url("${currentUserAvatarUrl}")` : 'none'};
background-position: center;
background-repeat: no-repeat;
background-size: cover;

View File

@@ -27,8 +27,15 @@ export function useComments(
encodeURIComponent(user?.full_name || ''),
canComment,
),
provider?.document,
);
}, [docId, canComment, provider?.awareness, user?.full_name]);
}, [
docId,
canComment,
provider?.awareness,
provider?.document,
user?.full_name,
]);
useEffect(() => {
if (canComment) {

View File

@@ -41,7 +41,7 @@ export const LinkSelected = ({
const router = useRouter();
const href = `/docs/${docId}/`;
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
// If ctrl or command is pressed, it opens a new tab. If shift is pressed, it opens a new window
@@ -53,7 +53,7 @@ export const LinkSelected = ({
};
// This triggers on middle-mouse click
const handleAuxClick = (e: React.MouseEvent<HTMLDivElement>) => {
const handleAuxClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (e.button !== 1) {
return;
}
@@ -66,6 +66,7 @@ export const LinkSelected = ({
<BoxButton
as="span"
className="--docs--interlinking-link-inline-content"
data-href={href}
onClick={handleClick}
onAuxClick={handleAuxClick}
draggable="false"

View File

@@ -39,7 +39,13 @@ const withMultiColumnNoDropHandler = <
});
};
let modulesXL = undefined;
type ModulesXL =
| (Omit<typeof XLMultiColumn, 'withMultiColumn'> & {
withMultiColumn: typeof withMultiColumnNoDropHandler;
})
| undefined;
let modulesXL: ModulesXL = undefined;
if (process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false') {
modulesXL = {
...XLMultiColumn,
@@ -47,10 +53,4 @@ if (process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false') {
};
}
type ModulesXL =
| (Omit<typeof XLMultiColumn, 'withMultiColumn'> & {
withMultiColumn: typeof withMultiColumnNoDropHandler;
})
| undefined;
export default modulesXL as ModulesXL;
export default modulesXL;

View File

@@ -2,6 +2,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { useCollaborationUrl } from '@/core/config';
import { KEY_DOC } from '@/docs/doc-management/api/useDoc';
import {
KEY_DOC_CONTENT,
useDocContent,
@@ -10,13 +11,15 @@ import { useProviderStore } from '@/docs/doc-management/stores/useProviderStore'
import { useIsOffline } from '@/features/service-worker/hooks/useOffline';
import { useBroadcastStore } from '@/stores/useBroadcastStore';
import { KEY_DOC } from '../api';
export const useCollaboration = (room: string) => {
const collaborationUrl = useCollaborationUrl(room);
const { addTask } = useBroadcastStore();
const queryClient = useQueryClient();
const { setBroadcastProvider, cleanupBroadcast } = useBroadcastStore();
const {
setBroadcastProvider,
cleanupBroadcast,
provider: broadcastProvider,
} = useBroadcastStore();
const {
provider,
createProvider,
@@ -65,7 +68,7 @@ export const useCollaboration = (room: string) => {
* when the document visibility changes.
*/
useEffect(() => {
if (!room || !isReady) {
if (!room || broadcastProvider?.document?.guid !== room) {
return;
}
@@ -74,7 +77,7 @@ export const useCollaboration = (room: string) => {
queryKey: [KEY_DOC, { id: room }],
});
});
}, [addTask, room, queryClient, isReady]);
}, [addTask, room, queryClient, broadcastProvider?.document?.guid]);
/**
* Set the provider when the collaboration URL and the document content are available.

View File

@@ -2,6 +2,7 @@ import { useRouter } from 'next/router';
import { useCallback, useEffect, useRef, useState } from 'react';
import * as Y from 'yjs';
import { COMMENT_UPDATE_ORIGIN } from '@/docs/doc-editor/components/comments/DocsThreadStore';
import { useDocContentUpdate } from '@/docs/doc-management/api/useDocContentUpdate';
import { useProviderStore } from '@/docs/doc-management/stores/useProviderStore';
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning/api/useDocVersions';
@@ -65,6 +66,16 @@ export const useSaveDoc = (docId: string, yDoc: Y.Doc) => {
const isAIChange =
!transaction.local && transactionOrigin !== PROVIDER_ORIGIN_CONSTRUCTOR;
/**
* notifySubscribers generate a transaction that can be
* interpreted as a local change.
* We intercept the update with this origin to
* avoid marking the change as local.
*/
if (transaction.origin === COMMENT_UPDATE_ORIGIN) {
return;
}
setIsLocalChange(transaction.local || isAIChange);
};

View File

@@ -1,266 +1,306 @@
import { css } from 'styled-components';
import { createGlobalStyle } from 'styled-components';
export const cssEditor = css`
.mantine-Menu-itemLabel,
.mantine-Button-label {
font-family: var(--c--components--button--font-family);
}
&,
& > .bn-container,
& .ProseMirror {
export const DocsEditorStyle = createGlobalStyle`
.bn-root {
height: 100%;
}
/**
* Token Mantime
*/
& > .bn-container {
.bn-editor {
height: 100%;
}
.mantine-Menu-itemLabel,
.mantine-Button-label {
font-family: var(--c--components--button--font-family);
}
/**
* Token Mantine
*/
--bn-colors-editor-text: var(
--c--contextuals--content--semantic--neutral--primary
);
--bn-colors-side-menu: var(
--c--contextuals--content--semantic--neutral--tertiary
);
}
/**
* Ensure long placeholder text is truncated with ellipsis
*/
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: inherit;
height: inherit;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child) {
position: relative;
}
/**
* Ensure images with unsafe URLs are not interactive
*/
img.bn-visual-media[src*='-unsafe'] {
pointer-events: none;
}
/**
* Collaboration cursor styles
*/
.collaboration-cursor-custom__base {
position: relative;
}
.collaboration-cursor-custom__caret {
position: absolute;
height: 100%;
width: 2px;
bottom: 4%;
left: -1px;
}
.collaboration-cursor-custom__label {
color: #0d0d0d;
font-size: 12px;
font-weight: 600;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
position: absolute;
top: -17px;
left: 0px;
padding: 0px 6px;
border-radius: 0px;
white-space: nowrap;
transition: clip-path 0.3s ease-in-out;
border-radius: 4px 4px 4px 0;
box-shadow: inset -2px 2px 6px #ffffff00;
clip-path: polygon(0 85%, 4% 85%, 4% 100%, 0% 100%);
}
.collaboration-cursor-custom__base[data-active]
.collaboration-cursor-custom__label {
pointer-events: none;
box-shadow: inset -2px 2px 6px #ffffff88;
clip-path: polygon(0 0, 100% 0%, 100% 100%, 0% 100%);
}
/**
* Side menu
*/
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 54px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 43px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
}
.bn-side-menu[data-block-type='divider'] {
height: 38px;
}
.bn-side-menu .mantine-UnstyledButton-root svg {
color: var(
--c--contextuals--content--semantic--neutral--tertiary
) !important;
}
/**
* Callout, Paragraph and Heading blocks
*/
.bn-block {
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block-outer {
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block > .bn-block-content[data-background-color] {
padding: var(--c--globals--spacings--3xs) var(--c--globals--spacings--3xs);
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block-content[data-content-type='checkListItem'][data-checked='true']
.bn-inline-content {
text-decoration: none;
}
.bn-default-styles h1 {
font-size: 1.875rem;
}
.bn-default-styles h2 {
font-size: 1.5rem;
}
.bn-default-styles h3 {
font-size: 1.25rem;
}
a {
color: var(--c--globals--colors--gray-600);
cursor: pointer;
}
.bn-block-group
.bn-block-group
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
.bn-toolbar {
max-width: 95vw;
}
/**
* Quotes
*/
blockquote {
border-left: 4px solid var(--c--globals--colors--gray-300);
font-style: italic;
}
/**
* AI
/**
* Ensure long placeholder text is truncated with ellipsis
*/
ins,
[data-type='modification'] {
background: var(--c--globals--colors--brand-100);
border-bottom: 2px solid var(--c--globals--colors--brand-300);
color: var(--c--globals--colors--brand-700);
}
/**
* Divider
*/
[data-content-type='divider'] hr {
background: #d3d2cf;
margin: 1rem 0;
width: 100%;
border: 1px solid #d3d2cf;
}
/**
* Checklist items
*/
.bn-block-content[data-content-type='checkListItem'] > div > input {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid
var(--c--contextuals--content--semantic--neutral--tertiary);
border-radius: 4px;
cursor: pointer;
position: relative;
align-self: center;
margin-top: 2px;
}
.bn-block-content[data-content-type='checkListItem'] > div > input:checked {
background-color: var(--c--contextuals--content--semantic--brand--tertiary);
border-color: var(--c--contextuals--content--semantic--brand--tertiary);
}
.bn-block-content[data-content-type='checkListItem']
> div
> input:checked::after {
content: 'check';
font-family: 'Material Symbols Outlined Variable', sans-serif;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--c--contextuals--content--semantic--overlay--primary);
font-size: 18px;
}
/**
* Ensure consistent spacing between headings and paragraphs
*/
& .bn-block-outer:not(:first-child) {
&:has(h1) {
margin-top: 32px;
}
&:has(h2) {
margin-top: 24px;
}
&:has(h3) {
margin-top: 16px;
}
}
& .bn-inline-content code {
background-color: gainsboro;
padding: 2px;
border-radius: 4px;
}
@media screen and (width <= 768px) {
& .bn-editor {
padding-right: 36px;
}
}
@media screen and (width <= 560px) {
.--docs--doc-readonly & .bn-editor {
padding-left: 10px;
}
& .bn-editor {
padding-right: 10px;
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 46px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 40px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 40px;
}
& .bn-editor h1 {
font-size: 1.6rem;
}
& .bn-editor h2 {
font-size: 1.35rem;
}
& .bn-editor h3 {
font-size: 1.2rem;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: inherit;
height: inherit;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child) {
position: relative;
}
/**
* Ensure images with unsafe URLs are not interactive
*/
img.bn-visual-media[src*='-unsafe'] {
pointer-events: none;
}
/**
* Collaboration cursor styles
*/
.collaboration-cursor-custom__base {
position: relative;
}
.collaboration-cursor-custom__caret {
position: absolute;
height: 100%;
width: 2px;
bottom: 4%;
left: -1px;
}
.collaboration-cursor-custom__label {
color: #0d0d0d;
font-size: 12px;
font-weight: 600;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
position: absolute;
top: -17px;
left: 0px;
padding: 0px 6px;
border-radius: 0px;
white-space: nowrap;
transition: clip-path 0.3s ease-in-out;
border-radius: 4px 4px 4px 0;
box-shadow: inset -2px 2px 6px #ffffff00;
clip-path: polygon(0 85%, 4% 85%, 4% 100%, 0% 100%);
}
.collaboration-cursor-custom__base[data-active]
.collaboration-cursor-custom__label {
pointer-events: none;
box-shadow: inset -2px 2px 6px #ffffff88;
clip-path: polygon(0 0, 100% 0%, 100% 100%, 0% 100%);
}
/**
* Side menu
*/
.bn-side-menu .mantine-UnstyledButton-root svg {
color: var(
--c--contextuals--content--semantic--neutral--tertiary
) !important;
}
/**
* Callout, Paragraph and Heading blocks
*/
.bn-block {
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block-outer {
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block > .bn-block-content[data-background-color] {
padding: var(--c--globals--spacings--3xs) var(--c--globals--spacings--3xs);
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block-content[data-content-type='checkListItem'][data-checked='true']
.bn-inline-content {
text-decoration: none;
}
a {
color: var(--c--globals--colors--gray-600);
cursor: pointer;
}
.bn-block-group
.bn-block-group
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
.bn-toolbar {
max-width: 95vw;
}
/**
* Quotes
*/
blockquote {
border-left: 4px solid var(--c--globals--colors--gray-300);
font-style: italic;
}
/**
* AI
*/
ins,
[data-type='modification'] {
background: var(--c--globals--colors--brand-100);
border-bottom: 2px solid var(--c--globals--colors--brand-300);
color: var(--c--globals--colors--brand-700);
}
/**
* Divider
*/
[data-content-type='divider'] hr {
background: #d3d2cf;
margin: 1rem 0;
width: 100%;
border: 1px solid #d3d2cf;
}
.bn-side-menu[data-block-type='divider'] {
height: 38px;
}
/**
* Checklist items
*/
.bn-block-content[data-content-type='checkListItem'] > div > input {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid
var(--c--contextuals--content--semantic--neutral--tertiary);
border-radius: 4px;
cursor: pointer;
position: relative;
align-self: center;
margin-top: 2px;
}
.bn-block-content[data-content-type='checkListItem'] > div > input:checked {
background-color: var(--c--contextuals--content--semantic--brand--tertiary);
border-color: var(--c--contextuals--content--semantic--brand--tertiary);
}
.bn-block-content[data-content-type='checkListItem']
> div
> input:checked::after {
content: 'check';
font-family: 'Material Symbols Outlined Variable', sans-serif;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--c--contextuals--content--semantic--overlay--primary);
font-size: 18px;
}
/**
* Headings
* Ensure consistent spacing between headings and paragraphs
*/
[data-content-type='heading'] {
--level: 1.875rem;
&[data-level='2'] {
--level: 1.5rem;
}
&[data-level='3'] {
--level: 1.25rem;
}
&[data-level='4'] {
--level: 1.125rem;
}
&[data-level='5'] {
--level: 1rem;
}
&[data-level='6'] {
--level: 0.875rem;
}
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 54px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 43px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
}
& .bn-default-styles h1 {
font-size: 1.875rem;
}
& .bn-default-styles h2 {
font-size: 1.5rem;
}
& .bn-default-styles h3 {
font-size: 1.25rem;
}
& .bn-default-styles h4 {
font-size: 1.125rem;
}
& .bn-default-styles h5 {
font-size: 1rem;
}
& .bn-default-styles h6 {
font-size: 0.875rem;
}
& .bn-block-outer:not(:first-child) {
&:has(h1) {
margin-top: 32px;
}
&:has(h2) {
margin-top: 24px;
}
&:has(h3) {
margin-top: 16px;
}
}
& .bn-inline-content code {
background-color: gainsboro;
padding: 2px;
border-radius: 4px;
}
@media screen and (width <= 768px) {
& .bn-editor {
padding-right: 36px;
}
}
@media screen and (width <= 560px) {
.--docs--doc-readonly & .bn-editor {
padding-left: 10px;
}
& .bn-editor {
padding-right: 10px;
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 46px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 40px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 40px;
}
[data-content-type='heading'] {
--level: 1.6rem;
&[data-level='2'] {
--level: 1.35rem;
}
&[data-level='3'] {
--level: 1.2rem;
}
&[data-level='4'] {
--level: 1.125rem;
}
&[data-level='5'] {
--level: 1rem;
}
&[data-level='6'] {
--level: 0.875rem;
}
}
& .bn-editor h1 {
font-size: 1.6rem;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
font-size: 14px;
}
}
}
`;

View File

@@ -1,3 +1,9 @@
const HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
export const sanitizeColor = (color: string): string => {
return HEX_COLOR_REGEX.test(color) ? color : randomColor();
};
export const randomColor = () => {
const randomInt = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1)) + min;

View File

@@ -11,7 +11,7 @@ import {
useToastProvider,
} from '@gouvfr-lasuite/cunningham-react';
import { DocumentProps, pdf } from '@react-pdf/renderer';
import jsonemoji from 'emoji-datasource-apple' assert { type: 'json' };
import jsonemoji from 'emoji-datasource-apple' with { type: 'json' };
import i18next from 'i18next';
import JSZip from 'jszip';
import { cloneElement, isValidElement, useState } from 'react';

View File

@@ -16,6 +16,4 @@ if (process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false') {
};
}
type ModulesExport = typeof modulesExport;
export default modulesExport as ModulesExport;
export default modulesExport;

View File

@@ -28,7 +28,8 @@ const PRINT_ONLY_CONTENT_CSS = `
[role="contentinfo"],
div[data-is-empty-and-focused="true"],
div[data-floating-ui-focusable],
.collaboration-cursor-custom__base
.collaboration-cursor-custom__base,
.c__toast__container
{
display: none !important;
}
@@ -84,7 +85,7 @@ const PRINT_ONLY_CONTENT_CSS = `
/* Ensure BlockNote content flows properly */
.bn-editor,
.bn-container,
.bn-root,
.--docs--main-editor,
.bn-block-outer {
height: auto !important;
@@ -243,6 +244,50 @@ function wrapMediaWithLink() {
};
}
/**
* Wraps interlink inline content with anchor tags for printing,
* so they appear as clickable links in the printed PDF.
*/
function wrapInterlinksWithAnchor() {
const wrappedElements: Array<{
el: Element;
anchor: HTMLAnchorElement;
parent: Node;
}> = [];
document
.querySelectorAll('.--docs--interlinking-link-inline-content[data-href]')
.forEach((el) => {
const href = el.getAttribute('data-href');
if (!href || !isSafeUrl(href)) {
return;
}
const parent = el.parentNode;
if (!parent) {
return;
}
const anchor = document.createElement('a');
anchor.href = href;
anchor.target = '_blank';
anchor.rel = 'noopener noreferrer';
anchor.setAttribute('data-print-link', 'true');
parent.insertBefore(anchor, el);
anchor.appendChild(el);
wrappedElements.push({ el, anchor, parent });
});
return () => {
wrappedElements.forEach(({ el, anchor, parent }) => {
parent.insertBefore(el, anchor);
anchor.remove();
});
};
}
export function printDocumentWithStyles() {
if (typeof window === 'undefined') {
return;
@@ -253,7 +298,9 @@ export function printDocumentWithStyles() {
// Small delay to ensure styles are applied
setTimeout(() => {
const cleanupLinks = wrapMediaWithLink();
const cleanupInterlinks = wrapInterlinksWithAnchor();
const cleanup = () => {
cleanupInterlinks();
cleanupLinks();
cleanupPrintStyles();
};

View File

@@ -68,7 +68,7 @@ export const BoutonShare = ({
/>
}
onClick={(e) => {
addLastFocus(e.currentTarget as HTMLElement);
addLastFocus(e.currentTarget);
open();
}}
size="medium"
@@ -85,7 +85,7 @@ export const BoutonShare = ({
color="brand"
variant="tertiary"
onClick={(e) => {
addLastFocus(e.currentTarget as HTMLElement);
addLastFocus(e.currentTarget);
open();
}}
size="medium"

View File

@@ -244,7 +244,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
<Icon iconName="download" $color="inherit" aria-hidden={true} />
}
onClick={(e) => {
addLastFocus(e.currentTarget as HTMLElement);
addLastFocus(e.currentTarget);
setIsModalExportOpen(true);
}}
size={isSmallMobile ? 'small' : 'medium'}

View File

@@ -1,4 +1,3 @@
export * from './useCollaboration';
export * from './useCopyDocLink';
export * from './useCreateChildDocTree';
export * from './useDocTitleUpdate';

View File

@@ -14,7 +14,7 @@ import { MAIN_LAYOUT_ID } from '@/layouts/conf';
import { Heading } from './Heading';
export const TableContent = () => {
export const TableContent = ({ selector }: { selector: string }) => {
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const [containerHeight, setContainerHeight] = useState('100vh');
const { headings } = useHeadingStore();
@@ -27,11 +27,29 @@ export const TableContent = () => {
* Calculate container height based on the scrollable content
*/
useEffect(() => {
const mainLayout = document.getElementById(MAIN_LAYOUT_ID);
if (mainLayout) {
setContainerHeight(`${mainLayout.scrollHeight}px`);
const layout = document.querySelector<HTMLElement>(selector);
if (!layout) {
return;
}
}, []);
let timeout: ReturnType<typeof setTimeout>;
const updateHeight = () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
setContainerHeight(`${layout.scrollHeight}px`);
}, 300);
};
updateHeight();
const observer = new ResizeObserver(updateHeight);
observer.observe(layout);
return () => {
clearTimeout(timeout);
observer.disconnect();
};
}, [selector]);
const onOpen = () => {
setIsOpen(true);

View File

@@ -274,11 +274,16 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
/* Remove outline from TreeViewItem wrapper elements */
.c__tree-view--row {
outline: none !important;
pointer-events: initial;
&:focus-visible {
outline: none !important;
}
}
.c__tree-view--node {
pointer-events: inherit;
}
.c__tree-view--container {
z-index: 1;
margin-top: -10px;

View File

@@ -85,15 +85,14 @@ export const DocVersionEditor = ({
return (
<DocEditorContainer
docHeader={<DocVersionHeader />}
docEditor={
<BlockNoteReader
initialContent={initialContent}
docId={version.id}
isMainEditor={false}
/>
}
isDeletedDoc={false}
readOnly={true}
/>
>
<BlockNoteReader
initialContent={initialContent}
docId={version.id}
isMainEditor={false}
/>
</DocEditorContainer>
);
};

View File

@@ -50,7 +50,7 @@ export const DocsGridItemSharedButton = ({ doc, disabled }: Props) => {
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
addLastFocus(event.currentTarget as HTMLElement);
addLastFocus(event.currentTarget);
shareModal.open();
}}
color="brand"

View File

@@ -109,6 +109,7 @@ export const useImport = ({ onDragOver }: UseImportProps) => {
});
},
noClick: true,
noKeyboard: true,
});
const { mutate: importDoc, isPending } = useImportDoc();

View File

@@ -2,7 +2,9 @@ import {
ModalSize,
OnboardingModal,
type OnboardingModalProps,
OnboardingStep,
} from '@gouvfr-lasuite/ui-kit';
import React, { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components';
@@ -10,9 +12,29 @@ import { useConfig } from '@/core/config/api/useConfig';
import { useOnboardingSteps } from '../hooks/useOnboardingSteps';
/**
* typing was not correct on ui-kit side for the description prop of OnboardingStep,
* it can be a string or a ReactNode but was typed as string only, so we need to override the
* type here to be able to use ReactNode
*/
type OnboardingStepFixed = Omit<OnboardingStep, 'description'> & {
description?: ReactNode;
};
type OnboardingModalPropsFixed = Omit<OnboardingModalProps, 'steps'> & {
steps?: OnboardingStepFixed[];
};
const OnboardingModalFixed =
OnboardingModal as React.ComponentType<OnboardingModalPropsFixed>;
const OnBoardingStyle = createGlobalStyle`
.c__onboarding-modal__steps{
height: auto;
& a{
color:inherit;
}
}
.c__onboarding-modal__content {
height: 350px;
@@ -32,7 +54,7 @@ const OnBoardingStyle = createGlobalStyle`
*:not(.material-icons):not(.material-icons-filled):not(
.material-symbols-outlined
) {
font-family: Marianne, Inter, Roboto Flex Variable, sans-serif;
font-family: var(--c--globals--font--families--base);
}
/* Separator between content and footer actions/link */
@@ -56,6 +78,10 @@ const OnBoardingStyle = createGlobalStyle`
display: flex;
flex-direction: column;
a{
color: inherit;
}
& .c__onboarding-modal__body{
justify-content: center;
}
@@ -81,7 +107,7 @@ export const OnBoarding = (props: OnBoardingProps) => {
return (
<>
{props.isOpen ? <OnBoardingStyle /> : null}
<OnboardingModal
<OnboardingModalFixed
size={ModalSize.LARGE}
appName={t('Discover Docs')}
mainTitle={t('Learn the core principles')}

View File

@@ -1,7 +1,8 @@
import { type OnboardingStep } from '@gouvfr-lasuite/ui-kit';
import Image from 'next/image';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import DragIndicatorIcon from '../assets/drag_indicator.svg';
@@ -16,6 +17,9 @@ export interface OnboardingStepsData {
export const useOnboardingSteps = () => {
const { t } = useTranslation();
const { data: config } = useConfig();
const readyTemplateUrl =
config?.theme_customization?.onboarding?.ready_template_url;
const { contextualTokens, colorsTokens } = useCunninghamTheme();
const activeColor =
contextualTokens.content.semantic.brand.tertiary ??
@@ -122,8 +126,21 @@ export const useOnboardingSteps = () => {
</OnboardingStepIcon>
),
title: t('Draw inspiration from the content library'),
description: t(
'Start from ready-made templates for common use cases, then customize them to match your workflow in minutes.',
description: (
<Trans
t={t}
i18nKey="Start from <Link>ready-made templates</Link> for common use cases, then customize them to match your workflow in minutes."
components={{
Link: (
<a
target="_blank"
rel="noopener noreferrer"
href={readyTemplateUrl}
aria-label={t('Ready-made templates (opens in a new tab)')}
/>
),
}}
/>
),
content: (
<Image

View File

@@ -50,7 +50,7 @@ export class RequestSerializer {
public static arrayBufferToString(buffer: ArrayBufferLike) {
const decoder = new TextDecoder();
return decoder.decode(buffer as ArrayBuffer);
return decoder.decode(buffer);
}
public static arrayBufferToJson<T>(buffer: ArrayBufferLike) {

View File

@@ -15,7 +15,7 @@ const mockServiceWorkerScope = {
(global as any).self = {
...global,
clients: mockServiceWorkerScope.clients,
} as unknown as ServiceWorkerGlobalScope;
};
describe('OfflinePlugin', () => {
afterEach(() => vi.clearAllMocks());

View File

@@ -1,13 +1,12 @@
import { Box } from '@/components';
import { css, keyframes } from 'styled-components';
import { Box, BoxType } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useResponsiveStore } from '@/stores';
import { SkeletonCircle, SkeletonLine } from './SkeletionUI';
export const DocEditorSkeleton = () => {
const { isDesktop } = useResponsiveStore();
const { spacingsTokens } = useCunninghamTheme();
return (
<>
{/* Main Editor Container */}
@@ -17,80 +16,117 @@ export const DocEditorSkeleton = () => {
$height="100%"
className="--docs--doc-editor-skeleton"
>
{/* Header Skeleton */}
<Box
$padding={{ horizontal: isDesktop ? '70px' : 'base' }}
className="--docs--doc-editor-header-skeleton"
>
<Box
$width="100%"
$padding={{ top: isDesktop ? '65px' : 'md' }}
$gap={spacingsTokens['base']}
>
<Box
$direction="row"
$align="center"
$width="100%"
$padding={{ bottom: 'xs' }}
>
<Box
$direction="row"
$justify="space-between"
$css="flex:1;"
$gap="0.5rem 1rem"
$align="center"
$maxWidth="100%"
>
{/* Title and metadata skeleton */}
<Box $gap="0.25rem" $css="flex:1;">
{/* Title - "Untitled Document" style */}
<SkeletonLine $width="35%" $height="40px" />
{/* Metadata (role and last update) */}
<Box $direction="row" $gap="0.5rem" $align="center">
<SkeletonLine $maxWidth="260px" $height="12px" />
</Box>
</Box>
{/* Toolbox skeleton (buttons) */}
<Box $direction="row" $gap="0.75rem" $align="center">
{/* Share button */}
<SkeletonLine $width="90px" $height="40px" />
{/* Download icon */}
<SkeletonCircle $width="40px" $height="40px" />
{/* Menu icon */}
<SkeletonCircle $width="40px" $height="40px" />
</Box>
</Box>
</Box>
{/* Separator */}
<SkeletonLine $height="1px" />
</Box>
</Box>
{/* Content Skeleton */}
<Box
$direction="row"
$width="100%"
$css="overflow-x: clip; flex: 1;"
$position="relative"
className="--docs--doc-editor-content-skeleton"
>
<Box
$css="flex:1;"
$position="relative"
$width="100%"
$padding={{ horizontal: isDesktop ? '70px' : 'base', top: 'lg' }}
>
{/* Placeholder text similar to screenshot */}
<Box $gap="0rem">
{/* Single placeholder line like in the screenshot */}
<SkeletonLine $width="85%" $height="20px" />
</Box>
</Box>
</Box>
<SkeletonEditorHeader />
<SkeletonEditorCore />
</Box>
</>
);
};
const SkeletonEditorHeader = () => {
const { isDesktop } = useResponsiveStore();
const { spacingsTokens } = useCunninghamTheme();
return (
<Box
$padding={{ horizontal: isDesktop ? '54px' : 'base' }}
className="--docs--doc-editor-header-skeleton"
>
<Box
$width="100%"
$padding={{ top: isDesktop ? '65px' : 'md' }}
$gap={spacingsTokens['base']}
>
<Box
$direction="row"
$align="center"
$width="100%"
$padding={{ bottom: 'xs' }}
>
<Box
$direction="row"
$justify="space-between"
$css="flex:1;"
$gap="0.5rem 1rem"
$align="center"
$maxWidth="100%"
>
{/* Title and metadata skeleton */}
<Box $gap="0.25rem" $css="flex:1;">
{/* Title - "Untitled Document" style */}
<SkeletonLine $width="35%" $height="40px" />
{/* Metadata (role and last update) */}
<Box $direction="row" $gap="0.5rem" $align="center">
<SkeletonLine $maxWidth="260px" $height="12px" />
</Box>
</Box>
{/* Toolbox skeleton (buttons) */}
<Box $direction="row" $gap={spacingsTokens['t']} $align="center">
{/* Share button */}
<SkeletonLine $width="90px" $height="40px" />
{/* Download icon */}
<SkeletonCircle $width="40px" $height="40px" />
{/* Menu icon */}
<SkeletonCircle $width="40px" $height="40px" />
</Box>
</Box>
</Box>
{/* Separator */}
<SkeletonLine $height="1px" />
</Box>
</Box>
);
};
export const SKELETON_FADE_DURATION_MS = 150;
const skeletonFadeOut = keyframes`
from { opacity: 1; }
to { opacity: 0; }
`;
type SkeletonEditorCoreProps = Partial<BoxType> & {
isFadingOut?: boolean;
};
export const SkeletonEditorCore = ({
isFadingOut,
$css,
...props
}: SkeletonEditorCoreProps) => {
const { isDesktop } = useResponsiveStore();
return (
<Box
$direction="row"
$width="100%"
$css="overflow-x: clip; flex: 1;"
$position="relative"
className="--docs--doc-editor-content-skeleton"
>
<Box
$position="relative"
$width="100%"
$padding={{ horizontal: isDesktop ? '54px' : 'base', top: 'md' }}
$flex="1"
$css={css`
${$css}
${isFadingOut &&
css`
animation: ${skeletonFadeOut} ${SKELETON_FADE_DURATION_MS}ms
ease-in-out forwards;
`}
`}
{...props}
>
<Box $gap="1.5rem">
<SkeletonLine $width="65%" $height="35px" />
<SkeletonLine $width="55%" $height="25px" />
<SkeletonLine $width="35%" $height="20px" />
</Box>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';
import { SKELETON_FADE_DURATION_MS } from '../components/DocEditorSkeleton';
export const useSkeletonFadeOut = (showContent: boolean) => {
const [skeletonVisible, setSkeletonVisible] = useState(!showContent);
const [isFadingOut, setIsFadingOut] = useState(false);
useEffect(() => {
if (showContent) {
setIsFadingOut(true);
const timer = setTimeout(
() => setSkeletonVisible(false),
SKELETON_FADE_DURATION_MS,
);
return () => clearTimeout(timer);
} else {
setSkeletonVisible(true);
setIsFadingOut(false);
}
}, [showContent]);
return { skeletonVisible, isFadingOut };
};

View File

@@ -36,7 +36,6 @@ if (!isInitialized && !i18next.isInitialized) {
lowerCaseLng: true,
nsSeparator: false,
keySeparator: false,
showSupportNotice: false,
})
.then(() => {
if (typeof document !== 'undefined') {

View File

@@ -1482,7 +1482,7 @@
"Simple document icon": "Icône simple du document",
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, veuillez réessayer.",
"Start Writing": "Commencer à écrire",
"Start from ready-made templates for common use cases, then customize them to match your workflow in minutes.": "Commencez à partir de modèles prêts à l'emploi pour les cas d'utilisation courants, puis personnalisez-les pour correspondre à votre flux de travail en quelques minutes.",
"Start from <Link>ready-made templates</Link> for common use cases, then customize them to match your workflow in minutes.": "Commencez à partir de <Link>modèles prêts à l'emploi</Link> pour les cas d'utilisation courants, puis personnalisez-les pour correspondre à votre flux de travail en quelques minutes.",
"Stop": "Arrêter",
"Summarize": "Résumer",
"Summary": "Sommaire",

View File

@@ -8,12 +8,16 @@
body {
margin: 0;
padding: 0;
overflow: hidden;
}
/* stylelint-disable-next-line selector-id-pattern */
body > #__next > .c__app > div:has(> .c__loader) {
min-height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
}
* {

View File

@@ -109,5 +109,7 @@ export const useBroadcastStore = create<BroadcastState>((set, get) => ({
Object.values(get().tasks).forEach(({ task, observer }) => {
task.unobserve(observer);
});
set({ tasks: {}, provider: undefined });
},
}));

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

View File

@@ -31,16 +31,17 @@
"server:test": "yarn COLLABORATION_SERVER run test"
},
"resolutions": {
"@tiptap/extensions": "3.20.4",
"@types/node": "24.12.0",
"@tiptap/extensions": "3.22.4",
"@types/node": "24.12.2",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"eslint": "10.1.0",
"eslint": "10.2.1",
"postcss": "8.5.10",
"prosemirror-view": "1.41.7",
"react": "19.2.4",
"react-dom": "19.2.4",
"typescript": "5.9.3",
"prosemirror-view": "1.41.8",
"react": "19.2.5",
"react-dom": "19.2.5",
"serialize-javascript": "7.0.5",
"typescript": "6.0.3",
"uuid": "14.0.0",
"wrap-ansi": "10.0.0",
"yjs": "13.6.30"

View File

@@ -18,23 +18,23 @@
},
"dependencies": {
"@eslint/js": "10.0.1",
"@next/eslint-plugin-next": "16.2.1",
"@tanstack/eslint-plugin-query": "5.95.0",
"@typescript-eslint/eslint-plugin": "8.57.1",
"@typescript-eslint/parser": "8.57.1",
"@typescript-eslint/utils": "8.57.1",
"@vitest/eslint-plugin": "1.6.13",
"eslint-config-next": "16.2.1",
"@next/eslint-plugin-next": "16.2.4",
"@tanstack/eslint-plugin-query": "5.99.2",
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"@typescript-eslint/utils": "8.59.0",
"@vitest/eslint-plugin": "1.6.16",
"eslint-config-next": "16.2.4",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import-x": "4.16.2",
"eslint-plugin-jest": "29.15.0",
"eslint-plugin-jest": "29.15.2",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-playwright": "2.10.1",
"eslint-plugin-playwright": "2.10.2",
"eslint-plugin-prettier": "5.5.5",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-testing-library": "7.16.1",
"prettier": "3.8.1"
"eslint-plugin-react-hooks": "7.1.1",
"eslint-plugin-testing-library": "7.16.2",
"prettier": "3.8.3"
},
"packageManager": "yarn@1.22.22"
}

View File

@@ -21,7 +21,7 @@
"eslint-plugin-import": "2.32.0",
"i18next-parser": "9.4.0",
"jest": "30.3.0",
"ts-jest": "29.4.6",
"ts-jest": "29.4.9",
"typescript": "*",
"yargs": "18.0.0"
},

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

View File

@@ -16,12 +16,12 @@
"node": ">=22"
},
"dependencies": {
"@blocknote/server-util": "0.47.3",
"@blocknote/server-util": "0.49.0",
"@hocuspocus/server": "3.4.4",
"@sentry/node": "10.45.0",
"@sentry/profiling-node": "10.45.0",
"@sentry/node": "10.49.0",
"@sentry/profiling-node": "10.49.0",
"@tiptap/extensions": "*",
"axios": "1.15.0",
"axios": "1.15.2",
"cors": "2.8.6",
"express": "5.2.1",
"express-ws": "5.0.2",
@@ -30,7 +30,7 @@
"yjs": "*"
},
"devDependencies": {
"@blocknote/core": "0.47.3",
"@blocknote/core": "0.49.0",
"@hocuspocus/provider": "3.4.4",
"@types/cors": "2.8.19",
"@types/express": "5.0.6",
@@ -45,8 +45,8 @@
"ts-node": "10.9.2",
"tsc-alias": "1.8.16",
"typescript": "*",
"vitest": "4.1.0",
"vitest-mock-extended": "3.1.0",
"vitest": "4.1.4",
"vitest-mock-extended": "4.0.0",
"ws": "8.20.0"
},
"packageManager": "yarn@1.22.22"

View File

@@ -62,7 +62,7 @@ const readers: InputReader[] = [
const ydoc = new Y.Doc();
try {
Y.applyUpdate(ydoc, data);
return editor.yDocToBlocks(ydoc, 'document-store') as PartialBlock[];
return editor.yDocToBlocks(ydoc, 'document-store');
} finally {
ydoc.destroy();
}

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

File diff suppressed because it is too large Load Diff

View File

@@ -6,4 +6,4 @@ DOCS_DIR_MAILS="${DOCS_DIR_MAILS:-../backend/core/templates/mail}/html/"
if [ ! -d "${DOCS_DIR_MAILS}" ]; then
mkdir -p "${DOCS_DIR_MAILS}";
fi
mjml mjml/*.mjml -o "${DOCS_DIR_MAILS}";
mjml mjml/*.mjml -o "${DOCS_DIR_MAILS}" --config.allowIncludes true;

View File

@@ -1,20 +1,16 @@
<mj-head>
<mj-title>{{ title }}</mj-title>
<mj-preview>
<!--
We load django tags here, in this way there are put within the body in html output
so the html-to-text command includes it within its output
-->
{% load i18n static extra_tags %}
{{ title }}
</mj-preview>
<mj-preview>{{ title }}</mj-preview>
<mj-font name="Inter" href="https://fonts.bunny.net/css?family=inter:300,400,500,700" />
<mj-attributes>
<mj-all
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Cantarell, 'Helvetica Neue', sans-serif"
font-family="Inter, sans-serif"
font-size="16px"
line-height="normal"
color="#3A3A3A"
/>
<mj-text font-family="Inter, sans-serif" />
<mj-button font-family="Inter, sans-serif" />
</mj-attributes>
<mj-style>
/* Reset */

View File

@@ -2,6 +2,11 @@
<mj-include path="./partial/header.mjml" />
<mj-body mj-class="bg--blue-100">
<!--
We load django tags here so they appear in the body of the HTML output.
This ensures html-to-text also includes them in the plain text template.
-->
<mj-raw>{% load i18n static extra_tags %}</mj-raw>
<mj-wrapper css-class="wrapper" padding="5px 25px 0px 25px">
<mj-section css-class="wrapper-logo">
<mj-column>
@@ -16,11 +21,11 @@
</mj-section>
<mj-section mj-class="bg--white-100" padding="0px 20px 60px 20px">
<mj-column>
<mj-text align="center">
<mj-text align="center" font-family="Inter, sans-serif">
<h1>{{title|capfirst}}</h1>
</mj-text>
<!-- Main Message -->
<mj-text>
<mj-text font-family="Inter, sans-serif">
{{message|capfirst}}
<a href="{{link}}">{{link_label}}</a>
</mj-text>
@@ -29,6 +34,7 @@
background-color="#000091"
color="white"
padding-bottom="30px"
font-family="Inter, sans-serif"
>
{{button_label}}
</mj-button>
@@ -39,13 +45,13 @@
width="30%"
align="center"
/>
<mj-text>
<mj-text font-family="Inter, sans-serif">
{% blocktrans %}
Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team.
{% endblocktrans %}
</mj-text>
<!-- Signature -->
<mj-text>
<mj-text font-family="Inter, sans-serif">
<p>
{% blocktrans %}
Brought to you by {{brandname}}

View File

@@ -5,7 +5,7 @@
"type": "module",
"dependencies": {
"@html-to/text-cli": "0.5.4",
"mjml": "4.18.0"
"mjml": "5.0.1"
},
"resolutions": {
"minimatch": "^10.0.0"

File diff suppressed because it is too large Load Diff