Compare commits

..

45 Commits

Author SHA1 Message Date
Philipinho b3d50558db react19 2026-01-24 01:08:42 +00:00
Philipinho 605bf760be Tiptap v3.17.0 2026-01-24 00:34:31 +00:00
Philipinho c5c9237b00 Merge branch 'main' into tiptap3-migration 2026-01-24 00:18:48 +00:00
Philipinho 511c822a90 fix collab connection status display 2026-01-20 16:17:14 +00:00
Philipinho 7af31fe228 update lock file 2026-01-20 15:26:45 +00:00
Philipinho dd460713ce Merge branch 'feat/asset-placeholders' into tiptap3-migration 2026-01-20 15:21:24 +00:00
Philipinho b0f3bec4d1 * add skeleton pulse animation
* add translation strings
* fix attachment view responsiveness
2026-01-20 15:03:49 +00:00
Arek Nawo 8ea92668b2 fix: Allow media as attachment 2026-01-20 13:52:02 +01:00
Arek Nawo dcf9228344 fix: Drag & drop, paste upload 2026-01-20 13:51:53 +01:00
Arek Nawo dbd1308c72 feat: Multiple file upload, improved placeholders, local previews 2026-01-20 11:10:11 +01:00
Arek Nawo 03ae58253a fix: Use imageDimensionsFromStream 2026-01-19 23:42:13 +01:00
Arek Nawo b99d803b81 fix: Transaction handling on asset upload 2026-01-19 20:53:59 +01:00
Arek Nawo b9b5ddb848 fix: Video view styles 2026-01-19 20:45:45 +01:00
Arek Nawo dd8c42e1f3 feat: Improved placeholder and upload handling for attachments 2026-01-19 20:45:24 +01:00
Arek Nawo 13c29545a2 refactor: Image node and view clean-up 2026-01-19 20:15:14 +01:00
Arek Nawo 0c5c83a17a feat: Improved placeholder and upload handling for videos 2026-01-19 20:14:48 +01:00
Arek Nawo 5bda5623f2 feat: Improved placeholder and upload handling for images 2026-01-19 16:54:24 +01:00
Philipinho 051bc80ab7 cleanup 2026-01-16 17:10:58 +00:00
Philipinho 78d363febb cleanup 2026-01-16 16:59:42 +00:00
Philipinho 8681d9a8c4 Merge branch 'tiptap3-migration' of https://github.com/areknawo/docmost into tiptap3-migration 2026-01-16 15:42:47 +00:00
Philipinho b9543b01bd Merge branch 'main' into tiptap3-migration 2026-01-16 15:42:16 +00:00
Arek Nawo 5510434221 fix: Connect/disconnect websocketProvider 2026-01-09 12:04:45 +01:00
Arek Nawo f671e7a3b9 feat: Update collaboration connection for HocusPocus v3 2026-01-09 01:01:48 +01:00
Arek Nawo 974bcea690 feat: Migrate tippy.js menus to Floating UI 2026-01-09 00:33:28 +01:00
Arek Nawo 601ed88931 fix: Set isInitialized to force immediate react node view rendering 2026-01-08 22:51:28 +01:00
Philipinho cfbaedcd63 Merge branch 'main' into tiptap3-migration 2025-12-16 15:56:00 +00:00
Philipinho 5fc04aa7df fix collaboration caret css 2025-12-13 01:05:34 +00:00
Philipinho c357f169e1 Merge branch 'main' into tiptap3-migration 2025-12-13 00:57:13 +00:00
Philipinho 1cbd2854bb fix menus 2025-09-28 20:51:57 +01:00
Philipinho 3af1482a31 fix converter 2025-09-27 21:10:40 +01:00
Philipinho d31d1f7bbd fix bubble menu 2025-09-27 14:50:27 +01:00
Philipinho cc0146d0cd Merge branch 'main' into tiptap3-migration 2025-09-26 18:53:18 +01:00
Philipinho 83ce9cf240 tiptap 3.6.1 2025-09-26 18:52:49 +01:00
Philipinho e7e85e9fdd merge main 2025-09-10 04:19:04 +01:00
Philipinho 2d710612b1 Merge branch 'main' into tiptap3-migration 2025-09-10 04:00:01 +01:00
Philipinho a0814ef49a add tippyoptions for reference 2025-08-18 13:45:41 -07:00
Philipinho bf17289ab2 fix editable state 2025-08-15 02:06:59 -07:00
Philipinho c2cd412ac7 Switch to useEditorState
- Set shouldRerenderOnTransaction to false
2025-08-15 01:19:05 -07:00
Philipinho 71dfcf6bce update tiptap version 2025-08-14 23:29:48 -07:00
Philipinho 23e8ab032e disable duplicate extensions 2025-08-03 19:01:50 -07:00
Philipinho 0e1d4e5eee fix flicker 2025-08-03 19:01:28 -07:00
Philipinho 6f83f32d5c remove unused code 2025-08-03 18:34:32 -07:00
Philipinho 63ea2f7663 fix collaboration 2025-08-03 18:24:55 -07:00
Philipinho 66a3dad632 Merge branch 'main' into tiptap3-migration 2025-08-03 17:45:02 -07:00
Philipinho 2adc6a60d2 Tiptap3 migration - WIP 2025-08-02 19:09:06 -07:00
54 changed files with 1657 additions and 1598 deletions
+10 -10
View File
@@ -42,15 +42,15 @@
"mermaid": "^11.12.2", "mermaid": "^11.12.2",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"posthog-js": "^1.255.1", "posthog-js": "^1.255.1",
"react": "^18.3.1", "react": "^19.2.3",
"react-arborist": "3.4.0", "react-arborist": "3.4.0",
"react-clear-modal": "^2.0.17", "react-clear-modal": "^2.0.17",
"react-dom": "^18.3.1", "react-dom": "^19.2.3",
"react-drawio": "^1.0.7", "react-drawio": "^1.0.7",
"react-error-boundary": "^4.1.2", "react-error-boundary": "^6.1.0",
"react-helmet-async": "^2.0.5", "react-helmet-async": "^2.0.5",
"react-i18next": "^15.0.1", "react-i18next": "^15.0.1",
"react-router-dom": "^7.12.0", "react-router-dom": "^7.13.0",
"semver": "^7.7.3", "semver": "^7.7.3",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"tiptap-extension-global-drag-handle": "^0.1.18", "tiptap-extension-global-drag-handle": "^0.1.18",
@@ -63,13 +63,13 @@
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/katex": "^0.16.7", "@types/katex": "^0.16.7",
"@types/node": "22.19.1", "@types/node": "22.19.1",
"@types/react": "^18.3.12", "@types/react": "^19.2.9",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.15.0", "eslint": "^9.15.0",
"eslint-plugin-react": "^7.37.2", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-react-refresh": "^0.4.26",
"globals": "^15.13.0", "globals": "^15.13.0",
"optics-ts": "^2.4.1", "optics-ts": "^2.4.1",
"postcss": "^8.4.49", "postcss": "^8.4.49",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "Wählen Sie Ihre bevorzugte Benutzersprache.", "Choose your preferred interface language.": "Wählen Sie Ihre bevorzugte Benutzersprache.",
"Choose your preferred page width.": "Wählen Sie Ihre bevorzugte Seitenbreite.", "Choose your preferred page width.": "Wählen Sie Ihre bevorzugte Seitenbreite.",
"Confirm": "Bestätigen", "Confirm": "Bestätigen",
"Copy as Markdown": "Als Markdown kopieren",
"Copy link": "Link kopieren", "Copy link": "Link kopieren",
"Create": "Erstellen", "Create": "Erstellen",
"Create group": "Gruppe erstellen", "Create group": "Gruppe erstellen",
@@ -235,9 +234,7 @@
"Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.", "Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.",
"Invite link": "Einladungslink", "Invite link": "Einladungslink",
"Copy": "Kopieren", "Copy": "Kopieren",
"Copy to space": "In Raum kopieren",
"Copied": "Kopiert", "Copied": "Kopiert",
"Duplicate": "Duplizieren",
"Select a user": "Benutzer auswählen", "Select a user": "Benutzer auswählen",
"Select a group": "Gruppe auswählen", "Select a group": "Gruppe auswählen",
"Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.", "Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.",
@@ -254,7 +251,6 @@
"Export failed:": "Export fehlgeschlagen:", "Export failed:": "Export fehlgeschlagen:",
"export error": "Exportfehler", "export error": "Exportfehler",
"Export page": "Seite exportieren", "Export page": "Seite exportieren",
"Export successful": "Export erfolgreich",
"Export space": "Bereich exportieren", "Export space": "Bereich exportieren",
"Export {{type}}": "Exportiere {{type}}", "Export {{type}}": "Exportiere {{type}}",
"File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}", "File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}",
@@ -330,8 +326,6 @@
"Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.", "Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.",
"Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.", "Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.",
"Upload any file from your device.": "Laden Sie eine beliebige Datei von Ihrem Gerät hoch.", "Upload any file from your device.": "Laden Sie eine beliebige Datei von Ihrem Gerät hoch.",
"Uploading {{name}}": "Lade {{name}} hoch",
"Uploading file": "Datei wird hochgeladen",
"Table": "Tabelle", "Table": "Tabelle",
"Insert a table.": "Tabelle einfügen.", "Insert a table.": "Tabelle einfügen.",
"Insert collapsible block.": "Einklappbaren Block einfügen.", "Insert collapsible block.": "Einklappbaren Block einfügen.",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "Choose your preferred interface language.", "Choose your preferred interface language.": "Choose your preferred interface language.",
"Choose your preferred page width.": "Choose your preferred page width.", "Choose your preferred page width.": "Choose your preferred page width.",
"Confirm": "Confirm", "Confirm": "Confirm",
"Copy as Markdown": "Copy as Markdown",
"Copy link": "Copy link", "Copy link": "Copy link",
"Create": "Create", "Create": "Create",
"Create group": "Create group", "Create group": "Create group",
@@ -254,7 +253,6 @@
"Export failed:": "Export failed:", "Export failed:": "Export failed:",
"export error": "export error", "export error": "export error",
"Export page": "Export page", "Export page": "Export page",
"Export successful": "Export successful",
"Export space": "Export space", "Export space": "Export space",
"Export {{type}}": "Export {{type}}", "Export {{type}}": "Export {{type}}",
"File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit", "File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "Elige tu idioma de interfaz preferido.", "Choose your preferred interface language.": "Elige tu idioma de interfaz preferido.",
"Choose your preferred page width.": "Elige el ancho de página que prefieras.", "Choose your preferred page width.": "Elige el ancho de página que prefieras.",
"Confirm": "Confirmar", "Confirm": "Confirmar",
"Copy as Markdown": "Copiar como Markdown",
"Copy link": "Copiar enlace", "Copy link": "Copiar enlace",
"Create": "Crear", "Create": "Crear",
"Create group": "Crear grupo", "Create group": "Crear grupo",
@@ -254,7 +253,6 @@
"Export failed:": "Exportación fallida:", "Export failed:": "Exportación fallida:",
"export error": "error de exportación", "export error": "error de exportación",
"Export page": "Exportar página", "Export page": "Exportar página",
"Export successful": "Exportación exitosa",
"Export space": "Exportar espacio", "Export space": "Exportar espacio",
"Export {{type}}": "Exportar {{type}}", "Export {{type}}": "Exportar {{type}}",
"File exceeds the {{limit}} attachment limit": "El archivo supera el límite de {{limit}} adjuntos", "File exceeds the {{limit}} attachment limit": "El archivo supera el límite de {{limit}} adjuntos",
@@ -330,8 +328,6 @@
"Upload any image from your device.": "Sube cualquier imagen desde tu dispositivo.", "Upload any image from your device.": "Sube cualquier imagen desde tu dispositivo.",
"Upload any video from your device.": "Sube cualquier video desde tu dispositivo.", "Upload any video from your device.": "Sube cualquier video desde tu dispositivo.",
"Upload any file from your device.": "Sube cualquier archivo desde tu dispositivo.", "Upload any file from your device.": "Sube cualquier archivo desde tu dispositivo.",
"Uploading {{name}}": "Subiendo {{name}}",
"Uploading file": "Subiendo archivo",
"Table": "Tabla", "Table": "Tabla",
"Insert a table.": "Insertar una tabla.", "Insert a table.": "Insertar una tabla.",
"Insert collapsible block.": "Insertar bloque desplegable.", "Insert collapsible block.": "Insertar bloque desplegable.",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "Choisissez votre langue d'interface préférée.", "Choose your preferred interface language.": "Choisissez votre langue d'interface préférée.",
"Choose your preferred page width.": "Choisissez votre largeur de page préférée.", "Choose your preferred page width.": "Choisissez votre largeur de page préférée.",
"Confirm": "Confirmer", "Confirm": "Confirmer",
"Copy as Markdown": "Copier comme Markdown",
"Copy link": "Copier le lien", "Copy link": "Copier le lien",
"Create": "Créer", "Create": "Créer",
"Create group": "Créer groupe", "Create group": "Créer groupe",
@@ -254,7 +253,6 @@
"Export failed:": "Échec de l'exportation :", "Export failed:": "Échec de l'exportation :",
"export error": "exporter l'erreur", "export error": "exporter l'erreur",
"Export page": "Exporter la page", "Export page": "Exporter la page",
"Export successful": "Exportation réussie",
"Export space": "Exporter l'espace", "Export space": "Exporter l'espace",
"Export {{type}}": "Exporter {{type}}", "Export {{type}}": "Exporter {{type}}",
"File exceeds the {{limit}} attachment limit": "Le fichier dépasse la limite de {{limit}} pièces jointes", "File exceeds the {{limit}} attachment limit": "Le fichier dépasse la limite de {{limit}} pièces jointes",
@@ -330,8 +328,6 @@
"Upload any image from your device.": "Téléchargez n'importe quelle image depuis votre appareil.", "Upload any image from your device.": "Téléchargez n'importe quelle image depuis votre appareil.",
"Upload any video from your device.": "Téléchargez n'importe quelle vidéo depuis votre appareil.", "Upload any video from your device.": "Téléchargez n'importe quelle vidéo depuis votre appareil.",
"Upload any file from your device.": "Téléchargez n'importe quel fichier depuis votre appareil.", "Upload any file from your device.": "Téléchargez n'importe quel fichier depuis votre appareil.",
"Uploading {{name}}": "Téléchargement de {{name}}",
"Uploading file": "Téléchargement du fichier",
"Table": "Tableau", "Table": "Tableau",
"Insert a table.": "Insérez un tableau.", "Insert a table.": "Insérez un tableau.",
"Insert collapsible block.": "Insérer un bloc repliable.", "Insert collapsible block.": "Insérer un bloc repliable.",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "Scegli la lingua da utilizzare per l'interfaccia.", "Choose your preferred interface language.": "Scegli la lingua da utilizzare per l'interfaccia.",
"Choose your preferred page width.": "Scegli la larghezza della pagina che preferisci.", "Choose your preferred page width.": "Scegli la larghezza della pagina che preferisci.",
"Confirm": "Conferma", "Confirm": "Conferma",
"Copy as Markdown": "Copia come Markdown",
"Copy link": "Copia link", "Copy link": "Copia link",
"Create": "Crea", "Create": "Crea",
"Create group": "Crea gruppo", "Create group": "Crea gruppo",
@@ -254,7 +253,6 @@
"Export failed:": "Esportazione fallita:", "Export failed:": "Esportazione fallita:",
"export error": "errore di esportazione", "export error": "errore di esportazione",
"Export page": "Esporta pagina", "Export page": "Esporta pagina",
"Export successful": "Esportazione riuscita",
"Export space": "Esporta spazio", "Export space": "Esporta spazio",
"Export {{type}}": "Esporta {{type}}", "Export {{type}}": "Esporta {{type}}",
"File exceeds the {{limit}} attachment limit": "Il file supera il limite per gli allegati di {{limit}}", "File exceeds the {{limit}} attachment limit": "Il file supera il limite per gli allegati di {{limit}}",
@@ -330,8 +328,6 @@
"Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.", "Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.",
"Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.", "Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.",
"Upload any file from your device.": "Carica qualsiasi file dal tuo dispositivo.", "Upload any file from your device.": "Carica qualsiasi file dal tuo dispositivo.",
"Uploading {{name}}": "Caricamento di {{name}}",
"Uploading file": "Caricamento file",
"Table": "Tabella", "Table": "Tabella",
"Insert a table.": "Inserisci una tabella.", "Insert a table.": "Inserisci una tabella.",
"Insert collapsible block.": "Inserisci blocco comprimibile.", "Insert collapsible block.": "Inserisci blocco comprimibile.",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "お好みの言語を選択してください", "Choose your preferred interface language.": "お好みの言語を選択してください",
"Choose your preferred page width.": "お好みのページ幅を選択してください", "Choose your preferred page width.": "お好みのページ幅を選択してください",
"Confirm": "確認", "Confirm": "確認",
"Copy as Markdown": "Markdownとしてコピー",
"Copy link": "リンクをコピー", "Copy link": "リンクをコピー",
"Create": "新規作成", "Create": "新規作成",
"Create group": "グループを作成", "Create group": "グループを作成",
@@ -254,7 +253,6 @@
"Export failed:": "エクスポートに失敗しました:", "Export failed:": "エクスポートに失敗しました:",
"export error": "エクスポートエラー", "export error": "エクスポートエラー",
"Export page": "エクスポートページ", "Export page": "エクスポートページ",
"Export successful": "エクスポート成功",
"Export space": "エクスポートスペース", "Export space": "エクスポートスペース",
"Export {{type}}": "{{type}}をエクスポート", "Export {{type}}": "{{type}}をエクスポート",
"File exceeds the {{limit}} attachment limit": "ファイルが{{limit}}の添付制限を超えています", "File exceeds the {{limit}} attachment limit": "ファイルが{{limit}}の添付制限を超えています",
@@ -330,8 +328,6 @@
"Upload any image from your device.": "デバイスから画像をアップロードします", "Upload any image from your device.": "デバイスから画像をアップロードします",
"Upload any video from your device.": "デバイスから動画をアップロードします", "Upload any video from your device.": "デバイスから動画をアップロードします",
"Upload any file from your device.": "デバイスからファイルをアップロードします", "Upload any file from your device.": "デバイスからファイルをアップロードします",
"Uploading {{name}}": "{{name}} をアップロード中",
"Uploading file": "ファイルをアップロード中",
"Table": "テーブル", "Table": "テーブル",
"Insert a table.": "テーブルを挿入します", "Insert a table.": "テーブルを挿入します",
"Insert collapsible block.": "折りたたみブロックを挿入します", "Insert collapsible block.": "折りたたみブロックを挿入します",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "선호하는 인터페이스 언어를 선택하세요.", "Choose your preferred interface language.": "선호하는 인터페이스 언어를 선택하세요.",
"Choose your preferred page width.": "선호하는 페이지 너비를 선택하세요.", "Choose your preferred page width.": "선호하는 페이지 너비를 선택하세요.",
"Confirm": "확인", "Confirm": "확인",
"Copy as Markdown": "Markdown으로 복사",
"Copy link": "링크 복사", "Copy link": "링크 복사",
"Create": "생성", "Create": "생성",
"Create group": "팀 생성", "Create group": "팀 생성",
@@ -254,7 +253,6 @@
"Export failed:": "내보내기 실패:", "Export failed:": "내보내기 실패:",
"export error": "내보내기 오류", "export error": "내보내기 오류",
"Export page": "페이지 내보내기", "Export page": "페이지 내보내기",
"Export successful": "내보내기 성공",
"Export space": "Space 내보내기", "Export space": "Space 내보내기",
"Export {{type}}": "{{type}} 내보내기", "Export {{type}}": "{{type}} 내보내기",
"File exceeds the {{limit}} attachment limit": "첨부 파일 크기 제한 {{limit}}을 초과했습니다", "File exceeds the {{limit}} attachment limit": "첨부 파일 크기 제한 {{limit}}을 초과했습니다",
@@ -330,8 +328,6 @@
"Upload any image from your device.": "기기에서 이미지를 업로드하세요.", "Upload any image from your device.": "기기에서 이미지를 업로드하세요.",
"Upload any video from your device.": "기기에서 비디오를 업로드하세요.", "Upload any video from your device.": "기기에서 비디오를 업로드하세요.",
"Upload any file from your device.": "기기에서 파일을 업로드하세요.", "Upload any file from your device.": "기기에서 파일을 업로드하세요.",
"Uploading {{name}}": "{{name}} 업로드 중",
"Uploading file": "파일 업로드 중",
"Table": "테이블", "Table": "테이블",
"Insert a table.": "테이블 삽입.", "Insert a table.": "테이블 삽입.",
"Insert collapsible block.": "접을 수 있는 블록 삽입.", "Insert collapsible block.": "접을 수 있는 블록 삽입.",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "Kies uw gewenste interfacetaal.", "Choose your preferred interface language.": "Kies uw gewenste interfacetaal.",
"Choose your preferred page width.": "Kies uw gewenste paginabreedte.", "Choose your preferred page width.": "Kies uw gewenste paginabreedte.",
"Confirm": "Bevestig", "Confirm": "Bevestig",
"Copy as Markdown": "Kopiëren als Markdown",
"Copy link": "Link kopiëren", "Copy link": "Link kopiëren",
"Create": "Aanmaken", "Create": "Aanmaken",
"Create group": "Groep aanmaken", "Create group": "Groep aanmaken",
@@ -254,7 +253,6 @@
"Export failed:": "Exporteren mislukt:", "Export failed:": "Exporteren mislukt:",
"export error": "Exporteer fout", "export error": "Exporteer fout",
"Export page": "Exporteer pagina", "Export page": "Exporteer pagina",
"Export successful": "Export succesvol",
"Export space": "Exporteer ruimte", "Export space": "Exporteer ruimte",
"Export {{type}}": "Exporteer {{type}}", "Export {{type}}": "Exporteer {{type}}",
"File exceeds the {{limit}} attachment limit": "Bestand overschrijdt de bijlagelimiet van {{limit}}", "File exceeds the {{limit}} attachment limit": "Bestand overschrijdt de bijlagelimiet van {{limit}}",
@@ -330,8 +328,6 @@
"Upload any image from your device.": "Upload een afbeelding vanaf uw apparaat.", "Upload any image from your device.": "Upload een afbeelding vanaf uw apparaat.",
"Upload any video from your device.": "Upload een video vanaf uw apparaat.", "Upload any video from your device.": "Upload een video vanaf uw apparaat.",
"Upload any file from your device.": "Upload een bestand vanaf uw apparaat.", "Upload any file from your device.": "Upload een bestand vanaf uw apparaat.",
"Uploading {{name}}": "Uploaden {{name}}",
"Uploading file": "Bestand uploaden",
"Table": "Tabel", "Table": "Tabel",
"Insert a table.": "Voeg een tabel in.", "Insert a table.": "Voeg een tabel in.",
"Insert collapsible block.": "Inklapbaar blok invoegen.", "Insert collapsible block.": "Inklapbaar blok invoegen.",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "Escolha o idioma da interface.", "Choose your preferred interface language.": "Escolha o idioma da interface.",
"Choose your preferred page width.": "Escolha a largura preferida da página.", "Choose your preferred page width.": "Escolha a largura preferida da página.",
"Confirm": "Confirmar", "Confirm": "Confirmar",
"Copy as Markdown": "Copiar como Markdown",
"Copy link": "Copiar link", "Copy link": "Copiar link",
"Create": "Criar", "Create": "Criar",
"Create group": "Criar grupo", "Create group": "Criar grupo",
@@ -254,7 +253,6 @@
"Export failed:": "Falha ao exportar:", "Export failed:": "Falha ao exportar:",
"export error": "erro de exportação", "export error": "erro de exportação",
"Export page": "Exportar página", "Export page": "Exportar página",
"Export successful": "Exportação bem-sucedida",
"Export space": "Exportar espaço", "Export space": "Exportar espaço",
"Export {{type}}": "Exportar para {{type}}", "Export {{type}}": "Exportar para {{type}}",
"File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}", "File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}",
@@ -330,8 +328,6 @@
"Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.", "Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.",
"Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.", "Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.",
"Upload any file from your device.": "Envie qualquer arquivo do seu dispositivo.", "Upload any file from your device.": "Envie qualquer arquivo do seu dispositivo.",
"Uploading {{name}}": "Enviando {{name}}",
"Uploading file": "Enviando arquivo",
"Table": "Tabela", "Table": "Tabela",
"Insert a table.": "Insira uma tabela.", "Insert a table.": "Insira uma tabela.",
"Insert collapsible block.": "Insira um bloco colapsável.", "Insert collapsible block.": "Insira um bloco colapsável.",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "Выберите предпочитаемый язык интерфейса.", "Choose your preferred interface language.": "Выберите предпочитаемый язык интерфейса.",
"Choose your preferred page width.": "Выберите предпочитаемую ширину страницы.", "Choose your preferred page width.": "Выберите предпочитаемую ширину страницы.",
"Confirm": "Подтвердить", "Confirm": "Подтвердить",
"Copy as Markdown": "Копировать как Markdown",
"Copy link": "Копировать ссылку", "Copy link": "Копировать ссылку",
"Create": "Создать", "Create": "Создать",
"Create group": "Создать группу", "Create group": "Создать группу",
@@ -254,7 +253,6 @@
"Export failed:": "Экспортирование не удалось:", "Export failed:": "Экспортирование не удалось:",
"export error": "ошибка экспорта", "export error": "ошибка экспорта",
"Export page": "Экспорт страницы", "Export page": "Экспорт страницы",
"Export successful": "Экспорт выполнен успешно",
"Export space": "Экспорт пространства", "Export space": "Экспорт пространства",
"Export {{type}}": "Экспорт {{type}}", "Export {{type}}": "Экспорт {{type}}",
"File exceeds the {{limit}} attachment limit": "Файл превышает лимит вложений {{limit}}", "File exceeds the {{limit}} attachment limit": "Файл превышает лимит вложений {{limit}}",
@@ -330,8 +328,6 @@
"Upload any image from your device.": "Загрузить любое изображение с вашего устройства.", "Upload any image from your device.": "Загрузить любое изображение с вашего устройства.",
"Upload any video from your device.": "Загрузить любое видео с вашего устройства.", "Upload any video from your device.": "Загрузить любое видео с вашего устройства.",
"Upload any file from your device.": "Загрузить любой файл с вашего устройства.", "Upload any file from your device.": "Загрузить любой файл с вашего устройства.",
"Uploading {{name}}": "Загрузка {{name}}",
"Uploading file": "Загрузка файла",
"Table": "Таблица", "Table": "Таблица",
"Insert a table.": "Вставить таблицу.", "Insert a table.": "Вставить таблицу.",
"Insert collapsible block.": "Вставить сворачиваемый блок.", "Insert collapsible block.": "Вставить сворачиваемый блок.",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "Оберіть бажану мову інтерфейсу.", "Choose your preferred interface language.": "Оберіть бажану мову інтерфейсу.",
"Choose your preferred page width.": "Оберіть бажану ширину сторінки.", "Choose your preferred page width.": "Оберіть бажану ширину сторінки.",
"Confirm": "Підтвердити", "Confirm": "Підтвердити",
"Copy as Markdown": "Скопіювати як Markdown",
"Copy link": "Копіювати посилання", "Copy link": "Копіювати посилання",
"Create": "Створити", "Create": "Створити",
"Create group": "Створити групу", "Create group": "Створити групу",
@@ -254,7 +253,6 @@
"Export failed:": "Експортування не вдалося:", "Export failed:": "Експортування не вдалося:",
"export error": "помилка експорту", "export error": "помилка експорту",
"Export page": "Експорт сторінки", "Export page": "Експорт сторінки",
"Export successful": "Експорт виконано успішно",
"Export space": "Експорт простору", "Export space": "Експорт простору",
"Export {{type}}": "Експорт {{type}}", "Export {{type}}": "Експорт {{type}}",
"File exceeds the {{limit}} attachment limit": "Файл перевищує ліміт вкладень {{limit}}", "File exceeds the {{limit}} attachment limit": "Файл перевищує ліміт вкладень {{limit}}",
@@ -330,8 +328,6 @@
"Upload any image from your device.": "Завантажити будь-яке зображення з вашого пристрою.", "Upload any image from your device.": "Завантажити будь-яке зображення з вашого пристрою.",
"Upload any video from your device.": "Завантажити будь-яке відео з вашого пристрою.", "Upload any video from your device.": "Завантажити будь-яке відео з вашого пристрою.",
"Upload any file from your device.": "Завантажити будь-який файл з вашого пристрою.", "Upload any file from your device.": "Завантажити будь-який файл з вашого пристрою.",
"Uploading {{name}}": "Завантаження {{name}}",
"Uploading file": "Завантаження файлу",
"Table": "Таблиця", "Table": "Таблиця",
"Insert a table.": "Вставити таблицю.", "Insert a table.": "Вставити таблицю.",
"Insert collapsible block.": "Вставити блок, що згортається.", "Insert collapsible block.": "Вставити блок, що згортається.",
@@ -29,7 +29,6 @@
"Choose your preferred interface language.": "选择您喜欢的界面语言。", "Choose your preferred interface language.": "选择您喜欢的界面语言。",
"Choose your preferred page width.": "选择您喜欢的页面宽度。", "Choose your preferred page width.": "选择您喜欢的页面宽度。",
"Confirm": "确认", "Confirm": "确认",
"Copy as Markdown": "复制为Markdown",
"Copy link": "复制链接", "Copy link": "复制链接",
"Create": "创建", "Create": "创建",
"Create group": "创建群组", "Create group": "创建群组",
@@ -254,7 +253,6 @@
"Export failed:": "导出失败:", "Export failed:": "导出失败:",
"export error": "导出出错", "export error": "导出出错",
"Export page": "导出页面", "Export page": "导出页面",
"Export successful": "导出成功",
"Export space": "导出空间", "Export space": "导出空间",
"Export {{type}}": "导出为 {{type}}", "Export {{type}}": "导出为 {{type}}",
"File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制", "File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制",
@@ -330,8 +328,6 @@
"Upload any image from your device.": "从设备上传任何图像", "Upload any image from your device.": "从设备上传任何图像",
"Upload any video from your device.": "从设备上传任何视频", "Upload any video from your device.": "从设备上传任何视频",
"Upload any file from your device.": "从设备上传任何文件", "Upload any file from your device.": "从设备上传任何文件",
"Uploading {{name}}": "正在上传{{name}}",
"Uploading file": "正在上传文件",
"Table": "表格", "Table": "表格",
"Insert a table.": "插入一个表格", "Insert a table.": "插入一个表格",
"Insert collapsible block.": "插入一个折叠块", "Insert collapsible block.": "插入一个折叠块",
@@ -30,11 +30,9 @@ export default function ExportModal({
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown); const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
const [includeChildren, setIncludeChildren] = useState<boolean>(false); const [includeChildren, setIncludeChildren] = useState<boolean>(false);
const [includeAttachments, setIncludeAttachments] = useState<boolean>(false); const [includeAttachments, setIncludeAttachments] = useState<boolean>(false);
const [isExporting, setIsExporting] = useState<boolean>(false);
const { t } = useTranslation(); const { t } = useTranslation();
const handleExport = async () => { const handleExport = async () => {
setIsExporting(true);
try { try {
if (type === "page") { if (type === "page") {
await exportPage({ await exportPage({
@@ -47,9 +45,6 @@ export default function ExportModal({
if (type === "space") { if (type === "space") {
await exportSpace({ spaceId: id, format, includeAttachments }); await exportSpace({ spaceId: id, format, includeAttachments });
} }
notifications.show({
message: t("Export successful"),
});
onClose(); onClose();
} catch (err) { } catch (err) {
notifications.show({ notifications.show({
@@ -57,8 +52,6 @@ export default function ExportModal({
color: "red", color: "red",
}); });
console.error("export error", err); console.error("export error", err);
} finally {
setIsExporting(false);
} }
}; };
@@ -143,7 +136,7 @@ export default function ExportModal({
<Button onClick={onClose} variant="default"> <Button onClick={onClose} variant="default">
{t("Cancel")} {t("Cancel")}
</Button> </Button>
<Button onClick={handleExport} loading={isExporting}>{t("Export")}</Button> <Button onClick={handleExport}>{t("Export")}</Button>
</Group> </Group>
</Modal.Body> </Modal.Body>
</Modal.Content> </Modal.Content>
@@ -164,7 +164,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
const enterHandler = () => { const enterHandler = () => {
if (!renderItems.length) return; if (!renderItems.length) return;
if (renderItems[selectedIndex]?.entityType !== "header") { if (renderItems[selectedIndex].entityType !== "header") {
selectItem(selectedIndex); selectItem(selectedIndex);
} }
}; };
@@ -77,7 +77,7 @@ const mentionRenderItems = () => {
{ {
placement: "bottom-start", placement: "bottom-start",
middleware: [offset(0), flip(), shift()], middleware: [offset(0), flip(), shift()],
}, }
).then(({ x, y }) => { ).then(({ x, y }) => {
Object.assign(element.style, { Object.assign(element.style, {
left: `${x}px`, left: `${x}px`,
@@ -86,7 +86,7 @@ const mentionRenderItems = () => {
zIndex: "9999", zIndex: "9999",
}); });
}); });
}, }
); );
}, },
onUpdate: (props: { onUpdate: (props: {
@@ -115,30 +115,23 @@ const mentionRenderItems = () => {
// destroy component if space is greater 3 without a match // destroy component if space is greater 3 without a match
if ( if (
whitespaceCount > 4 && whitespaceCount > 3 &&
//@ts-ignore //@ts-ignore
props.editor.storage.mentionItems.length === 1 props.editor.storage.mentionItems.length === 0
) { ) {
destroy(); destroy();
return; return;
} }
// fallback exit
if (whitespaceCount > 7) {
destroy();
return;
}
}, },
onKeyDown: (props: { event: KeyboardEvent }) => { onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key === "Escape") { if (props.event.key)
destroy(); if (
return true; props.event.key === "Escape" ||
} (props.event.key === "Enter" && !component)
) {
if (props.event.key === "Enter" && !component) { destroy();
destroy(); return false;
return false; }
}
return (component?.ref as any)?.onKeyDown(props); return (component?.ref as any)?.onKeyDown(props);
}, },
onExit: () => { onExit: () => {
@@ -1,7 +1,7 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { posToDOMRect, findParentNode } from "@tiptap/react"; import { posToDOMRect, findParentNode } from "@tiptap/react";
import { Node as PMNode } from "@tiptap/pm/model"; import { Node as PMNode } from "@tiptap/pm/model";
import React, { useCallback } from "react"; import React, { JSX, useCallback } from "react";
import { ActionIcon, Tooltip } from "@mantine/core"; import { ActionIcon, Tooltip } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react"; import { IconTrash } from "@tabler/icons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -1,4 +1,4 @@
import React, { useCallback } from "react"; import React, { JSX, useCallback } from "react";
import { import {
EditorMenuProps, EditorMenuProps,
ShouldShowProps, ShouldShowProps,
@@ -1,6 +1,6 @@
import { posToDOMRect, findParentNode } from "@tiptap/react"; import { posToDOMRect, findParentNode } from "@tiptap/react";
import { Node as PMNode } from "@tiptap/pm/model"; import { Node as PMNode } from "@tiptap/pm/model";
import React, { useCallback } from "react"; import React, { JSX, useCallback } from "react";
import { import {
EditorMenuProps, EditorMenuProps,
ShouldShowProps, ShouldShowProps,
@@ -248,7 +248,7 @@ export const mainExtensions = [
Escape: () => { Escape: () => {
const event = new CustomEvent("closeFindDialogFromEditor", {}); const event = new CustomEvent("closeFindDialogFromEditor", {});
document.dispatchEvent(event); document.dispatchEvent(event);
return false; return true;
}, },
}; };
}, },
@@ -1,9 +1,8 @@
import "@/features/editor/styles/index.css"; import '@/features/editor/styles/index.css';
import React, { useEffect } from "react"; import React, { useEffect } from 'react';
import { EditorContent, useEditor } from "@tiptap/react"; import { EditorContent, useEditor } from '@tiptap/react';
import { mainExtensions } from "@/features/editor/extensions/extensions"; import { mainExtensions } from '@/features/editor/extensions/extensions';
import { Title } from "@mantine/core"; import { Title } from '@mantine/core';
import classes from "./history.module.css";
export interface HistoryEditorProps { export interface HistoryEditorProps {
title: string; title: string;
@@ -27,9 +26,7 @@ export function HistoryEditor({ title, content }: HistoryEditorProps) {
<div> <div>
<Title order={1}>{title}</Title> <Title order={1}>{title}</Title>
{editor && ( {editor && <EditorContent editor={editor} />}
<EditorContent editor={editor} className={classes.historyEditor} />
)}
</div> </div>
</> </>
); );
@@ -1,49 +1,37 @@
.history { .history {
display: block; display: block;
width: 100%; width: 100%;
padding: var(--mantine-spacing-md); padding: var(--mantine-spacing-md);
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
@mixin hover { @mixin hover {
background-color: light-dark( background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8));
var(--mantine-color-gray-2), }
var(--mantine-color-dark-8)
);
}
}
.historyEditor {
:global(.ProseMirror) {
padding: 0 !important;
}
} }
.active { .active {
background-color: light-dark( background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8));
var(--mantine-color-gray-2),
var(--mantine-color-dark-8)
);
} }
.sidebar { .sidebar {
max-height: rem(700px); max-height: rem(700px);
width: rem(250px); width: rem(250px);
padding: var(--mantine-spacing-sm); padding: var(--mantine-spacing-sm);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-right: rem(1px) solid border-right: rem(1px) solid
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
} }
.sidebarFlex { .sidebarFlex {
display: flex; display: flex;
} }
.sidebarMain { .sidebarMain {
flex: 1; flex: 1;
} }
.sidebarRightSection { .sidebarRightSection {
flex: 1; flex: 1;
padding: rem(16px) rem(40px); padding: rem(16px) rem(40px);
} }
@@ -7,7 +7,6 @@ import {
IconHistory, IconHistory,
IconLink, IconLink,
IconList, IconList,
IconMarkdown,
IconMessage, IconMessage,
IconPrinter, IconPrinter,
IconTrash, IconTrash,
@@ -29,7 +28,6 @@ import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx"; import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import ExportModal from "@/components/common/export-modal"; import ExportModal from "@/components/common/export-modal";
import { htmlToMarkdown } from "@docmost/editor-ext";
import { import {
pageEditorAtom, pageEditorAtom,
yjsConnectionStatusAtom, yjsConnectionStatusAtom,
@@ -62,7 +60,6 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
const event = new CustomEvent("closeFindDialogFromEditor", {}); const event = new CustomEvent("closeFindDialogFromEditor", {});
document.dispatchEvent(event); document.dispatchEvent(event);
}, },
{ preventDefault: false },
], ],
], ],
[], [],
@@ -131,15 +128,6 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
notifications.show({ message: t("Link copied") }); notifications.show({ message: t("Link copied") });
}; };
const handleCopyAsMarkdown = () => {
if (!pageEditor) return;
const html = pageEditor.getHTML();
const markdown = htmlToMarkdown(html);
const title = page?.title ? `# ${page.title}\n\n` : "";
clipboard.copy(`${title}${markdown}`);
notifications.show({ message: t("Copied") });
};
const handlePrint = () => { const handlePrint = () => {
setTimeout(() => { setTimeout(() => {
window.print(); window.print();
@@ -177,13 +165,6 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
> >
{t("Copy link")} {t("Copy link")}
</Menu.Item> </Menu.Item>
<Menu.Item
leftSection={<IconMarkdown size={16} />}
onClick={handleCopyAsMarkdown}
>
{t("Copy as Markdown")}
</Menu.Item>
<Menu.Divider /> <Menu.Divider />
<Menu.Item leftSection={<IconArrowsHorizontal size={16} />}> <Menu.Item leftSection={<IconArrowsHorizontal size={16} />}>
@@ -299,9 +280,7 @@ function ConnectionWarning() {
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => { useEffect(() => {
const isDisconnected = ["disconnected", "connecting"].includes( const isDisconnected = ["disconnected", "connecting"].includes(yjsConnectionStatus);
yjsConnectionStatus,
);
if (isDisconnected) { if (isDisconnected) {
if (!timeoutRef.current) { if (!timeoutRef.current) {
@@ -172,10 +172,6 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
queryKey: ["root-sidebar-pages", fileTask.spaceId], queryKey: ["root-sidebar-pages", fileTask.spaceId],
}); });
await queryClient.invalidateQueries({
queryKey: ["recent-changes", fileTask.spaceId],
});
setTimeout(() => { setTimeout(() => {
emit({ emit({
operation: "refetchRootTreeNodeEvent", operation: "refetchRootTreeNodeEvent",
@@ -163,6 +163,9 @@ export function useDeletePageMutation() {
export function useMovePageMutation() { export function useMovePageMutation() {
return useMutation<void, Error, IMovePage>({ return useMutation<void, Error, IMovePage>({
mutationFn: (data) => movePage(data), mutationFn: (data) => movePage(data),
onSuccess: () => {
invalidateOnMovePage();
},
}); });
} }
@@ -455,127 +458,17 @@ export function invalidateOnUpdatePage(
}); });
} }
export function updateCacheOnMovePage( export function invalidateOnMovePage() {
spaceId: string, //for move invalidate all sidebars for now (how to do???)
pageId: string, //invalidate all root sidebar pages
oldParentId: string | null, queryClient.invalidateQueries({
newParentId: string | null, queryKey: ["root-sidebar-pages"],
pageData: Partial<IPage>, });
) { //invalidate all sub sidebar pages
// Remove page from old parent's cache queryClient.invalidateQueries({
const oldQueryKey = queryKey: ["sidebar-pages"],
oldParentId === null });
? ["root-sidebar-pages", spaceId] // ---
: ["sidebar-pages", { pageId: oldParentId, spaceId }];
queryClient.setQueryData<InfiniteData<IPagination<IPage>>>(
oldQueryKey,
(old) => {
if (!old) return old;
return {
...old,
pages: old.pages.map((page) => ({
...page,
items: page.items.filter((item) => item.id !== pageId),
})),
};
},
);
// Update old parent's hasChildren flag if it has no more children
if (oldParentId !== null) {
const oldParentCache = queryClient.getQueryData<
InfiniteData<IPagination<IPage>>
>(["sidebar-pages", { pageId: oldParentId, spaceId }]);
const remainingChildren =
oldParentCache?.pages.flatMap((p) => p.items).length ?? 0;
if (remainingChildren === 0) {
// Update hasChildren in all caches where old parent appears
const allSideBarMatches = queryClient.getQueriesData({
predicate: (query) =>
query.queryKey[0] === "root-sidebar-pages" ||
query.queryKey[0] === "sidebar-pages",
});
allSideBarMatches.forEach(([key]) => {
queryClient.setQueryData<InfiniteData<IPagination<IPage>>>(
key,
(old) => {
if (!old) return old;
return {
...old,
pages: old.pages.map((page) => ({
...page,
items: page.items.map((item) =>
item.id === oldParentId
? { ...item, hasChildren: false }
: item,
),
})),
};
},
);
});
}
}
// Add page to new parent's cache
const newQueryKey =
newParentId === null
? ["root-sidebar-pages", spaceId]
: ["sidebar-pages", { pageId: newParentId, spaceId }];
queryClient.setQueryData<InfiniteData<IPagination<Partial<IPage>>>>(
newQueryKey,
(old) => {
if (!old) return old;
// Check if page already exists in new location
const exists = old.pages.some((page) =>
page.items.some((item) => item.id === pageId),
);
if (exists) return old;
return {
...old,
pages: old.pages.map((page, index) => {
if (index === old.pages.length - 1) {
return {
...page,
items: [...page.items, pageData],
};
}
return page;
}),
};
},
);
// Update new parent's hasChildren flag
if (newParentId !== null) {
const allSideBarMatches = queryClient.getQueriesData({
predicate: (query) =>
query.queryKey[0] === "root-sidebar-pages" ||
query.queryKey[0] === "sidebar-pages",
});
allSideBarMatches.forEach(([key]) => {
queryClient.setQueryData<InfiniteData<IPagination<IPage>>>(key, (old) => {
if (!old) return old;
return {
...old,
pages: old.pages.map((page) => ({
...page,
items: page.items.map((item) =>
item.id === newParentId ? { ...item, hasChildren: true } : item,
),
})),
};
});
});
}
} }
export function invalidateOnDeletePage(pageId: string) { export function invalidateOnDeletePage(pageId: string) {
@@ -118,14 +118,7 @@ export async function exportPage(data: IExportPageParams): Promise<void> {
.split("filename=")[1] .split("filename=")[1]
.replace(/"/g, ""); .replace(/"/g, "");
let decodedFileName = fileName; saveAs(req.data, decodeURIComponent(fileName));
try {
decodedFileName = decodeURIComponent(fileName);
} catch (err) {
// fallback to raw filename
}
saveAs(req.data, decodedFileName);
} }
export async function importPage(file: File, spaceId: string) { export async function importPage(file: File, spaceId: string) {
@@ -94,9 +94,9 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
spaceId, spaceId,
}); });
const [, setTreeApi] = useAtom<TreeApi<SpaceTreeNode>>(treeApiAtom); const [, setTreeApi] = useAtom<TreeApi<SpaceTreeNode>>(treeApiAtom);
const treeApiRef = useRef<TreeApi<SpaceTreeNode>>(); const treeApiRef = useRef<TreeApi<SpaceTreeNode>>(null);
const [openTreeNodes, setOpenTreeNodes] = useAtom<OpenMap>(openTreeNodesAtom); const [openTreeNodes, setOpenTreeNodes] = useAtom<OpenMap>(openTreeNodesAtom);
const rootElement = useRef<HTMLDivElement>(); const rootElement = useRef<HTMLDivElement>(null);
const [isRootReady, setIsRootReady] = useState(false); const [isRootReady, setIsRootReady] = useState(false);
const { ref: sizeRef, width, height } = useElementSize(); const { ref: sizeRef, width, height } = useElementSize();
const mergedRef = useMergedRef((element) => { const mergedRef = useMergedRef((element) => {
@@ -16,7 +16,6 @@ import {
useRemovePageMutation, useRemovePageMutation,
useMovePageMutation, useMovePageMutation,
useUpdatePageMutation, useUpdatePageMutation,
updateCacheOnMovePage,
} from "@/features/page/queries/page-query.ts"; } from "@/features/page/queries/page-query.ts";
import { generateJitteredKeyBetween } from "fractional-indexing-jittered"; import { generateJitteredKeyBetween } from "fractional-indexing-jittered";
import { SpaceTreeNode } from "@/features/page/tree/types.ts"; import { SpaceTreeNode } from "@/features/page/tree/types.ts";
@@ -176,25 +175,9 @@ export function useTreeMutation<T>(spaceId: string) {
parentPageId: args.parentId, parentPageId: args.parentId,
}; };
const draggedNode = args.dragNodes[0];
const nodeData = draggedNode.data as SpaceTreeNode;
const oldParentId = nodeData.parentPageId ?? null;
const pageData = {
id: nodeData.id,
slugId: nodeData.slugId,
title: nodeData.name,
icon: nodeData.icon,
position: newPosition,
spaceId: nodeData.spaceId,
parentPageId: args.parentId,
hasChildren: nodeData.hasChildren,
};
try { try {
await movePageMutation.mutateAsync(payload); await movePageMutation.mutateAsync(payload);
updateCacheOnMovePage(spaceId, draggedNodeId, oldParentId, args.parentId, pageData);
setTimeout(() => { setTimeout(() => {
emit({ emit({
operation: "moveTreeNode", operation: "moveTreeNode",
@@ -202,10 +185,8 @@ export function useTreeMutation<T>(spaceId: string) {
payload: { payload: {
id: draggedNodeId, id: draggedNodeId,
parentId: args.parentId, parentId: args.parentId,
oldParentId,
index: args.index, index: args.index,
position: newPosition, position: newPosition,
pageData,
}, },
}); });
}, 50); }, 50);
@@ -36,7 +36,7 @@ export default function SharedTree({ sharedPageTree }: SharedTree) {
const [tree, setTree] = useState< const [tree, setTree] = useState<
TreeApi<SharedPageTreeNode> | null | undefined TreeApi<SharedPageTreeNode> | null | undefined
>(null); >(null);
const rootElement = useRef<HTMLDivElement>(); const rootElement = useRef<HTMLDivElement>(null);
const { ref: sizeRef, width, height } = useElementSize(); const { ref: sizeRef, width, height } = useElementSize();
const mergedRef = useMergedRef(rootElement, sizeRef); const mergedRef = useMergedRef(rootElement, sizeRef);
const { pageSlug } = useParams(); const { pageSlug } = useParams();
@@ -6,7 +6,6 @@ import { ISpace } from "../types/space.types";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import APP_ROUTE from "@/lib/app-route"; import APP_ROUTE from "@/lib/app-route";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { useState } from "react";
interface DeleteSpaceModalProps { interface DeleteSpaceModalProps {
space: ISpace; space: ISpace;
@@ -15,7 +14,6 @@ interface DeleteSpaceModalProps {
export default function DeleteSpaceModal({ space }: DeleteSpaceModalProps) { export default function DeleteSpaceModal({ space }: DeleteSpaceModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [isDeleting, setIsDeleting] = useState(false);
const deleteSpaceMutation = useDeleteSpaceMutation(); const deleteSpaceMutation = useDeleteSpaceMutation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -37,15 +35,12 @@ export default function DeleteSpaceModal({ space }: DeleteSpaceModalProps) {
return; return;
} }
setIsDeleting(true);
try { try {
// pass slug too so we can clear the local cache // pass slug too so we can clear the local cache
await deleteSpaceMutation.mutateAsync({ id: space.id, slug: space.slug }); await deleteSpaceMutation.mutateAsync({ id: space.id, slug: space.slug });
navigate(APP_ROUTE.HOME); navigate(APP_ROUTE.HOME);
} catch (error) { } catch (error) {
console.error("Failed to delete space", error); console.error("Failed to delete space", error);
} finally {
setIsDeleting(false);
} }
}; };
@@ -84,7 +79,7 @@ export default function DeleteSpaceModal({ space }: DeleteSpaceModalProps) {
<Button onClick={close} variant="default"> <Button onClick={close} variant="default">
{t("Cancel")} {t("Cancel")}
</Button> </Button>
<Button onClick={handleDelete} color="red" loading={isDeleting}> <Button onClick={handleDelete} color="red">
{t("Confirm")} {t("Confirm")}
</Button> </Button>
</Group> </Group>
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
import { SpaceSelect } from "./space-select"; import { SpaceSelect } from "./space-select";
import { getSpaceUrl } from "@/lib/config"; import { getSpaceUrl } from "@/lib/config";
import { Button, Popover, Text } from "@mantine/core"; import { Button, Popover, Text } from "@mantine/core";
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { IconChevronDown } from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts"; import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
@@ -21,7 +21,7 @@ export function SwitchSpace({
spaceIcon, spaceIcon,
}: SwitchSpaceProps) { }: SwitchSpaceProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [opened, { close, toggle }] = useDisclosure(false); const [opened, { close, open, toggle }] = useDisclosure(false);
const handleSelect = (value: string) => { const handleSelect = (value: string) => {
if (value) { if (value) {
@@ -44,9 +44,9 @@ export function SwitchSpace({
variant="subtle" variant="subtle"
fullWidth fullWidth
justify="space-between" justify="space-between"
rightSection={opened ? <IconChevronUp size={18} /> : <IconChevronDown size={18} />} rightSection={<IconChevronDown size={18} />}
color="gray" color="gray"
onClick={toggle} onClick={open}
> >
<CustomAvatar <CustomAvatar
name={spaceName} name={spaceName}
@@ -69,12 +69,5 @@ export async function exportSpace(data: IExportSpaceParams): Promise<void> {
.split("filename=")[1] .split("filename=")[1]
.replace(/"/g, ""); .replace(/"/g, "");
let decodedFileName = fileName; saveAs(req.data, decodeURIComponent(fileName));
try {
decodedFileName = decodeURIComponent(fileName);
} catch (err) {
// fallback to raw filename
}
saveAs(req.data, decodedFileName);
} }
@@ -45,10 +45,8 @@ export type MoveTreeNodeEvent = {
payload: { payload: {
id: string; id: string;
parentId: string; parentId: string;
oldParentId: string | null;
index: number; index: number;
position: string; position: string;
pageData: Partial<IPage>;
}; };
}; };
@@ -8,7 +8,7 @@ import { IPagination } from "@/lib/types";
import { import {
invalidateOnCreatePage, invalidateOnCreatePage,
invalidateOnDeletePage, invalidateOnDeletePage,
updateCacheOnMovePage, invalidateOnMovePage,
invalidateOnUpdatePage, invalidateOnUpdatePage,
} from "../page/queries/page-query"; } from "../page/queries/page-query";
import { RQ_KEY } from "../comment/queries/comment-query"; import { RQ_KEY } from "../comment/queries/comment-query";
@@ -41,13 +41,7 @@ export const useQuerySubscription = () => {
invalidateOnCreatePage(data.payload.data); invalidateOnCreatePage(data.payload.data);
break; break;
case "moveTreeNode": case "moveTreeNode":
updateCacheOnMovePage( invalidateOnMovePage();
data.spaceId,
data.payload.id,
data.payload.oldParentId,
data.payload.parentId,
data.payload.pageData,
);
break; break;
case "deleteTreeNode": case "deleteTreeNode":
invalidateOnDeletePage(data.payload.node.id); invalidateOnDeletePage(data.payload.node.id);
@@ -41,8 +41,7 @@ import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
// see: https://github.com/ueberdosis/tiptap/issues/5352 // see: https://github.com/ueberdosis/tiptap/issues/5352
// see:https://github.com/ueberdosis/tiptap/issues/4089 // see:https://github.com/ueberdosis/tiptap/issues/4089
//import { generateJSON } from '@tiptap/html'; //import { generateJSON } from '@tiptap/html';
import { Node, Schema } from '@tiptap/pm/model'; import { Node } from '@tiptap/pm/model';
import { Logger } from '@nestjs/common';
export const tiptapExtensions = [ export const tiptapExtensions = [
StarterKit.configure({ StarterKit.configure({
@@ -111,53 +110,9 @@ export function jsonToText(tiptapJson: JSONContent) {
} }
export function jsonToNode(tiptapJson: JSONContent) { export function jsonToNode(tiptapJson: JSONContent) {
const schema = getSchema(tiptapExtensions); return Node.fromJSON(getSchema(tiptapExtensions), tiptapJson);
try {
return Node.fromJSON(schema, tiptapJson);
} catch (error) {
if (
error instanceof RangeError &&
error.message.includes('Unknown node type')
) {
Logger.warn('Stripping unknown node types from document:', error.message);
const cleanedJson = stripUnknownNodes(tiptapJson, schema);
return Node.fromJSON(schema, cleanedJson);
}
throw error;
}
} }
export function getPageId(documentName: string) { export function getPageId(documentName: string) {
return documentName.split('.')[1]; return documentName.split('.')[1];
} }
function stripUnknownNodes(
json: JSONContent,
schema: Schema,
): JSONContent | null {
if (!json || typeof json !== 'object') return json;
// Recursively clean children first, flattening any unwrapped content
if (json.content && Array.isArray(json.content)) {
const newContent: JSONContent[] = [];
for (const child of json.content) {
const cleaned = stripUnknownNodes(child, schema);
if (Array.isArray(cleaned)) {
newContent.push(...cleaned);
} else if (cleaned) {
newContent.push(cleaned);
}
}
json.content = newContent;
}
// Check if this node is unknown AFTER processing children
if (json.type && !schema.nodes[json.type]) {
// Unwrap: return cleaned children directly instead of wrapping
return (
json.content && json.content.length > 0 ? json.content : null
) as any;
}
return json;
}
@@ -1,14 +0,0 @@
export type ExportPageMetadata = {
pageId: string;
slugId: string;
icon: string | null;
position: string;
parentPath: string | null;
};
export type ExportMetadata = {
exportedAt: string;
source: 'docmost';
version: string;
pages: Record<string, ExportPageMetadata>;
};
+8 -15
View File
@@ -5,14 +5,13 @@ const CONTEXTS_TO_IGNORE = [
'InstanceLoader', 'InstanceLoader',
'RoutesResolver', 'RoutesResolver',
'RouterExplorer', 'RouterExplorer',
'LegacyRouteConverter',
'WebSocketsController', 'WebSocketsController',
]; ];
export function createPinoConfig(): Params { export function createPinoConfig(): Params {
const isProduction = process.env.NODE_ENV?.toLowerCase() === 'production'; const isProduction = process.env.NODE_ENV === 'production';
const isDebugMode = process.env.DEBUG_MODE?.toLowerCase() === 'true'; const isDebugMode = process.env.DEBUG_MODE === 'true';
const logHttp = process.env.LOG_HTTP?.toLowerCase() === 'true'; const logHttp = process.env.LOG_HTTP === 'true';
const level = isProduction && !isDebugMode ? 'info' : 'debug'; const level = isProduction && !isDebugMode ? 'info' : 'debug';
@@ -33,20 +32,14 @@ export function createPinoConfig(): Params {
: undefined, : undefined,
formatters: { formatters: {
level: (label) => ({ level: label }), level: (label) => ({ level: label }),
}, log: (object: Record<string, unknown>) => {
hooks: {
logMethod(inputArgs, method) {
if (isProduction && !isDebugMode) { if (isProduction && !isDebugMode) {
for (const arg of inputArgs) { const context = object['context'] as string | undefined;
if (typeof arg === 'object' && arg !== null && 'context' in arg) { if (context && CONTEXTS_TO_IGNORE.includes(context)) {
const context = (arg as Record<string, unknown>)['context']; return { filtered: true };
if (typeof context === 'string' && CONTEXTS_TO_IGNORE.includes(context)) {
return;
}
}
} }
} }
return method.apply(this, inputArgs); return object;
}, },
}, },
serializers: { serializers: {
@@ -181,9 +181,7 @@ export class AttachmentController {
} }
try { try {
const fileStream = await this.storageService.readStream( const fileStream = await this.storageService.read(attachment.filePath);
attachment.filePath,
);
res.headers({ res.headers({
'Content-Type': attachment.mimeType, 'Content-Type': attachment.mimeType,
'Cache-Control': 'private, max-age=3600', 'Cache-Control': 'private, max-age=3600',
@@ -243,9 +241,7 @@ export class AttachmentController {
} }
try { try {
const fileStream = await this.storageService.readStream( const fileStream = await this.storageService.read(attachment.filePath);
attachment.filePath,
);
res.headers({ res.headers({
'Content-Type': attachment.mimeType, 'Content-Type': attachment.mimeType,
'Cache-Control': 'public, max-age=3600', 'Cache-Control': 'public, max-age=3600',
@@ -371,14 +367,14 @@ export class AttachmentController {
const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`; const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`;
try { try {
const fileStream = await this.storageService.readStream(filePath); const fileStream = await this.storageService.read(filePath);
res.headers({ res.headers({
'Content-Type': getMimeType(filePath), 'Content-Type': getMimeType(filePath),
'Cache-Control': 'private, max-age=86400', 'Cache-Control': 'private, max-age=86400',
}); });
return res.send(fileStream); return res.send(fileStream);
} catch (err) { } catch (err) {
// this.logger.error(err); // this.logger.error(err);
throw new NotFoundException('File not found'); throw new NotFoundException('File not found');
} }
} }
@@ -55,7 +55,7 @@ export class ExportController {
throw new ForbiddenException(); throw new ForbiddenException();
} }
const zipFileStream = await this.exportService.exportPages( const zipFileBuffer = await this.exportService.exportPages(
dto.pageId, dto.pageId,
dto.format, dto.format,
dto.includeAttachments, dto.includeAttachments,
@@ -70,7 +70,7 @@ export class ExportController {
'attachment; filename="' + encodeURIComponent(fileName) + '"', 'attachment; filename="' + encodeURIComponent(fileName) + '"',
}); });
res.send(zipFileStream); res.send(zipFileBuffer);
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@@ -100,6 +100,6 @@ export class ExportController {
'"', '"',
}); });
res.send(exportFile.fileStream); res.send(exportFile.fileBuffer);
} }
} }
@@ -5,6 +5,7 @@ import {
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
import { jsonToHtml, jsonToNode } from '../../collaboration/collaboration.util'; import { jsonToHtml, jsonToNode } from '../../collaboration/collaboration.util';
import { turndown } from './turndown-utils';
import { ExportFormat } from './dto/export-dto'; import { ExportFormat } from './dto/export-dto';
import { Page } from '@docmost/db/types/entity.types'; import { Page } from '@docmost/db/types/entity.types';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
@@ -20,23 +21,16 @@ import {
replaceInternalLinks, replaceInternalLinks,
updateAttachmentUrlsToLocalPaths, updateAttachmentUrlsToLocalPaths,
} from './utils'; } from './utils';
import {
ExportMetadata,
ExportPageMetadata,
} from '../../common/helpers/types/export-metadata.types';
import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { Node } from '@tiptap/pm/model'; import { Node } from '@tiptap/pm/model';
import { EditorState } from '@tiptap/pm/state'; import { EditorState } from '@tiptap/pm/state';
// eslint-disable-next-line @typescript-eslint/no-require-imports // eslint-disable-next-line @typescript-eslint/no-require-imports
import slugify = require('@sindresorhus/slugify'); import slugify = require('@sindresorhus/slugify');
// eslint-disable-next-line @typescript-eslint/no-require-imports
const packageJson = require('../../../package.json');
import { EnvironmentService } from '../environment/environment.service'; import { EnvironmentService } from '../environment/environment.service';
import { import {
getAttachmentIds, getAttachmentIds,
getProsemirrorContent, getProsemirrorContent,
} from '../../common/helpers/prosemirror/utils'; } from '../../common/helpers/prosemirror/utils';
import { htmlToMarkdown } from '@docmost/editor-ext';
@Injectable() @Injectable()
export class ExportService { export class ExportService {
@@ -89,7 +83,7 @@ export class ExportService {
/<colgroup[^>]*>[\s\S]*?<\/colgroup>/gim, /<colgroup[^>]*>[\s\S]*?<\/colgroup>/gim,
'', '',
); );
return htmlToMarkdown(newPageHtml); return turndown(newPageHtml);
} }
return; return;
@@ -161,15 +155,12 @@ export class ExportService {
'pages.id', 'pages.id',
'pages.slugId', 'pages.slugId',
'pages.title', 'pages.title',
'pages.icon',
'pages.position',
'pages.content', 'pages.content',
'pages.parentPageId', 'pages.parentPageId',
'pages.spaceId', 'pages.spaceId',
'pages.workspaceId', 'pages.workspaceId',
]) ])
.where('spaceId', '=', spaceId) .where('spaceId', '=', spaceId)
.where('deletedAt', 'is', null)
.execute(); .execute();
const tree = buildTree(pages as Page[]); const tree = buildTree(pages as Page[]);
@@ -186,7 +177,7 @@ export class ExportService {
const fileName = `${space.name}-space-export.zip`; const fileName = `${space.name}-space-export.zip`;
return { return {
fileStream: zipFile, fileBuffer: zipFile,
fileName, fileName,
}; };
} }
@@ -198,12 +189,10 @@ export class ExportService {
includeAttachments: boolean, includeAttachments: boolean,
): Promise<void> { ): Promise<void> {
const slugIdToPath: Record<string, string> = {}; const slugIdToPath: Record<string, string> = {};
const pageIdToFilePath: Record<string, string> = {};
const pagesMetadata: Record<string, ExportPageMetadata> = {};
computeLocalPath(tree, format, null, '', slugIdToPath); computeLocalPath(tree, format, null, '', slugIdToPath);
const stack: { folder: JSZip; parentPageId: string | null }[] = [ const stack: { folder: JSZip; parentPageId: string }[] = [
{ folder: zip, parentPageId: null }, { folder: zip, parentPageId: null },
]; ];
@@ -243,33 +232,12 @@ export class ExportService {
`${pageTitle}${getExportExtension(format)}`, `${pageTitle}${getExportExtension(format)}`,
pageExportContent, pageExportContent,
); );
pageIdToFilePath[page.id] = currentPagePath;
const parentPath = parentPageId ? pageIdToFilePath[parentPageId] : null;
pagesMetadata[currentPagePath] = {
pageId: page.id,
slugId: page.slugId,
icon: page.icon ?? null,
position: page.position,
parentPath,
};
if (childPages.length > 0) { if (childPages.length > 0) {
const pageFolder = folder.folder(pageTitle); const pageFolder = folder.folder(pageTitle);
stack.push({ folder: pageFolder, parentPageId: page.id }); stack.push({ folder: pageFolder, parentPageId: page.id });
} }
} }
} }
const metadata: ExportMetadata = {
exportedAt: new Date().toISOString(),
source: 'docmost',
version: packageJson.version,
pages: pagesMetadata,
};
zip.file('docmost-metadata.json', JSON.stringify(metadata, null, 2));
} }
async zipAttachments(prosemirrorJson: any, spaceId: string, zip: JSZip) { async zipAttachments(prosemirrorJson: any, spaceId: string, zip: JSZip) {
@@ -1,22 +1,22 @@
import * as _TurndownService from '@joplin/turndown'; import * as TurndownService from '@joplin/turndown';
import * as TurndownPluginGfm from '@joplin/turndown-plugin-gfm'; import * as TurndownPluginGfm from '@joplin/turndown-plugin-gfm';
import { getBasename } from './basename'; import * as path from 'path';
// CJS/ESM interop: .default exists in Vite, not in NestJS export function turndown(html: string): string {
const TurndownService = (_TurndownService as any).default || _TurndownService;
export function htmlToMarkdown(html: string): string {
const turndownService = new TurndownService({ const turndownService = new TurndownService({
headingStyle: 'atx', headingStyle: 'atx',
codeBlockStyle: 'fenced', codeBlockStyle: 'fenced',
hr: '---', hr: '---',
bulletListMarker: '-', bulletListMarker: '-',
}); });
const tables = TurndownPluginGfm.tables;
const strikethrough = TurndownPluginGfm.strikethrough;
const highlightedCodeBlock = TurndownPluginGfm.highlightedCodeBlock;
turndownService.use([ turndownService.use([
TurndownPluginGfm.tables, tables,
TurndownPluginGfm.strikethrough, strikethrough,
TurndownPluginGfm.highlightedCodeBlock, highlightedCodeBlock,
taskList, taskList,
callout, callout,
preserveDetail, preserveDetail,
@@ -29,33 +29,34 @@ export function htmlToMarkdown(html: string): string {
return turndownService.turndown(html).replaceAll('<br>', ' '); return turndownService.turndown(html).replaceAll('<br>', ' ');
} }
function listParagraph(turndownService: _TurndownService) { function listParagraph(turndownService: TurndownService) {
turndownService.addRule('paragraph', { turndownService.addRule('paragraph', {
filter: ['p'], filter: ['p'],
replacement: (content: string, node: HTMLInputElement) => { replacement: (content: any, node: HTMLInputElement) => {
if (node.parentElement?.nodeName === 'LI') { if (node.parentElement?.nodeName === 'LI') {
return content; return content;
} }
return `\n\n${content}\n\n`; return `\n\n${content}\n\n`;
}, },
}); });
} }
function callout(turndownService: _TurndownService) { function callout(turndownService: TurndownService) {
turndownService.addRule('callout', { turndownService.addRule('callout', {
filter: function (node: HTMLInputElement) { filter: function (node: HTMLInputElement) {
return ( return (
node.nodeName === 'DIV' && node.getAttribute('data-type') === 'callout' node.nodeName === 'DIV' && node.getAttribute('data-type') === 'callout'
); );
}, },
replacement: function (content: string, node: HTMLInputElement) { replacement: function (content: any, node: HTMLInputElement) {
const calloutType = node.getAttribute('data-callout-type'); const calloutType = node.getAttribute('data-callout-type');
return `\n\n:::${calloutType}\n${content.trim()}\n:::\n\n`; return `\n\n:::${calloutType}\n${content.trim()}\n:::\n\n`;
}, },
}); });
} }
function taskList(turndownService: _TurndownService) { function taskList(turndownService: TurndownService) {
turndownService.addRule('taskListItem', { turndownService.addRule('taskListItem', {
filter: function (node: HTMLInputElement) { filter: function (node: HTMLInputElement) {
return ( return (
@@ -63,7 +64,7 @@ function taskList(turndownService: _TurndownService) {
node.parentNode.nodeName === 'UL' node.parentNode.nodeName === 'UL'
); );
}, },
replacement: function (content: string, node: HTMLInputElement) { replacement: function (content: any, node: HTMLInputElement) {
const checkbox = node.querySelector( const checkbox = node.querySelector(
'input[type="checkbox"]', 'input[type="checkbox"]',
) as HTMLInputElement; ) as HTMLInputElement;
@@ -78,21 +79,17 @@ function taskList(turndownService: _TurndownService) {
// Create the checkbox prefix // Create the checkbox prefix
const prefix = `- ${isChecked ? '[x]' : '[ ]'} `; const prefix = `- ${isChecked ? '[x]' : '[ ]'} `;
return ( return prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
prefix +
content +
(node.nextSibling && !/\n$/.test(content) ? '\n' : '')
);
}, },
}); });
} }
function preserveDetail(turndownService: _TurndownService) { function preserveDetail(turndownService: TurndownService) {
turndownService.addRule('preserveDetail', { turndownService.addRule('preserveDetail', {
filter: function (node: HTMLInputElement) { filter: function (node: HTMLInputElement) {
return node.nodeName === 'DETAILS'; return node.nodeName === 'DETAILS';
}, },
replacement: function (_content: string, node: HTMLInputElement) { replacement: function (content: any, node: HTMLInputElement) {
const summary = node.querySelector(':scope > summary'); const summary = node.querySelector(':scope > summary');
let detailSummary = ''; let detailSummary = '';
@@ -114,7 +111,7 @@ function preserveDetail(turndownService: _TurndownService) {
}); });
} }
function mathInline(turndownService: _TurndownService) { function mathInline(turndownService: TurndownService) {
turndownService.addRule('mathInline', { turndownService.addRule('mathInline', {
filter: function (node: HTMLInputElement) { filter: function (node: HTMLInputElement) {
return ( return (
@@ -122,13 +119,13 @@ function mathInline(turndownService: _TurndownService) {
node.getAttribute('data-type') === 'mathInline' node.getAttribute('data-type') === 'mathInline'
); );
}, },
replacement: function (content: string) { replacement: function (content: any, node: HTMLInputElement) {
return `$${content}$`; return `$${content}$`;
}, },
}); });
} }
function mathBlock(turndownService: _TurndownService) { function mathBlock(turndownService: TurndownService) {
turndownService.addRule('mathBlock', { turndownService.addRule('mathBlock', {
filter: function (node: HTMLInputElement) { filter: function (node: HTMLInputElement) {
return ( return (
@@ -136,32 +133,32 @@ function mathBlock(turndownService: _TurndownService) {
node.getAttribute('data-type') === 'mathBlock' node.getAttribute('data-type') === 'mathBlock'
); );
}, },
replacement: function (content: string) { replacement: function (content: any, node: HTMLInputElement) {
return `\n$$\n${content}\n$$\n`; return `\n$$\n${content}\n$$\n`;
}, },
}); });
} }
function iframeEmbed(turndownService: _TurndownService) { function iframeEmbed(turndownService: TurndownService) {
turndownService.addRule('iframeEmbed', { turndownService.addRule('iframeEmbed', {
filter: function (node: HTMLInputElement) { filter: function (node: HTMLInputElement) {
return node.nodeName === 'IFRAME'; return node.nodeName === 'IFRAME';
}, },
replacement: function (_content: string, node: HTMLInputElement) { replacement: function (content: any, node: HTMLInputElement) {
const src = node.getAttribute('src'); const src = node.getAttribute('src');
return '[' + src + '](' + src + ')'; return '[' + src + '](' + src + ')';
}, },
}); });
} }
function video(turndownService: _TurndownService) { function video(turndownService: TurndownService) {
turndownService.addRule('video', { turndownService.addRule('video', {
filter: function (node: HTMLInputElement) { filter: function (node: HTMLInputElement) {
return node.tagName === 'VIDEO'; return node.tagName === 'VIDEO';
}, },
replacement: function (_content: string, node: HTMLInputElement) { replacement: function (content: any, node: HTMLInputElement) {
const src = node.getAttribute('src') || ''; const src = node.getAttribute('src') || '';
const name = getBasename(src) || src; const name = path.basename(src);
return '[' + name + '](' + src + ')'; return '[' + name + '](' + src + ')';
}, },
}); });
+5 -15
View File
@@ -1,5 +1,4 @@
import { jsonToNode } from 'src/collaboration/collaboration.util'; import { jsonToNode } from 'src/collaboration/collaboration.util';
import { Logger } from '@nestjs/common';
import { ExportFormat } from './dto/export-dto'; import { ExportFormat } from './dto/export-dto';
import { Node } from '@tiptap/pm/model'; import { Node } from '@tiptap/pm/model';
import { validate as isValidUUID } from 'uuid'; import { validate as isValidUUID } from 'uuid';
@@ -89,7 +88,7 @@ export function replaceInternalLinks(
// if link and text are same, use page title // if link and text are same, use page title
if (markLink === node.text) { if (markLink === node.text) {
//@ts-expect-error //@ts-expect-error
node.text = getInternalLinkPageName(relativePath, currentPagePath); node.text = getInternalLinkPageName(relativePath);
} }
} }
} }
@@ -100,19 +99,10 @@ export function replaceInternalLinks(
return doc.toJSON(); return doc.toJSON();
} }
export function getInternalLinkPageName(path: string, currentFilePath?: string): string { export function getInternalLinkPageName(path: string): string {
const name = path?.split('/').pop().split('.').slice(0, -1).join('.'); return decodeURIComponent(
try { path?.split('/').pop().split('.').slice(0, -1).join('.'),
return decodeURIComponent(name); );
} catch (err) {
if (currentFilePath) {
Logger.warn(
`URI malformed in page ${currentFilePath}: ${name}. Falling back to raw name.`,
'ExportUtils',
);
}
return name;
}
} }
export function extractPageSlugId(input: string): string { export function extractPageSlugId(input: string): string {
@@ -15,5 +15,4 @@ export type ImportPageNode = {
parentPageId: string | null; parentPageId: string | null;
fileExtension: string; fileExtension: string;
filePath: string; filePath: string;
icon?: string | null;
}; };
@@ -24,8 +24,6 @@ import { formatImportHtml } from '../utils/import-formatter';
import { import {
buildAttachmentCandidates, buildAttachmentCandidates,
collectMarkdownAndHtmlFiles, collectMarkdownAndHtmlFiles,
encodeFilePath,
readDocmostMetadata,
stripNotionID, stripNotionID,
} from '../utils/import.utils'; } from '../utils/import.utils';
import { executeTx } from '@docmost/db/utils'; import { executeTx } from '@docmost/db/utils';
@@ -156,7 +154,6 @@ export class FileImportTaskService {
const { extractDir, fileTask } = opts; const { extractDir, fileTask } = opts;
const allFiles = await collectMarkdownAndHtmlFiles(extractDir); const allFiles = await collectMarkdownAndHtmlFiles(extractDir);
const attachmentCandidates = await buildAttachmentCandidates(extractDir); const attachmentCandidates = await buildAttachmentCandidates(extractDir);
const docmostMetadata = await readDocmostMetadata(extractDir);
const pagesMap = new Map<string, ImportPageNode>(); const pagesMap = new Map<string, ImportPageNode>();
@@ -167,9 +164,6 @@ export class FileImportTaskService {
.join('/'); // normalize to forward-slashes .join('/'); // normalize to forward-slashes
const ext = path.extname(relPath).toLowerCase(); const ext = path.extname(relPath).toLowerCase();
const encodedPath = encodeFilePath(relPath);
const pageMetadata = docmostMetadata?.pages[encodedPath];
pagesMap.set(relPath, { pagesMap.set(relPath, {
id: v7(), id: v7(),
slugId: generateSlugId(), slugId: generateSlugId(),
@@ -178,7 +172,6 @@ export class FileImportTaskService {
parentPageId: null, parentPageId: null,
fileExtension: ext, fileExtension: ext,
filePath: relPath, filePath: relPath,
icon: pageMetadata?.icon ?? null,
}); });
} }
@@ -231,8 +224,6 @@ export class FileImportTaskService {
if (!pagesMap.has(mdPath) && !pagesMap.has(htmlPath)) { if (!pagesMap.has(mdPath) && !pagesMap.has(htmlPath)) {
const folderName = path.basename(folderPath); const folderName = path.basename(folderPath);
const encodedMdPath = encodeFilePath(mdPath);
const placeholderMetadata = docmostMetadata?.pages[encodedMdPath];
pagesMap.set(mdPath, { pagesMap.set(mdPath, {
id: v7(), id: v7(),
slugId: generateSlugId(), slugId: generateSlugId(),
@@ -241,7 +232,6 @@ export class FileImportTaskService {
parentPageId: null, parentPageId: null,
fileExtension: '.md', fileExtension: '.md',
filePath: mdPath, filePath: mdPath,
icon: placeholderMetadata?.icon ?? null,
}); });
} }
}); });
@@ -276,39 +266,11 @@ export class FileImportTaskService {
siblingsMap.set(page.parentPageId, group); siblingsMap.set(page.parentPageId, group);
}); });
const encodedPathsMap = new Map<string, string>();
if (docmostMetadata) {
pagesMap.forEach((_, filePath) => {
encodedPathsMap.set(filePath, encodeFilePath(filePath));
});
}
// Sort siblings by metadata position if available, otherwise alphabetically
const sortSiblings = (siblings: ImportPageNode[]) => {
if (docmostMetadata) {
siblings.sort((a, b) => {
const posA =
docmostMetadata.pages[encodedPathsMap.get(a.filePath)]?.position;
const posB =
docmostMetadata.pages[encodedPathsMap.get(b.filePath)]?.position;
if (posA && posB) {
// Use direct comparison to match PostgreSQL collation 'C' (byte order)
if (posA < posB) return -1;
if (posA > posB) return 1;
return 0;
}
return a.name.localeCompare(b.name);
});
} else {
siblings.sort((a, b) => a.name.localeCompare(b.name));
}
};
// get root pages // get root pages
const rootSibs = siblingsMap.get(null); const rootSibs = siblingsMap.get(null);
if (rootSibs?.length) { if (rootSibs?.length) {
sortSiblings(rootSibs); rootSibs.sort((a, b) => a.name.localeCompare(b.name));
// get first position key from the server // get first position key from the server
const nextPosition = await this.pageService.nextPagePosition( const nextPosition = await this.pageService.nextPagePosition(
@@ -330,7 +292,7 @@ export class FileImportTaskService {
siblingsMap.forEach((sibs, parentId) => { siblingsMap.forEach((sibs, parentId) => {
if (parentId === null) return; // root already done if (parentId === null) return; // root already done
sortSiblings(sibs); sibs.sort((a, b) => a.name.localeCompare(b.name));
let prevPos: string | null = null; let prevPos: string | null = null;
for (const page of sibs) { for (const page of sibs) {
@@ -464,7 +426,7 @@ export class FileImportTaskService {
id: page.id, id: page.id,
slugId: page.slugId, slugId: page.slugId,
title: title || page.name, title: title || page.name,
icon: page.icon || pageIcon || null, icon: pageIcon || null,
content: prosemirrorJson, content: prosemirrorJson,
textContent: jsonToText(prosemirrorJson), textContent: jsonToText(prosemirrorJson),
ydoc: await this.importService.createYdoc(prosemirrorJson), ydoc: await this.importService.createYdoc(prosemirrorJson),
@@ -1,5 +1,4 @@
import { getEmbedUrlAndProvider } from '@docmost/editor-ext'; import { getEmbedUrlAndProvider } from '@docmost/editor-ext';
import { Logger } from '@nestjs/common';
import * as path from 'path'; import * as path from 'path';
import { v7 } from 'uuid'; import { v7 } from 'uuid';
import { InsertableBacklink } from '@docmost/db/types/entity.types'; import { InsertableBacklink } from '@docmost/db/types/entity.types';
@@ -281,18 +280,8 @@ export async function rewriteInternalLinksToMentionHtml(
const $a = $(el); const $a = $(el);
const raw = $a.attr('href')!; const raw = $a.attr('href')!;
if (raw.startsWith('http') || raw.startsWith('/api/')) return; if (raw.startsWith('http') || raw.startsWith('/api/')) return;
let decodedRaw = raw;
try {
decodedRaw = decodeURIComponent(raw);
} catch (err) {
Logger.warn(
`URI malformed in page ${currentFilePath}: ${raw}. Falling back to raw path.`,
'ImportFormatter',
);
}
const resolved = normalize( const resolved = normalize(
path.join(path.dirname(currentFilePath), decodedRaw), path.join(path.dirname(currentFilePath), decodeURIComponent(raw)),
); );
const meta = filePathToPageMetaMap.get(resolved); const meta = filePathToPageMetaMap.get(resolved);
if (!meta) return; if (!meta) return;
@@ -1,7 +1,5 @@
import { Logger } from '@nestjs/common';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as path from 'path'; import * as path from 'path';
import { ExportMetadata } from '../../../common/helpers/types/export-metadata.types';
export async function buildAttachmentCandidates( export async function buildAttachmentCandidates(
extractDir: string, extractDir: string,
@@ -32,19 +30,8 @@ export function resolveRelativeAttachmentPath(
pageDir: string, pageDir: string,
attachmentCandidates: Map<string, string>, attachmentCandidates: Map<string, string>,
): string | null { ): string | null {
let mainRel = raw.replace(/^\.?\/+/, ''); const mainRel = decodeURIComponent(raw.replace(/^\.?\/+/, ''));
try { const fallback = path.normalize(path.join(pageDir, mainRel));
mainRel = decodeURIComponent(mainRel);
} catch (err) {
Logger.warn(
`URI malformed for attachment path: ${mainRel}. Falling back to raw path.`,
'ImportUtils',
);
}
const fallback = path
.normalize(path.join(pageDir, mainRel))
.split(path.sep)
.join('/');
if (attachmentCandidates.has(mainRel)) { if (attachmentCandidates.has(mainRel)) {
return mainRel; return mainRel;
@@ -83,26 +70,3 @@ export function stripNotionID(fileName: string): string {
const notionIdPattern = /[ -]?[a-z0-9]{32}$/i; const notionIdPattern = /[ -]?[a-z0-9]{32}$/i;
return fileName.replace(notionIdPattern, '').trim(); return fileName.replace(notionIdPattern, '').trim();
} }
export function encodeFilePath(filePath: string): string {
return filePath
.split('/')
.map((segment) => encodeURIComponent(segment))
.join('/');
}
export async function readDocmostMetadata(
extractDir: string,
): Promise<ExportMetadata | null> {
const metadataPath = path.join(extractDir, 'docmost-metadata.json');
try {
const content = await fs.readFile(metadataPath, 'utf-8');
const metadata = JSON.parse(content) as ExportMetadata;
if (metadata.source === 'docmost' && metadata.pages) {
return metadata;
}
return null;
} catch {
return null;
}
}
+27 -28
View File
@@ -30,33 +30,33 @@
"@joplin/turndown": "^4.0.74", "@joplin/turndown": "^4.0.74",
"@joplin/turndown-plugin-gfm": "^1.0.56", "@joplin/turndown-plugin-gfm": "^1.0.56",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@tiptap/core": "3.17.1", "@tiptap/core": "3.17.0",
"@tiptap/extension-code-block": "3.17.1", "@tiptap/extension-code-block": "3.17.0",
"@tiptap/extension-collaboration": "3.17.1", "@tiptap/extension-collaboration": "3.17.0",
"@tiptap/extension-collaboration-caret": "3.17.1", "@tiptap/extension-collaboration-caret": "3.17.0",
"@tiptap/extension-color": "3.17.1", "@tiptap/extension-color": "3.17.0",
"@tiptap/extension-document": "3.17.1", "@tiptap/extension-document": "3.17.0",
"@tiptap/extension-heading": "3.17.1", "@tiptap/extension-heading": "3.17.0",
"@tiptap/extension-highlight": "3.17.1", "@tiptap/extension-highlight": "3.17.0",
"@tiptap/extension-history": "3.17.1", "@tiptap/extension-history": "3.17.0",
"@tiptap/extension-image": "3.17.1", "@tiptap/extension-image": "3.17.0",
"@tiptap/extension-link": "3.17.1", "@tiptap/extension-link": "3.17.0",
"@tiptap/extension-list": "3.17.1", "@tiptap/extension-list": "3.17.0",
"@tiptap/extension-placeholder": "3.17.1", "@tiptap/extension-placeholder": "3.17.0",
"@tiptap/extension-subscript": "3.17.1", "@tiptap/extension-subscript": "3.17.0",
"@tiptap/extension-superscript": "3.17.1", "@tiptap/extension-superscript": "3.17.0",
"@tiptap/extension-table": "3.17.1", "@tiptap/extension-table": "3.17.0",
"@tiptap/extension-text": "3.17.1", "@tiptap/extension-text": "3.17.0",
"@tiptap/extension-text-align": "3.17.1", "@tiptap/extension-text-align": "3.17.0",
"@tiptap/extension-text-style": "3.17.1", "@tiptap/extension-text-style": "3.17.0",
"@tiptap/extension-typography": "3.17.1", "@tiptap/extension-typography": "3.17.0",
"@tiptap/extension-unique-id": "^3.17.1", "@tiptap/extension-unique-id": "^3.17.0",
"@tiptap/extension-youtube": "3.17.1", "@tiptap/extension-youtube": "3.17.0",
"@tiptap/html": "3.17.1", "@tiptap/html": "3.17.0",
"@tiptap/pm": "3.17.1", "@tiptap/pm": "3.17.0",
"@tiptap/react": "3.17.1", "@tiptap/react": "3.17.0",
"@tiptap/starter-kit": "3.17.1", "@tiptap/starter-kit": "3.17.0",
"@tiptap/suggestion": "3.17.1", "@tiptap/suggestion": "3.17.0",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"bytes": "^3.1.2", "bytes": "^3.1.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@@ -79,7 +79,6 @@
"devDependencies": { "devDependencies": {
"@nx/js": "20.4.5", "@nx/js": "20.4.5",
"@types/bytes": "^3.1.5", "@types/bytes": "^3.1.5",
"@types/turndown": "^5.0.6",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"nx": "20.4.5", "nx": "20.4.5",
-4
View File
@@ -1,4 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all"
}
@@ -99,7 +99,7 @@ export const embedProviders: IEmbedProvider[] = [
id: "gsheets", id: "gsheets",
name: "Google Sheets", name: "Google Sheets",
regex: regex:
/^((?:https?:)?\/\/)?((?:www|m)\.)?(docs\.google\.com)\/spreadsheets\/d\/([a-zA-Z0-9_-]+)\/.*$/, /^((?:https?:)?\/\/)?((?:www|m)\.)?(docs\.google\.com)\/spreadsheets\/d\/e\/([a-zA-Z0-9_-]+)\/.*$/,
getEmbedUrl: (match, url: string) => { getEmbedUrl: (match, url: string) => {
return url; return url;
}, },
@@ -1,2 +1 @@
export * from "./utils/marked.utils"; export * from "./utils/marked.utils";
export * from "./utils/turndown.utils";
@@ -1,29 +0,0 @@
/**
* Flexible `basename` implementation for node and the browser
* @see https://stackoverflow.com/a/59907288/2228771
*/
export function getBasename(path: string) {
// make sure the basename is not empty, if string ends with separator
let end = path.length - 1;
while (path[end] === '/' || path[end] === '\\') {
--end;
}
// support mixing of Win + Unix path separators
const i1 = path.lastIndexOf('/', end);
const i2 = path.lastIndexOf('\\', end);
let start: number;
if (i1 === -1) {
if (i2 === -1) {
// no separator in the whole thing
return path;
}
start = i2;
} else if (i2 === -1) {
start = i1;
} else {
start = Math.max(i1, i2);
}
return path.substring(start + 1, end + 1);
}
@@ -1,12 +0,0 @@
// Map @joplin/turndown types to @types/turndown
declare module "@joplin/turndown" {
import TurndownService from "turndown";
export = TurndownService;
}
declare module "@joplin/turndown-plugin-gfm" {
import TurndownService from "turndown";
export const tables: TurndownService.Plugin;
export const strikethrough: TurndownService.Plugin;
export const highlightedCodeBlock: TurndownService.Plugin;
}
+1485 -923
View File
File diff suppressed because it is too large Load Diff