mirror of
https://github.com/docmost/docmost.git
synced 2026-05-08 07:13:06 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a86890d856 | |||
| bef23b6738 |
+12
-12
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.25.3",
|
"version": "0.25.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
@@ -14,18 +14,18 @@
|
|||||||
"@docmost/editor-ext": "workspace:*",
|
"@docmost/editor-ext": "workspace:*",
|
||||||
"@emoji-mart/data": "^1.2.1",
|
"@emoji-mart/data": "^1.2.1",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@excalidraw/excalidraw": "0.18.0-3a5ef40",
|
"@excalidraw/excalidraw": "0.18.0-c158187",
|
||||||
"@mantine/core": "^8.3.14",
|
"@mantine/core": "^8.3.12",
|
||||||
"@mantine/dates": "^8.3.14",
|
"@mantine/dates": "^8.3.12",
|
||||||
"@mantine/form": "^8.3.14",
|
"@mantine/form": "^8.3.12",
|
||||||
"@mantine/hooks": "^8.3.14",
|
"@mantine/hooks": "^8.3.12",
|
||||||
"@mantine/modals": "^8.3.14",
|
"@mantine/modals": "^8.3.12",
|
||||||
"@mantine/notifications": "^8.3.14",
|
"@mantine/notifications": "^8.3.12",
|
||||||
"@mantine/spotlight": "^8.3.14",
|
"@mantine/spotlight": "^8.3.12",
|
||||||
"@tabler/icons-react": "^3.36.1",
|
"@tabler/icons-react": "^3.36.1",
|
||||||
"@tanstack/react-query": "^5.90.17",
|
"@tanstack/react-query": "^5.90.17",
|
||||||
"alfaaz": "^1.1.0",
|
"alfaaz": "^1.1.0",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"emoji-mart": "^5.6.0",
|
"emoji-mart": "^5.6.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"mantine-form-zod-resolver": "^1.3.0",
|
"mantine-form-zod-resolver": "^1.3.0",
|
||||||
"mermaid": "^11.12.2",
|
"mermaid": "^11.12.2",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"posthog-js": "1.345.5",
|
"posthog-js": "^1.255.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-arborist": "3.4.0",
|
"react-arborist": "3.4.0",
|
||||||
"react-clear-modal": "^2.0.17",
|
"react-clear-modal": "^2.0.17",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.15.0",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.16",
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"Date": "Datum",
|
"Date": "Datum",
|
||||||
"Delete": "Löschen",
|
"Delete": "Löschen",
|
||||||
"Delete group": "Gruppe löschen",
|
"Delete group": "Gruppe löschen",
|
||||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dabei werden auch alle Unterseiten und der Seitenverlauf gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
|
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dadurch werden ihre Unterseiten und die Seitengeschichte gelöscht. Diese Aktion ist unwiderruflich.",
|
||||||
"Description": "Beschreibung",
|
"Description": "Beschreibung",
|
||||||
"Details": "Details",
|
"Details": "Details",
|
||||||
"e.g ACME": "z.B. ACME",
|
"e.g ACME": "z.B. ACME",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"Enter your new preferred email": "Geben Sie Ihre neue bevorzugte E-Mail ein",
|
"Enter your new preferred email": "Geben Sie Ihre neue bevorzugte E-Mail ein",
|
||||||
"Enter your password": "Geben Sie Ihr Passwort ein",
|
"Enter your password": "Geben Sie Ihr Passwort ein",
|
||||||
"Error fetching page data.": "Fehler beim Abrufen der Seitendaten.",
|
"Error fetching page data.": "Fehler beim Abrufen der Seitendaten.",
|
||||||
"Error loading page history.": "Fehler beim Laden des Seitenverlaufs.",
|
"Error loading page history.": "Fehler beim Laden der Seitengeschichte.",
|
||||||
"Export": "Exportieren",
|
"Export": "Exportieren",
|
||||||
"Failed to create page": "Erstellung der Seite fehlgeschlagen",
|
"Failed to create page": "Erstellung der Seite fehlgeschlagen",
|
||||||
"Failed to delete page": "Löschen der Seite fehlgeschlagen",
|
"Failed to delete page": "Löschen der Seite fehlgeschlagen",
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
"New page": "Neue Seite",
|
"New page": "Neue Seite",
|
||||||
"New password": "Neues Passwort",
|
"New password": "Neues Passwort",
|
||||||
"No group found": "Keine Gruppe gefunden",
|
"No group found": "Keine Gruppe gefunden",
|
||||||
"No page history saved yet.": "Es wurde noch kein Seitenverlauf gespeichert.",
|
"No page history saved yet.": "Es wurde noch keine Seitengeschichte gespeichert.",
|
||||||
"No pages yet": "Noch keine Seiten",
|
"No pages yet": "Noch keine Seiten",
|
||||||
"No results found...": "Keine Ergebnisse gefunden...",
|
"No results found...": "Keine Ergebnisse gefunden...",
|
||||||
"No user found": "Kein Benutzer gefunden",
|
"No user found": "Kein Benutzer gefunden",
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
"Owner": "Besitzer",
|
"Owner": "Besitzer",
|
||||||
"page": "Seite",
|
"page": "Seite",
|
||||||
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
||||||
"Page history": "Seitenverlauf",
|
"Page history": "Seitengeschichte",
|
||||||
"Select version": "Version auswählen",
|
"Select version": "Version auswählen",
|
||||||
"Highlight changes": "Änderungen hervorheben",
|
"Highlight changes": "Änderungen hervorheben",
|
||||||
"Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.",
|
"Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.",
|
||||||
@@ -407,21 +407,6 @@
|
|||||||
"Share deleted successfully": "Freigabe erfolgreich gelöscht",
|
"Share deleted successfully": "Freigabe erfolgreich gelöscht",
|
||||||
"Share not found": "Freigabe nicht gefunden",
|
"Share not found": "Freigabe nicht gefunden",
|
||||||
"Failed to share page": "Fehler beim Teilen der Seite",
|
"Failed to share page": "Fehler beim Teilen der Seite",
|
||||||
"Disable public sharing": "Öffentliches Teilen deaktivieren",
|
|
||||||
"Prevent members from sharing pages publicly.": "Verhindern Sie, dass Mitglieder Seiten öffentlich teilen.",
|
|
||||||
"Toggle public sharing": "Öffentliches Teilen umschalten",
|
|
||||||
"Toggle space public sharing": "Öffentliches Teilen im Bereich umschalten",
|
|
||||||
"Public sharing is disabled at the workspace level": "Öffentliches Teilen ist auf der Arbeitsbereichsebene deaktiviert",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "Verhindern Sie, dass Seiten in diesem Bereich öffentlich geteilt werden.",
|
|
||||||
"Requires an enterprise license": "Erfordert eine Unternehmenslizenz",
|
|
||||||
"Enable public sharing": "Öffentliches Teilen aktivieren",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Sind Sie sicher, dass Sie das öffentliche Teilen aktivieren möchten? Mitglieder können Seiten öffentlich teilen.",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Sind Sie sicher, dass Sie das öffentliche Teilen deaktivieren möchten? Alle bestehenden Freigabelinks in diesem Arbeitsbereich werden gelöscht.",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "Sind Sie sicher, dass Sie das öffentliche Teilen für diesen Bereich aktivieren möchten?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Sind Sie sicher, dass Sie das öffentliche Teilen deaktivieren möchten? Alle bestehenden Freigabelinks in diesem Bereich werden gelöscht.",
|
|
||||||
"Public sharing is disabled": "Öffentliches Teilen ist deaktiviert",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "Das öffentliche Teilen wurde auf der Arbeitsbereichsebene deaktiviert.",
|
|
||||||
"Public sharing has been disabled for this space.": "Das öffentliche Teilen wurde für diesen Bereich deaktiviert.",
|
|
||||||
"Copy page": "Seite kopieren",
|
"Copy page": "Seite kopieren",
|
||||||
"Copy page to a different space.": "Seite in einen anderen Bereich kopieren.",
|
"Copy page to a different space.": "Seite in einen anderen Bereich kopieren.",
|
||||||
"Page copied successfully": "Seite erfolgreich kopiert",
|
"Page copied successfully": "Seite erfolgreich kopiert",
|
||||||
|
|||||||
@@ -407,21 +407,6 @@
|
|||||||
"Share deleted successfully": "Compartición eliminada con éxito",
|
"Share deleted successfully": "Compartición eliminada con éxito",
|
||||||
"Share not found": "Compartición no encontrada",
|
"Share not found": "Compartición no encontrada",
|
||||||
"Failed to share page": "Error al compartir la página",
|
"Failed to share page": "Error al compartir la página",
|
||||||
"Disable public sharing": "Desactivar el uso compartido público",
|
|
||||||
"Prevent members from sharing pages publicly.": "Evitar que los miembros compartan páginas públicamente.",
|
|
||||||
"Toggle public sharing": "Alternar el uso compartido público",
|
|
||||||
"Toggle space public sharing": "Alternar el uso compartido público del espacio",
|
|
||||||
"Public sharing is disabled at the workspace level": "El uso compartido público está desactivado a nivel de espacio de trabajo",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "Evitar que las páginas en este espacio se compartan públicamente.",
|
|
||||||
"Requires an enterprise license": "Requiere una licencia empresarial",
|
|
||||||
"Enable public sharing": "Activar el uso compartido público",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "¿Está seguro de que desea activar el uso compartido público? Los miembros podrán compartir páginas públicamente.",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "¿Está seguro de que desea desactivar el uso compartido público? Todos los enlaces compartidos existentes en este espacio de trabajo se eliminarán.",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "¿Está seguro de que desea activar el uso compartido público para este espacio?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "¿Está seguro de que desea desactivar el uso compartido público? Todos los enlaces compartidos existentes en este espacio se eliminarán.",
|
|
||||||
"Public sharing is disabled": "El uso compartido público está desactivado",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "El uso compartido público se ha desactivado a nivel de espacio de trabajo.",
|
|
||||||
"Public sharing has been disabled for this space.": "El uso compartido público se ha desactivado para este espacio.",
|
|
||||||
"Copy page": "Copiar página",
|
"Copy page": "Copiar página",
|
||||||
"Copy page to a different space.": "Copiar página en otro espacio",
|
"Copy page to a different space.": "Copiar página en otro espacio",
|
||||||
"Page copied successfully": "Página copiada exitosamente",
|
"Page copied successfully": "Página copiada exitosamente",
|
||||||
|
|||||||
@@ -407,21 +407,6 @@
|
|||||||
"Share deleted successfully": "Partage supprimé avec succès",
|
"Share deleted successfully": "Partage supprimé avec succès",
|
||||||
"Share not found": "Partage non trouvé",
|
"Share not found": "Partage non trouvé",
|
||||||
"Failed to share page": "Échec du partage de la page",
|
"Failed to share page": "Échec du partage de la page",
|
||||||
"Disable public sharing": "Désactiver le partage public",
|
|
||||||
"Prevent members from sharing pages publicly.": "Empêcher les membres de partager des pages publiquement.",
|
|
||||||
"Toggle public sharing": "Basculer le partage public",
|
|
||||||
"Toggle space public sharing": "Basculer le partage public de l'espace",
|
|
||||||
"Public sharing is disabled at the workspace level": "Le partage public est désactivé au niveau de l'espace de travail",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "Empêcher les pages de cet espace d'être partagées publiquement.",
|
|
||||||
"Requires an enterprise license": "Nécessite une licence d'entreprise",
|
|
||||||
"Enable public sharing": "Activer le partage public",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Êtes-vous sûr de vouloir activer le partage public ? Les membres pourront partager des pages publiquement.",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Êtes-vous sûr de vouloir désactiver le partage public ? Tous les liens partagés existants dans cet espace de travail seront supprimés.",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "Êtes-vous sûr de vouloir activer le partage public pour cet espace ?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Êtes-vous sûr de vouloir désactiver le partage public ? Tous les liens partagés existants dans cet espace seront supprimés.",
|
|
||||||
"Public sharing is disabled": "Le partage public est désactivé",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "Le partage public a été désactivé au niveau de l'espace de travail.",
|
|
||||||
"Public sharing has been disabled for this space.": "Le partage public a été désactivé pour cet espace.",
|
|
||||||
"Copy page": "Copier la page",
|
"Copy page": "Copier la page",
|
||||||
"Copy page to a different space.": "Copier la page dans un autre espace.",
|
"Copy page to a different space.": "Copier la page dans un autre espace.",
|
||||||
"Page copied successfully": "Page copiée avec succès",
|
"Page copied successfully": "Page copiée avec succès",
|
||||||
|
|||||||
@@ -407,21 +407,6 @@
|
|||||||
"Share deleted successfully": "Condivisione eliminata con successo",
|
"Share deleted successfully": "Condivisione eliminata con successo",
|
||||||
"Share not found": "Condivisione non trovata",
|
"Share not found": "Condivisione non trovata",
|
||||||
"Failed to share page": "Condivisione della pagina fallita",
|
"Failed to share page": "Condivisione della pagina fallita",
|
||||||
"Disable public sharing": "Disabilita la condivisione pubblica",
|
|
||||||
"Prevent members from sharing pages publicly.": "Impedisci ai membri di condividere pubblicamente le pagine.",
|
|
||||||
"Toggle public sharing": "Attiva/disattiva la condivisione pubblica",
|
|
||||||
"Toggle space public sharing": "Attiva/disattiva la condivisione pubblica nello spazio",
|
|
||||||
"Public sharing is disabled at the workspace level": "La condivisione pubblica è disabilitata a livello di area di lavoro",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "Impedisci che le pagine in questo spazio vengano condivise pubblicamente.",
|
|
||||||
"Requires an enterprise license": "Richiede una licenza enterprise",
|
|
||||||
"Enable public sharing": "Abilita la condivisione pubblica",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Sei sicuro di voler abilitare la condivisione pubblica? I membri potranno condividere le pagine pubblicamente.",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Sei sicuro di voler disabilitare la condivisione pubblica? Tutti i link condivisi esistenti in questa area di lavoro verranno eliminati.",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "Sei sicuro di voler abilitare la condivisione pubblica per questo spazio?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Sei sicuro di voler disabilitare la condivisione pubblica? Tutti i link condivisi esistenti in questo spazio verranno eliminati.",
|
|
||||||
"Public sharing is disabled": "La condivisione pubblica è disabilitata",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "La condivisione pubblica è stata disabilitata a livello di area di lavoro.",
|
|
||||||
"Public sharing has been disabled for this space.": "La condivisione pubblica è stata disabilitata per questo spazio.",
|
|
||||||
"Copy page": "Copia pagina",
|
"Copy page": "Copia pagina",
|
||||||
"Copy page to a different space.": "Copia pagina in un altro spazio.",
|
"Copy page to a different space.": "Copia pagina in un altro spazio.",
|
||||||
"Page copied successfully": "Pagina copiata con successo",
|
"Page copied successfully": "Pagina copiata con successo",
|
||||||
|
|||||||
@@ -407,21 +407,6 @@
|
|||||||
"Share deleted successfully": "共有を削除しました",
|
"Share deleted successfully": "共有を削除しました",
|
||||||
"Share not found": "共有が見つかりません",
|
"Share not found": "共有が見つかりません",
|
||||||
"Failed to share page": "ページの共有に失敗しました",
|
"Failed to share page": "ページの共有に失敗しました",
|
||||||
"Disable public sharing": "公開共有を無効にする",
|
|
||||||
"Prevent members from sharing pages publicly.": "メンバーがページを公開で共有するのを防ぐ。",
|
|
||||||
"Toggle public sharing": "公開共有を切り替える",
|
|
||||||
"Toggle space public sharing": "スペースの公開共有を切り替える",
|
|
||||||
"Public sharing is disabled at the workspace level": "ワークスペースレベルで公開共有が無効になっています",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "このスペース内のページが公開で共有されるのを防ぐ。",
|
|
||||||
"Requires an enterprise license": "エンタープライズライセンスが必要です",
|
|
||||||
"Enable public sharing": "公開共有を有効にする",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "本当に公開共有を有効にしますか?メンバーはページを公開で共有できるようになります。",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "本当に公開共有を無効にしますか?このワークスペース内のすべての既存の共有リンクが削除されます。",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "本当にこのスペースの公開共有を有効にしますか?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "本当に公開共有を無効にしますか?このスペースのすべての既存の共有リンクが削除されます。",
|
|
||||||
"Public sharing is disabled": "公開共有が無効になっています",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "ワークスペースレベルで公開共有が無効になりました。",
|
|
||||||
"Public sharing has been disabled for this space.": "このスペースで公開共有が無効になりました。",
|
|
||||||
"Copy page": "ページをコピー",
|
"Copy page": "ページをコピー",
|
||||||
"Copy page to a different space.": "ページを別のスペースにコピーします",
|
"Copy page to a different space.": "ページを別のスペースにコピーします",
|
||||||
"Page copied successfully": "ページをコピーしました",
|
"Page copied successfully": "ページをコピーしました",
|
||||||
|
|||||||
@@ -407,21 +407,6 @@
|
|||||||
"Share deleted successfully": "공유가 성공적으로 삭제되었습니다",
|
"Share deleted successfully": "공유가 성공적으로 삭제되었습니다",
|
||||||
"Share not found": "공유를 찾을 수 없습니다",
|
"Share not found": "공유를 찾을 수 없습니다",
|
||||||
"Failed to share page": "페이지 공유에 실패했습니다",
|
"Failed to share page": "페이지 공유에 실패했습니다",
|
||||||
"Disable public sharing": "공유 비활성화",
|
|
||||||
"Prevent members from sharing pages publicly.": "멤버들이 페이지를 공개적으로 공유하지 못하도록 방지하십시오.",
|
|
||||||
"Toggle public sharing": "공유 전환",
|
|
||||||
"Toggle space public sharing": "공간 공유 전환",
|
|
||||||
"Public sharing is disabled at the workspace level": "워크스페이스 수준에서 공유가 비활성화되었습니다.",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "이 공간의 페이지가 공개적으로 공유되지 않도록 방지하십시오.",
|
|
||||||
"Requires an enterprise license": "기업 라이센스가 필요합니다.",
|
|
||||||
"Enable public sharing": "공유 활성화",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "공유를 활성화하시겠습니까? 멤버들이 페이지를 공개적으로 공유할 수 있게 됩니다.",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "정말로 공유를 비활성화하시겠습니까? 이 워크스페이스의 모든 기존 공유 링크가 삭제됩니다.",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "이 공간의 공유를 활성화하시겠습니까?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "정말로 공유를 비활성화하시겠습니까? 이 공간의 모든 기존 공유 링크가 삭제됩니다.",
|
|
||||||
"Public sharing is disabled": "공유가 비활성화되었습니다.",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "워크스페이스 수준에서 공유가 비활성화되었습니다.",
|
|
||||||
"Public sharing has been disabled for this space.": "이 공간의 공유가 비활성화되었습니다.",
|
|
||||||
"Copy page": "페이지 복사하기",
|
"Copy page": "페이지 복사하기",
|
||||||
"Copy page to a different space.": "다른 공간으로 페이지 복사하기.",
|
"Copy page to a different space.": "다른 공간으로 페이지 복사하기.",
|
||||||
"Page copied successfully": "페이지가 성공적으로 복사되었습니다",
|
"Page copied successfully": "페이지가 성공적으로 복사되었습니다",
|
||||||
|
|||||||
@@ -407,21 +407,6 @@
|
|||||||
"Share deleted successfully": "Delen succesvol verwijderd",
|
"Share deleted successfully": "Delen succesvol verwijderd",
|
||||||
"Share not found": "Delen niet gevonden",
|
"Share not found": "Delen niet gevonden",
|
||||||
"Failed to share page": "Pagina delen mislukt",
|
"Failed to share page": "Pagina delen mislukt",
|
||||||
"Disable public sharing": "Openbaar delen uitschakelen",
|
|
||||||
"Prevent members from sharing pages publicly.": "Voorkom dat leden pagina's openbaar delen.",
|
|
||||||
"Toggle public sharing": "Wissel openbaar delen",
|
|
||||||
"Toggle space public sharing": "Wissel openbaar delen van ruimte",
|
|
||||||
"Public sharing is disabled at the workspace level": "Openbaar delen is uitgeschakeld op werkruimteniveau",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "Voorkom dat pagina's in deze ruimte openbaar worden gedeeld.",
|
|
||||||
"Requires an enterprise license": "Vereist een bedrijfslicentie",
|
|
||||||
"Enable public sharing": "Openbaar delen inschakelen",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Weet je zeker dat je openbaar delen wilt inschakelen? Leden kunnen pagina's openbaar delen.",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Weet je zeker dat je openbaar delen wilt uitschakelen? Alle bestaande gedeelde links in deze werkruimte zullen worden verwijderd.",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "Weet je zeker dat je openbaar delen voor deze ruimte wilt inschakelen?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Weet je zeker dat je openbaar delen wilt uitschakelen? Alle bestaande gedeelde links in deze ruimte zullen worden verwijderd.",
|
|
||||||
"Public sharing is disabled": "Openbaar delen is uitgeschakeld",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "Openbaar delen is uitgeschakeld op werkruimteniveau.",
|
|
||||||
"Public sharing has been disabled for this space.": "Openbaar delen is uitgeschakeld voor deze ruimte.",
|
|
||||||
"Copy page": "Pagina kopiëren",
|
"Copy page": "Pagina kopiëren",
|
||||||
"Copy page to a different space.": "Kopieer pagina naar een andere ruimte.",
|
"Copy page to a different space.": "Kopieer pagina naar een andere ruimte.",
|
||||||
"Page copied successfully": "Pagina succesvol gekopieerd",
|
"Page copied successfully": "Pagina succesvol gekopieerd",
|
||||||
|
|||||||
@@ -407,21 +407,6 @@
|
|||||||
"Share deleted successfully": "Compartilhamento excluído com sucesso",
|
"Share deleted successfully": "Compartilhamento excluído com sucesso",
|
||||||
"Share not found": "Compartilhamento não encontrado",
|
"Share not found": "Compartilhamento não encontrado",
|
||||||
"Failed to share page": "Falha ao compartilhar página",
|
"Failed to share page": "Falha ao compartilhar página",
|
||||||
"Disable public sharing": "Desativar compartilhamento público",
|
|
||||||
"Prevent members from sharing pages publicly.": "Impedir que os membros compartilhem páginas publicamente.",
|
|
||||||
"Toggle public sharing": "Alternar compartilhamento público",
|
|
||||||
"Toggle space public sharing": "Alternar compartilhamento público do espaço",
|
|
||||||
"Public sharing is disabled at the workspace level": "O compartilhamento público está desativado no nível do espaço de trabalho",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "Impedir que as páginas neste espaço sejam compartilhadas publicamente.",
|
|
||||||
"Requires an enterprise license": "Requer uma licença empresarial",
|
|
||||||
"Enable public sharing": "Ativar compartilhamento público",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Tem certeza de que deseja ativar o compartilhamento público? Os membros poderão compartilhar páginas publicamente.",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Tem certeza de que deseja desativar o compartilhamento público? Todos os links compartilhados existentes neste espaço de trabalho serão excluídos.",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "Tem certeza de que deseja ativar o compartilhamento público para este espaço?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Tem certeza de que deseja desativar o compartilhamento público? Todos os links compartilhados existentes neste espaço serão excluídos.",
|
|
||||||
"Public sharing is disabled": "Compartilhamento público está desativado",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "O compartilhamento público foi desativado no nível do espaço de trabalho.",
|
|
||||||
"Public sharing has been disabled for this space.": "O compartilhamento público foi desativado para este espaço.",
|
|
||||||
"Copy page": "Copiar página",
|
"Copy page": "Copiar página",
|
||||||
"Copy page to a different space.": "Copiar página para um espaço diferente.",
|
"Copy page to a different space.": "Copiar página para um espaço diferente.",
|
||||||
"Page copied successfully": "Página copiada com sucesso",
|
"Page copied successfully": "Página copiada com sucesso",
|
||||||
|
|||||||
@@ -407,21 +407,6 @@
|
|||||||
"Share deleted successfully": "Общий доступ успешно удален",
|
"Share deleted successfully": "Общий доступ успешно удален",
|
||||||
"Share not found": "Общий доступ не найден",
|
"Share not found": "Общий доступ не найден",
|
||||||
"Failed to share page": "Не удалось поделиться страницей",
|
"Failed to share page": "Не удалось поделиться страницей",
|
||||||
"Disable public sharing": "Отключить общий доступ",
|
|
||||||
"Prevent members from sharing pages publicly.": "Запретить участникам делиться страницами публично.",
|
|
||||||
"Toggle public sharing": "Переключить общий доступ",
|
|
||||||
"Toggle space public sharing": "Переключить общий доступ для пространства",
|
|
||||||
"Public sharing is disabled at the workspace level": "Общий доступ отключен на уровне рабочего пространства",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "Запретить делиться страницами в этом пространстве публично.",
|
|
||||||
"Requires an enterprise license": "Требуется корпоративная лицензия",
|
|
||||||
"Enable public sharing": "Включить общий доступ",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Вы уверены, что хотите включить общий доступ? Участники смогут делиться страницами публично.",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Вы уверены, что хотите отключить общий доступ? Все существующие ссылки в этом рабочем пространстве будут удалены.",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "Вы уверены, что хотите включить общий доступ для этого пространства?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Вы уверены, что хотите отключить общий доступ? Все существующие ссылки в этом пространстве будут удалены.",
|
|
||||||
"Public sharing is disabled": "Общий доступ отключен",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "Общий доступ был отключен на уровне рабочего пространства.",
|
|
||||||
"Public sharing has been disabled for this space.": "Общий доступ был отключен для этого пространства.",
|
|
||||||
"Copy page": "Копировать страницу",
|
"Copy page": "Копировать страницу",
|
||||||
"Copy page to a different space.": "Копировать страницу в другое пространство.",
|
"Copy page to a different space.": "Копировать страницу в другое пространство.",
|
||||||
"Page copied successfully": "Страница успешно скопирована",
|
"Page copied successfully": "Страница успешно скопирована",
|
||||||
|
|||||||
@@ -407,21 +407,6 @@
|
|||||||
"Share deleted successfully": "Спільний доступ успішно видалено",
|
"Share deleted successfully": "Спільний доступ успішно видалено",
|
||||||
"Share not found": "Спільний доступ не знайдено",
|
"Share not found": "Спільний доступ не знайдено",
|
||||||
"Failed to share page": "Не вдалося поділитися сторінкою",
|
"Failed to share page": "Не вдалося поділитися сторінкою",
|
||||||
"Disable public sharing": "Вимкнути публічний доступ",
|
|
||||||
"Prevent members from sharing pages publicly.": "Перешкодити учасникам публічно ділитися сторінками.",
|
|
||||||
"Toggle public sharing": "Перемикання публічного доступу",
|
|
||||||
"Toggle space public sharing": "Перемикання публічного доступу до просторів",
|
|
||||||
"Public sharing is disabled at the workspace level": "Публічний доступ вимкнуто на рівні робочого простору",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "Перешкодити публічному поширенню сторінок у цьому просторі.",
|
|
||||||
"Requires an enterprise license": "Потребує корпоративної ліцензії",
|
|
||||||
"Enable public sharing": "Увімкнути публічний доступ",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Ви впевнені, що хочете увімкнути публічний доступ? Учасники зможуть публічно ділитися сторінками.",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Ви впевнені, що хочете вимкнути публічний доступ? Усі існуючі посилання для спільного доступу в цьому робочому просторі будуть видалені.",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "Ви впевнені, що хочете увімкнути публічний доступ для цього простору?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Ви впевнені, що хочете вимкнути публічний доступ? Усі існуючі посилання для спільного доступу в цьому просторі будуть видалені.",
|
|
||||||
"Public sharing is disabled": "Публічний доступ вимкнуто",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "Публічний доступ було вимкнено на рівні робочого простору.",
|
|
||||||
"Public sharing has been disabled for this space.": "Публічний доступ було вимкнено для цього простору.",
|
|
||||||
"Copy page": "Копіювати сторінки",
|
"Copy page": "Копіювати сторінки",
|
||||||
"Copy page to a different space.": "Скопіювати сторінку в інший простір.",
|
"Copy page to a different space.": "Скопіювати сторінку в інший простір.",
|
||||||
"Page copied successfully": "Сторінку успішно скопійовано",
|
"Page copied successfully": "Сторінку успішно скопійовано",
|
||||||
|
|||||||
@@ -407,21 +407,6 @@
|
|||||||
"Share deleted successfully": "分享已成功删除",
|
"Share deleted successfully": "分享已成功删除",
|
||||||
"Share not found": "未找到分享",
|
"Share not found": "未找到分享",
|
||||||
"Failed to share page": "页面分享失败",
|
"Failed to share page": "页面分享失败",
|
||||||
"Disable public sharing": "禁用公开分享",
|
|
||||||
"Prevent members from sharing pages publicly.": "阻止成员公开分享页面。",
|
|
||||||
"Toggle public sharing": "切换公开分享",
|
|
||||||
"Toggle space public sharing": "切换空间公开分享",
|
|
||||||
"Public sharing is disabled at the workspace level": "公开分享在工作区级别被禁用",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "阻止此空间中的页面被公开分享。",
|
|
||||||
"Requires an enterprise license": "需要企业许可证",
|
|
||||||
"Enable public sharing": "启用公开分享",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "您确定要启用公开分享吗?成员将能够公开分享页面。",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "您确定要禁用公开分享吗?此工作区中的所有现有共享链接都将被删除。",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "您确定要为此空间启用公开分享吗?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "您确定要禁用公开分享吗?此空间中的所有现有共享链接都将被删除。",
|
|
||||||
"Public sharing is disabled": "公开分享已被禁用",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "公开分享已在工作区级别被禁用。",
|
|
||||||
"Public sharing has been disabled for this space.": "此空间的公开分享已被禁用。",
|
|
||||||
"Copy page": "复制页面",
|
"Copy page": "复制页面",
|
||||||
"Copy page to a different space.": "将页面复制到不同的空间。",
|
"Copy page to a different space.": "将页面复制到不同的空间。",
|
||||||
"Page copied successfully": "页面复制成功",
|
"Page copied successfully": "页面复制成功",
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
// Source: https://github.com/mantinedev/mantine/blob/master/packages/@mantine/core/src/components/CopyButton/CopyButton.tsx - MIT
|
|
||||||
// modified to use the polyfilled clipboard api
|
|
||||||
import React from "react";
|
|
||||||
import { useClipboard } from "@/hooks/use-clipboard";
|
|
||||||
import { useProps } from "@mantine/core";
|
|
||||||
|
|
||||||
interface CopyButtonProps {
|
|
||||||
/** Children callback, provides current status and copy function as an argument */
|
|
||||||
children: (payload: { copied: boolean; copy: () => void }) => React.ReactNode;
|
|
||||||
|
|
||||||
/** Value that is copied to the clipboard when the button is clicked */
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
/** Copied status timeout in ms @default `1000` */
|
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
timeout: 1000,
|
|
||||||
} satisfies Partial<CopyButtonProps>;
|
|
||||||
|
|
||||||
export function CopyButton(props: CopyButtonProps) {
|
|
||||||
const { children, timeout, value, ...others } = useProps(
|
|
||||||
"CopyButton",
|
|
||||||
defaultProps,
|
|
||||||
props,
|
|
||||||
);
|
|
||||||
const clipboard = useClipboard({ timeout });
|
|
||||||
const copy = () => clipboard.copy(value);
|
|
||||||
return <>{children({ copy, copied: clipboard.copied, ...others })}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
CopyButton.displayName = "@mantine/core/CopyButton";
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
import { ActionIcon, CopyButton, Tooltip } from "@mantine/core";
|
||||||
import { CopyButton } from "@/components/common/copy-button";
|
|
||||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
List,
|
List,
|
||||||
Code,
|
Code,
|
||||||
|
CopyButton,
|
||||||
Alert,
|
Alert,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { CopyButton } from "@/components/common/copy-button";
|
|
||||||
import {
|
import {
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
PinInput,
|
PinInput,
|
||||||
Alert,
|
Alert,
|
||||||
List,
|
List,
|
||||||
|
CopyButton,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -19,7 +20,6 @@ import {
|
|||||||
Collapse,
|
Collapse,
|
||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { CopyButton } from "@/components/common/copy-button";
|
|
||||||
import {
|
import {
|
||||||
IconQrcode,
|
IconQrcode,
|
||||||
IconShieldCheck,
|
IconShieldCheck,
|
||||||
|
|||||||
@@ -84,14 +84,9 @@ const CommentEditor = forwardRef(
|
|||||||
autofocus: (autofocus && "end") || false,
|
autofocus: (autofocus && "end") || false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync content from props for read-only editors (e.g. when updated via
|
|
||||||
// websocket on another browser). Skip for editable editors to avoid
|
|
||||||
// resetting the cursor position on every keystroke.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editable && commentEditor && defaultContent) {
|
commentEditor.commands.setContent(defaultContent);
|
||||||
commentEditor.commands.setContent(defaultContent);
|
}, [defaultContent]);
|
||||||
}
|
|
||||||
}, [defaultContent, editable, commentEditor]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Group, Text, Box, Badge } from "@mantine/core";
|
import { Group, Text, Box, Badge } from "@mantine/core";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import classes from "./comment.module.css";
|
import classes from "./comment.module.css";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { timeAgo } from "@/lib/time";
|
import { timeAgo } from "@/lib/time";
|
||||||
@@ -40,7 +40,6 @@ function CommentListItem({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const editor = useAtomValue(pageEditorAtom);
|
const editor = useAtomValue(pageEditorAtom);
|
||||||
const [content, setContent] = useState<string>(comment.content);
|
const [content, setContent] = useState<string>(comment.content);
|
||||||
const editContentRef = useRef<any>(null);
|
|
||||||
const updateCommentMutation = useUpdateCommentMutation();
|
const updateCommentMutation = useUpdateCommentMutation();
|
||||||
const deleteCommentMutation = useDeleteCommentMutation(comment.pageId);
|
const deleteCommentMutation = useDeleteCommentMutation(comment.pageId);
|
||||||
const resolveCommentMutation = useResolveCommentMutation();
|
const resolveCommentMutation = useResolveCommentMutation();
|
||||||
@@ -57,13 +56,9 @@ function CommentListItem({
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const commentToUpdate = {
|
const commentToUpdate = {
|
||||||
commentId: comment.id,
|
commentId: comment.id,
|
||||||
content: JSON.stringify(editContentRef.current ?? content),
|
content: JSON.stringify(content),
|
||||||
};
|
};
|
||||||
await updateCommentMutation.mutateAsync(commentToUpdate);
|
await updateCommentMutation.mutateAsync(commentToUpdate);
|
||||||
if (editContentRef.current) {
|
|
||||||
setContent(editContentRef.current);
|
|
||||||
editContentRef.current = null;
|
|
||||||
}
|
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
|
|
||||||
emit({
|
emit({
|
||||||
@@ -133,7 +128,6 @@ function CommentListItem({
|
|||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
}
|
}
|
||||||
function cancelEdit() {
|
function cancelEdit() {
|
||||||
editContentRef.current = null;
|
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +194,7 @@ function CommentListItem({
|
|||||||
<CommentEditor
|
<CommentEditor
|
||||||
defaultContent={content}
|
defaultContent={content}
|
||||||
editable={true}
|
editable={true}
|
||||||
onUpdate={(newContent: any) => { editContentRef.current = newContent; }}
|
onUpdate={(newContent: any) => setContent(newContent)}
|
||||||
onSave={handleUpdateComment}
|
onSave={handleUpdateComment}
|
||||||
autofocus={true}
|
autofocus={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||||
import { ActionIcon, Group, Select, Tooltip } from "@mantine/core";
|
import { ActionIcon, CopyButton, Group, Select, Tooltip } from "@mantine/core";
|
||||||
import { CopyButton } from "@/components/common/copy-button";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||||
import classes from "./code-block.module.css";
|
import classes from "./code-block.module.css";
|
||||||
|
|||||||
@@ -170,8 +170,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "image/*";
|
input.accept = "image/*";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
input.style.display = "none";
|
|
||||||
document.body.appendChild(input);
|
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
for (const file of input.files) {
|
for (const file of input.files) {
|
||||||
@@ -181,7 +179,8 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input.remove();
|
// Reset the input value to allow uploading the same file again if needed
|
||||||
|
input.value = "";
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
},
|
},
|
||||||
@@ -203,8 +202,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "video/*";
|
input.accept = "video/*";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
input.style.display = "none";
|
|
||||||
document.body.appendChild(input);
|
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
for (const file of input.files) {
|
for (const file of input.files) {
|
||||||
@@ -214,7 +211,8 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input.remove();
|
// Reset the input value to allow uploading the same file again if needed
|
||||||
|
input.value = "";
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
},
|
},
|
||||||
@@ -236,8 +234,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "";
|
input.accept = "";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
input.style.display = "none";
|
|
||||||
document.body.appendChild(input);
|
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
for (const file of input.files) {
|
for (const file of input.files) {
|
||||||
@@ -247,7 +243,8 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input.remove();
|
// Reset the input value to allow uploading the same file again if needed
|
||||||
|
input.value = "";
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -157,10 +157,8 @@ export function TitleEditor({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// guard against Cannot access view['hasFocus'] error
|
titleEditor?.commands.focus("end");
|
||||||
if (!titleEditor?.isInitialized) return;
|
}, 500);
|
||||||
titleEditor?.commands?.focus("end");
|
|
||||||
}, 300);
|
|
||||||
}, [titleEditor]);
|
}, [titleEditor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Text, Group, UnstyledButton, Avatar, Tooltip } from "@mantine/core";
|
import { Text, Group, UnstyledButton } from "@mantine/core";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
import { formattedDate } from "@/lib/time";
|
import { formattedDate } from "@/lib/time";
|
||||||
import classes from "./css/history.module.css";
|
import classes from "./css/history.module.css";
|
||||||
@@ -6,8 +6,6 @@ import clsx from "clsx";
|
|||||||
import { IPageHistory } from "@/features/page-history/types/page.types";
|
import { IPageHistory } from "@/features/page-history/types/page.types";
|
||||||
import { memo, useCallback } from "react";
|
import { memo, useCallback } from "react";
|
||||||
|
|
||||||
const MAX_VISIBLE_AVATARS = 5;
|
|
||||||
|
|
||||||
interface HistoryItemProps {
|
interface HistoryItemProps {
|
||||||
historyItem: IPageHistory;
|
historyItem: IPageHistory;
|
||||||
index: number;
|
index: number;
|
||||||
@@ -33,9 +31,6 @@ const HistoryItem = memo(function HistoryItem({
|
|||||||
onHover?.(historyItem.id, index);
|
onHover?.(historyItem.id, index);
|
||||||
}, [onHover, historyItem.id, index]);
|
}, [onHover, historyItem.id, index]);
|
||||||
|
|
||||||
const contributors = historyItem.contributors;
|
|
||||||
const hasContributors = contributors && contributors.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
p="xs"
|
p="xs"
|
||||||
@@ -44,54 +39,25 @@ const HistoryItem = memo(function HistoryItem({
|
|||||||
onMouseLeave={onHoverEnd}
|
onMouseLeave={onHoverEnd}
|
||||||
className={clsx(classes.history, { [classes.active]: isActive })}
|
className={clsx(classes.history, { [classes.active]: isActive })}
|
||||||
>
|
>
|
||||||
<Text size="sm">{formattedDate(new Date(historyItem.createdAt))}</Text>
|
<Group wrap="nowrap">
|
||||||
|
<div>
|
||||||
|
<Text size="sm">
|
||||||
|
{formattedDate(new Date(historyItem.createdAt))}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Group gap={6} wrap="nowrap" mt={4}>
|
<div style={{ flex: 1 }}>
|
||||||
{hasContributors ? (
|
<Group gap={4} wrap="nowrap">
|
||||||
<>
|
<CustomAvatar
|
||||||
<Tooltip.Group openDelay={300} closeDelay={100}>
|
size="sm"
|
||||||
<Avatar.Group spacing={8}>
|
avatarUrl={historyItem.lastUpdatedBy?.avatarUrl}
|
||||||
{contributors.slice(0, MAX_VISIBLE_AVATARS).map((contributor) => (
|
name={historyItem.lastUpdatedBy?.name}
|
||||||
<Tooltip key={contributor.id} label={contributor.name} withArrow>
|
/>
|
||||||
<CustomAvatar
|
|
||||||
size="sm"
|
|
||||||
avatarUrl={contributor.avatarUrl}
|
|
||||||
name={contributor.name}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
{contributors.length > MAX_VISIBLE_AVATARS && (
|
|
||||||
<Tooltip
|
|
||||||
withArrow
|
|
||||||
label={contributors.slice(MAX_VISIBLE_AVATARS).map((c) => (
|
|
||||||
<div key={c.id}>{c.name}</div>
|
|
||||||
))}
|
|
||||||
>
|
|
||||||
<Avatar size="sm" color="gray">
|
|
||||||
+{contributors.length - MAX_VISIBLE_AVATARS}
|
|
||||||
</Avatar>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Avatar.Group>
|
|
||||||
</Tooltip.Group>
|
|
||||||
{contributors.length === 1 && (
|
|
||||||
<Text size="sm" c="dimmed" lineClamp={1}>
|
<Text size="sm" c="dimmed" lineClamp={1}>
|
||||||
{contributors[0].name}
|
{historyItem.lastUpdatedBy?.name}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
</Group>
|
||||||
</>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<>
|
|
||||||
<CustomAvatar
|
|
||||||
size="sm"
|
|
||||||
avatarUrl={historyItem.lastUpdatedBy?.avatarUrl}
|
|
||||||
name={historyItem.lastUpdatedBy?.name}
|
|
||||||
/>
|
|
||||||
<Text size="sm" c="dimmed" lineClamp={1}>
|
|
||||||
{historyItem.lastUpdatedBy?.name}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,18 +62,11 @@ export default function HistoryModalMobile({ pageId, pageTitle }: Props) {
|
|||||||
|
|
||||||
const selectData = useMemo(
|
const selectData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
historyItems.map((item) => {
|
historyItems.map((item) => ({
|
||||||
const contributors = item.contributors;
|
value: item.id,
|
||||||
const hasContributors = contributors && contributors.length > 0;
|
label: formattedDate(new Date(item.createdAt)),
|
||||||
const names = hasContributors
|
userName: item.lastUpdatedBy?.name,
|
||||||
? contributors.map((c) => c.name).join(", ")
|
})),
|
||||||
: item.lastUpdatedBy?.name;
|
|
||||||
return {
|
|
||||||
value: item.id,
|
|
||||||
label: formattedDate(new Date(item.createdAt)),
|
|
||||||
userName: names,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
[historyItems],
|
[historyItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -18,5 +18,4 @@ export interface IPageHistory {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
lastUpdatedBy: IPageHistoryUser;
|
lastUpdatedBy: IPageHistoryUser;
|
||||||
contributors?: IPageHistoryUser[];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ import React, { useEffect, useRef, useState } from "react";
|
|||||||
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
|
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
|
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
|
||||||
import { useDisclosure, useHotkeys } from "@mantine/hooks";
|
import { useClipboard, useDisclosure, useHotkeys } from "@mantine/hooks";
|
||||||
import { useClipboard } from "@/hooks/use-clipboard";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
IconBrandNotion,
|
IconBrandNotion,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconFileCode,
|
IconFileCode,
|
||||||
IconFileTypeDocx,
|
|
||||||
IconFileTypeZip,
|
IconFileTypeZip,
|
||||||
IconMarkdown,
|
IconMarkdown,
|
||||||
IconX,
|
IconX,
|
||||||
@@ -87,13 +86,11 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
|
|
||||||
const markdownFileRef = useRef<() => void>(null);
|
const markdownFileRef = useRef<() => void>(null);
|
||||||
const htmlFileRef = useRef<() => void>(null);
|
const htmlFileRef = useRef<() => void>(null);
|
||||||
const docxFileRef = useRef<() => void>(null);
|
|
||||||
const notionFileRef = useRef<() => void>(null);
|
const notionFileRef = useRef<() => void>(null);
|
||||||
const confluenceFileRef = useRef<() => void>(null);
|
const confluenceFileRef = useRef<() => void>(null);
|
||||||
const zipFileRef = useRef<() => void>(null);
|
const zipFileRef = useRef<() => void>(null);
|
||||||
|
|
||||||
const canUseConfluence = isCloud() || workspace?.hasLicenseKey;
|
const canUseConfluence = isCloud() || workspace?.hasLicenseKey;
|
||||||
const canUseDocx = isCloud() || workspace?.hasLicenseKey;
|
|
||||||
|
|
||||||
const handleZipUpload = async (selectedFile: File, source: string) => {
|
const handleZipUpload = async (selectedFile: File, source: string) => {
|
||||||
if (!selectedFile) {
|
if (!selectedFile) {
|
||||||
@@ -268,7 +265,6 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
// Reset file inputs after successful upload
|
// Reset file inputs after successful upload
|
||||||
if (markdownFileRef.current) markdownFileRef.current();
|
if (markdownFileRef.current) markdownFileRef.current();
|
||||||
if (htmlFileRef.current) htmlFileRef.current();
|
if (htmlFileRef.current) htmlFileRef.current();
|
||||||
if (docxFileRef.current) docxFileRef.current();
|
|
||||||
|
|
||||||
const pageCountText =
|
const pageCountText =
|
||||||
pageCount === 1 ? `1 ${t("page")}` : `${pageCount} ${t("pages")}`;
|
pageCount === 1 ? `1 ${t("page")}` : `${pageCount} ${t("pages")}`;
|
||||||
@@ -325,30 +321,6 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
)}
|
)}
|
||||||
</FileButton>
|
</FileButton>
|
||||||
|
|
||||||
<FileButton
|
|
||||||
onChange={handleFileUpload}
|
|
||||||
accept=".docx"
|
|
||||||
multiple
|
|
||||||
resetRef={docxFileRef}
|
|
||||||
>
|
|
||||||
{(props) => (
|
|
||||||
<Tooltip
|
|
||||||
label={t("Available in enterprise edition")}
|
|
||||||
disabled={canUseDocx}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
disabled={!canUseDocx}
|
|
||||||
justify="start"
|
|
||||||
variant="default"
|
|
||||||
leftSection={<IconFileTypeDocx size={18} />}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
Word (DOCX)
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</FileButton>
|
|
||||||
|
|
||||||
<FileButton
|
<FileButton
|
||||||
onChange={(file) => handleZipUpload(file, "notion")}
|
onChange={(file) => handleZipUpload(file, "notion")}
|
||||||
accept="application/zip"
|
accept="application/zip"
|
||||||
|
|||||||
@@ -54,11 +54,11 @@ import { IPage, SidebarPagesParams } from "@/features/page/types/page.types.ts";
|
|||||||
import { queryClient } from "@/main.tsx";
|
import { queryClient } from "@/main.tsx";
|
||||||
import { OpenMap } from "react-arborist/dist/main/state/open-slice";
|
import { OpenMap } from "react-arborist/dist/main/state/open-slice";
|
||||||
import {
|
import {
|
||||||
|
useClipboard,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
useElementSize,
|
useElementSize,
|
||||||
useMergedRef,
|
useMergedRef,
|
||||||
} from "@mantine/hooks";
|
} from "@mantine/hooks";
|
||||||
import { useClipboard } from "@/hooks/use-clipboard";
|
|
||||||
import { dfs } from "react-arborist/dist/module/utils";
|
import { dfs } from "react-arborist/dist/module/utils";
|
||||||
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
|
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
buildPageUrl,
|
buildPageUrl,
|
||||||
buildSharedPageUrl,
|
buildSharedPageUrl,
|
||||||
} from "@/features/page/page.utils.ts";
|
} from "@/features/page/page.utils.ts";
|
||||||
import { useClipboard } from "@/hooks/use-clipboard";
|
import { useClipboard } from "@mantine/hooks";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useDeleteShareMutation } from "@/features/share/queries/share-query.ts";
|
import { useDeleteShareMutation } from "@/features/share/queries/share-query.ts";
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@/features/workspace/queries/workspace-query.ts";
|
} from "@/features/workspace/queries/workspace-query.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { useClipboard } from "@/hooks/use-clipboard";
|
import { useClipboard } from "@mantine/hooks";
|
||||||
import { getInviteLink } from "@/features/workspace/services/workspace-service.ts";
|
import { getInviteLink } from "@/features/workspace/services/workspace-service.ts";
|
||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
import { isCloud } from "@/lib/config.ts";
|
import { isCloud } from "@/lib/config.ts";
|
||||||
|
|||||||
+1
-2
@@ -1,8 +1,7 @@
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Group, Text, TextInput } from "@mantine/core";
|
import { Button, CopyButton, Group, Text, TextInput } from "@mantine/core";
|
||||||
import { CopyButton } from "@/components/common/copy-button";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function WorkspaceInviteSection() {
|
export default function WorkspaceInviteSection() {
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
// Source: https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/src/use-clipboard/use-clipboard.ts
|
|
||||||
// polyfilled to support execCommand fallback
|
|
||||||
import { useState } from "react";
|
|
||||||
import { execCommandCopy } from "@docmost/editor-ext";
|
|
||||||
|
|
||||||
export type UseClipboardOptions = {
|
|
||||||
timeout?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UseClipboardReturnValue = {
|
|
||||||
copy: (value: string) => void;
|
|
||||||
reset: () => void;
|
|
||||||
error: Error | null;
|
|
||||||
copied: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useClipboard(
|
|
||||||
options: UseClipboardOptions = { timeout: 2000 },
|
|
||||||
): UseClipboardReturnValue {
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const [copyTimeout, setCopyTimeout] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const handleCopyResult = (value: boolean) => {
|
|
||||||
window.clearTimeout(copyTimeout!);
|
|
||||||
setCopyTimeout(window.setTimeout(() => setCopied(false), options.timeout));
|
|
||||||
setCopied(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const copy = (value: string) => {
|
|
||||||
if ("clipboard" in navigator) {
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(value)
|
|
||||||
.then(() => handleCopyResult(true))
|
|
||||||
.catch(() => {
|
|
||||||
try {
|
|
||||||
execCommandCopy(value);
|
|
||||||
handleCopyResult(true);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err : new Error("Failed to copy"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
execCommandCopy(value);
|
|
||||||
handleCopyResult(true);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err : new Error("Failed to copy"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
setCopied(false);
|
|
||||||
setError(null);
|
|
||||||
window.clearTimeout(copyTimeout!);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { copy, reset, error, copied };
|
|
||||||
}
|
|
||||||
+12
-12
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.25.3",
|
"version": "0.25.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -56,8 +56,8 @@
|
|||||||
"@nestjs/terminus": "^11.0.0",
|
"@nestjs/terminus": "^11.0.0",
|
||||||
"@nestjs/websockets": "^11.1.13",
|
"@nestjs/websockets": "^11.1.13",
|
||||||
"@node-saml/passport-saml": "^5.1.0",
|
"@node-saml/passport-saml": "^5.1.0",
|
||||||
"@react-email/components": "1.0.7",
|
"@react-email/components": "0.0.28",
|
||||||
"@react-email/render": "2.0.4",
|
"@react-email/render": "1.0.2",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"ai": "^6.0.37",
|
"ai": "^6.0.37",
|
||||||
"ai-sdk-ollama": "^3.1.1",
|
"ai-sdk-ollama": "^3.1.1",
|
||||||
@@ -111,32 +111,32 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.20.0",
|
"@eslint/js": "^9.20.0",
|
||||||
"@nestjs/cli": "^11.0.16",
|
"@nestjs/cli": "^11.0.4",
|
||||||
"@nestjs/schematics": "^11.0.1",
|
"@nestjs/schematics": "^11.0.1",
|
||||||
"@nestjs/testing": "^11.0.10",
|
"@nestjs/testing": "^11.0.10",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/debounce": "^1.2.4",
|
"@types/debounce": "^1.2.4",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/node": "^22.13.4",
|
"@types/node": "^22.13.4",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/passport-google-oauth20": "^2.0.16",
|
"@types/passport-google-oauth20": "^2.0.16",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.3",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/ws": "^8.5.14",
|
"@types/ws": "^8.5.14",
|
||||||
"@types/yauzl": "^2.10.3",
|
"@types/yauzl": "^2.10.3",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.20.1",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^29.7.0",
|
||||||
"kysely-codegen": "^0.19.0",
|
"kysely-codegen": "^0.19.0",
|
||||||
"prettier": "^3.5.1",
|
"prettier": "^3.5.1",
|
||||||
"react-email": "5.2.8",
|
"react-email": "3.0.2",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.2.2",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.4.6",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-loader": "^9.5.4",
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Hocuspocus, Document } from '@hocuspocus/server';
|
import { Hocuspocus, Document } from '@hocuspocus/server';
|
||||||
import { TiptapTransformer } from '@hocuspocus/transformer';
|
|
||||||
import {
|
|
||||||
prosemirrorNodeToYElement,
|
|
||||||
tiptapExtensions,
|
|
||||||
} from './collaboration.util';
|
|
||||||
import * as Y from 'yjs';
|
|
||||||
import { User } from '@docmost/db/types/entity.types';
|
|
||||||
|
|
||||||
export type CollabEventHandlers = ReturnType<
|
export type CollabEventHandlers = ReturnType<
|
||||||
CollaborationHandler['getHandlers']
|
CollaborationHandler['getHandlers']
|
||||||
@@ -27,47 +20,6 @@ export class CollaborationHandler {
|
|||||||
// const fragment = doc.getXmlFragment('default');
|
// const fragment = doc.getXmlFragment('default');
|
||||||
//});
|
//});
|
||||||
},
|
},
|
||||||
updatePageContent: async (
|
|
||||||
documentName: string,
|
|
||||||
payload: {
|
|
||||||
pageId: string;
|
|
||||||
prosemirrorJson: any;
|
|
||||||
operation: string;
|
|
||||||
user: User;
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const { pageId, prosemirrorJson, operation, user } = payload;
|
|
||||||
this.logger.debug(
|
|
||||||
'Updating page content via yjs',
|
|
||||||
documentName,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
await this.withYdocConnection(
|
|
||||||
hocuspocus,
|
|
||||||
documentName,
|
|
||||||
{ user },
|
|
||||||
(doc) => {
|
|
||||||
const fragment = doc.getXmlFragment('default');
|
|
||||||
|
|
||||||
if (operation === 'replace') {
|
|
||||||
if (fragment.length > 0) {
|
|
||||||
fragment.delete(0, fragment.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDoc = TiptapTransformer.toYdoc(
|
|
||||||
prosemirrorJson,
|
|
||||||
'default',
|
|
||||||
tiptapExtensions,
|
|
||||||
);
|
|
||||||
Y.applyUpdate(doc, Y.encodeStateAsUpdate(newDoc));
|
|
||||||
} else {
|
|
||||||
const newContent = prosemirrorJson.content || [];
|
|
||||||
const yElements = newContent.map(prosemirrorNodeToYElement);
|
|
||||||
fragment.insert(fragment.length, yElements);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import { CollabWsAdapter } from './adapter/collab-ws.adapter';
|
|||||||
import { IncomingMessage } from 'http';
|
import { IncomingMessage } from 'http';
|
||||||
import { WebSocket } from 'ws';
|
import { WebSocket } from 'ws';
|
||||||
import { TokenModule } from '../core/auth/token.module';
|
import { TokenModule } from '../core/auth/token.module';
|
||||||
import { HistoryProcessor } from './processors/history.processor';
|
import { HistoryListener } from './listeners/history.listener';
|
||||||
import { LoggerExtension } from './extensions/logger.extension';
|
import { LoggerExtension } from './extensions/logger.extension';
|
||||||
import { CollaborationHandler } from './collaboration.handler';
|
import { CollaborationHandler } from './collaboration.handler';
|
||||||
import { CollabHistoryService } from './services/collab-history.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -18,8 +17,7 @@ import { CollabHistoryService } from './services/collab-history.service';
|
|||||||
AuthenticationExtension,
|
AuthenticationExtension,
|
||||||
PersistenceExtension,
|
PersistenceExtension,
|
||||||
LoggerExtension,
|
LoggerExtension,
|
||||||
HistoryProcessor,
|
HistoryListener,
|
||||||
CollabHistoryService,
|
|
||||||
CollaborationHandler,
|
CollaborationHandler,
|
||||||
],
|
],
|
||||||
exports: [CollaborationGateway],
|
exports: [CollaborationGateway],
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
Highlight,
|
Highlight,
|
||||||
UniqueID,
|
UniqueID,
|
||||||
addUniqueIdsToDoc,
|
addUniqueIdsToDoc,
|
||||||
htmlToMarkdown,
|
|
||||||
} from '@docmost/editor-ext';
|
} from '@docmost/editor-ext';
|
||||||
import { generateText, getSchema, JSONContent } from '@tiptap/core';
|
import { generateText, getSchema, JSONContent } from '@tiptap/core';
|
||||||
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
|
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
|
||||||
@@ -43,7 +42,6 @@ import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
|
|||||||
// 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, Schema } from '@tiptap/pm/model';
|
||||||
import * as Y from 'yjs';
|
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
export const tiptapExtensions = [
|
export const tiptapExtensions = [
|
||||||
@@ -163,37 +161,3 @@ function stripUnknownNodes(
|
|||||||
|
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prosemirrorNodeToYElement(node: any): Y.XmlElement | Y.XmlText {
|
|
||||||
if (node.type === 'text') {
|
|
||||||
const ytext = new Y.XmlText();
|
|
||||||
ytext.insert(0, node.text || '');
|
|
||||||
if (node.marks?.length > 0) {
|
|
||||||
const attrs: Record<string, any> = {};
|
|
||||||
for (const mark of node.marks) {
|
|
||||||
attrs[mark.type] = mark.attrs || true;
|
|
||||||
}
|
|
||||||
ytext.format(0, node.text?.length || 0, attrs);
|
|
||||||
}
|
|
||||||
return ytext;
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = new Y.XmlElement(node.type);
|
|
||||||
if (node.attrs) {
|
|
||||||
for (const [key, value] of Object.entries(node.attrs)) {
|
|
||||||
if (value !== null && value !== undefined) {
|
|
||||||
element.setAttribute(key, value as any);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (node.content?.length > 0) {
|
|
||||||
const children = node.content.map(prosemirrorNodeToYElement);
|
|
||||||
element.insert(0, children);
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function jsonToMarkdown(tiptapJson: any): string {
|
|
||||||
const html = jsonToHtml(tiptapJson);
|
|
||||||
return htmlToMarkdown(html);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export const HISTORY_INTERVAL = 5 * 60 * 1000;
|
|
||||||
export const HISTORY_FAST_INTERVAL = 60 * 1000;
|
|
||||||
export const HISTORY_FAST_THRESHOLD = 5 * 60 * 1000;
|
|
||||||
@@ -13,6 +13,7 @@ import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
|||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||||
import { executeTx } from '@docmost/db/utils';
|
import { executeTx } from '@docmost/db/utils';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { InjectQueue } from '@nestjs/bullmq';
|
import { InjectQueue } from '@nestjs/bullmq';
|
||||||
import { QueueJob, QueueName } from '../../integrations/queue/constants';
|
import { QueueJob, QueueName } from '../../integrations/queue/constants';
|
||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
@@ -21,17 +22,8 @@ import {
|
|||||||
extractPageMentions,
|
extractPageMentions,
|
||||||
} from '../../common/helpers/prosemirror/utils';
|
} from '../../common/helpers/prosemirror/utils';
|
||||||
import { isDeepStrictEqual } from 'node:util';
|
import { isDeepStrictEqual } from 'node:util';
|
||||||
import {
|
import { IPageBacklinkJob } from '../../integrations/queue/constants/queue.interface';
|
||||||
IPageBacklinkJob,
|
|
||||||
IPageHistoryJob,
|
|
||||||
} from '../../integrations/queue/constants/queue.interface';
|
|
||||||
import { Page } from '@docmost/db/types/entity.types';
|
import { Page } from '@docmost/db/types/entity.types';
|
||||||
import { CollabHistoryService } from '../services/collab-history.service';
|
|
||||||
import {
|
|
||||||
HISTORY_FAST_INTERVAL,
|
|
||||||
HISTORY_FAST_THRESHOLD,
|
|
||||||
HISTORY_INTERVAL,
|
|
||||||
} from '../constants';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PersistenceExtension implements Extension {
|
export class PersistenceExtension implements Extension {
|
||||||
@@ -41,10 +33,9 @@ export class PersistenceExtension implements Extension {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly pageRepo: PageRepo,
|
private readonly pageRepo: PageRepo,
|
||||||
@InjectKysely() private readonly db: KyselyDB,
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
@InjectQueue(QueueName.GENERAL_QUEUE) private generalQueue: Queue,
|
@InjectQueue(QueueName.GENERAL_QUEUE) private generalQueue: Queue,
|
||||||
@InjectQueue(QueueName.AI_QUEUE) private aiQueue: Queue,
|
@InjectQueue(QueueName.AI_QUEUE) private aiQueue: Queue,
|
||||||
@InjectQueue(QueueName.HISTORY_QUEUE) private historyQueue: Queue,
|
|
||||||
private readonly collabHistory: CollabHistoryService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onLoadDocument(data: onLoadDocumentPayload) {
|
async onLoadDocument(data: onLoadDocumentPayload) {
|
||||||
@@ -110,7 +101,6 @@ export class PersistenceExtension implements Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let page: Page = null;
|
let page: Page = null;
|
||||||
const editingUserIds = this.consumeContributors(documentName);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executeTx(this.db, async (trx) => {
|
await executeTx(this.db, async (trx) => {
|
||||||
@@ -133,9 +123,13 @@ export class PersistenceExtension implements Extension {
|
|||||||
let contributorIds = undefined;
|
let contributorIds = undefined;
|
||||||
try {
|
try {
|
||||||
const existingContributors = page.contributorIds || [];
|
const existingContributors = page.contributorIds || [];
|
||||||
|
const contributorSet = this.contributors.get(documentName);
|
||||||
|
contributorSet.add(page.creatorId);
|
||||||
|
const newContributors = [...contributorSet];
|
||||||
contributorIds = Array.from(
|
contributorIds = Array.from(
|
||||||
new Set([...existingContributors, ...editingUserIds, page.creatorId]),
|
new Set([...existingContributors, ...newContributors]),
|
||||||
);
|
);
|
||||||
|
this.contributors.delete(documentName);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//this.logger.debug('Contributors error:' + err?.['message']);
|
//this.logger.debug('Contributors error:' + err?.['message']);
|
||||||
}
|
}
|
||||||
@@ -159,7 +153,13 @@ export class PersistenceExtension implements Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (page) {
|
if (page) {
|
||||||
await this.collabHistory.addContributors(pageId, editingUserIds);
|
this.eventEmitter.emit('collab.page.updated', {
|
||||||
|
page: {
|
||||||
|
...page,
|
||||||
|
content: tiptapJson,
|
||||||
|
lastUpdatedById: context.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const mentions = extractMentions(tiptapJson);
|
const mentions = extractMentions(tiptapJson);
|
||||||
const pageMentions = extractPageMentions(mentions);
|
const pageMentions = extractPageMentions(mentions);
|
||||||
@@ -174,14 +174,12 @@ export class PersistenceExtension implements Extension {
|
|||||||
pageIds: [pageId],
|
pageIds: [pageId],
|
||||||
workspaceId: page.workspaceId,
|
workspaceId: page.workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.enqueuePageHistory(page);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onChange(data: onChangePayload) {
|
async onChange(data: onChangePayload) {
|
||||||
const documentName = data.documentName;
|
const documentName = data.documentName;
|
||||||
const userId = data.context?.user?.id;
|
const userId = data.context?.user.id;
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
if (!this.contributors.has(documentName)) {
|
if (!this.contributors.has(documentName)) {
|
||||||
@@ -195,26 +193,4 @@ export class PersistenceExtension implements Extension {
|
|||||||
const documentName = data.documentName;
|
const documentName = data.documentName;
|
||||||
this.contributors.delete(documentName);
|
this.contributors.delete(documentName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private consumeContributors(documentName: string): string[] {
|
|
||||||
const contributorSet = this.contributors.get(documentName);
|
|
||||||
if (!contributorSet) return [];
|
|
||||||
const userIds = [...contributorSet];
|
|
||||||
this.contributors.delete(documentName);
|
|
||||||
return userIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async enqueuePageHistory(page: Page): Promise<void> {
|
|
||||||
const pageAge = Date.now() - new Date(page.createdAt).getTime();
|
|
||||||
const delay =
|
|
||||||
pageAge < HISTORY_FAST_THRESHOLD
|
|
||||||
? HISTORY_FAST_INTERVAL
|
|
||||||
: HISTORY_INTERVAL;
|
|
||||||
|
|
||||||
await this.historyQueue.add(
|
|
||||||
QueueJob.PAGE_HISTORY,
|
|
||||||
{ pageId: page.id } as IPageHistoryJob,
|
|
||||||
{ jobId: page.id, delay },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo';
|
||||||
|
import { Page } from '@docmost/db/types/entity.types';
|
||||||
|
import { isDeepStrictEqual } from 'node:util';
|
||||||
|
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
||||||
|
|
||||||
|
export class UpdatedPageEvent {
|
||||||
|
page: Page;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HistoryListener {
|
||||||
|
private readonly logger = new Logger(HistoryListener.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly pageHistoryRepo: PageHistoryRepo,
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@OnEvent('collab.page.updated')
|
||||||
|
async handleCreatePageHistory(event: UpdatedPageEvent) {
|
||||||
|
const { page } = event;
|
||||||
|
|
||||||
|
const pageCreationTime = new Date(page.createdAt).getTime();
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const FIVE_MINUTES = this.environmentService.isDevelopment()
|
||||||
|
? 60 * 1000
|
||||||
|
: 5 * 60 * 1000;
|
||||||
|
|
||||||
|
if (currentTime - pageCreationTime < FIVE_MINUTES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastHistory = await this.pageHistoryRepo.findPageLastHistory(page.id, {
|
||||||
|
includeContent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!lastHistory ||
|
||||||
|
(!isDeepStrictEqual(lastHistory.content, page.content) &&
|
||||||
|
currentTime - new Date(lastHistory.createdAt).getTime() >= FIVE_MINUTES)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.pageHistoryRepo.saveHistory(page);
|
||||||
|
this.logger.debug(`New history created for: ${page.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`Failed to create history for page: ${page.id}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { Logger, OnModuleDestroy } from '@nestjs/common';
|
|
||||||
import { OnWorkerEvent, Processor, WorkerHost } from '@nestjs/bullmq';
|
|
||||||
import { Job } from 'bullmq';
|
|
||||||
import { QueueJob, QueueName } from '../../integrations/queue/constants';
|
|
||||||
import { IPageHistoryJob } from '../../integrations/queue/constants/queue.interface';
|
|
||||||
import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo';
|
|
||||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
|
||||||
import { isDeepStrictEqual } from 'node:util';
|
|
||||||
import { CollabHistoryService } from '../services/collab-history.service';
|
|
||||||
|
|
||||||
@Processor(QueueName.HISTORY_QUEUE)
|
|
||||||
export class HistoryProcessor extends WorkerHost implements OnModuleDestroy {
|
|
||||||
private readonly logger = new Logger(HistoryProcessor.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly pageHistoryRepo: PageHistoryRepo,
|
|
||||||
private readonly pageRepo: PageRepo,
|
|
||||||
private readonly collabHistory: CollabHistoryService,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async process(job: Job<IPageHistoryJob, void>): Promise<void> {
|
|
||||||
if (job.name !== QueueJob.PAGE_HISTORY) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { pageId } = job.data;
|
|
||||||
|
|
||||||
const page = await this.pageRepo.findById(pageId, {
|
|
||||||
includeContent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
this.logger.warn(`Page ${pageId} not found, skipping history`);
|
|
||||||
await this.collabHistory.clearContributors(pageId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastHistory = await this.pageHistoryRepo.findPageLastHistory(
|
|
||||||
pageId,
|
|
||||||
{ includeContent: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!lastHistory ||
|
|
||||||
!isDeepStrictEqual(lastHistory.content, page.content)
|
|
||||||
) {
|
|
||||||
const contributorIds =
|
|
||||||
await this.collabHistory.popContributors(pageId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.pageHistoryRepo.saveHistory(page, { contributorIds });
|
|
||||||
this.logger.debug(`History created for page: ${pageId}`);
|
|
||||||
} catch (err) {
|
|
||||||
await this.collabHistory.addContributors(
|
|
||||||
pageId,
|
|
||||||
contributorIds,
|
|
||||||
);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnWorkerEvent('active')
|
|
||||||
onActive(job: Job) {
|
|
||||||
this.logger.debug(`Processing ${job.name} for page: ${job.data.pageId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnWorkerEvent('failed')
|
|
||||||
onError(job: Job) {
|
|
||||||
this.logger.error(
|
|
||||||
`Failed ${job.name} for page: ${job.data.pageId}. Reason: ${job.failedReason}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onModuleDestroy(): Promise<void> {
|
|
||||||
if (this.worker) {
|
|
||||||
await this.worker.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,6 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
|
|||||||
import { HealthModule } from '../../integrations/health/health.module';
|
import { HealthModule } from '../../integrations/health/health.module';
|
||||||
import { CollaborationController } from './collaboration.controller';
|
import { CollaborationController } from './collaboration.controller';
|
||||||
import { LoggerModule } from '../../common/logger/logger.module';
|
import { LoggerModule } from '../../common/logger/logger.module';
|
||||||
import { RedisModule } from '@nestjs-labs/nestjs-ioredis';
|
|
||||||
import { RedisConfigService } from '../../integrations/redis/redis-config.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -21,9 +19,6 @@ import { RedisConfigService } from '../../integrations/redis/redis-config.servic
|
|||||||
QueueModule,
|
QueueModule,
|
||||||
HealthModule,
|
HealthModule,
|
||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
RedisModule.forRootAsync({
|
|
||||||
useClass: RedisConfigService,
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
AppController,
|
AppController,
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { RedisService } from '@nestjs-labs/nestjs-ioredis';
|
|
||||||
import type { Redis } from 'ioredis';
|
|
||||||
|
|
||||||
const REDIS_KEY_PREFIX = 'history:contributors:';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CollabHistoryService {
|
|
||||||
private readonly redis: Redis;
|
|
||||||
|
|
||||||
constructor(private readonly redisService: RedisService) {
|
|
||||||
this.redis = this.redisService.getOrThrow();
|
|
||||||
}
|
|
||||||
|
|
||||||
async addContributors(pageId: string, userIds: string[]): Promise<void> {
|
|
||||||
if (userIds.length === 0) return;
|
|
||||||
await this.redis.sadd(REDIS_KEY_PREFIX + pageId, ...userIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
async popContributors(pageId: string): Promise<string[]> {
|
|
||||||
const key = REDIS_KEY_PREFIX + pageId;
|
|
||||||
const count = await this.redis.scard(key);
|
|
||||||
if (count === 0) return [];
|
|
||||||
return await this.redis.spop(key, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearContributors(pageId: string): Promise<void> {
|
|
||||||
await this.redis.del(REDIS_KEY_PREFIX + pageId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,13 +17,13 @@ import {
|
|||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AttachmentService } from './services/attachment.service';
|
import { AttachmentService } from './services/attachment.service';
|
||||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
import { FastifyReply } from 'fastify';
|
||||||
import { FileInterceptor } from '../../common/interceptors/file.interceptor';
|
import { FileInterceptor } from '../../common/interceptors/file.interceptor';
|
||||||
import * as bytes from 'bytes';
|
import * as bytes from 'bytes';
|
||||||
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
||||||
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { Attachment, User, Workspace } from '@docmost/db/types/entity.types';
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
import { StorageService } from '../../integrations/storage/storage.service';
|
import { StorageService } from '../../integrations/storage/storage.service';
|
||||||
import {
|
import {
|
||||||
getAttachmentFolderPath,
|
getAttachmentFolderPath,
|
||||||
@@ -151,7 +151,6 @@ export class AttachmentController {
|
|||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get('/files/:fileId/:fileName')
|
@Get('/files/:fileId/:fileName')
|
||||||
async getFile(
|
async getFile(
|
||||||
@Req() req: FastifyRequest,
|
|
||||||
@Res() res: FastifyReply,
|
@Res() res: FastifyReply,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@@ -182,7 +181,22 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.sendFileResponse(req, res, attachment, 'private');
|
const fileStream = await this.storageService.readStream(
|
||||||
|
attachment.filePath,
|
||||||
|
);
|
||||||
|
res.headers({
|
||||||
|
'Content-Type': attachment.mimeType,
|
||||||
|
'Cache-Control': 'private, max-age=3600',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!inlineFileExtensions.includes(attachment.fileExt)) {
|
||||||
|
res.header(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
@@ -191,7 +205,6 @@ export class AttachmentController {
|
|||||||
|
|
||||||
@Get('/files/public/:fileId/:fileName')
|
@Get('/files/public/:fileId/:fileName')
|
||||||
async getPublicFile(
|
async getPublicFile(
|
||||||
@Req() req: FastifyRequest,
|
|
||||||
@Res() res: FastifyReply,
|
@Res() res: FastifyReply,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@Param('fileId') fileId: string,
|
@Param('fileId') fileId: string,
|
||||||
@@ -230,7 +243,22 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.sendFileResponse(req, res, attachment, 'public');
|
const fileStream = await this.storageService.readStream(
|
||||||
|
attachment.filePath,
|
||||||
|
);
|
||||||
|
res.headers({
|
||||||
|
'Content-Type': attachment.mimeType,
|
||||||
|
'Cache-Control': 'public, max-age=3600',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!inlineFileExtensions.includes(attachment.fileExt)) {
|
||||||
|
res.header(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
@@ -405,70 +433,4 @@ export class AttachmentController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendFileResponse(
|
|
||||||
req: FastifyRequest,
|
|
||||||
res: FastifyReply,
|
|
||||||
attachment: Attachment,
|
|
||||||
cacheScope: 'private' | 'public',
|
|
||||||
) {
|
|
||||||
const fileSize = Number(attachment.fileSize);
|
|
||||||
const rangeHeader = req.headers.range;
|
|
||||||
|
|
||||||
res.header('Accept-Ranges', 'bytes');
|
|
||||||
|
|
||||||
if (!inlineFileExtensions.includes(attachment.fileExt)) {
|
|
||||||
res.header(
|
|
||||||
'Content-Disposition',
|
|
||||||
`attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rangeHeader && fileSize) {
|
|
||||||
const match = rangeHeader.match(/bytes=(\d+)-(\d*)/);
|
|
||||||
if (match) {
|
|
||||||
const start = parseInt(match[1], 10);
|
|
||||||
const end = match[2]
|
|
||||||
? Math.min(parseInt(match[2], 10), fileSize - 1)
|
|
||||||
: fileSize - 1;
|
|
||||||
|
|
||||||
if (start >= fileSize || start > end) {
|
|
||||||
res.status(416);
|
|
||||||
res.header('Content-Range', `bytes */${fileSize}`);
|
|
||||||
return res.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileStream = await this.storageService.readRangeStream(
|
|
||||||
attachment.filePath,
|
|
||||||
{ start, end },
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(206);
|
|
||||||
res.headers({
|
|
||||||
'Content-Type': attachment.mimeType,
|
|
||||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
|
||||||
'Content-Length': end - start + 1,
|
|
||||||
'Cache-Control': `${cacheScope}, max-age=3600`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.send(fileStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileStream = await this.storageService.readStream(
|
|
||||||
attachment.filePath,
|
|
||||||
);
|
|
||||||
|
|
||||||
res.headers({
|
|
||||||
'Content-Type': attachment.mimeType,
|
|
||||||
'Cache-Control': `${cacheScope}, max-age=3600`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isSvg = attachment.fileExt === '.svg';
|
|
||||||
if (fileSize && !isSvg) {
|
|
||||||
res.header('Content-Length', fileSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send(fileStream);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ export class AttachmentService {
|
|||||||
if (isUpdate) {
|
if (isUpdate) {
|
||||||
attachment = await this.attachmentRepo.updateAttachment(
|
attachment = await this.attachmentRepo.updateAttachment(
|
||||||
{
|
{
|
||||||
fileSize: preparedFile.fileSize,
|
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
attachmentId,
|
attachmentId,
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
||||||
IsIn,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
IsUUID,
|
|
||||||
ValidateIf,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
export type InputFormat = 'json' | 'markdown' | 'html';
|
|
||||||
|
|
||||||
export class CreatePageDto {
|
export class CreatePageDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -23,11 +15,4 @@ export class CreatePageDto {
|
|||||||
|
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
content?: string | object;
|
|
||||||
|
|
||||||
@ValidateIf((o) => o.content !== undefined)
|
|
||||||
@IsIn(['json', 'markdown', 'html'])
|
|
||||||
input?: InputFormat;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsIn,
|
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
@@ -23,20 +22,14 @@ export class PageHistoryIdDto {
|
|||||||
historyId: string;
|
historyId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OutputFormat = 'json' | 'markdown' | 'html';
|
|
||||||
|
|
||||||
export class PageInfoDto extends PageIdDto {
|
export class PageInfoDto extends PageIdDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
includeSpace?: boolean;
|
includeSpace: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
includeContent?: boolean;
|
includeContent: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsIn(['json', 'markdown', 'html'])
|
|
||||||
output?: OutputFormat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DeletePageDto extends PageIdDto {
|
export class DeletePageDto extends PageIdDto {
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types';
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
import { CreatePageDto, InputFormat } from './create-page.dto';
|
import { CreatePageDto } from './create-page.dto';
|
||||||
import { IsIn, IsOptional, IsString, ValidateIf } from 'class-validator';
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
export type ContentOperation = 'append' | 'replace';
|
|
||||||
|
|
||||||
export class UpdatePageDto extends PartialType(CreatePageDto) {
|
export class UpdatePageDto extends PartialType(CreatePageDto) {
|
||||||
@IsString()
|
@IsString()
|
||||||
pageId: string;
|
pageId: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
content?: string | object;
|
|
||||||
|
|
||||||
@ValidateIf((o) => o.content !== undefined)
|
|
||||||
@IsIn(['append', 'replace'])
|
|
||||||
operation?: ContentOperation;
|
|
||||||
|
|
||||||
@ValidateIf((o) => o.content !== undefined)
|
|
||||||
@IsIn(['json', 'markdown', 'html'])
|
|
||||||
input?: InputFormat;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,10 +35,6 @@ import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
|||||||
import { RecentPageDto } from './dto/recent-page.dto';
|
import { RecentPageDto } from './dto/recent-page.dto';
|
||||||
import { DuplicatePageDto } from './dto/duplicate-page.dto';
|
import { DuplicatePageDto } from './dto/duplicate-page.dto';
|
||||||
import { DeletedPageDto } from './dto/deleted-page.dto';
|
import { DeletedPageDto } from './dto/deleted-page.dto';
|
||||||
import {
|
|
||||||
jsonToHtml,
|
|
||||||
jsonToMarkdown,
|
|
||||||
} from '../../collaboration/collaboration.util';
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('pages')
|
@Controller('pages')
|
||||||
@@ -70,17 +66,6 @@ export class PageController {
|
|||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.output && dto.output !== 'json' && page.content) {
|
|
||||||
const contentOutput =
|
|
||||||
dto.output === 'markdown'
|
|
||||||
? jsonToMarkdown(page.content)
|
|
||||||
: jsonToHtml(page.content);
|
|
||||||
return {
|
|
||||||
...page,
|
|
||||||
content: contentOutput,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +101,7 @@ export class PageController {
|
|||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.pageService.update(page, updatePageDto, user);
|
return this.pageService.update(page, updatePageDto, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import { PageController } from './page.controller';
|
|||||||
import { PageHistoryService } from './services/page-history.service';
|
import { PageHistoryService } from './services/page-history.service';
|
||||||
import { TrashCleanupService } from './services/trash-cleanup.service';
|
import { TrashCleanupService } from './services/trash-cleanup.service';
|
||||||
import { StorageModule } from '../../integrations/storage/storage.module';
|
import { StorageModule } from '../../integrations/storage/storage.module';
|
||||||
import { CollaborationModule } from '../../collaboration/collaboration.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [PageController],
|
controllers: [PageController],
|
||||||
providers: [PageService, PageHistoryService, TrashCleanupService],
|
providers: [PageService, PageHistoryService, TrashCleanupService],
|
||||||
exports: [PageService, PageHistoryService],
|
exports: [PageService, PageHistoryService],
|
||||||
imports: [StorageModule, CollaborationModule],
|
imports: [StorageModule],
|
||||||
})
|
})
|
||||||
export class PageModule {}
|
export class PageModule {}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CreatePageDto, InputFormat } from '../dto/create-page.dto';
|
import { CreatePageDto } from '../dto/create-page.dto';
|
||||||
import { ContentOperation, UpdatePageDto } from '../dto/update-page.dto';
|
import { UpdatePageDto } from '../dto/update-page.dto';
|
||||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||||
import { InsertablePage, Page, User } from '@docmost/db/types/entity.types';
|
import { InsertablePage, Page, User } from '@docmost/db/types/entity.types';
|
||||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||||
@@ -28,11 +28,7 @@ import {
|
|||||||
isAttachmentNode,
|
isAttachmentNode,
|
||||||
removeMarkTypeFromDoc,
|
removeMarkTypeFromDoc,
|
||||||
} from '../../../common/helpers/prosemirror/utils';
|
} from '../../../common/helpers/prosemirror/utils';
|
||||||
import {
|
import { jsonToNode, jsonToText } from 'src/collaboration/collaboration.util';
|
||||||
htmlToJson,
|
|
||||||
jsonToNode,
|
|
||||||
jsonToText,
|
|
||||||
} from 'src/collaboration/collaboration.util';
|
|
||||||
import {
|
import {
|
||||||
CopyPageMapEntry,
|
CopyPageMapEntry,
|
||||||
ICopyPageAttachment,
|
ICopyPageAttachment,
|
||||||
@@ -44,8 +40,6 @@ import { Queue } from 'bullmq';
|
|||||||
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
||||||
import { EventName } from '../../../common/events/event.contants';
|
import { EventName } from '../../../common/events/event.contants';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { CollaborationGateway } from '../../../collaboration/collaboration.gateway';
|
|
||||||
import { markdownToHtml } from '@docmost/editor-ext';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PageService {
|
export class PageService {
|
||||||
@@ -59,7 +53,6 @@ export class PageService {
|
|||||||
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
|
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
|
||||||
@InjectQueue(QueueName.AI_QUEUE) private aiQueue: Queue,
|
@InjectQueue(QueueName.AI_QUEUE) private aiQueue: Queue,
|
||||||
private eventEmitter: EventEmitter2,
|
private eventEmitter: EventEmitter2,
|
||||||
private collaborationGateway: CollaborationGateway,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findById(
|
async findById(
|
||||||
@@ -95,42 +88,7 @@ export class PageService {
|
|||||||
parentPageId = parentPage.id;
|
parentPageId = parentPage.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = undefined;
|
const createdPage = await this.pageRepo.insertPage({
|
||||||
let textContent = undefined;
|
|
||||||
let ydoc = undefined;
|
|
||||||
|
|
||||||
if (createPageDto?.content && createPageDto?.input) {
|
|
||||||
let prosemirrorJson: any;
|
|
||||||
|
|
||||||
switch (createPageDto.input) {
|
|
||||||
case 'markdown': {
|
|
||||||
const html = await markdownToHtml(createPageDto.content as string);
|
|
||||||
prosemirrorJson = htmlToJson(html as string);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'html': {
|
|
||||||
prosemirrorJson = htmlToJson(createPageDto.content as string);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'json':
|
|
||||||
default: {
|
|
||||||
prosemirrorJson = createPageDto.content;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
jsonToNode(prosemirrorJson);
|
|
||||||
} catch (err) {
|
|
||||||
throw new BadRequestException('Invalid content format');
|
|
||||||
}
|
|
||||||
|
|
||||||
content = prosemirrorJson;
|
|
||||||
textContent = jsonToText(prosemirrorJson);
|
|
||||||
ydoc = createYdocFromJson(prosemirrorJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.pageRepo.insertPage({
|
|
||||||
slugId: generateSlugId(),
|
slugId: generateSlugId(),
|
||||||
title: createPageDto.title,
|
title: createPageDto.title,
|
||||||
position: await this.nextPagePosition(
|
position: await this.nextPagePosition(
|
||||||
@@ -143,10 +101,9 @@ export class PageService {
|
|||||||
creatorId: userId,
|
creatorId: userId,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
lastUpdatedById: userId,
|
lastUpdatedById: userId,
|
||||||
content,
|
|
||||||
textContent,
|
|
||||||
ydoc,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return createdPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async nextPagePosition(spaceId: string, parentPageId?: string) {
|
async nextPagePosition(spaceId: string, parentPageId?: string) {
|
||||||
@@ -193,37 +150,23 @@ export class PageService {
|
|||||||
async update(
|
async update(
|
||||||
page: Page,
|
page: Page,
|
||||||
updatePageDto: UpdatePageDto,
|
updatePageDto: UpdatePageDto,
|
||||||
user: User,
|
userId: string,
|
||||||
): Promise<Page> {
|
): Promise<Page> {
|
||||||
const contributors = new Set<string>(page.contributorIds);
|
const contributors = new Set<string>(page.contributorIds);
|
||||||
contributors.add(user.id);
|
contributors.add(userId);
|
||||||
const contributorIds = Array.from(contributors);
|
const contributorIds = Array.from(contributors);
|
||||||
|
|
||||||
await this.pageRepo.updatePage(
|
await this.pageRepo.updatePage(
|
||||||
{
|
{
|
||||||
title: updatePageDto.title,
|
title: updatePageDto.title,
|
||||||
icon: updatePageDto.icon,
|
icon: updatePageDto.icon,
|
||||||
lastUpdatedById: user.id,
|
lastUpdatedById: userId,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
contributorIds: contributorIds,
|
contributorIds: contributorIds,
|
||||||
},
|
},
|
||||||
page.id,
|
page.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
|
||||||
updatePageDto.content &&
|
|
||||||
updatePageDto.operation &&
|
|
||||||
updatePageDto.input
|
|
||||||
) {
|
|
||||||
await this.updatePageContent(
|
|
||||||
page.id,
|
|
||||||
updatePageDto.content,
|
|
||||||
updatePageDto.operation,
|
|
||||||
updatePageDto.input,
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.pageRepo.findById(page.id, {
|
return await this.pageRepo.findById(page.id, {
|
||||||
includeSpace: true,
|
includeSpace: true,
|
||||||
includeContent: true,
|
includeContent: true,
|
||||||
@@ -233,46 +176,6 @@ export class PageService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePageContent(
|
|
||||||
pageId: string,
|
|
||||||
content: string | object,
|
|
||||||
operation: ContentOperation,
|
|
||||||
input: InputFormat,
|
|
||||||
user: User,
|
|
||||||
): Promise<void> {
|
|
||||||
let prosemirrorJson: any;
|
|
||||||
|
|
||||||
switch (input) {
|
|
||||||
case 'markdown': {
|
|
||||||
const html = await markdownToHtml(content as string);
|
|
||||||
prosemirrorJson = htmlToJson(html as string);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'html': {
|
|
||||||
prosemirrorJson = htmlToJson(content as string);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'json':
|
|
||||||
default: {
|
|
||||||
prosemirrorJson = content;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
jsonToNode(prosemirrorJson);
|
|
||||||
} catch (err) {
|
|
||||||
throw new BadRequestException('Invalid content format');
|
|
||||||
}
|
|
||||||
|
|
||||||
const documentName = `page.${pageId}`;
|
|
||||||
await this.collaborationGateway.handleYjsEvent(
|
|
||||||
'updatePageContent',
|
|
||||||
documentName,
|
|
||||||
{ pageId, operation, prosemirrorJson, user },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSidebarPages(
|
async getSidebarPages(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
pagination: PaginationOptions,
|
pagination: PaginationOptions,
|
||||||
@@ -306,11 +209,7 @@ export class PageService {
|
|||||||
cursor: pagination.cursor,
|
cursor: pagination.cursor,
|
||||||
beforeCursor: pagination.beforeCursor,
|
beforeCursor: pagination.beforeCursor,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ expression: 'position', direction: 'asc', orderModifier: (ob) => ob.collate('C').asc() },
|
||||||
expression: 'position',
|
|
||||||
direction: 'asc',
|
|
||||||
orderModifier: (ob) => ob.collate('C').asc(),
|
|
||||||
},
|
|
||||||
{ expression: 'id', direction: 'asc' },
|
{ expression: 'id', direction: 'asc' },
|
||||||
],
|
],
|
||||||
parseCursor: (cursor) => ({
|
parseCursor: (cursor) => ({
|
||||||
|
|||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
import { type Kysely, sql } from 'kysely';
|
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
|
||||||
await db.schema
|
|
||||||
.alterTable('page_history')
|
|
||||||
.addColumn('contributor_ids', sql`uuid[]`, (col) => col.defaultTo('{}'))
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
|
||||||
await db.schema
|
|
||||||
.alterTable('page_history')
|
|
||||||
.dropColumn('contributor_ids')
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
} from '@docmost/db/types/entity.types';
|
} from '@docmost/db/types/entity.types';
|
||||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||||
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
|
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
|
||||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import { ExpressionBuilder, sql } from 'kysely';
|
import { ExpressionBuilder } from 'kysely';
|
||||||
import { DB } from '@docmost/db/types/db';
|
import { DB } from '@docmost/db/types/db';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -25,7 +25,6 @@ export class PageHistoryRepo {
|
|||||||
'icon',
|
'icon',
|
||||||
'coverPhoto',
|
'coverPhoto',
|
||||||
'lastUpdatedById',
|
'lastUpdatedById',
|
||||||
'contributorIds',
|
|
||||||
'spaceId',
|
'spaceId',
|
||||||
'workspaceId',
|
'workspaceId',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
@@ -45,7 +44,6 @@ export class PageHistoryRepo {
|
|||||||
.select(this.baseFields)
|
.select(this.baseFields)
|
||||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
||||||
.select((eb) => this.withLastUpdatedBy(eb))
|
.select((eb) => this.withLastUpdatedBy(eb))
|
||||||
.select((eb) => this.withContributors(eb))
|
|
||||||
.where('id', '=', pageHistoryId)
|
.where('id', '=', pageHistoryId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
@@ -62,10 +60,7 @@ export class PageHistoryRepo {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveHistory(
|
async saveHistory(page: Page, trx?: KyselyTransaction): Promise<void> {
|
||||||
page: Page,
|
|
||||||
opts?: { contributorIds?: string[]; trx?: KyselyTransaction },
|
|
||||||
): Promise<void> {
|
|
||||||
await this.insertPageHistory(
|
await this.insertPageHistory(
|
||||||
{
|
{
|
||||||
pageId: page.id,
|
pageId: page.id,
|
||||||
@@ -75,11 +70,10 @@ export class PageHistoryRepo {
|
|||||||
icon: page.icon,
|
icon: page.icon,
|
||||||
coverPhoto: page.coverPhoto,
|
coverPhoto: page.coverPhoto,
|
||||||
lastUpdatedById: page.lastUpdatedById ?? page.creatorId,
|
lastUpdatedById: page.lastUpdatedById ?? page.creatorId,
|
||||||
contributorIds: opts?.contributorIds,
|
|
||||||
spaceId: page.spaceId,
|
spaceId: page.spaceId,
|
||||||
workspaceId: page.workspaceId,
|
workspaceId: page.workspaceId,
|
||||||
},
|
},
|
||||||
opts?.trx,
|
trx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +82,6 @@ export class PageHistoryRepo {
|
|||||||
.selectFrom('pageHistory')
|
.selectFrom('pageHistory')
|
||||||
.select(this.baseFields)
|
.select(this.baseFields)
|
||||||
.select((eb) => this.withLastUpdatedBy(eb))
|
.select((eb) => this.withLastUpdatedBy(eb))
|
||||||
.select((eb) => this.withContributors(eb))
|
|
||||||
.where('pageId', '=', pageId);
|
.where('pageId', '=', pageId);
|
||||||
|
|
||||||
return executeWithCursorPagination(query, {
|
return executeWithCursorPagination(query, {
|
||||||
@@ -127,17 +120,4 @@ export class PageHistoryRepo {
|
|||||||
.whereRef('users.id', '=', 'pageHistory.lastUpdatedById'),
|
.whereRef('users.id', '=', 'pageHistory.lastUpdatedById'),
|
||||||
).as('lastUpdatedBy');
|
).as('lastUpdatedBy');
|
||||||
}
|
}
|
||||||
|
|
||||||
withContributors(eb: ExpressionBuilder<DB, 'pageHistory'>) {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('users')
|
|
||||||
.select(['users.id', 'users.name', 'users.avatarUrl'])
|
|
||||||
.whereRef(
|
|
||||||
'users.id',
|
|
||||||
'=',
|
|
||||||
sql`ANY(${eb.ref('pageHistory.contributorIds')})`,
|
|
||||||
),
|
|
||||||
).as('contributors');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -199,7 +199,6 @@ export interface GroupUsers {
|
|||||||
|
|
||||||
export interface PageHistory {
|
export interface PageHistory {
|
||||||
content: Json | null;
|
content: Json | null;
|
||||||
contributorIds: Generated<string[] | null>;
|
|
||||||
coverPhoto: string | null;
|
coverPhoto: string | null;
|
||||||
createdAt: Generated<Timestamp>;
|
createdAt: Generated<Timestamp>;
|
||||||
icon: string | null;
|
icon: string | null;
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 247e5eb7d1...6d3eb76d4e
@@ -44,7 +44,7 @@ export class ImportController {
|
|||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
const validFileExtensions = ['.md', '.html', '.docx'];
|
const validFileExtensions = ['.md', '.html'];
|
||||||
|
|
||||||
const maxFileSize = bytes('10mb');
|
const maxFileSize = bytes('10mb');
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import { StorageService } from '../../storage/storage.service';
|
|||||||
import { InjectQueue } from '@nestjs/bullmq';
|
import { InjectQueue } from '@nestjs/bullmq';
|
||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
import { QueueJob, QueueName } from '../../queue/constants';
|
import { QueueJob, QueueName } from '../../queue/constants';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImportService {
|
export class ImportService {
|
||||||
@@ -41,7 +40,6 @@ export class ImportService {
|
|||||||
@InjectKysely() private readonly db: KyselyDB,
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
@InjectQueue(QueueName.FILE_TASK_QUEUE)
|
@InjectQueue(QueueName.FILE_TASK_QUEUE)
|
||||||
private readonly fileTaskQueue: Queue,
|
private readonly fileTaskQueue: Queue,
|
||||||
private moduleRef: ModuleRef,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async importPage(
|
async importPage(
|
||||||
@@ -61,22 +59,11 @@ export class ImportService {
|
|||||||
let prosemirrorState = null;
|
let prosemirrorState = null;
|
||||||
let createdPage = null;
|
let createdPage = null;
|
||||||
|
|
||||||
// For DOCX, we need the page ID upfront so images can reference it
|
|
||||||
const pageId = fileExtension === '.docx' ? uuid7() : undefined;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (fileExtension.endsWith('.md')) {
|
if (fileExtension.endsWith('.md')) {
|
||||||
prosemirrorState = await this.processMarkdown(fileContent);
|
prosemirrorState = await this.processMarkdown(fileContent);
|
||||||
} else if (fileExtension.endsWith('.html')) {
|
} else if (fileExtension.endsWith('.html')) {
|
||||||
prosemirrorState = await this.processHTML(fileContent);
|
prosemirrorState = await this.processHTML(fileContent);
|
||||||
} else if (fileExtension.endsWith('.docx')) {
|
|
||||||
prosemirrorState = await this.processDocx(
|
|
||||||
fileBuffer,
|
|
||||||
workspaceId,
|
|
||||||
spaceId,
|
|
||||||
pageId,
|
|
||||||
userId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = 'Error processing file content';
|
const message = 'Error processing file content';
|
||||||
@@ -100,7 +87,6 @@ export class ImportService {
|
|||||||
const pagePosition = await this.getNewPagePosition(spaceId);
|
const pagePosition = await this.getNewPagePosition(spaceId);
|
||||||
|
|
||||||
createdPage = await this.pageRepo.insertPage({
|
createdPage = await this.pageRepo.insertPage({
|
||||||
...(pageId ? { id: pageId } : {}),
|
|
||||||
slugId: generateSlugId(),
|
slugId: generateSlugId(),
|
||||||
title: pageTitle,
|
title: pageTitle,
|
||||||
content: prosemirrorJson,
|
content: prosemirrorJson,
|
||||||
@@ -143,42 +129,6 @@ export class ImportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async processDocx(
|
|
||||||
fileBuffer: Buffer,
|
|
||||||
workspaceId: string,
|
|
||||||
spaceId: string,
|
|
||||||
pageId: string,
|
|
||||||
userId: string,
|
|
||||||
): Promise<any> {
|
|
||||||
let DocxImportModule: any;
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
||||||
DocxImportModule = require('./../../../ee/docx-import/docx-import.service');
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(
|
|
||||||
'DOCX import requested but EE module not bundled in this build',
|
|
||||||
);
|
|
||||||
throw new BadRequestException(
|
|
||||||
'This feature requires a valid enterprise license.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const docxImportService = this.moduleRef.get(
|
|
||||||
DocxImportModule.DocxImportService,
|
|
||||||
{ strict: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const html = await docxImportService.convertDocxToHtml(
|
|
||||||
fileBuffer,
|
|
||||||
workspaceId,
|
|
||||||
spaceId,
|
|
||||||
pageId,
|
|
||||||
userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.processHTML(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createYdoc(prosemirrorJson: any): Promise<Buffer | null> {
|
async createYdoc(prosemirrorJson: any): Promise<Buffer | null> {
|
||||||
if (prosemirrorJson) {
|
if (prosemirrorJson) {
|
||||||
// this.logger.debug(`Converting prosemirror json state to ydoc`);
|
// this.logger.debug(`Converting prosemirror json state to ydoc`);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export enum QueueName {
|
|||||||
FILE_TASK_QUEUE = '{file-task-queue}',
|
FILE_TASK_QUEUE = '{file-task-queue}',
|
||||||
SEARCH_QUEUE = '{search-queue}',
|
SEARCH_QUEUE = '{search-queue}',
|
||||||
AI_QUEUE = '{ai-queue}',
|
AI_QUEUE = '{ai-queue}',
|
||||||
HISTORY_QUEUE = '{history-queue}',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJob {
|
export enum QueueJob {
|
||||||
@@ -59,6 +58,4 @@ export enum QueueJob {
|
|||||||
|
|
||||||
GENERATE_PAGE_EMBEDDINGS = 'generate-page-embeddings',
|
GENERATE_PAGE_EMBEDDINGS = 'generate-page-embeddings',
|
||||||
DELETE_PAGE_EMBEDDINGS = 'delete-page-embeddings',
|
DELETE_PAGE_EMBEDDINGS = 'delete-page-embeddings',
|
||||||
|
|
||||||
PAGE_HISTORY = 'page-history',
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,4 @@ export interface IPageBacklinkJob {
|
|||||||
|
|
||||||
export interface IStripeSeatsSyncJob {
|
export interface IStripeSeatsSyncJob {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPageHistoryJob {
|
|
||||||
pageId: string;
|
|
||||||
}
|
}
|
||||||
@@ -73,14 +73,6 @@ import { BacklinksProcessor } from './processors/backlinks.processor';
|
|||||||
attempts: 1,
|
attempts: 1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
BullModule.registerQueue({
|
|
||||||
name: QueueName.HISTORY_QUEUE,
|
|
||||||
defaultJobOptions: {
|
|
||||||
removeOnComplete: true,
|
|
||||||
removeOnFail: true,
|
|
||||||
attempts: 2,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
exports: [BullModule],
|
exports: [BullModule],
|
||||||
providers: [BacklinksProcessor],
|
providers: [BacklinksProcessor],
|
||||||
|
|||||||
@@ -73,20 +73,6 @@ export class LocalDriver implements StorageDriver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readRangeStream(
|
|
||||||
filePath: string,
|
|
||||||
range: { start: number; end: number },
|
|
||||||
): Promise<Readable> {
|
|
||||||
try {
|
|
||||||
return createReadStream(this._fullPath(filePath), {
|
|
||||||
start: range.start,
|
|
||||||
end: range.end,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Failed to read file: ${(err as Error).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async exists(filePath: string): Promise<boolean> {
|
async exists(filePath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
return await fs.pathExists(this._fullPath(filePath));
|
return await fs.pathExists(this._fullPath(filePath));
|
||||||
|
|||||||
@@ -130,25 +130,6 @@ export class S3Driver implements StorageDriver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readRangeStream(
|
|
||||||
filePath: string,
|
|
||||||
range: { start: number; end: number },
|
|
||||||
): Promise<Readable> {
|
|
||||||
try {
|
|
||||||
const command = new GetObjectCommand({
|
|
||||||
Bucket: this.config.bucket,
|
|
||||||
Key: filePath,
|
|
||||||
Range: `bytes=${range.start}-${range.end}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await this.s3Client.send(command);
|
|
||||||
|
|
||||||
return response.Body as Readable;
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Failed to read file from S3: ${(err as Error).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async exists(filePath: string): Promise<boolean> {
|
async exists(filePath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const command = new HeadObjectCommand({
|
const command = new HeadObjectCommand({
|
||||||
|
|||||||
@@ -11,11 +11,6 @@ export interface StorageDriver {
|
|||||||
|
|
||||||
readStream(filePath: string): Promise<Readable>;
|
readStream(filePath: string): Promise<Readable>;
|
||||||
|
|
||||||
readRangeStream(
|
|
||||||
filePath: string,
|
|
||||||
range: { start: number; end: number },
|
|
||||||
): Promise<Readable>;
|
|
||||||
|
|
||||||
exists(filePath: string): Promise<boolean>;
|
exists(filePath: string): Promise<boolean>;
|
||||||
|
|
||||||
getUrl(filePath: string): string;
|
getUrl(filePath: string): string;
|
||||||
|
|||||||
@@ -33,13 +33,6 @@ export class StorageService {
|
|||||||
return this.storageDriver.readStream(filePath);
|
return this.storageDriver.readStream(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async readRangeStream(
|
|
||||||
filePath: string,
|
|
||||||
range: { start: number; end: number },
|
|
||||||
): Promise<Readable> {
|
|
||||||
return this.storageDriver.readRangeStream(filePath, range);
|
|
||||||
}
|
|
||||||
|
|
||||||
async exists(filePath: string): Promise<boolean> {
|
async exists(filePath: string): Promise<boolean> {
|
||||||
return this.storageDriver.exists(filePath);
|
return this.storageDriver.exists(filePath);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-5
@@ -7,7 +7,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
APP_URL: 'http://localhost:3000'
|
APP_URL: 'http://localhost:3000'
|
||||||
APP_SECRET: 'REPLACE_WITH_LONG_SECRET'
|
APP_SECRET: 'REPLACE_WITH_LONG_SECRET'
|
||||||
DATABASE_URL: 'postgresql://docmost:STRONG_DB_PASSWORD@db:5432/docmost'
|
DATABASE_URL: 'postgresql://docmost:STRONG_DB_PASSWORD@db:5432/docmost?schema=public'
|
||||||
REDIS_URL: 'redis://redis:6379'
|
REDIS_URL: 'redis://redis:6379'
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
@@ -16,18 +16,17 @@ services:
|
|||||||
- docmost:/app/data/storage
|
- docmost:/app/data/storage
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:18
|
image: postgres:16-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: docmost
|
POSTGRES_DB: docmost
|
||||||
POSTGRES_USER: docmost
|
POSTGRES_USER: docmost
|
||||||
POSTGRES_PASSWORD: STRONG_DB_PASSWORD
|
POSTGRES_PASSWORD: STRONG_DB_PASSWORD
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- db_data:/var/lib/postgresql
|
- db_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:8
|
image: redis:7.2-alpine
|
||||||
command: ["redis-server", "--appendonly", "yes", "--maxmemory-policy", "noeviction"]
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
|
|||||||
+5
-22
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.25.3",
|
"version": "0.25.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
@@ -56,13 +56,12 @@
|
|||||||
"@tiptap/react": "3.17.1",
|
"@tiptap/react": "3.17.1",
|
||||||
"@tiptap/starter-kit": "3.17.1",
|
"@tiptap/starter-kit": "3.17.1",
|
||||||
"@tiptap/suggestion": "3.17.1",
|
"@tiptap/suggestion": "3.17.1",
|
||||||
"@tiptap/y-tiptap": "^3.0.2",
|
|
||||||
"@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",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"diff": "8.0.3",
|
"diff": "8.0.3",
|
||||||
"dompurify": "^3.3.1",
|
"dompurify": "^3.2.6",
|
||||||
"fractional-indexing-jittered": "^1.0.0",
|
"fractional-indexing-jittered": "^1.0.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"image-dimensions": "^2.5.0",
|
"image-dimensions": "^2.5.0",
|
||||||
@@ -79,12 +78,12 @@
|
|||||||
"yjs": "^13.6.29"
|
"yjs": "^13.6.29"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nx/js": "22.5.0",
|
"@nx/js": "20.4.5",
|
||||||
"@types/bytes": "^3.1.5",
|
"@types/bytes": "^3.1.5",
|
||||||
"@types/turndown": "^5.0.6",
|
"@types/turndown": "^5.0.6",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"nx": "22.5.0",
|
"nx": "20.4.5",
|
||||||
"tsx": "^4.19.3"
|
"tsx": "^4.19.3"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
@@ -102,23 +101,7 @@
|
|||||||
"jsdom": "25.0.1",
|
"jsdom": "25.0.1",
|
||||||
"jsonwebtoken": "9.0.3",
|
"jsonwebtoken": "9.0.3",
|
||||||
"prosemirror-changeset": "2.3.1",
|
"prosemirror-changeset": "2.3.1",
|
||||||
"y-prosemirror": "1.3.7",
|
"y-prosemirror": "1.3.7"
|
||||||
"glob": "10.5.0",
|
|
||||||
"lodash": "4.17.23",
|
|
||||||
"ws": "8.19.0",
|
|
||||||
"cross-spawn": "7.0.5",
|
|
||||||
"dompurify": "3.3.1",
|
|
||||||
"tmp": "0.2.5",
|
|
||||||
"lodash-es": "4.17.23",
|
|
||||||
"@tiptap/core": "3.17.1",
|
|
||||||
"@tiptap/pm": "3.17.1",
|
|
||||||
"@tiptap/starter-kit": "3.17.1",
|
|
||||||
"@tiptap/extension-blockquote": "3.17.1",
|
|
||||||
"@tiptap/extension-bold": "3.17.0",
|
|
||||||
"@tiptap/extension-bubble-menu": "3.17.1",
|
|
||||||
"@tiptap/extension-bullet-list": "3.17.1",
|
|
||||||
"@tiptap/extension-list": "3.17.1",
|
|
||||||
"@tiptap/extension-code": "3.17.1"
|
|
||||||
},
|
},
|
||||||
"neverBuiltDependencies": []
|
"neverBuiltDependencies": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import TiptapHeading, {
|
|||||||
import { mergeAttributes } from "@tiptap/react";
|
import { mergeAttributes } from "@tiptap/react";
|
||||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||||
import { Plugin } from "prosemirror-state";
|
import { Plugin } from "prosemirror-state";
|
||||||
import { copyToClipboard } from "../utils";
|
|
||||||
|
|
||||||
const copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><!-- Icon from Material Symbols Light by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M10.616 16.077H7.077q-1.692 0-2.884-1.192T3 12t1.193-2.885t2.884-1.193h3.539v1H7.077q-1.27 0-2.173.904Q4 10.731 4 12t.904 2.173t2.173.904h3.539zM8.5 12.5v-1h7v1zm4.885 3.577v-1h3.538q1.27 0 2.173-.904Q20 13.269 20 12t-.904-2.173t-2.173-.904h-3.538v-1h3.538q1.692 0 2.885 1.192T21 12t-1.193 2.885t-2.884 1.193z"/></svg>`;
|
const copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><!-- Icon from Material Symbols Light by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M10.616 16.077H7.077q-1.692 0-2.884-1.192T3 12t1.193-2.885t2.884-1.193h3.539v1H7.077q-1.27 0-2.173.904Q4 10.731 4 12t.904 2.173t2.173.904h3.539zM8.5 12.5v-1h7v1zm4.885 3.577v-1h3.538q1.27 0 2.173-.904Q20 13.269 20 12t-.904-2.173t-2.173-.904h-3.538v-1h3.538q1.692 0 2.885 1.192T21 12t-1.193 2.885t-2.884 1.193z"/></svg>`;
|
||||||
const successIcon = `<svg xmlns="http://www.w3.org/2000/svg" style="color: forestgreen;" width="18" height="18" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m10.6 16.6l7.05-7.05l-1.4-1.4l-5.65 5.65l-2.85-2.85l-1.4 1.4zM12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"/></svg>`;
|
const successIcon = `<svg xmlns="http://www.w3.org/2000/svg" style="color: forestgreen;" width="18" height="18" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m10.6 16.6l7.05-7.05l-1.4-1.4l-5.65 5.65l-2.85-2.85l-1.4 1.4zM12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"/></svg>`;
|
||||||
|
|
||||||
export const Heading = TiptapHeading.extend<TiptapHeadingOptions>({
|
export const Heading = TiptapHeading.extend<TiptapHeadingOptions>({
|
||||||
// @ts-ignore
|
|
||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
return [
|
return [
|
||||||
new Plugin({
|
new Plugin({
|
||||||
@@ -43,7 +41,7 @@ export const Heading = TiptapHeading.extend<TiptapHeadingOptions>({
|
|||||||
const id = node.attrs.id;
|
const id = node.attrs.id;
|
||||||
const baseUrl = window.location.href.split('#')[0];
|
const baseUrl = window.location.href.split('#')[0];
|
||||||
const url = `${baseUrl}#${id}`;
|
const url = `${baseUrl}#${id}`;
|
||||||
copyToClipboard(url);
|
navigator.clipboard.writeText(url);
|
||||||
linkBtnContent.innerHTML = successIcon;
|
linkBtnContent.innerHTML = successIcon;
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => (linkBtnContent.innerHTML = copyIcon),
|
() => (linkBtnContent.innerHTML = copyIcon),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { imageDimensionsFromData } from 'image-dimensions';
|
import { imageDimensionsFromStream } from "image-dimensions";
|
||||||
import { MediaUploadOptions, UploadFn } from '../media-utils';
|
import { MediaUploadOptions, UploadFn } from "../media-utils";
|
||||||
import { IAttachment } from '../types';
|
import { IAttachment } from "../types";
|
||||||
import { generateNodeId } from '../utils';
|
import { generateNodeId } from "../utils";
|
||||||
import { Node } from '@tiptap/pm/model';
|
import { Node } from "@tiptap/pm/model";
|
||||||
import { Command } from '@tiptap/core';
|
import { Command } from "@tiptap/core";
|
||||||
|
|
||||||
const findImageNodeByPlaceholderId = (
|
const findImageNodeByPlaceholderId = (
|
||||||
doc: Node,
|
doc: Node,
|
||||||
@@ -14,7 +14,7 @@ const findImageNodeByPlaceholderId = (
|
|||||||
doc.descendants((node, pos) => {
|
doc.descendants((node, pos) => {
|
||||||
if (result) return false;
|
if (result) return false;
|
||||||
if (
|
if (
|
||||||
node.type.name === 'image' &&
|
node.type.name === "image" &&
|
||||||
node.attrs.placeholder?.id === placeholderId
|
node.attrs.placeholder?.id === placeholderId
|
||||||
) {
|
) {
|
||||||
result = { node, pos };
|
result = { node, pos };
|
||||||
@@ -34,11 +34,7 @@ const handleImageUpload =
|
|||||||
if (!validated) return;
|
if (!validated) return;
|
||||||
|
|
||||||
const objectUrl = URL.createObjectURL(file);
|
const objectUrl = URL.createObjectURL(file);
|
||||||
|
const imageDimensions = await imageDimensionsFromStream(file.stream());
|
||||||
const imageDimensions = imageDimensionsFromData(
|
|
||||||
new Uint8Array(await file.arrayBuffer()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const placeholderId = generateNodeId();
|
const placeholderId = generateNodeId();
|
||||||
const aspectRatio = imageDimensions
|
const aspectRatio = imageDimensions
|
||||||
? imageDimensions.width / imageDimensions.height
|
? imageDimensions.width / imageDimensions.height
|
||||||
|
|||||||
@@ -384,25 +384,3 @@ export function sanitizeUrl(url: string | undefined): string {
|
|||||||
|
|
||||||
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
||||||
export const generateNodeId = customAlphabet(alphabet, 12);
|
export const generateNodeId = customAlphabet(alphabet, 12);
|
||||||
|
|
||||||
export function copyToClipboard(text: string): void {
|
|
||||||
if ("clipboard" in navigator) {
|
|
||||||
navigator.clipboard.writeText(text).catch(() => {
|
|
||||||
execCommandCopy(text);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
execCommandCopy(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function execCommandCopy(text: string): void {
|
|
||||||
const textarea = document.createElement("textarea");
|
|
||||||
textarea.value = text;
|
|
||||||
textarea.style.position = "fixed";
|
|
||||||
textarea.style.left = "-9999px";
|
|
||||||
textarea.style.top = "-9999px";
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
document.execCommand("copy");
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
|
|||||||
Generated
+4051
-4352
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user