diff --git a/apps/client/package.json b/apps/client/package.json index e9197ef9..504f0f5f 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -24,7 +24,6 @@ "@mantine/spotlight": "^8.3.12", "@tabler/icons-react": "^3.36.1", "@tanstack/react-query": "^5.90.17", - "@tiptap/extension-character-count": "^2.27.1", "alfaaz": "^1.1.0", "axios": "^1.13.2", "clsx": "^2.1.1", @@ -54,7 +53,6 @@ "react-router-dom": "^7.12.0", "semver": "^7.7.3", "socket.io-client": "^4.8.3", - "tippy.js": "^6.3.7", "tiptap-extension-global-drag-handle": "^0.1.18", "zod": "^3.25.76" }, diff --git a/apps/client/public/locales/de-DE/translation.json b/apps/client/public/locales/de-DE/translation.json index 1763e428..34fb5bb4 100644 --- a/apps/client/public/locales/de-DE/translation.json +++ b/apps/client/public/locales/de-DE/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "Wählen Sie Ihre bevorzugte Benutzersprache.", "Choose your preferred page width.": "Wählen Sie Ihre bevorzugte Seitenbreite.", "Confirm": "Bestätigen", + "Copy as Markdown": "Copy as Markdown", "Copy link": "Link kopieren", "Create": "Erstellen", "Create group": "Gruppe erstellen", @@ -234,7 +235,9 @@ "Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.", "Invite link": "Einladungslink", "Copy": "Kopieren", + "Copy to space": "In Raum kopieren", "Copied": "Kopiert", + "Duplicate": "Duplizieren", "Select a user": "Benutzer auswählen", "Select a group": "Gruppe auswählen", "Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.", @@ -251,6 +254,7 @@ "Export failed:": "Export fehlgeschlagen:", "export error": "Exportfehler", "Export page": "Seite exportieren", + "Export successful": "Export successful", "Export space": "Bereich exportieren", "Export {{type}}": "Exportiere {{type}}", "File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}", @@ -326,6 +330,8 @@ "Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.", "Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.", "Upload any file from your device.": "Laden Sie eine beliebige Datei von Ihrem Gerät hoch.", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "Tabelle", "Insert a table.": "Tabelle einfügen.", "Insert collapsible block.": "Einklappbaren Block einfügen.", diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 8cb33378..c0578d2b 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "Choose your preferred interface language.", "Choose your preferred page width.": "Choose your preferred page width.", "Confirm": "Confirm", + "Copy as Markdown": "Copy as Markdown", "Copy link": "Copy link", "Create": "Create", "Create group": "Create group", @@ -253,6 +254,7 @@ "Export failed:": "Export failed:", "export error": "export error", "Export page": "Export page", + "Export successful": "Export successful", "Export space": "Export space", "Export {{type}}": "Export {{type}}", "File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit", @@ -328,6 +330,8 @@ "Upload any image from your device.": "Upload any image from your device.", "Upload any video from your device.": "Upload any video from your device.", "Upload any file from your device.": "Upload any file from your device.", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "Table", "Insert a table.": "Insert a table.", "Insert collapsible block.": "Insert collapsible block.", diff --git a/apps/client/public/locales/es-ES/translation.json b/apps/client/public/locales/es-ES/translation.json index f99e8541..d68f64a7 100644 --- a/apps/client/public/locales/es-ES/translation.json +++ b/apps/client/public/locales/es-ES/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "Elige tu idioma de interfaz preferido.", "Choose your preferred page width.": "Elige el ancho de página que prefieras.", "Confirm": "Confirmar", + "Copy as Markdown": "Copy as Markdown", "Copy link": "Copiar enlace", "Create": "Crear", "Create group": "Crear grupo", @@ -253,6 +254,7 @@ "Export failed:": "Exportación fallida:", "export error": "error de exportación", "Export page": "Exportar página", + "Export successful": "Export successful", "Export space": "Exportar espacio", "Export {{type}}": "Exportar {{type}}", "File exceeds the {{limit}} attachment limit": "El archivo supera el límite de {{limit}} adjuntos", @@ -328,6 +330,8 @@ "Upload any image from your device.": "Sube cualquier imagen desde tu dispositivo.", "Upload any video from your device.": "Sube cualquier video desde tu dispositivo.", "Upload any file from your device.": "Sube cualquier archivo desde tu dispositivo.", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "Tabla", "Insert a table.": "Insertar una tabla.", "Insert collapsible block.": "Insertar bloque desplegable.", diff --git a/apps/client/public/locales/fr-FR/translation.json b/apps/client/public/locales/fr-FR/translation.json index 5644d719..4a70b735 100644 --- a/apps/client/public/locales/fr-FR/translation.json +++ b/apps/client/public/locales/fr-FR/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "Choisissez votre langue d'interface préférée.", "Choose your preferred page width.": "Choisissez votre largeur de page préférée.", "Confirm": "Confirmer", + "Copy as Markdown": "Copy as Markdown", "Copy link": "Copier le lien", "Create": "Créer", "Create group": "Créer groupe", @@ -253,6 +254,7 @@ "Export failed:": "Échec de l'exportation :", "export error": "exporter l'erreur", "Export page": "Exporter la page", + "Export successful": "Export successful", "Export space": "Exporter l'espace", "Export {{type}}": "Exporter {{type}}", "File exceeds the {{limit}} attachment limit": "Le fichier dépasse la limite de {{limit}} pièces jointes", @@ -328,6 +330,8 @@ "Upload any image from your device.": "Téléchargez n'importe quelle image depuis votre appareil.", "Upload any video from your device.": "Téléchargez n'importe quelle vidéo depuis votre appareil.", "Upload any file from your device.": "Téléchargez n'importe quel fichier depuis votre appareil.", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "Tableau", "Insert a table.": "Insérez un tableau.", "Insert collapsible block.": "Insérer un bloc repliable.", diff --git a/apps/client/public/locales/it-IT/translation.json b/apps/client/public/locales/it-IT/translation.json index 8d00f451..a716a86a 100644 --- a/apps/client/public/locales/it-IT/translation.json +++ b/apps/client/public/locales/it-IT/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "Scegli la lingua da utilizzare per l'interfaccia.", "Choose your preferred page width.": "Scegli la larghezza della pagina che preferisci.", "Confirm": "Conferma", + "Copy as Markdown": "Copy as Markdown", "Copy link": "Copia link", "Create": "Crea", "Create group": "Crea gruppo", @@ -253,6 +254,7 @@ "Export failed:": "Esportazione fallita:", "export error": "errore di esportazione", "Export page": "Esporta pagina", + "Export successful": "Export successful", "Export space": "Esporta spazio", "Export {{type}}": "Esporta {{type}}", "File exceeds the {{limit}} attachment limit": "Il file supera il limite per gli allegati di {{limit}}", @@ -328,6 +330,8 @@ "Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.", "Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.", "Upload any file from your device.": "Carica qualsiasi file dal tuo dispositivo.", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "Tabella", "Insert a table.": "Inserisci una tabella.", "Insert collapsible block.": "Inserisci blocco comprimibile.", diff --git a/apps/client/public/locales/ja-JP/translation.json b/apps/client/public/locales/ja-JP/translation.json index 2c3cce5a..be341728 100644 --- a/apps/client/public/locales/ja-JP/translation.json +++ b/apps/client/public/locales/ja-JP/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "お好みの言語を選択してください", "Choose your preferred page width.": "お好みのページ幅を選択してください", "Confirm": "確認", + "Copy as Markdown": "Copy as Markdown", "Copy link": "リンクをコピー", "Create": "新規作成", "Create group": "グループを作成", @@ -253,6 +254,7 @@ "Export failed:": "エクスポートに失敗しました:", "export error": "エクスポートエラー", "Export page": "エクスポートページ", + "Export successful": "Export successful", "Export space": "エクスポートスペース", "Export {{type}}": "{{type}}をエクスポート", "File exceeds the {{limit}} attachment limit": "ファイルが{{limit}}の添付制限を超えています", @@ -328,6 +330,8 @@ "Upload any image from your device.": "デバイスから画像をアップロードします", "Upload any video from your device.": "デバイスから動画をアップロードします", "Upload any file from your device.": "デバイスからファイルをアップロードします", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "テーブル", "Insert a table.": "テーブルを挿入します", "Insert collapsible block.": "折りたたみブロックを挿入します", diff --git a/apps/client/public/locales/ko-KR/translation.json b/apps/client/public/locales/ko-KR/translation.json index 6e1f5b24..6e83db5f 100644 --- a/apps/client/public/locales/ko-KR/translation.json +++ b/apps/client/public/locales/ko-KR/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "선호하는 인터페이스 언어를 선택하세요.", "Choose your preferred page width.": "선호하는 페이지 너비를 선택하세요.", "Confirm": "확인", + "Copy as Markdown": "Copy as Markdown", "Copy link": "링크 복사", "Create": "생성", "Create group": "팀 생성", @@ -253,6 +254,7 @@ "Export failed:": "내보내기 실패:", "export error": "내보내기 오류", "Export page": "페이지 내보내기", + "Export successful": "Export successful", "Export space": "Space 내보내기", "Export {{type}}": "{{type}} 내보내기", "File exceeds the {{limit}} attachment limit": "첨부 파일 크기 제한 {{limit}}을 초과했습니다", @@ -328,6 +330,8 @@ "Upload any image from your device.": "기기에서 이미지를 업로드하세요.", "Upload any video from your device.": "기기에서 비디오를 업로드하세요.", "Upload any file from your device.": "기기에서 파일을 업로드하세요.", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "테이블", "Insert a table.": "테이블 삽입.", "Insert collapsible block.": "접을 수 있는 블록 삽입.", diff --git a/apps/client/public/locales/nl-NL/translation.json b/apps/client/public/locales/nl-NL/translation.json index 7db6836d..9c16efe3 100644 --- a/apps/client/public/locales/nl-NL/translation.json +++ b/apps/client/public/locales/nl-NL/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "Kies uw gewenste interfacetaal.", "Choose your preferred page width.": "Kies uw gewenste paginabreedte.", "Confirm": "Bevestig", + "Copy as Markdown": "Copy as Markdown", "Copy link": "Link kopiëren", "Create": "Aanmaken", "Create group": "Groep aanmaken", @@ -253,6 +254,7 @@ "Export failed:": "Exporteren mislukt:", "export error": "Exporteer fout", "Export page": "Exporteer pagina", + "Export successful": "Export successful", "Export space": "Exporteer ruimte", "Export {{type}}": "Exporteer {{type}}", "File exceeds the {{limit}} attachment limit": "Bestand overschrijdt de bijlagelimiet van {{limit}}", @@ -328,6 +330,8 @@ "Upload any image from your device.": "Upload een afbeelding vanaf uw apparaat.", "Upload any video from your device.": "Upload een video vanaf uw apparaat.", "Upload any file from your device.": "Upload een bestand vanaf uw apparaat.", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "Tabel", "Insert a table.": "Voeg een tabel in.", "Insert collapsible block.": "Inklapbaar blok invoegen.", diff --git a/apps/client/public/locales/pt-BR/translation.json b/apps/client/public/locales/pt-BR/translation.json index 5d11ec7a..eb1442b2 100644 --- a/apps/client/public/locales/pt-BR/translation.json +++ b/apps/client/public/locales/pt-BR/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "Escolha o idioma da interface.", "Choose your preferred page width.": "Escolha a largura preferida da página.", "Confirm": "Confirmar", + "Copy as Markdown": "Copy as Markdown", "Copy link": "Copiar link", "Create": "Criar", "Create group": "Criar grupo", @@ -253,6 +254,7 @@ "Export failed:": "Falha ao exportar:", "export error": "erro de exportação", "Export page": "Exportar página", + "Export successful": "Export successful", "Export space": "Exportar espaço", "Export {{type}}": "Exportar para {{type}}", "File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}", @@ -328,6 +330,8 @@ "Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.", "Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.", "Upload any file from your device.": "Envie qualquer arquivo do seu dispositivo.", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "Tabela", "Insert a table.": "Insira uma tabela.", "Insert collapsible block.": "Insira um bloco colapsável.", diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json index f1a9cd85..b39d13a4 100644 --- a/apps/client/public/locales/ru-RU/translation.json +++ b/apps/client/public/locales/ru-RU/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "Выберите предпочитаемый язык интерфейса.", "Choose your preferred page width.": "Выберите предпочитаемую ширину страницы.", "Confirm": "Подтвердить", + "Copy as Markdown": "Copy as Markdown", "Copy link": "Копировать ссылку", "Create": "Создать", "Create group": "Создать группу", @@ -253,6 +254,7 @@ "Export failed:": "Экспортирование не удалось:", "export error": "ошибка экспорта", "Export page": "Экспорт страницы", + "Export successful": "Export successful", "Export space": "Экспорт пространства", "Export {{type}}": "Экспорт {{type}}", "File exceeds the {{limit}} attachment limit": "Файл превышает лимит вложений {{limit}}", @@ -328,6 +330,8 @@ "Upload any image from your device.": "Загрузить любое изображение с вашего устройства.", "Upload any video from your device.": "Загрузить любое видео с вашего устройства.", "Upload any file from your device.": "Загрузить любой файл с вашего устройства.", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "Таблица", "Insert a table.": "Вставить таблицу.", "Insert collapsible block.": "Вставить сворачиваемый блок.", diff --git a/apps/client/public/locales/uk-UA/translation.json b/apps/client/public/locales/uk-UA/translation.json index 2fb44ad1..2460f38f 100644 --- a/apps/client/public/locales/uk-UA/translation.json +++ b/apps/client/public/locales/uk-UA/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "Оберіть бажану мову інтерфейсу.", "Choose your preferred page width.": "Оберіть бажану ширину сторінки.", "Confirm": "Підтвердити", + "Copy as Markdown": "Copy as Markdown", "Copy link": "Копіювати посилання", "Create": "Створити", "Create group": "Створити групу", @@ -253,6 +254,7 @@ "Export failed:": "Експортування не вдалося:", "export error": "помилка експорту", "Export page": "Експорт сторінки", + "Export successful": "Export successful", "Export space": "Експорт простору", "Export {{type}}": "Експорт {{type}}", "File exceeds the {{limit}} attachment limit": "Файл перевищує ліміт вкладень {{limit}}", @@ -328,6 +330,8 @@ "Upload any image from your device.": "Завантажити будь-яке зображення з вашого пристрою.", "Upload any video from your device.": "Завантажити будь-яке відео з вашого пристрою.", "Upload any file from your device.": "Завантажити будь-який файл з вашого пристрою.", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "Таблиця", "Insert a table.": "Вставити таблицю.", "Insert collapsible block.": "Вставити блок, що згортається.", diff --git a/apps/client/public/locales/zh-CN/translation.json b/apps/client/public/locales/zh-CN/translation.json index d4b25deb..ed26024f 100644 --- a/apps/client/public/locales/zh-CN/translation.json +++ b/apps/client/public/locales/zh-CN/translation.json @@ -29,6 +29,7 @@ "Choose your preferred interface language.": "选择您喜欢的界面语言。", "Choose your preferred page width.": "选择您喜欢的页面宽度。", "Confirm": "确认", + "Copy as Markdown": "Copy as Markdown", "Copy link": "复制链接", "Create": "创建", "Create group": "创建群组", @@ -253,6 +254,7 @@ "Export failed:": "导出失败:", "export error": "导出出错", "Export page": "导出页面", + "Export successful": "Export successful", "Export space": "导出空间", "Export {{type}}": "导出为 {{type}}", "File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制", @@ -328,6 +330,8 @@ "Upload any image from your device.": "从设备上传任何图像", "Upload any video from your device.": "从设备上传任何视频", "Upload any file from your device.": "从设备上传任何文件", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "表格", "Insert a table.": "插入一个表格", "Insert collapsible block.": "插入一个折叠块", diff --git a/apps/client/src/components/common/export-modal.tsx b/apps/client/src/components/common/export-modal.tsx index 25f4d328..53de8246 100644 --- a/apps/client/src/components/common/export-modal.tsx +++ b/apps/client/src/components/common/export-modal.tsx @@ -30,9 +30,11 @@ export default function ExportModal({ const [format, setFormat] = useState(ExportFormat.Markdown); const [includeChildren, setIncludeChildren] = useState(false); const [includeAttachments, setIncludeAttachments] = useState(false); + const [isExporting, setIsExporting] = useState(false); const { t } = useTranslation(); const handleExport = async () => { + setIsExporting(true); try { if (type === "page") { await exportPage({ @@ -45,6 +47,9 @@ export default function ExportModal({ if (type === "space") { await exportSpace({ spaceId: id, format, includeAttachments }); } + notifications.show({ + message: t("Export successful"), + }); onClose(); } catch (err) { notifications.show({ @@ -52,6 +57,8 @@ export default function ExportModal({ color: "red", }); console.error("export error", err); + } finally { + setIsExporting(false); } }; @@ -136,7 +143,7 @@ export default function ExportModal({ - + diff --git a/apps/client/src/features/editor/components/attachment/attachment-view.tsx b/apps/client/src/features/editor/components/attachment/attachment-view.tsx index d3858520..e3281e64 100644 --- a/apps/client/src/features/editor/components/attachment/attachment-view.tsx +++ b/apps/client/src/features/editor/components/attachment/attachment-view.tsx @@ -1,11 +1,13 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; -import { Group, Text, Paper, ActionIcon } from "@mantine/core"; +import { Group, Text, Paper, ActionIcon, Loader } from "@mantine/core"; import { getFileUrl } from "@/lib/config.ts"; import { IconDownload, IconPaperclip } from "@tabler/icons-react"; import { useHover } from "@mantine/hooks"; import { formatBytes } from "@/lib"; +import { useTranslation } from "react-i18next"; export default function AttachmentView(props: NodeViewProps) { + const { t } = useTranslation(); const { node, selected } = props; const { url, name, size } = node.attrs; const { hovered, ref } = useHover(); @@ -20,26 +22,28 @@ export default function AttachmentView(props: NodeViewProps) { wrap="nowrap" h={25} > - - + + {url ? ( + + ) : ( + + )} - - {name} + + {url ? name : t("Uploading {{name}}", { name })} - + {formatBytes(size)} - {selected || hovered ? ( + {url && (selected || hovered) && ( - ) : ( - "" )} diff --git a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx index e8085ca6..a6d143ff 100644 --- a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx @@ -1,10 +1,6 @@ -import { - BubbleMenu, - BubbleMenuProps, - isNodeSelection, - useEditor, - useEditorState, -} from "@tiptap/react"; +import { BubbleMenu, BubbleMenuProps } from "@tiptap/react/menus"; +import { isNodeSelection, useEditorState } from "@tiptap/react"; +import type { Editor } from "@tiptap/react"; import { FC, useEffect, useRef, useState } from "react"; import { IconBold, @@ -38,7 +34,7 @@ export interface BubbleMenuItem { } type EditorBubbleMenuProps = Omit & { - editor: ReturnType; + editor: Editor | null; }; export const EditorBubbleMenu: FC = (props) => { @@ -133,14 +129,9 @@ export const EditorBubbleMenu: FC = (props) => { } return isTextSelected(editor); }, - tippyOptions: { - moveTransition: "transform 0.15s ease-out", - onCreate: (instance) => { - instance.popper.firstChild?.addEventListener("blur", (event) => { - event.preventDefault(); - event.stopImmediatePropagation(); - }); - }, + options: { + placement: "top", + offset: 8, onHide: () => { setIsNodeSelectorOpen(false); setIsTextAlignmentOpen(false); @@ -156,7 +147,7 @@ export const EditorBubbleMenu: FC = (props) => { const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false); return ( - +
{ + const getReferencedVirtualElement = useCallback(() => { + if (!editor) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "callout"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const domRect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const domRect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; }, [editor]); const setCalloutType = useCallback( @@ -112,14 +117,12 @@ export function CalloutMenu({ editor }: EditorMenuProps) { editor={editor} pluginKey={`callout-menu`} updateDelay={0} - tippyOptions={{ - getReferenceClientRect, - offset: [0, 10], + getReferencedVirtualElement={getReferencedVirtualElement} + options={{ placement: "bottom", - zIndex: 99, - popperOptions: { - modifiers: [{ name: "flip", enabled: false }], - }, + // offset: 233, // // offset: [0, 10], + // zIndex: 99, + flip: false, }} shouldShow={shouldShow} > diff --git a/apps/client/src/features/editor/components/code-block/code-block-view.tsx b/apps/client/src/features/editor/components/code-block/code-block-view.tsx index 07ad2ad0..130016a3 100644 --- a/apps/client/src/features/editor/components/code-block/code-block-view.tsx +++ b/apps/client/src/features/editor/components/code-block/code-block-view.tsx @@ -90,6 +90,7 @@ export default function CodeBlockView(props: NodeViewProps) { node.textContent.length > 0 } > + {/* @ts-ignore */} diff --git a/apps/client/src/features/editor/components/common/editor-paste-handler.tsx b/apps/client/src/features/editor/components/common/editor-paste-handler.tsx index 8eee02fc..61d7534e 100644 --- a/apps/client/src/features/editor/components/common/editor-paste-handler.tsx +++ b/apps/client/src/features/editor/components/common/editor-paste-handler.tsx @@ -1,13 +1,12 @@ -import type { EditorView } from "@tiptap/pm/view"; import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx"; import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx"; import { uploadAttachmentAction } from "../attachment/upload-attachment-action"; import { createMentionAction } from "@/features/editor/components/link/internal-link-paste.ts"; -import { Slice } from "@tiptap/pm/model"; import { INTERNAL_LINK_REGEX } from "@/lib/constants.ts"; +import { Editor } from "@tiptap/core"; export const handlePaste = ( - view: EditorView, + editor: Editor, event: ClipboardEvent, pageId: string, creatorId?: string, @@ -18,7 +17,7 @@ export const handlePaste = ( // we have to do this validation here to allow the default link extension to takeover if needs be event.preventDefault(); const url = clipboardData.trim(); - const { from: pos, empty } = view.state.selection; + const { from: pos, empty } = editor.state.selection; const match = INTERNAL_LINK_REGEX.exec(url); const currentPageMatch = INTERNAL_LINK_REGEX.exec(window.location.href); @@ -34,19 +33,27 @@ export const handlePaste = ( return false; } - const anchorId = match[6] ? match[6].split('#')[0] : undefined; - const urlWithoutAnchor = anchorId ? url.substring(0, url.indexOf("#")) : url; - createMentionAction(urlWithoutAnchor, view, pos, creatorId, anchorId); + const anchorId = match[6] ? match[6].split("#")[0] : undefined; + const urlWithoutAnchor = anchorId + ? url.substring(0, url.indexOf("#")) + : url; + createMentionAction( + urlWithoutAnchor, + editor.view, + pos, + creatorId, + anchorId, + ); return true; } if (event.clipboardData?.files.length) { event.preventDefault(); for (const file of event.clipboardData.files) { - const pos = view.state.selection.from; - uploadImageAction(file, view, pos, pageId); - uploadVideoAction(file, view, pos, pageId); - uploadAttachmentAction(file, view, pos, pageId); + const pos = editor.state.selection.from; + uploadImageAction(file, editor, pos, pageId); + uploadVideoAction(file, editor, pos, pageId); + uploadAttachmentAction(file, editor, pos, pageId); } return true; } @@ -54,7 +61,7 @@ export const handlePaste = ( }; export const handleFileDrop = ( - view: EditorView, + editor: Editor, event: DragEvent, moved: boolean, pageId: string, @@ -63,14 +70,14 @@ export const handleFileDrop = ( event.preventDefault(); for (const file of event.dataTransfer.files) { - const coordinates = view.posAtCoords({ + const coordinates = editor.view.posAtCoords({ left: event.clientX, top: event.clientY, }); - uploadImageAction(file, view, coordinates?.pos ?? 0 - 1, pageId); - uploadVideoAction(file, view, coordinates?.pos ?? 0 - 1, pageId); - uploadAttachmentAction(file, view, coordinates?.pos ?? 0 - 1, pageId); + uploadImageAction(file, editor, coordinates?.pos ?? 0 - 1, pageId); + uploadVideoAction(file, editor, coordinates?.pos ?? 0 - 1, pageId); + uploadAttachmentAction(file, editor, coordinates?.pos ?? 0 - 1, pageId); } return true; } diff --git a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx index 0efc2ec0..937b8e7d 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx @@ -1,11 +1,6 @@ -import { - BubbleMenu as BaseBubbleMenu, - findParentNode, - posToDOMRect, - useEditorState, -} from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; -import { sticky } from "tippy.js"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, @@ -40,17 +35,26 @@ export function DrawioMenu({ editor }: EditorMenuProps) { }, }); - const getReferenceClientRect = useCallback(() => { + const getReferencedVirtualElement = useCallback(() => { + if (!editor) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "drawio"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const domRect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const domRect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; }, [editor]); const onWidthChange = useCallback( @@ -65,15 +69,11 @@ export function DrawioMenu({ editor }: EditorMenuProps) { editor={editor} pluginKey={`drawio-menu`} updateDelay={0} - tippyOptions={{ - getReferenceClientRect, - offset: [0, 8], - zIndex: 99, - popperOptions: { - modifiers: [{ name: "flip", enabled: false }], - }, - plugins: [sticky], - sticky: "popper", + getReferencedVirtualElement={getReferencedVirtualElement} + options={{ + placement: "top", + offset: 8, + flip: false, }} shouldShow={shouldShow} > diff --git a/apps/client/src/features/editor/components/drawio/drawio-view.tsx b/apps/client/src/features/editor/components/drawio/drawio-view.tsx index 5a0fbd86..b51e8936 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-view.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-view.tsx @@ -66,6 +66,7 @@ export default function DrawioView(props: NodeViewProps) { const fileName = "diagram.drawio.svg"; const drawioSVGFile = await svgStringToFile(svgString, fileName); + //@ts-ignore const pageId = editor.storage?.pageId; let attachment: IAttachment = null; diff --git a/apps/client/src/features/editor/components/emoji-menu/render-emoji-items.ts b/apps/client/src/features/editor/components/emoji-menu/render-emoji-items.ts index 82fb24a9..0ae5e24a 100644 --- a/apps/client/src/features/editor/components/emoji-menu/render-emoji-items.ts +++ b/apps/client/src/features/editor/components/emoji-menu/render-emoji-items.ts @@ -1,16 +1,41 @@ import { ReactRenderer, useEditor } from "@tiptap/react"; import EmojiList from "./emoji-list"; -import tippy from "tippy.js"; import { init } from "emoji-mart"; +import { + autoUpdate, + computePosition, + flip, + offset, + shift, +} from "@floating-ui/dom"; const renderEmojiItems = () => { let component: ReactRenderer | null = null; - let popup: any | null = null; + let popup: HTMLDivElement | null = null; + let cleanup: (() => void) | null = null; + let getReferenceClientRect: (() => DOMRect) | null = null; + + const destroy = () => { + if (cleanup) { + cleanup(); + cleanup = null; + } + + if (popup) { + popup.remove(); + popup = null; + } + + if (component) { + component.destroy(); + component = null; + } + }; return { onBeforeStart: (props: { editor: ReturnType; - clientRect: DOMRect; + clientRect: () => DOMRect; }) => { init({ data: async () => (await import("@emoji-mart/data")).default, @@ -25,51 +50,61 @@ const renderEmojiItems = () => { return; } - // @ts-ignore - popup = tippy("body", { - getReferenceClientRect: props.clientRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom", + getReferenceClientRect = props.clientRect; + popup = document.createElement("div"); + popup.style.zIndex = "9999"; + popup.style.position = "absolute"; + popup.style.top = "0"; + popup.style.left = "0"; + popup.appendChild(component.element); + document.body.appendChild(popup); + + const virtualElement = { + getBoundingClientRect: () => { + return getReferenceClientRect + ? getReferenceClientRect() + : new DOMRect(0, 0, 0, 0); + }, + }; + + cleanup = autoUpdate(virtualElement, popup, () => { + if (!popup) return; + + computePosition(virtualElement, popup, { + placement: "bottom-start", + middleware: [offset(10), flip(), shift()], + }).then(({ x, y }) => { + if (!popup) return; + + Object.assign(popup.style, { + transform: `translate(${x}px, ${y}px)`, + }); + }); }); }, onStart: (props: { editor: ReturnType; - clientRect: DOMRect; + clientRect: () => DOMRect; }) => { - component?.updateProps({...props, isLoading: false}); + component?.updateProps({ ...props, isLoading: false }); - if (!props.clientRect) { - return; + if (props.clientRect) { + getReferenceClientRect = props.clientRect; } - - popup && - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); }, onUpdate: (props: { editor: ReturnType; - clientRect: DOMRect; + clientRect: () => DOMRect; }) => { component?.updateProps(props); - if (!props.clientRect) { - return; + if (props.clientRect) { + getReferenceClientRect = props.clientRect; } - - popup && - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); }, onKeyDown: (props: { event: KeyboardEvent }) => { if (props.event.key === "Escape") { - popup?.[0].hide(); - component?.destroy() + destroy(); return true; } @@ -78,13 +113,7 @@ const renderEmojiItems = () => { return component?.ref?.onKeyDown(props); }, onExit: () => { - if (popup && !popup[0]?.state.isDestroyed) { - popup[0]?.destroy(); - } - - if (component) { - component?.destroy(); - } + destroy(); }, }; }; diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx index 42329e5c..06e79515 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx @@ -1,11 +1,6 @@ -import { - BubbleMenu as BaseBubbleMenu, - findParentNode, - posToDOMRect, - useEditorState, -} from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; -import { sticky } from "tippy.js"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, @@ -42,17 +37,26 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { }, }); - const getReferenceClientRect = useCallback(() => { + const getReferencedVirtualElement = useCallback(() => { + if (!editor) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "excalidraw"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const domRect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const domRect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; }, [editor]); const onWidthChange = useCallback( @@ -65,17 +69,13 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { return ( diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx index 779a826d..86c9665e 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx @@ -98,6 +98,7 @@ export default function ExcalidrawView(props: NodeViewProps) { const fileName = "diagram.excalidraw.svg"; const excalidrawSvgFile = await svgStringToFile(svgString, fileName); + // @ts-ignore const pageId = editor.storage?.pageId; let attachment: IAttachment = null; diff --git a/apps/client/src/features/editor/components/image/image-menu.tsx b/apps/client/src/features/editor/components/image/image-menu.tsx index 723ec299..a1699f93 100644 --- a/apps/client/src/features/editor/components/image/image-menu.tsx +++ b/apps/client/src/features/editor/components/image/image-menu.tsx @@ -1,11 +1,6 @@ -import { - BubbleMenu as BaseBubbleMenu, - findParentNode, - posToDOMRect, - useEditorState, -} from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import React, { useCallback } from "react"; -import { sticky } from "tippy.js"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, @@ -22,16 +17,6 @@ import { useTranslation } from "react-i18next"; export function ImageMenu({ editor }: EditorMenuProps) { const { t } = useTranslation(); - const shouldShow = useCallback( - ({ state }: ShouldShowProps) => { - if (!state) { - return false; - } - - return editor.isActive("image"); - }, - [editor], - ); const editorState = useEditorState({ editor, @@ -52,17 +37,37 @@ export function ImageMenu({ editor }: EditorMenuProps) { }, }); - const getReferenceClientRect = useCallback(() => { + const shouldShow = useCallback( + ({ state }: ShouldShowProps) => { + if (!state) { + return false; + } + + return editor.isActive("image") && editor.getAttributes("image").src; + }, + [editor], + ); + + const getReferencedVirtualElement = useCallback(() => { + if (!editor) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "image"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const domRect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const domRect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; }, [editor]); const alignImageLeft = useCallback(() => { @@ -105,15 +110,11 @@ export function ImageMenu({ editor }: EditorMenuProps) { editor={editor} pluginKey={`image-menu`} updateDelay={0} - tippyOptions={{ - getReferenceClientRect, - offset: [0, 8], - zIndex: 99, - popperOptions: { - modifiers: [{ name: "flip", enabled: false }], - }, - plugins: [sticky], - sticky: "popper", + getReferencedVirtualElement={getReferencedVirtualElement} + options={{ + placement: "top", + offset: 8, + flip: false, }} shouldShow={shouldShow} > diff --git a/apps/client/src/features/editor/components/image/image-view.module.css b/apps/client/src/features/editor/components/image/image-view.module.css new file mode 100644 index 00000000..5d02184b --- /dev/null +++ b/apps/client/src/features/editor/components/image/image-view.module.css @@ -0,0 +1,27 @@ +.imageWrapper { + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; + overflow: hidden; + animation: pulse 1.2s ease-in-out infinite; + + @mixin light { + background: linear-gradient(-90deg, var(--mantine-color-gray-3) 0%, var(--mantine-color-gray-1) 50%, var(--mantine-color-gray-3) 100%); + background-size: 400% 400%; + } + + @mixin dark { + background: linear-gradient(-90deg, var(--mantine-color-dark-6) 0%, var(--mantine-color-dark-5) 50%, var(--mantine-color-dark-6) 100%); + background-size: 400% 400%; + } + + @keyframes pulse { + 0% { + background-position: 0% 0%; + } + 100% { + background-position: -135% 0%; + } + } +} diff --git a/apps/client/src/features/editor/components/image/image-view.tsx b/apps/client/src/features/editor/components/image/image-view.tsx index dbdb8396..defb64c4 100644 --- a/apps/client/src/features/editor/components/image/image-view.tsx +++ b/apps/client/src/features/editor/components/image/image-view.tsx @@ -1,30 +1,70 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; +import { Group, Image, Loader, Text } from "@mantine/core"; import { useMemo } from "react"; -import { Image } from "@mantine/core"; import { getFileUrl } from "@/lib/config.ts"; import clsx from "clsx"; +import classes from "./image-view.module.css"; +import { useTranslation } from "react-i18next"; export default function ImageView(props: NodeViewProps) { - const { node, selected } = props; - const { src, width, align, title } = node.attrs; - + const { t } = useTranslation(); + const { editor, node, selected } = props; + const { src, width, align, title, aspectRatio, placeholder } = node.attrs; const alignClass = useMemo(() => { if (align === "left") return "alignLeft"; if (align === "right") return "alignRight"; if (align === "center") return "alignCenter"; return "alignCenter"; }, [align]); + const previewSrc = useMemo(() => { + editor.storage.shared.imagePreviews = + editor.storage.shared.imagePreviews || {}; + + if (placeholder?.id) { + return editor.storage.shared.imagePreviews[placeholder.id]; + } + + return null; + }, [placeholder, editor]); return ( - {title} +
+ {src && ( + {title} + )} + {!src && previewSrc && ( + + {placeholder?.name} + + + )} + {!src && !previewSrc && ( + + + + {placeholder?.name + ? t("Uploading {{name}}", { name: placeholder.name }) + : t("Uploading file")} + + + )} +
); } diff --git a/apps/client/src/features/editor/components/link/link-menu.tsx b/apps/client/src/features/editor/components/link/link-menu.tsx index 69f7c449..63fd10bf 100644 --- a/apps/client/src/features/editor/components/link/link-menu.tsx +++ b/apps/client/src/features/editor/components/link/link-menu.tsx @@ -1,9 +1,10 @@ -import { BubbleMenu as BaseBubbleMenu, useEditorState } from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import React, { useCallback, useState } from "react"; import { EditorMenuProps } from "@/features/editor/components/table/types/types.ts"; import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx"; import { LinkPreviewPanel } from "@/features/editor/components/link/link-preview.tsx"; import { Card } from "@mantine/core"; +import { useEditorState } from "@tiptap/react"; export function LinkMenu({ editor, appendTo }: EditorMenuProps) { const [showEdit, setShowEdit] = useState(false); @@ -59,18 +60,15 @@ export function LinkMenu({ editor, appendTo }: EditorMenuProps) { return ( { - return appendTo?.current; - }, - onHidden: () => { + options={{ + onHide: () => { setShowEdit(false); }, placement: "bottom", - offset: [0, 5], - zIndex: 101, + offset: 5, + // zIndex: 101, }} shouldShow={shouldShow} > diff --git a/apps/client/src/features/editor/components/mention/mention-list.tsx b/apps/client/src/features/editor/components/mention/mention-list.tsx index 389c2ce5..32959146 100644 --- a/apps/client/src/features/editor/components/mention/mention-list.tsx +++ b/apps/client/src/features/editor/components/mention/mention-list.tsx @@ -106,6 +106,7 @@ const MentionList = forwardRef((props, ref) => { setRenderItems(items); // update editor storage + //@ts-ignore props.editor.storage.mentionItems = items; } }, [suggestion, isLoading]); @@ -163,7 +164,7 @@ const MentionList = forwardRef((props, ref) => { const enterHandler = () => { if (!renderItems.length) return; - if (renderItems[selectedIndex].entityType !== "header") { + if (renderItems[selectedIndex]?.entityType !== "header") { selectItem(selectedIndex); } }; @@ -203,7 +204,7 @@ const MentionList = forwardRef((props, ref) => { parentPageId: page.id || null, title: title }; - + let createdPage: IPage; try { createdPage = await createPageMutation.mutateAsync(payload); diff --git a/apps/client/src/features/editor/components/mention/mention-suggestion.ts b/apps/client/src/features/editor/components/mention/mention-suggestion.ts index 11710639..d53c422c 100644 --- a/apps/client/src/features/editor/components/mention/mention-suggestion.ts +++ b/apps/client/src/features/editor/components/mention/mention-suggestion.ts @@ -1,5 +1,11 @@ import { ReactRenderer, useEditor } from "@tiptap/react"; -import tippy from "tippy.js"; +import { + autoUpdate, + computePosition, + flip, + offset, + shift, +} from "@floating-ui/dom"; import MentionList from "@/features/editor/components/mention/mention-list.tsx"; function getWhitespaceCount(query: string) { @@ -9,16 +15,27 @@ function getWhitespaceCount(query: string) { const mentionRenderItems = () => { let component: ReactRenderer | null = null; - let popup: any | null = null; + let activeClientRect: (() => DOMRect) | null = null; + let updatePositionCleanup: (() => void) | null = null; + + const destroy = () => { + updatePositionCleanup?.(); + updatePositionCleanup = null; + component?.destroy(); + if (component?.element?.parentNode) { + component.element.parentNode.removeChild(component.element); + } + component = null; + }; return { onStart: (props: { editor: ReturnType; - clientRect: DOMRect; + clientRect: () => DOMRect; query: string; }) => { // query must not start with a whitespace - if (props.query.charAt(0) === ' '){ + if (props.query.charAt(0) === " ") { return; } @@ -37,75 +54,95 @@ const mentionRenderItems = () => { return; } - // @ts-ignore - popup = tippy("body", { - getReferenceClientRect: props.clientRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom-start", - }); + activeClientRect = props.clientRect; + + const { element } = component; + document.body.appendChild(element); + + updatePositionCleanup = autoUpdate( + { + getBoundingClientRect: () => + activeClientRect ? activeClientRect() : new DOMRect(), + }, + element, + () => { + if (!component?.element) return; + computePosition( + { + getBoundingClientRect: () => { + return activeClientRect ? activeClientRect() : new DOMRect(); + }, + }, + element, + { + placement: "bottom-start", + middleware: [offset(0), flip(), shift()], + }, + ).then(({ x, y }) => { + Object.assign(element.style, { + left: `${x}px`, + top: `${y}px`, + position: "absolute", + zIndex: "9999", + }); + }); + }, + ); }, onUpdate: (props: { editor: ReturnType; - clientRect: DOMRect; + clientRect: () => DOMRect; query: string; }) => { // query must not start with a whitespace - if (props.query.charAt(0) === ' '){ - component?.destroy(); + if (props.query.charAt(0) === " ") { + destroy(); return; } // only update component if popup is not destroyed - if (!popup?.[0].state.isDestroyed) { - component?.updateProps(props); + if (component) { + component.updateProps(props); } if (!props || !props.clientRect) { return; } + activeClientRect = props.clientRect; + const whitespaceCount = getWhitespaceCount(props.query); // destroy component if space is greater 3 without a match if ( - whitespaceCount > 3 && - props.editor.storage.mentionItems.length === 0 + whitespaceCount > 4 && + //@ts-ignore + props.editor.storage.mentionItems.length === 1 ) { - popup?.[0]?.destroy(); - component?.destroy(); + destroy(); + return; + } + // fallback exit + if (whitespaceCount > 7) { + destroy(); return; } - - popup && - !popup?.[0].state.isDestroyed && - popup?.[0].setProps({ - getReferenceClientRect: props.clientRect, - }); }, onKeyDown: (props: { event: KeyboardEvent }) => { - if (props.event.key) - if ( - props.event.key === "Escape" || - (props.event.key === "Enter" && !popup?.[0].state.isShown) - ) { - popup?.[0].destroy(); - component?.destroy(); - return false; - } + if (props.event.key === "Escape") { + destroy(); + return true; + } + + if (props.event.key === "Enter" && !component) { + destroy(); + return false; + } + return (component?.ref as any)?.onKeyDown(props); }, onExit: () => { - if (popup && !popup?.[0].state.isDestroyed) { - popup[0].destroy(); - } - - if (component) { - component.destroy(); - } + destroy(); }, }; }; diff --git a/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx b/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx index df6f0031..f5c17661 100644 --- a/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx +++ b/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx @@ -73,6 +73,8 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo if (!editor) return; const { results, resultIndex } = editor.storage.searchAndReplace; + //TODO: check type error + //@ts-ignore const position: Range = results[resultIndex]; if (!position) return; diff --git a/apps/client/src/features/editor/components/slash-menu/menu-items.ts b/apps/client/src/features/editor/components/slash-menu/menu-items.ts index f56d7f04..bebefed4 100644 --- a/apps/client/src/features/editor/components/slash-menu/menu-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/menu-items.ts @@ -161,6 +161,7 @@ const CommandGroups: SlashMenuGroupedItemsType = { command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); + // @ts-ignore const pageId = editor.storage?.pageId; if (!pageId) return; @@ -173,9 +174,13 @@ const CommandGroups: SlashMenuGroupedItemsType = { if (input.files?.length) { for (const file of input.files) { const pos = editor.view.state.selection.from; - uploadImageAction(file, editor.view, pos, pageId); + + uploadImageAction(file, editor, pos, pageId); } } + + // Reset the input value to allow uploading the same file again if needed + input.value = ""; }; input.click(); }, @@ -188,6 +193,7 @@ const CommandGroups: SlashMenuGroupedItemsType = { command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); + // @ts-ignore const pageId = editor.storage?.pageId; if (!pageId) return; @@ -195,12 +201,18 @@ const CommandGroups: SlashMenuGroupedItemsType = { const input = document.createElement("input"); input.type = "file"; input.accept = "video/*"; + input.multiple = true; input.onchange = async () => { if (input.files?.length) { - const file = input.files[0]; - const pos = editor.view.state.selection.from; - uploadVideoAction(file, editor.view, pos, pageId); + for (const file of input.files) { + const pos = editor.view.state.selection.from; + + uploadVideoAction(file, editor, pos, pageId); + } } + + // Reset the input value to allow uploading the same file again if needed + input.value = ""; }; input.click(); }, @@ -213,6 +225,7 @@ const CommandGroups: SlashMenuGroupedItemsType = { command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); + // @ts-ignore const pageId = editor.storage?.pageId; if (!pageId) return; @@ -220,12 +233,18 @@ const CommandGroups: SlashMenuGroupedItemsType = { const input = document.createElement("input"); input.type = "file"; input.accept = ""; + input.multiple = true; input.onchange = async () => { if (input.files?.length) { - const file = input.files[0]; - const pos = editor.view.state.selection.from; - uploadAttachmentAction(file, editor.view, pos, pageId, true); + for (const file of input.files) { + const pos = editor.view.state.selection.from; + + uploadAttachmentAction(file, editor, pos, pageId, true); + } } + + // Reset the input value to allow uploading the same file again if needed + input.value = ""; }; input.click(); }, diff --git a/apps/client/src/features/editor/components/slash-menu/render-items.ts b/apps/client/src/features/editor/components/slash-menu/render-items.ts index db6424e8..057e8214 100644 --- a/apps/client/src/features/editor/components/slash-menu/render-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/render-items.ts @@ -1,10 +1,35 @@ import { ReactRenderer, useEditor } from "@tiptap/react"; import CommandList from "@/features/editor/components/slash-menu/command-list"; -import tippy from "tippy.js"; +import { + autoUpdate, + computePosition, + flip, + offset, + shift, +} from "@floating-ui/dom"; const renderItems = () => { let component: ReactRenderer | null = null; - let popup: any | null = null; + let popup: HTMLElement | null = null; + let cleanup: (() => void) | null = null; + let getReferenceClientRect: (() => DOMRect) | null = null; + + const updatePosition = () => { + if (!popup || !getReferenceClientRect) return; + + // @ts-ignore + const rect = getReferenceClientRect(); + + computePosition({ getBoundingClientRect: () => rect }, popup, { + placement: "bottom-start", + middleware: [offset(0), flip(), shift()], + }).then(({ x, y }) => { + if (popup) { + popup.style.left = `${x}px`; + popup.style.top = `${y}px`; + } + }); + }; return { onStart: (props: { @@ -21,15 +46,29 @@ const renderItems = () => { } // @ts-ignore - popup = tippy("body", { - getReferenceClientRect: props.clientRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom-start", - }); + getReferenceClientRect = props.clientRect; + + popup = document.createElement("div"); + popup.style.zIndex = "9999"; + popup.style.position = "absolute"; + popup.style.top = "0"; + popup.style.left = "0"; + + document.body.appendChild(popup); + popup.appendChild(component.element); + + cleanup = autoUpdate( + // @ts-ignore + { + getBoundingClientRect: () => { + return getReferenceClientRect + ? getReferenceClientRect() + : new DOMRect(); + }, + }, + popup, + updatePosition + ); }, onUpdate: (props: { editor: ReturnType; @@ -41,14 +80,15 @@ const renderItems = () => { return; } - popup && - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); + // @ts-ignore + getReferenceClientRect = props.clientRect; + updatePosition(); }, onKeyDown: (props: { event: KeyboardEvent }) => { if (props.event.key === "Escape") { - popup?.[0].hide(); + if (popup) { + popup.style.display = "none"; + } return true; } @@ -57,12 +97,19 @@ const renderItems = () => { return component?.ref?.onKeyDown(props); }, onExit: () => { - if (popup && !popup[0].state.isDestroyed) { - popup[0].destroy(); + if (cleanup) { + cleanup(); + cleanup = null; + } + + if (popup) { + popup.remove(); + popup = null; } if (component) { component.destroy(); + component = null; } }, }; diff --git a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx index 6cc017e2..9f0544e6 100644 --- a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx +++ b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx @@ -1,15 +1,11 @@ -import { - BubbleMenu as BaseBubbleMenu, - posToDOMRect, - findParentNode, -} from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; +import { posToDOMRect, findParentNode } from "@tiptap/react"; import { Node as PMNode } from "@tiptap/pm/model"; import React, { useCallback } from "react"; import { ActionIcon, Tooltip } from "@mantine/core"; import { IconTrash } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { Editor } from "@tiptap/core"; -import { sticky } from "tippy.js"; interface SubpagesMenuProps { editor: Editor; @@ -33,7 +29,7 @@ export const SubpagesMenu = React.memo( return editor.isActive("subpages"); }, - [editor], + [editor] ); const getReferenceClientRect = useCallback(() => { @@ -62,18 +58,8 @@ export const SubpagesMenu = React.memo( return ( @@ -89,7 +75,7 @@ export const SubpagesMenu = React.memo( ); - }, + } ); export default SubpagesMenu; diff --git a/apps/client/src/features/editor/components/subpages/subpages-view.tsx b/apps/client/src/features/editor/components/subpages/subpages-view.tsx index 0da33028..697c1213 100644 --- a/apps/client/src/features/editor/components/subpages/subpages-view.tsx +++ b/apps/client/src/features/editor/components/subpages/subpages-view.tsx @@ -19,6 +19,7 @@ export default function SubpagesView(props: NodeViewProps) { const { spaceSlug, shareId } = useParams(); const { t } = useTranslation(); + //@ts-ignore const currentPageId = editor.storage.pageId; // Get subpages from shared tree if we're in a shared context diff --git a/apps/client/src/features/editor/components/table/table-cell-menu.tsx b/apps/client/src/features/editor/components/table/table-cell-menu.tsx index 2ea2e8dd..8af896b3 100644 --- a/apps/client/src/features/editor/components/table/table-cell-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-cell-menu.tsx @@ -1,6 +1,4 @@ -import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react"; import React, { useCallback } from "react"; - import { EditorMenuProps, ShouldShowProps, @@ -17,6 +15,7 @@ import { import { useTranslation } from "react-i18next"; import { TableBackgroundColor } from "./table-background-color"; import { TableTextAlignment } from "./table-text-alignment"; +import { BubbleMenu } from "@tiptap/react/menus"; export const TableCellMenu = React.memo( ({ editor, appendTo }: EditorMenuProps): JSX.Element => { @@ -29,7 +28,7 @@ export const TableCellMenu = React.memo( return isCellSelection(state.selection); }, - [editor], + [editor] ); const mergeCells = useCallback(() => { @@ -53,23 +52,27 @@ export const TableCellMenu = React.memo( }, [editor]); return ( - { - return appendTo?.current; + appendTo={() => { + return appendTo?.current; + }} + ref={(element) => { + element.style.zIndex = "99"; + }} + options={{ + offset: { + mainAxis: 15, }, - offset: [0, 15], - zIndex: 99, }} shouldShow={shouldShow} > - + - + ); - }, + } ); export default TableCellMenu; diff --git a/apps/client/src/features/editor/components/table/table-menu.tsx b/apps/client/src/features/editor/components/table/table-menu.tsx index 1d2985e8..e54a06af 100644 --- a/apps/client/src/features/editor/components/table/table-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-menu.tsx @@ -1,11 +1,6 @@ -import { - BubbleMenu as BaseBubbleMenu, - posToDOMRect, - findParentNode, -} from "@tiptap/react"; +import { posToDOMRect, findParentNode } from "@tiptap/react"; import { Node as PMNode } from "@tiptap/pm/model"; import React, { useCallback } from "react"; - import { EditorMenuProps, ShouldShowProps, @@ -17,9 +12,12 @@ import { IconColumnRemove, IconRowInsertBottom, IconRowInsertTop, - IconRowRemove, IconTableColumn, IconTableRow, + IconRowRemove, + IconTableColumn, + IconTableRow, IconTrashX, -} from '@tabler/icons-react'; +} from "@tabler/icons-react"; +import { BubbleMenu } from "@tiptap/react/menus"; import { isCellSelection } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; @@ -34,20 +32,28 @@ export const TableMenu = React.memo( return editor.isActive("table") && !isCellSelection(state.selection); }, - [editor], + [editor] ); - const getReferenceClientRect = useCallback(() => { + const getReferencedVirtualElement = useCallback(() => { const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "table"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const rect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => rect, + getClientRects: () => [rect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const rect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => rect, + getClientRects: () => [rect], + }; }, [editor]); const toggleHeaderColumn = useCallback(() => { @@ -87,42 +93,33 @@ export const TableMenu = React.memo( }, [editor]); return ( - { + element.style.zIndex = "99"; + }} + options={{ + placement: "top", + offset: { + mainAxis: 15, + }, + flip: { + fallbackPlacements: ["top", "bottom"], + padding: { top: 35 + 15, left: 8, right: 8, bottom: -Infinity }, + boundary: editor.options.element as HTMLElement, + }, + shift: { + padding: 8 + 15, + crossAxis: true, }, }} shouldShow={shouldShow} > - + - + - + - + ); - }, + } ); export default TableMenu; diff --git a/apps/client/src/features/editor/components/video/video-menu.tsx b/apps/client/src/features/editor/components/video/video-menu.tsx index 3252e621..dfece398 100644 --- a/apps/client/src/features/editor/components/video/video-menu.tsx +++ b/apps/client/src/features/editor/components/video/video-menu.tsx @@ -1,11 +1,6 @@ -import { - BubbleMenu as BaseBubbleMenu, - findParentNode, - posToDOMRect, - useEditorState, -} from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import React, { useCallback } from "react"; -import { sticky } from "tippy.js"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, @@ -22,16 +17,6 @@ import { useTranslation } from "react-i18next"; export function VideoMenu({ editor }: EditorMenuProps) { const { t } = useTranslation(); - const shouldShow = useCallback( - ({ state }: ShouldShowProps) => { - if (!state) { - return false; - } - - return editor.isActive("video"); - }, - [editor], - ); const editorState = useEditorState({ editor, @@ -52,17 +37,37 @@ export function VideoMenu({ editor }: EditorMenuProps) { }, }); - const getReferenceClientRect = useCallback(() => { + const shouldShow = useCallback( + ({ state }: ShouldShowProps) => { + if (!state) { + return false; + } + + return editor.isActive("video") && editor.getAttributes("video").src; + }, + [editor], + ); + + const getReferencedVirtualElement = useCallback(() => { + if (!editor) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "video"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const domRect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const domRect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; }, [editor]); const alignVideoLeft = useCallback(() => { @@ -105,15 +110,11 @@ export function VideoMenu({ editor }: EditorMenuProps) { editor={editor} pluginKey={`video-menu`} updateDelay={0} - tippyOptions={{ - getReferenceClientRect, - offset: [0, 8], - zIndex: 99, - popperOptions: { - modifiers: [{ name: "flip", enabled: false }], - }, - plugins: [sticky], - sticky: "popper", + getReferencedVirtualElement={getReferencedVirtualElement} + options={{ + placement: "top", + offset: 8, + flip: false, }} shouldShow={shouldShow} > diff --git a/apps/client/src/features/editor/components/video/video-view.module.css b/apps/client/src/features/editor/components/video/video-view.module.css new file mode 100644 index 00000000..c0e7f99d --- /dev/null +++ b/apps/client/src/features/editor/components/video/video-view.module.css @@ -0,0 +1,33 @@ +.videoWrapper { + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; + overflow: hidden; + animation: pulse 1.2s ease-in-out infinite; + + @mixin light { + background: linear-gradient(-90deg, var(--mantine-color-gray-3) 0%, var(--mantine-color-gray-1) 50%, var(--mantine-color-gray-3) 100%); + background-size: 400% 400%; + } + + @mixin dark { + background: linear-gradient(-90deg, var(--mantine-color-dark-6) 0%, var(--mantine-color-dark-5) 50%, var(--mantine-color-dark-6) 100%); + background-size: 400% 400%; + } + + @keyframes pulse { + 0% { + background-position: 0% 0%; + } + 100% { + background-position: -135% 0%; + } + } +} +.video { + display: block; + width: 100%; + height: 100%; + border-radius: 8px; +} diff --git a/apps/client/src/features/editor/components/video/video-view.tsx b/apps/client/src/features/editor/components/video/video-view.tsx index d47d9a4a..e2473afc 100644 --- a/apps/client/src/features/editor/components/video/video-view.tsx +++ b/apps/client/src/features/editor/components/video/video-view.tsx @@ -1,29 +1,75 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; +import { Group, Loader, Text } from "@mantine/core"; import { useMemo } from "react"; import { getFileUrl } from "@/lib/config.ts"; import clsx from "clsx"; +import classes from "./video-view.module.css"; +import { useTranslation } from "react-i18next"; export default function VideoView(props: NodeViewProps) { - const { node, selected } = props; - const { src, width, align } = node.attrs; - + const { t } = useTranslation(); + const { editor, node, selected } = props; + const { src, width, align, aspectRatio, placeholder } = node.attrs; const alignClass = useMemo(() => { if (align === "left") return "alignLeft"; if (align === "right") return "alignRight"; if (align === "center") return "alignCenter"; return "alignCenter"; }, [align]); + const previewSrc = useMemo(() => { + editor.storage.shared.videoPreviews = + editor.storage.shared.videoPreviews || {}; + + if (placeholder?.id) { + return editor.storage.shared.videoPreviews[placeholder.id]; + } + + return null; + }, [placeholder, editor]); return ( - ); } diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index ecdea2e7..ef03108b 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -1,11 +1,7 @@ import { StarterKit } from "@tiptap/starter-kit"; -import { Placeholder } from "@tiptap/extension-placeholder"; import { TextAlign } from "@tiptap/extension-text-align"; -import { CharacterCount } from "@tiptap/extension-character-count"; -import { TaskList } from "@tiptap/extension-task-list"; -import { ListKeymap } from "@tiptap/extension-list-keymap"; -import { TaskItem } from "@tiptap/extension-task-item"; -import { Underline } from "@tiptap/extension-underline"; +import { TaskList, TaskItem } from "@tiptap/extension-list"; +import { Placeholder, CharacterCount } from "@tiptap/extensions"; import { Superscript } from "@tiptap/extension-superscript"; import SubScript from "@tiptap/extension-subscript"; import { Typography } from "@tiptap/extension-typography"; @@ -15,7 +11,7 @@ import GlobalDragHandle from "tiptap-extension-global-drag-handle"; import { Youtube } from "@tiptap/extension-youtube"; import SlashCommand from "@/features/editor/extensions/slash-command"; import { Collaboration, isChangeOrigin } from "@tiptap/extension-collaboration"; -import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor"; +import { CollaborationCaret } from "@tiptap/extension-collaboration-caret"; import { HocuspocusProvider } from "@hocuspocus/provider"; import { Comment, @@ -41,11 +37,12 @@ import { Embed, SearchAndReplace, Mention, - Subpages, TableDndExtension, + Subpages, Heading, Highlight, UniqueID, + SharedStorage, } from "@docmost/editor-ext"; import { randomElement, @@ -97,7 +94,9 @@ lowlight.register("scala", scala); export const mainExtensions = [ StarterKit.configure({ heading: false, - history: false, + undoRedo: false, + link: false, + trailingNode: false, dropcursor: { width: 3, color: "#70CFF8", @@ -109,6 +108,7 @@ export const mainExtensions = [ }, }, }), + SharedStorage, Heading, UniqueID.configure({ types: ["heading", "paragraph"], @@ -134,8 +134,6 @@ export const mainExtensions = [ TaskItem.configure({ nested: true, }), - ListKeymap, - Underline, LinkExtension.configure({ openOnClick: false, }), @@ -170,6 +168,9 @@ export const mainExtensions = [ }, }).extend({ addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(MentionView); }, }), @@ -208,6 +209,7 @@ export const mainExtensions = [ }), CustomCodeBlock.configure({ view: CodeBlockView, + //@ts-ignore lowlight, HTMLAttributes: { spellcheck: false, @@ -246,7 +248,7 @@ export const mainExtensions = [ Escape: () => { const event = new CustomEvent("closeFindDialogFromEditor", {}); document.dispatchEvent(event); - return true; + return false; }, }; }, @@ -258,8 +260,9 @@ type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[]; export const collabExtensions: CollabExtensions = (provider, user) => [ Collaboration.configure({ document: provider.document, + provider, }), - CollaborationCursor.configure({ + CollaborationCaret.configure({ provider, user: { name: user.name, diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index b4478920..da8bd84a 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -1,13 +1,22 @@ import "@/features/editor/styles/index.css"; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { IndexeddbPersistence } from "y-indexeddb"; import * as Y from "yjs"; import { HocuspocusProvider, - onAuthenticationFailedParameters, + onStatusParameters, WebSocketStatus, + HocuspocusProviderWebsocket, + onSyncedParameters, } from "@hocuspocus/provider"; import { + Editor, EditorContent, EditorProvider, useEditor, @@ -69,161 +78,140 @@ export default function PageEditor({ editable, content, }: PageEditorProps) { - - const collaborationURL = useCollaborationUrl(); const isComponentMounted = useRef(false); - const editorCreated = useRef(false); + const editorRef = useRef(null); useEffect(() => { isComponentMounted.current = true; }, []); - + const [currentUser] = useAtom(currentUserAtom); const [, setEditor] = useAtom(pageEditorAtom); const [, setAsideState] = useAtom(asideStateAtom); const [, setActiveCommentId] = useAtom(activeCommentIdAtom); const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom); - const ydocRef = useRef(null); - if (!ydocRef.current) { - ydocRef.current = new Y.Doc(); - } - const ydoc = ydocRef.current; - const [isLocalSynced, setLocalSynced] = useState(false); - const [isRemoteSynced, setRemoteSynced] = useState(false); + const [isLocalSynced, setIsLocalSynced] = useState(false); + const [isRemoteSynced, setIsRemoteSynced] = useState(false); const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom( yjsConnectionStatusAtom, ); const menuContainerRef = useRef(null); - const documentName = `page.${pageId}`; const { data: collabQuery, refetch: refetchCollabToken } = useCollabToken(); const { isIdle, resetIdle } = useIdle(FIVE_MINUTES, { initialState: false }); const documentState = useDocumentVisibility(); - const [isCollabReady, setIsCollabReady] = useState(false); const { pageSlug } = useParams(); const slugId = extractPageSlugId(pageSlug); const userPageEditMode = currentUser?.user?.settings?.preferences?.pageEditMode ?? PageEditMode.Edit; - - const canScroll = useCallback(() => isComponentMounted.current && editorCreated.current, [isComponentMounted, editorCreated]); + const canScroll = useCallback( + () => Boolean(isComponentMounted.current && editorRef.current), + [isComponentMounted], + ); const { handleScrollTo } = useEditorScroll({ canScroll }); // Providers only created once per pageId const providersRef = useRef<{ local: IndexeddbPersistence; remote: HocuspocusProvider; + socket: HocuspocusProviderWebsocket; } | null>(null); const [providersReady, setProvidersReady] = useState(false); - const localProvider = providersRef.current?.local; - const remoteProvider = providersRef.current?.remote; - - // Track when collaborative provider is ready and synced - const [collabReady, setCollabReady] = useState(false); - useEffect(() => { - if ( - remoteProvider?.status === WebSocketStatus.Connected && - isLocalSynced && - isRemoteSynced - ) { - setCollabReady(true); - } - }, [remoteProvider?.status, isLocalSynced, isRemoteSynced]); - useEffect(() => { if (!providersRef.current) { + const documentName = `page.${pageId}`; + const ydoc = new Y.Doc(); const local = new IndexeddbPersistence(documentName, ydoc); - local.on("synced", () => setLocalSynced(true)); - const remote = new HocuspocusProvider({ - name: documentName, + const socket = new HocuspocusProviderWebsocket({ url: collaborationURL, + }); + const onLocalSyncedHandler = () => { + setIsLocalSynced(true); + }; + const onStatusHandler = (event: onStatusParameters) => { + setYjsConnectionStatus(event.status); + }; + const onSyncedHandler = (event: onSyncedParameters) => { + setIsRemoteSynced(event.state); + }; + const onAuthenticationFailedHandler = () => { + const payload = jwtDecode(collabQuery?.token); + const now = Date.now().valueOf() / 1000; + const isTokenExpired = now >= payload.exp; + if (isTokenExpired) { + refetchCollabToken().then((result) => { + if (result.data?.token) { + socket.disconnect(); + setTimeout(() => { + remote.configuration.token = result.data.token; + socket.connect(); + }, 100); + } + }); + } + }; + const remote = new HocuspocusProvider({ + websocketProvider: socket, + name: documentName, document: ydoc, token: collabQuery?.token, - connect: true, - preserveConnection: false, - onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => { - const payload = jwtDecode(collabQuery?.token); - const now = Date.now().valueOf() / 1000; - const isTokenExpired = now >= payload.exp; - if (isTokenExpired) { - refetchCollabToken().then((result) => { - if (result.data?.token) { - remote.disconnect(); - setTimeout(() => { - remote.configuration.token = result.data.token; - remote.connect(); - }, 100); - } - }); - } - }, - onStatus: (status) => { - if (status.status === "connected") { - setYjsConnectionStatus(status.status); - } - }, + onAuthenticationFailed: onAuthenticationFailedHandler, + onStatus: onStatusHandler, + onSynced: onSyncedHandler, }); - remote.on("synced", () => setRemoteSynced(true)); - remote.on("disconnect", () => { - setYjsConnectionStatus(WebSocketStatus.Disconnected); - }); - providersRef.current = { local, remote }; + + local.on("synced", onLocalSyncedHandler); + providersRef.current = { socket, local, remote }; setProvidersReady(true); } else { setProvidersReady(true); } // Only destroy on final unmount return () => { + providersRef.current?.socket.destroy(); providersRef.current?.remote.destroy(); providersRef.current?.local.destroy(); providersRef.current = null; }; }, [pageId]); - /* - useEffect(() => { - // Handle token updates by reconnecting with new token - if (providersRef.current?.remote && collabQuery?.token) { - const currentToken = providersRef.current.remote.configuration.token; - if (currentToken !== collabQuery.token) { - // Token has changed, need to reconnect with new token - providersRef.current.remote.disconnect(); - providersRef.current.remote.configuration.token = collabQuery.token; - providersRef.current.remote.connect(); - } - } - }, [collabQuery?.token]); - */ - // Only connect/disconnect on tab/idle, not destroy useEffect(() => { if (!providersReady || !providersRef.current) return; - const remoteProvider = providersRef.current.remote; + const socket = providersRef.current.socket; + if ( isIdle && documentState === "hidden" && - remoteProvider.status === WebSocketStatus.Connected + yjsConnectionStatus === WebSocketStatus.Connected ) { - remoteProvider.disconnect(); - setIsCollabReady(false); + socket.disconnect(); return; } if ( documentState === "visible" && - remoteProvider.status === WebSocketStatus.Disconnected + yjsConnectionStatus === WebSocketStatus.Disconnected ) { resetIdle(); - remoteProvider.connect(); - setTimeout(() => setIsCollabReady(true), 500); + socket.connect(); } }, [isIdle, documentState, providersReady, resetIdle]); + // Attach here, to make sure the connection gets properly established + providersRef.current?.remote.attach(); + const extensions = useMemo(() => { - if (!remoteProvider || !currentUser?.user) return mainExtensions; + if (!providersReady || !providersRef.current || !currentUser?.user) { + return mainExtensions; + } + + const remoteProvider = providersRef.current.remote; + return [ ...mainExtensions, ...collabExtensions(remoteProvider, currentUser?.user), ]; - }, [remoteProvider, currentUser?.user]); + }, [providersReady, currentUser?.user]); const editor = useEditor( { @@ -266,18 +254,30 @@ export default function PageEditor({ } }, }, - handlePaste: (view, event, slice) => - handlePaste(view, event, pageId, currentUser?.user.id), - handleDrop: (view, event, _slice, moved) => - handleFileDrop(view, event, moved, pageId), + handlePaste: (_view, event) => { + if (!editorRef.current) return false; + + return handlePaste( + editorRef.current, + event, + pageId, + currentUser?.user.id, + ); + }, + handleDrop: (_view, event, _slice, moved) => { + if (!editorRef.current) return false; + + return handleFileDrop(editorRef.current, event, moved, pageId); + }, }, onCreate({ editor }) { if (editor) { // @ts-ignore setEditor(editor); + // @ts-ignore editor.storage.pageId = pageId; handleScrollTo(editor); - editorCreated.current = true; + editorRef.current = editor; } }, onUpdate({ editor }) { @@ -287,7 +287,7 @@ export default function PageEditor({ debouncedUpdateContent(editorJson); }, }, - [pageId, editable, remoteProvider], + [pageId, editable, extensions], ); const editorIsEditable = useEditorState({ @@ -343,30 +343,17 @@ export default function PageEditor({ setAsideState({ tab: "", isAsideOpen: false }); }, [pageId]); - useEffect(() => { - if (remoteProvider?.status === WebSocketStatus.Connecting) { - const timeout = setTimeout(() => { - setYjsConnectionStatus(WebSocketStatus.Disconnected); - }, 5000); - return () => clearTimeout(timeout); - } - }, [remoteProvider?.status]); - const isSynced = isLocalSynced && isRemoteSynced; useEffect(() => { - const collabReadyTimeout = setTimeout(() => { - if ( - !isCollabReady && - isSynced && - remoteProvider?.status === WebSocketStatus.Connected - ) { - setIsCollabReady(true); + const timeout = setTimeout(() => { + if (yjsConnectionStatus === WebSocketStatus.Connecting || !isSynced) { + setYjsConnectionStatus(WebSocketStatus.Disconnected); } - }, 500); - return () => clearTimeout(collabReadyTimeout); - }, [isRemoteSynced, isLocalSynced, remoteProvider?.status]); + }, 7500); + return () => clearTimeout(timeout); + }, [yjsConnectionStatus, isSynced]); useEffect(() => { // Only honor user default page edit mode preference and permissions if (editor) { @@ -388,12 +375,13 @@ export default function PageEditor({ useEffect(() => { if ( !hasConnectedOnceRef.current && - remoteProvider?.status === WebSocketStatus.Connected + yjsConnectionStatus === WebSocketStatus.Connected && + isSynced ) { hasConnectedOnceRef.current = true; setShowStatic(false); } - }, [remoteProvider?.status]); + }, [yjsConnectionStatus, isSynced]); if (showStatic) { return ( diff --git a/apps/client/src/features/editor/readonly-page-editor.tsx b/apps/client/src/features/editor/readonly-page-editor.tsx index c81e4d19..77496fcd 100644 --- a/apps/client/src/features/editor/readonly-page-editor.tsx +++ b/apps/client/src/features/editor/readonly-page-editor.tsx @@ -81,6 +81,7 @@ export default function ReadonlyPageEditor({ onCreate={({ editor }) => { if (editor) { if (pageId) { + // @ts-ignore editor.storage.pageId = pageId; } // @ts-ignore diff --git a/apps/client/src/features/editor/styles/collaboration.css b/apps/client/src/features/editor/styles/collaboration.css index 4a43ac25..a13d2180 100644 --- a/apps/client/src/features/editor/styles/collaboration.css +++ b/apps/client/src/features/editor/styles/collaboration.css @@ -1,5 +1,5 @@ /* Give a remote user a caret */ -.collaboration-cursor__caret { +.collaboration-carets__caret { border-left: 1px solid #0d0d0d; border-right: 1px solid #0d0d0d; margin-left: -1px; @@ -10,7 +10,7 @@ } /* Render the username above the caret */ -.collaboration-cursor__label { +.collaboration-carets__label { border-radius: 3px 3px 3px 0; color: #0d0d0d; font-size: 0.75rem; diff --git a/apps/client/src/features/page-history/components/history-editor.tsx b/apps/client/src/features/page-history/components/history-editor.tsx index 4b5839af..5fa8cf42 100644 --- a/apps/client/src/features/page-history/components/history-editor.tsx +++ b/apps/client/src/features/page-history/components/history-editor.tsx @@ -1,8 +1,9 @@ -import '@/features/editor/styles/index.css'; -import React, { useEffect } from 'react'; -import { EditorContent, useEditor } from '@tiptap/react'; -import { mainExtensions } from '@/features/editor/extensions/extensions'; -import { Title } from '@mantine/core'; +import "@/features/editor/styles/index.css"; +import React, { useEffect } from "react"; +import { EditorContent, useEditor } from "@tiptap/react"; +import { mainExtensions } from "@/features/editor/extensions/extensions"; +import { Title } from "@mantine/core"; +import classes from "./history.module.css"; export interface HistoryEditorProps { title: string; @@ -26,7 +27,9 @@ export function HistoryEditor({ title, content }: HistoryEditorProps) {
{title} - {editor && } + {editor && ( + + )}
); diff --git a/apps/client/src/features/page-history/components/history-list.tsx b/apps/client/src/features/page-history/components/history-list.tsx index af178eac..7b0d9ea2 100644 --- a/apps/client/src/features/page-history/components/history-list.tsx +++ b/apps/client/src/features/page-history/components/history-list.tsx @@ -67,7 +67,7 @@ function HistoryList({ pageId }: Props) { mainEditorTitle .chain() .clearContent() - .setContent(activeHistoryData.title, true) + .setContent(activeHistoryData.title, { emitUpdate: true }) .run(); mainEditor .chain() diff --git a/apps/client/src/features/page-history/components/history.module.css b/apps/client/src/features/page-history/components/history.module.css index 5d23cb0c..66415146 100644 --- a/apps/client/src/features/page-history/components/history.module.css +++ b/apps/client/src/features/page-history/components/history.module.css @@ -1,37 +1,49 @@ .history { - display: block; - width: 100%; - padding: var(--mantine-spacing-md); - color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); + display: block; + width: 100%; + padding: var(--mantine-spacing-md); + color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); - @mixin hover { - background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8)); - } + @mixin hover { + background-color: light-dark( + var(--mantine-color-gray-2), + var(--mantine-color-dark-8) + ); + } +} + +.historyEditor { + :global(.ProseMirror) { + padding: 0 !important; + } } .active { - background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8)); + background-color: light-dark( + var(--mantine-color-gray-2), + var(--mantine-color-dark-8) + ); } .sidebar { - max-height: rem(700px); - width: rem(250px); - padding: var(--mantine-spacing-sm); - display: flex; - flex-direction: column; - border-right: rem(1px) solid + max-height: rem(700px); + width: rem(250px); + padding: var(--mantine-spacing-sm); + display: flex; + flex-direction: column; + border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); } .sidebarFlex { - display: flex; + display: flex; } .sidebarMain { - flex: 1; + flex: 1; } .sidebarRightSection { - flex: 1; - padding: rem(16px) rem(40px); + flex: 1; + padding: rem(16px) rem(40px); } diff --git a/apps/client/src/features/page/components/header/page-header-menu.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx index b9663427..a29f02d1 100644 --- a/apps/client/src/features/page/components/header/page-header-menu.tsx +++ b/apps/client/src/features/page/components/header/page-header-menu.tsx @@ -7,22 +7,17 @@ import { IconHistory, IconLink, IconList, + IconMarkdown, IconMessage, IconPrinter, - IconSearch, IconTrash, IconWifiOff, } from "@tabler/icons-react"; -import React from "react"; +import React, { useEffect, useRef, useState } from "react"; import useToggleAside from "@/hooks/use-toggle-aside.tsx"; -import { useAtom } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts"; -import { - getHotkeyHandler, - useClipboard, - useDisclosure, - useHotkeys, -} from "@mantine/hooks"; +import { useClipboard, useDisclosure, useHotkeys } from "@mantine/hooks"; import { useParams } from "react-router-dom"; import { usePageQuery } from "@/features/page/queries/page-query.ts"; import { buildPageUrl } from "@/features/page/page.utils.ts"; @@ -34,12 +29,12 @@ import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal. import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx"; import { Trans, useTranslation } from "react-i18next"; import ExportModal from "@/components/common/export-modal"; +import { htmlToMarkdown } from "@docmost/editor-ext"; import { pageEditorAtom, yjsConnectionStatusAtom, } from "@/features/editor/atoms/editor-atoms.ts"; -import { searchAndReplaceStateAtom } from "@/features/editor/components/search-and-replace/atoms/search-and-replace-state-atom.ts"; -import { formattedDate, timeAgo } from "@/lib/time.ts"; +import { formattedDate } from "@/lib/time.ts"; import { PageStateSegmentedControl } from "@/features/user/components/page-state-pref.tsx"; import MovePageModal from "@/features/page/components/move-page-modal.tsx"; import { useTimeAgo } from "@/hooks/use-time-ago.tsx"; @@ -52,7 +47,6 @@ interface PageHeaderMenuProps { export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { const { t } = useTranslation(); const toggleAside = useToggleAside(); - const [yjsConnectionStatus] = useAtom(yjsConnectionStatusAtom); useHotkeys( [ @@ -69,6 +63,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { const event = new CustomEvent("closeFindDialogFromEditor", {}); document.dispatchEvent(event); }, + { preventDefault: false }, ], ], [], @@ -76,17 +71,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { return ( <> - {yjsConnectionStatus === "disconnected" && ( - - - - - - )} + {!readOnly && } @@ -149,6 +134,15 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) { notifications.show({ message: t("Link copied") }); }; + const handleCopyAsMarkdown = () => { + if (!pageEditor) return; + const html = pageEditor.getHTML(); + const markdown = htmlToMarkdown(html); + const title = page?.title ? `# ${page.title}\n\n` : ""; + clipboard.copy(`${title}${markdown}`); + notifications.show({ message: t("Copied") }); + }; + const handlePrint = () => { setTimeout(() => { window.print(); @@ -186,6 +180,13 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) { > {t("Copy link")} + + } + onClick={handleCopyAsMarkdown} + > + {t("Copy as Markdown")} + }> @@ -293,3 +294,51 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) { ); } + +function ConnectionWarning() { + const { t } = useTranslation(); + const yjsConnectionStatus = useAtomValue(yjsConnectionStatusAtom); + const [showWarning, setShowWarning] = useState(false); + const timeoutRef = useRef | null>(null); + + useEffect(() => { + const isDisconnected = ["disconnected", "connecting"].includes( + yjsConnectionStatus, + ); + + if (isDisconnected) { + if (!timeoutRef.current) { + timeoutRef.current = setTimeout(() => setShowWarning(true), 5000); + } + } else { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + setShowWarning(false); + } + }, [yjsConnectionStatus]); + + // Cleanup only on unmount + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + if (!showWarning) return null; + + return ( + + + + + + ); +} diff --git a/apps/client/src/features/page/services/page-service.ts b/apps/client/src/features/page/services/page-service.ts index 8d76438a..c5b6f252 100644 --- a/apps/client/src/features/page/services/page-service.ts +++ b/apps/client/src/features/page/services/page-service.ts @@ -118,7 +118,14 @@ export async function exportPage(data: IExportPageParams): Promise { .split("filename=")[1] .replace(/"/g, ""); - saveAs(req.data, decodeURIComponent(fileName)); + let decodedFileName = fileName; + try { + decodedFileName = decodeURIComponent(fileName); + } catch (err) { + // fallback to raw filename + } + + saveAs(req.data, decodedFileName); } export async function importPage(file: File, spaceId: string) { diff --git a/apps/client/src/features/space/components/sidebar/switch-space.tsx b/apps/client/src/features/space/components/sidebar/switch-space.tsx index 60349ecc..89e0f64e 100644 --- a/apps/client/src/features/space/components/sidebar/switch-space.tsx +++ b/apps/client/src/features/space/components/sidebar/switch-space.tsx @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import { SpaceSelect } from "./space-select"; import { getSpaceUrl } from "@/lib/config"; import { Button, Popover, Text } from "@mantine/core"; -import { IconChevronDown } from "@tabler/icons-react"; +import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { useDisclosure } from "@mantine/hooks"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts"; @@ -21,7 +21,7 @@ export function SwitchSpace({ spaceIcon, }: SwitchSpaceProps) { const navigate = useNavigate(); - const [opened, { close, open, toggle }] = useDisclosure(false); + const [opened, { close, toggle }] = useDisclosure(false); const handleSelect = (value: string) => { if (value) { @@ -44,9 +44,9 @@ export function SwitchSpace({ variant="subtle" fullWidth justify="space-between" - rightSection={} + rightSection={opened ? : } color="gray" - onClick={open} + onClick={toggle} > { .split("filename=")[1] .replace(/"/g, ""); - saveAs(req.data, decodeURIComponent(fileName)); + let decodedFileName = fileName; + try { + decodedFileName = decodeURIComponent(fileName); + } catch (err) { + // fallback to raw filename + } + + saveAs(req.data, decodedFileName); } diff --git a/apps/server/package.json b/apps/server/package.json index 5e2dc3d8..3b033268 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -76,6 +76,7 @@ "jsonwebtoken": "^9.0.3", "kysely": "^0.28.2", "kysely-migration-cli": "^0.4.2", + "kysely-postgres-js": "^3.0.0", "ldapts": "^7.4.0", "mammoth": "^1.11.0", "mime-types": "^2.1.35", @@ -89,9 +90,9 @@ "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "pdfjs-dist": "^5.4.394", - "pg": "^8.16.3", "pg-tsquery": "^8.4.2", "pgvector": "^0.2.1", + "postgres": "^3.4.8", "pino-http": "^11.0.0", "pino-pretty": "^13.1.3", "postmark": "^4.0.5", @@ -121,7 +122,6 @@ "@types/nodemailer": "^6.4.17", "@types/passport-google-oauth20": "^2.0.16", "@types/passport-jwt": "^4.0.1", - "@types/pg": "^8.11.11", "@types/supertest": "^6.0.2", "@types/ws": "^8.5.14", "@types/yauzl": "^2.10.3", diff --git a/apps/server/src/collaboration/collaboration.gateway.ts b/apps/server/src/collaboration/collaboration.gateway.ts index 3f894572..f1d50671 100644 --- a/apps/server/src/collaboration/collaboration.gateway.ts +++ b/apps/server/src/collaboration/collaboration.gateway.ts @@ -26,7 +26,7 @@ export class CollaborationGateway { ) { this.redisConfig = parseRedisUrl(this.environmentService.getRedisUrl()); - this.hocuspocus = HocuspocusServer.configure({ + this.hocuspocus = new Hocuspocus({ debounce: 10000, maxDebounce: 45000, unloadImmediately: false, @@ -65,6 +65,6 @@ export class CollaborationGateway { } async destroy(): Promise { - await this.hocuspocus.destroy(); + //await this.hocuspocus.destroy(); } } diff --git a/apps/server/src/collaboration/collaboration.util.ts b/apps/server/src/collaboration/collaboration.util.ts index 06133c3f..16ca5bd5 100644 --- a/apps/server/src/collaboration/collaboration.util.ts +++ b/apps/server/src/collaboration/collaboration.util.ts @@ -1,14 +1,12 @@ import { StarterKit } from '@tiptap/starter-kit'; import { TextAlign } from '@tiptap/extension-text-align'; -import { TaskList } from '@tiptap/extension-task-list'; -import { TaskItem } from '@tiptap/extension-task-item'; -import { Underline } from '@tiptap/extension-underline'; import { Superscript } from '@tiptap/extension-superscript'; import SubScript from '@tiptap/extension-subscript'; import { Typography } from '@tiptap/extension-typography'; import { TextStyle } from '@tiptap/extension-text-style'; import { Color } from '@tiptap/extension-color'; import { Youtube } from '@tiptap/extension-youtube'; +import { TaskList, TaskItem } from '@tiptap/extension-list'; import { Heading, Callout, @@ -42,11 +40,14 @@ import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html'; // @tiptap/html library works best for generating prosemirror json state but not HTML // see: https://github.com/ueberdosis/tiptap/issues/5352 // see:https://github.com/ueberdosis/tiptap/issues/4089 +//import { generateJSON } from '@tiptap/html'; import { Node } from '@tiptap/pm/model'; export const tiptapExtensions = [ StarterKit.configure({ codeBlock: false, + link: false, + trailingNode: false, heading: false, }), Heading, @@ -59,7 +60,6 @@ export const tiptapExtensions = [ TaskItem.configure({ nested: true, }), - Underline, LinkExtension, Superscript, SubScript, diff --git a/apps/server/src/collaboration/extensions/authentication.extension.ts b/apps/server/src/collaboration/extensions/authentication.extension.ts index fbcf2408..da8b5562 100644 --- a/apps/server/src/collaboration/extensions/authentication.extension.ts +++ b/apps/server/src/collaboration/extensions/authentication.extension.ts @@ -83,7 +83,7 @@ export class AuthenticationExtension implements Extension { } if (!canEdit) { - data.connection.readOnly = true; + data.connectionConfig.readOnly = true; this.logger.debug( `User ${user.id} granted readonly access to restricted page: ${pageId}`, ); @@ -91,7 +91,7 @@ export class AuthenticationExtension implements Extension { } else { // No restrictions - use space-level permissions if (userSpaceRole === SpaceRole.READER) { - data.connection.readOnly = true; + data.connectionConfig.readOnly = true; this.logger.debug(`User granted readonly access to page: ${pageId}`); } } diff --git a/apps/server/src/common/helpers/utils.ts b/apps/server/src/common/helpers/utils.ts index 738c455b..7c94bb48 100644 --- a/apps/server/src/common/helpers/utils.ts +++ b/apps/server/src/common/helpers/utils.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import * as bcrypt from 'bcrypt'; import { sanitize } from 'sanitize-filename-ts'; import { FastifyRequest } from 'fastify'; +import { Readable, Transform } from 'stream'; export const envPath = path.resolve(process.cwd(), '..', '..', '.env'); @@ -98,3 +99,38 @@ export function hasLicenseOrEE(opts: { const { licenseKey, plan, isCloud } = opts; return Boolean(licenseKey) || (isCloud && plan === 'business'); } + +/** + * Normalizes a database URL for postgres.js compatibility. + * - Removes `sslmode=no-verify` (not supported by postgres.js), keeps other sslmode values + * - Removes `schema` parameter (has no effect via connection string) + * Note: If we don't strip them, the connection will fail + */ +export function normalizePostgresUrl(url: string): string { + const parsed = new URL(url); + const newParams = new URLSearchParams(); + + for (const [key, value] of parsed.searchParams) { + if (key === 'sslmode' && value === 'no-verify') continue; + if (key === 'schema') continue; + newParams.append(key, value); + } + + parsed.search = newParams.toString(); + return parsed.toString(); +} + +export function createByteCountingStream(source: Readable) { + let bytesRead = 0; + const stream = new Transform({ + transform(chunk, encoding, callback) { + bytesRead += chunk.length; + callback(null, chunk); + }, + }); + + source.pipe(stream); + source.on('error', (err) => stream.emit('error', err)); + + return { stream, getBytesRead: () => bytesRead }; +} diff --git a/apps/server/src/common/logger/pino.config.ts b/apps/server/src/common/logger/pino.config.ts index 9d9a14f7..7299a8e9 100644 --- a/apps/server/src/common/logger/pino.config.ts +++ b/apps/server/src/common/logger/pino.config.ts @@ -5,13 +5,14 @@ const CONTEXTS_TO_IGNORE = [ 'InstanceLoader', 'RoutesResolver', 'RouterExplorer', + 'LegacyRouteConverter', 'WebSocketsController', ]; export function createPinoConfig(): Params { - const isProduction = process.env.NODE_ENV === 'production'; - const isDebugMode = process.env.DEBUG_MODE === 'true'; - const logHttp = process.env.LOG_HTTP === 'true'; + const isProduction = process.env.NODE_ENV?.toLowerCase() === 'production'; + const isDebugMode = process.env.DEBUG_MODE?.toLowerCase() === 'true'; + const logHttp = process.env.LOG_HTTP?.toLowerCase() === 'true'; const level = isProduction && !isDebugMode ? 'info' : 'debug'; @@ -32,14 +33,20 @@ export function createPinoConfig(): Params { : undefined, formatters: { level: (label) => ({ level: label }), - log: (object: Record) => { + }, + hooks: { + logMethod(inputArgs, method) { if (isProduction && !isDebugMode) { - const context = object['context'] as string | undefined; - if (context && CONTEXTS_TO_IGNORE.includes(context)) { - return { filtered: true }; + for (const arg of inputArgs) { + if (typeof arg === 'object' && arg !== null && 'context' in arg) { + const context = (arg as Record)['context']; + if (typeof context === 'string' && CONTEXTS_TO_IGNORE.includes(context)) { + return; + } + } } } - return object; + return method.apply(this, inputArgs); }, }, serializers: { diff --git a/apps/server/src/core/attachment/attachment.controller.ts b/apps/server/src/core/attachment/attachment.controller.ts index 8e2a9e87..a733cad0 100644 --- a/apps/server/src/core/attachment/attachment.controller.ts +++ b/apps/server/src/core/attachment/attachment.controller.ts @@ -175,7 +175,9 @@ export class AttachmentController { await this.pageAccessService.validateCanView(page, user); try { - const fileStream = await this.storageService.read(attachment.filePath); + const fileStream = await this.storageService.readStream( + attachment.filePath, + ); res.headers({ 'Content-Type': attachment.mimeType, 'Cache-Control': 'private, max-age=3600', @@ -235,7 +237,9 @@ export class AttachmentController { } try { - const fileStream = await this.storageService.read(attachment.filePath); + const fileStream = await this.storageService.readStream( + attachment.filePath, + ); res.headers({ 'Content-Type': attachment.mimeType, 'Cache-Control': 'public, max-age=3600', @@ -361,14 +365,14 @@ export class AttachmentController { const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`; try { - const fileStream = await this.storageService.read(filePath); + const fileStream = await this.storageService.readStream(filePath); res.headers({ 'Content-Type': getMimeType(filePath), 'Cache-Control': 'private, max-age=86400', }); return res.send(fileStream); } catch (err) { - // this.logger.error(err); + // this.logger.error(err); throw new NotFoundException('File not found'); } } diff --git a/apps/server/src/core/attachment/attachment.utils.ts b/apps/server/src/core/attachment/attachment.utils.ts index ee72dc9f..23512002 100644 --- a/apps/server/src/core/attachment/attachment.utils.ts +++ b/apps/server/src/core/attachment/attachment.utils.ts @@ -5,15 +5,17 @@ import { sanitizeFileName } from '../../common/helpers'; import * as sharp from 'sharp'; export interface PreparedFile { - buffer: Buffer; + buffer?: Buffer; fileName: string; fileSize: number; fileExtension: string; mimeType: string; + multiPartFile?: MultipartFile; } export async function prepareFile( filePromise: Promise, + options: { skipBuffer?: boolean } = {}, ): Promise { const file = await filePromise; @@ -22,10 +24,16 @@ export async function prepareFile( } try { - const buffer = await file.toBuffer(); + let buffer: Buffer | undefined; + let fileSize = 0; + + if (!options.skipBuffer) { + buffer = await file.toBuffer(); + fileSize = buffer.length; + } + const sanitizedFilename = sanitizeFileName(file.filename); const fileName = sanitizedFilename.slice(0, 255); - const fileSize = buffer.length; const fileExtension = path.extname(file.filename).toLowerCase(); return { @@ -34,6 +42,7 @@ export async function prepareFile( fileSize, fileExtension, mimeType: file.mimetype, + multiPartFile: file, }; } catch (error) { throw error; diff --git a/apps/server/src/core/attachment/services/attachment.service.ts b/apps/server/src/core/attachment/services/attachment.service.ts index 77a044a2..ea94b983 100644 --- a/apps/server/src/core/attachment/services/attachment.service.ts +++ b/apps/server/src/core/attachment/services/attachment.service.ts @@ -4,6 +4,7 @@ import { Logger, NotFoundException, } from '@nestjs/common'; +import { Readable } from 'stream'; import { StorageService } from '../../../integrations/storage/storage.service'; import { MultipartFile } from '@fastify/multipart'; import { @@ -26,6 +27,7 @@ import { SpaceRepo } from '@docmost/db/repos/space/space.repo'; import { InjectQueue } from '@nestjs/bullmq'; import { QueueJob, QueueName } from '../../../integrations/queue/constants'; import { Queue } from 'bullmq'; +import { createByteCountingStream } from '../../../common/helpers/utils'; @Injectable() export class AttachmentService { @@ -49,7 +51,9 @@ export class AttachmentService { attachmentId?: string; }) { const { filePromise, pageId, spaceId, userId, workspaceId } = opts; - const preparedFile: PreparedFile = await prepareFile(filePromise); + const preparedFile: PreparedFile = await prepareFile(filePromise, { + skipBuffer: true, + }); let isUpdate = false; let attachmentId = null; @@ -81,7 +85,14 @@ export class AttachmentService { const filePath = `${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/${attachmentId}/${preparedFile.fileName}`; - await this.uploadToDrive(filePath, preparedFile.buffer); + const { stream, getBytesRead } = createByteCountingStream( + preparedFile.multiPartFile.file, + ); + + await this.uploadToDrive(filePath, stream); + + // Update fileSize from the consumed stream + preparedFile.fileSize = getBytesRead(); let attachment: Attachment = null; try { @@ -142,7 +153,10 @@ export class AttachmentService { const preparedFile: PreparedFile = await prepareFile(filePromise); validateFileType(preparedFile.fileExtension, validImageExtensions); - const processedBuffer = await compressAndResizeIcon(preparedFile.buffer, type); + const processedBuffer = await compressAndResizeIcon( + preparedFile.buffer, + type, + ); preparedFile.buffer = processedBuffer; preparedFile.fileSize = processedBuffer.length; preparedFile.fileName = uuid4() + preparedFile.fileExtension; @@ -232,9 +246,9 @@ export class AttachmentService { } } - async uploadToDrive(filePath: string, fileBuffer: any) { + async uploadToDrive(filePath: string, fileContent: Buffer | Readable) { try { - await this.storageService.upload(filePath, fileBuffer); + await this.storageService.upload(filePath, fileContent); } catch (err) { this.logger.error('Error uploading file to drive:', err); throw new BadRequestException('Error uploading file to drive'); diff --git a/apps/server/src/database/database.module.ts b/apps/server/src/database/database.module.ts index ea346dd1..3c0d1f01 100644 --- a/apps/server/src/database/database.module.ts +++ b/apps/server/src/database/database.module.ts @@ -7,8 +7,7 @@ import { } from '@nestjs/common'; import { InjectKysely, KyselyModule } from 'nestjs-kysely'; import { EnvironmentService } from '../integrations/environment/environment.service'; -import { CamelCasePlugin, LogEvent, PostgresDialect, sql } from 'kysely'; -import { Pool, types } from 'pg'; +import { CamelCasePlugin, LogEvent, sql } from 'kysely'; import { GroupRepo } from '@docmost/db/repos/group/group.repo'; import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo'; import { UserRepo } from '@docmost/db/repos/user/user.repo'; @@ -27,9 +26,9 @@ import { UserTokenRepo } from './repos/user-token/user-token.repo'; import { BacklinkRepo } from '@docmost/db/repos/backlink/backlink.repo'; import { ShareRepo } from '@docmost/db/repos/share/share.repo'; import { PageListener } from '@docmost/db/listeners/page.listener'; - -// https://github.com/brianc/node-postgres/issues/811 -types.setTypeParser(types.builtins.INT8, (val) => Number(val)); +import { PostgresJSDialect } from 'kysely-postgres-js'; +import * as postgres from 'postgres'; +import { normalizePostgresUrl } from '../common/helpers'; @Global() @Module({ @@ -38,26 +37,30 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val)); imports: [], inject: [EnvironmentService], useFactory: (environmentService: EnvironmentService) => ({ - dialect: new PostgresDialect({ - pool: new Pool({ - connectionString: environmentService.getDatabaseURL(), - max: environmentService.getDatabaseMaxPool(), - }).on('error', (err) => { - console.error('Database error:', err.message); - }), + dialect: new PostgresJSDialect({ + postgres: postgres( + normalizePostgresUrl(environmentService.getDatabaseURL()), + { + max: environmentService.getDatabaseMaxPool(), + onnotice: () => {}, + types: { + bigint: { + to: 20, + from: [20, 1700], + serialize: (value: number) => value.toString(), + parse: (value: string) => Number.parseInt(value), + }, + }, + }, + ), }), plugins: [new CamelCasePlugin()], log: (event: LogEvent) => { if (environmentService.getNodeEnv() !== 'development') return; const logger = new Logger(DatabaseModule.name); - if (event.level) { - if (process.env.DEBUG_DB?.toLowerCase() === 'true') { - logger.debug(event.query.sql); - logger.debug('query time: ' + event.queryDurationMillis + ' ms'); - //if (event.query.parameters.length > 0) { - // logger.debug('parameters: ' + event.query.parameters); - //} - } + if (process.env.DEBUG_DB?.toLowerCase() === 'true') { + logger.debug(event.query.sql); + logger.debug('query time: ' + event.queryDurationMillis + ' ms'); } }, }), diff --git a/apps/server/src/database/migrate.ts b/apps/server/src/database/migrate.ts index 22e62491..a5d58766 100644 --- a/apps/server/src/database/migrate.ts +++ b/apps/server/src/database/migrate.ts @@ -1,25 +1,19 @@ import * as path from 'path'; import { promises as fs } from 'fs'; -import pg from 'pg'; -import { - Kysely, - Migrator, - PostgresDialect, - FileMigrationProvider, -} from 'kysely'; +import { Kysely, Migrator, FileMigrationProvider } from 'kysely'; import { run } from 'kysely-migration-cli'; import * as dotenv from 'dotenv'; -import { envPath } from '../common/helpers/utils'; +import { envPath, normalizePostgresUrl } from '../common/helpers'; +import { PostgresJSDialect } from 'kysely-postgres-js'; +import postgres from 'postgres'; dotenv.config({ path: envPath }); const migrationFolder = path.join(__dirname, './migrations'); const db = new Kysely({ - dialect: new PostgresDialect({ - pool: new pg.Pool({ - connectionString: process.env.DATABASE_URL, - }) as any, + dialect: new PostgresJSDialect({ + postgres: postgres(normalizePostgresUrl(process.env.DATABASE_URL)), }), }); diff --git a/apps/server/src/ee b/apps/server/src/ee index fce3e9e9..88e3d01f 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit fce3e9e945da114c4f7cdc4de86a6729b072515e +Subproject commit 88e3d01f8135c2dbc628b9636ba91bb9ffd2f0eb diff --git a/apps/server/src/integrations/export/export.controller.ts b/apps/server/src/integrations/export/export.controller.ts index 2681dac9..3585a216 100644 --- a/apps/server/src/integrations/export/export.controller.ts +++ b/apps/server/src/integrations/export/export.controller.ts @@ -55,7 +55,7 @@ export class ExportController { throw new ForbiddenException(); } - const zipFileBuffer = await this.exportService.exportPages( + const zipFileStream = await this.exportService.exportPages( dto.pageId, dto.format, dto.includeAttachments, @@ -71,7 +71,7 @@ export class ExportController { 'attachment; filename="' + encodeURIComponent(fileName) + '"', }); - res.send(zipFileBuffer); + res.send(zipFileStream); } @UseGuards(JwtAuthGuard) @@ -102,6 +102,6 @@ export class ExportController { '"', }); - res.send(exportFile.fileBuffer); + res.send(exportFile.fileStream); } } diff --git a/apps/server/src/integrations/export/export.service.ts b/apps/server/src/integrations/export/export.service.ts index b5d57667..0a824813 100644 --- a/apps/server/src/integrations/export/export.service.ts +++ b/apps/server/src/integrations/export/export.service.ts @@ -5,7 +5,6 @@ import { NotFoundException, } from '@nestjs/common'; import { jsonToHtml, jsonToNode } from '../../collaboration/collaboration.util'; -import { turndown } from './turndown-utils'; import { ExportFormat } from './dto/export-dto'; import { Page } from '@docmost/db/types/entity.types'; import { InjectKysely } from 'nestjs-kysely'; @@ -32,6 +31,7 @@ import { getAttachmentIds, getProsemirrorContent, } from '../../common/helpers/prosemirror/utils'; +import { htmlToMarkdown } from '@docmost/editor-ext'; @Injectable() export class ExportService { @@ -85,7 +85,7 @@ export class ExportService { /]*>[\s\S]*?<\/colgroup>/gim, '', ); - return turndown(newPageHtml); + return htmlToMarkdown(newPageHtml); } return; @@ -227,7 +227,7 @@ export class ExportService { const fileName = `${space.name}-space-export.zip`; return { - fileBuffer: zipFile, + fileStream: zipFile, fileName, }; } diff --git a/apps/server/src/integrations/export/utils.ts b/apps/server/src/integrations/export/utils.ts index fe1815b0..266141c2 100644 --- a/apps/server/src/integrations/export/utils.ts +++ b/apps/server/src/integrations/export/utils.ts @@ -1,4 +1,5 @@ import { jsonToNode } from 'src/collaboration/collaboration.util'; +import { Logger } from '@nestjs/common'; import { ExportFormat } from './dto/export-dto'; import { Node } from '@tiptap/pm/model'; import { validate as isValidUUID } from 'uuid'; @@ -88,7 +89,7 @@ export function replaceInternalLinks( // if link and text are same, use page title if (markLink === node.text) { //@ts-expect-error - node.text = getInternalLinkPageName(relativePath); + node.text = getInternalLinkPageName(relativePath, currentPagePath); } } } @@ -99,10 +100,19 @@ export function replaceInternalLinks( return doc.toJSON(); } -export function getInternalLinkPageName(path: string): string { - return decodeURIComponent( - path?.split('/').pop().split('.').slice(0, -1).join('.'), - ); +export function getInternalLinkPageName(path: string, currentFilePath?: string): string { + const name = path?.split('/').pop().split('.').slice(0, -1).join('.'); + try { + return decodeURIComponent(name); + } catch (err) { + if (currentFilePath) { + Logger.warn( + `URI malformed in page ${currentFilePath}: ${name}. Falling back to raw name.`, + 'ExportUtils', + ); + } + return name; + } } export function extractPageSlugId(input: string): string { diff --git a/apps/server/src/integrations/import/services/import.service.ts b/apps/server/src/integrations/import/services/import.service.ts index 7901122a..aeeebcee 100644 --- a/apps/server/src/integrations/import/services/import.service.ts +++ b/apps/server/src/integrations/import/services/import.service.ts @@ -10,7 +10,11 @@ import { } from '../../../collaboration/collaboration.util'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB } from '@docmost/db/types/kysely.types'; -import { generateSlugId, sanitizeFileName } from '../../../common/helpers'; +import { + generateSlugId, + sanitizeFileName, + createByteCountingStream, +} from '../../../common/helpers'; import { generateJitteredKeyBetween } from 'fractional-indexing-jittered'; import { TiptapTransformer } from '@hocuspocus/transformer'; import * as Y from 'yjs'; @@ -173,15 +177,24 @@ export class ImportService { }; } - async getNewPagePosition(spaceId: string): Promise { - const lastPage = await this.db + async getNewPagePosition( + spaceId: string, + parentPageId?: string, + ): Promise { + let query = this.db .selectFrom('pages') .select(['id', 'position']) .where('spaceId', '=', spaceId) .orderBy('position', (ob) => ob.collate('C').desc()) - .limit(1) - .where('parentPageId', 'is', null) - .executeTakeFirst(); + .limit(1); + + if (parentPageId) { + query = query.where('parentPageId', '=', parentPageId); + } else { + query = query.where('parentPageId', 'is', null); + } + + const lastPage = await query.executeTakeFirst(); if (lastPage) { return generateJitteredKeyBetween(lastPage.position, null); @@ -198,20 +211,21 @@ export class ImportService { workspaceId: string, ) { const file = await filePromise; - const fileBuffer = await file.toBuffer(); const fileExtension = path.extname(file.filename).toLowerCase(); const fileName = sanitizeFileName( path.basename(file.filename, fileExtension), ); - const fileSize = fileBuffer.length; - const fileNameWithExt = fileName + fileExtension; const fileTaskId = uuid7(); const filePath = `${getFileTaskFolderPath(FileTaskType.Import, workspaceId)}/${fileTaskId}/${fileNameWithExt}`; // upload file - await this.storageService.upload(filePath, fileBuffer); + const { stream, getBytesRead } = createByteCountingStream(file.file); + + await this.storageService.upload(filePath, stream); + + const fileSize = getBytesRead(); const fileTask = await this.db .insertInto('fileTasks') diff --git a/apps/server/src/integrations/import/utils/import-formatter.ts b/apps/server/src/integrations/import/utils/import-formatter.ts index 14a2530c..59f5eeec 100644 --- a/apps/server/src/integrations/import/utils/import-formatter.ts +++ b/apps/server/src/integrations/import/utils/import-formatter.ts @@ -1,4 +1,5 @@ import { getEmbedUrlAndProvider } from '@docmost/editor-ext'; +import { Logger } from '@nestjs/common'; import * as path from 'path'; import { v7 } from 'uuid'; import { InsertableBacklink } from '@docmost/db/types/entity.types'; @@ -280,8 +281,18 @@ export async function rewriteInternalLinksToMentionHtml( const $a = $(el); const raw = $a.attr('href')!; if (raw.startsWith('http') || raw.startsWith('/api/')) return; + let decodedRaw = raw; + try { + decodedRaw = decodeURIComponent(raw); + } catch (err) { + Logger.warn( + `URI malformed in page ${currentFilePath}: ${raw}. Falling back to raw path.`, + 'ImportFormatter', + ); + } + const resolved = normalize( - path.join(path.dirname(currentFilePath), decodeURIComponent(raw)), + path.join(path.dirname(currentFilePath), decodedRaw), ); const meta = filePathToPageMetaMap.get(resolved); if (!meta) return; diff --git a/apps/server/src/integrations/import/utils/import.utils.ts b/apps/server/src/integrations/import/utils/import.utils.ts index 1fa10d7a..c8f5fe51 100644 --- a/apps/server/src/integrations/import/utils/import.utils.ts +++ b/apps/server/src/integrations/import/utils/import.utils.ts @@ -1,3 +1,4 @@ +import { Logger } from '@nestjs/common'; import { promises as fs } from 'fs'; import * as path from 'path'; @@ -30,8 +31,13 @@ export function resolveRelativeAttachmentPath( pageDir: string, attachmentCandidates: Map, ): string | null { - const mainRel = decodeURIComponent(raw.replace(/^\.?\/+/, '')); - const fallback = path.normalize(path.join(pageDir, mainRel)); + let mainRel = raw.replace(/^\.?\/+/, ''); + try { + mainRel = decodeURIComponent(mainRel); + } catch (err) { + Logger.warn(`URI malformed for attachment path: ${mainRel}. Falling back to raw path.`, 'ImportUtils'); + } + const fallback = path.normalize(path.join(pageDir, mainRel)).split(path.sep).join('/'); if (attachmentCandidates.has(mainRel)) { return mainRel; diff --git a/apps/server/src/integrations/storage/drivers/local.driver.ts b/apps/server/src/integrations/storage/drivers/local.driver.ts index 5171066c..aada2c05 100644 --- a/apps/server/src/integrations/storage/drivers/local.driver.ts +++ b/apps/server/src/integrations/storage/drivers/local.driver.ts @@ -20,9 +20,15 @@ export class LocalDriver implements StorageDriver { return join(this.config.storagePath, filePath); } - async upload(filePath: string, file: Buffer): Promise { + async upload(filePath: string, file: Buffer | Readable): Promise { try { - await fs.outputFile(this._fullPath(filePath), file); + const fullPath = this._fullPath(filePath); + if (file instanceof Buffer) { + await fs.outputFile(fullPath, file); + } else { + await fs.mkdir(dirname(fullPath), { recursive: true }); + await pipeline(file, createWriteStream(fullPath)); + } } catch (err) { throw new Error(`Failed to upload file: ${(err as Error).message}`); } @@ -42,7 +48,7 @@ export class LocalDriver implements StorageDriver { try { const fromFullPath = this._fullPath(fromFilePath); const toFullPath = this._fullPath(toFilePath); - + if (await this.exists(fromFilePath)) { await fs.copy(fromFullPath, toFullPath); } diff --git a/apps/server/src/integrations/storage/drivers/s3.driver.ts b/apps/server/src/integrations/storage/drivers/s3.driver.ts index f6d48677..ed44fded 100644 --- a/apps/server/src/integrations/storage/drivers/s3.driver.ts +++ b/apps/server/src/integrations/storage/drivers/s3.driver.ts @@ -23,19 +23,21 @@ export class S3Driver implements StorageDriver { this.s3Client = new S3Client(config as any); } - async upload(filePath: string, file: Buffer): Promise { + async upload(filePath: string, file: Buffer | Readable): Promise { try { const contentType = getMimeType(filePath); - const command = new PutObjectCommand({ - Bucket: this.config.bucket, - Key: filePath, - Body: file, - ContentType: contentType, - // ACL: "public-read", + const upload = new Upload({ + client: this.s3Client, + params: { + Bucket: this.config.bucket, + Key: filePath, + Body: file, + ContentType: contentType, + }, }); - await this.s3Client.send(command); + await upload.done(); } catch (err) { throw new Error(`Failed to upload file: ${(err as Error).message}`); } diff --git a/apps/server/src/integrations/storage/interfaces/storage-driver.interface.ts b/apps/server/src/integrations/storage/interfaces/storage-driver.interface.ts index 22a86d2b..f376c56f 100644 --- a/apps/server/src/integrations/storage/interfaces/storage-driver.interface.ts +++ b/apps/server/src/integrations/storage/interfaces/storage-driver.interface.ts @@ -1,7 +1,7 @@ import { Readable } from 'stream'; export interface StorageDriver { - upload(filePath: string, file: Buffer): Promise; + upload(filePath: string, file: Buffer | Readable): Promise; uploadStream(filePath: string, file: Readable, options?: { recreateClient?: boolean }): Promise; diff --git a/apps/server/src/integrations/storage/storage.service.ts b/apps/server/src/integrations/storage/storage.service.ts index d796351b..3ed887af 100644 --- a/apps/server/src/integrations/storage/storage.service.ts +++ b/apps/server/src/integrations/storage/storage.service.ts @@ -8,9 +8,9 @@ export class StorageService { private readonly logger = new Logger(StorageService.name); constructor( @Inject(STORAGE_DRIVER_TOKEN) private storageDriver: StorageDriver, - ) {} + ) { } - async upload(filePath: string, fileContent: Buffer | any) { + async upload(filePath: string, fileContent: Buffer | Readable) { await this.storageDriver.upload(filePath, fileContent); this.logger.debug(`File uploaded successfully. Path: ${filePath}`); } diff --git a/package.json b/package.json index e7550c6a..2b5096ef 100644 --- a/package.json +++ b/package.json @@ -20,56 +20,51 @@ }, "dependencies": { "@braintree/sanitize-url": "^7.1.0", - "@casl/ability": "^6.7.5", + "@casl/ability": "6.8.0", "@docmost/editor-ext": "workspace:*", "@floating-ui/dom": "^1.7.3", - "@hocuspocus/extension-redis": "^2.15.3", - "@hocuspocus/provider": "^2.15.3", - "@hocuspocus/server": "^2.15.3", - "@hocuspocus/transformer": "^2.15.3", + "@hocuspocus/extension-redis": "3.4.3", + "@hocuspocus/provider": "3.4.3", + "@hocuspocus/server": "3.4.3", + "@hocuspocus/transformer": "3.4.3", "@joplin/turndown": "^4.0.74", "@joplin/turndown-plugin-gfm": "^1.0.56", "@sindresorhus/slugify": "1.1.0", - "@tiptap/core": "2.27.1", - "@tiptap/extension-code-block": "2.27.1", - "@tiptap/extension-code-block-lowlight": "2.27.1", - "@tiptap/extension-collaboration": "2.27.1", - "@tiptap/extension-collaboration-cursor": "2.27.1", - "@tiptap/extension-color": "2.27.1", - "@tiptap/extension-document": "2.27.1", - "@tiptap/extension-heading": "2.27.1", - "@tiptap/extension-highlight": "2.27.1", - "@tiptap/extension-history": "2.27.1", - "@tiptap/extension-image": "2.27.1", - "@tiptap/extension-link": "2.27.1", - "@tiptap/extension-list-item": "2.27.1", - "@tiptap/extension-list-keymap": "2.27.1", - "@tiptap/extension-placeholder": "2.27.1", - "@tiptap/extension-subscript": "2.27.1", - "@tiptap/extension-superscript": "2.27.1", - "@tiptap/extension-table": "2.27.1", - "@tiptap/extension-table-cell": "2.27.1", - "@tiptap/extension-table-header": "2.27.1", - "@tiptap/extension-table-row": "2.27.1", - "@tiptap/extension-task-item": "2.27.1", - "@tiptap/extension-task-list": "2.27.1", - "@tiptap/extension-text": "2.27.1", - "@tiptap/extension-text-align": "2.27.1", - "@tiptap/extension-text-style": "2.27.1", - "@tiptap/extension-typography": "2.27.1", - "@tiptap/extension-underline": "2.27.1", - "@tiptap/extension-youtube": "2.27.1", - "@tiptap/html": "2.27.1", - "@tiptap/pm": "2.27.1", - "@tiptap/react": "2.27.1", - "@tiptap/starter-kit": "2.27.1", - "@tiptap/suggestion": "2.27.1", + "@tiptap/core": "3.17.1", + "@tiptap/extension-code-block": "3.17.1", + "@tiptap/extension-collaboration": "3.17.1", + "@tiptap/extension-collaboration-caret": "3.17.1", + "@tiptap/extension-color": "3.17.1", + "@tiptap/extension-document": "3.17.1", + "@tiptap/extension-heading": "3.17.1", + "@tiptap/extension-highlight": "3.17.1", + "@tiptap/extension-history": "3.17.1", + "@tiptap/extension-image": "3.17.1", + "@tiptap/extension-link": "3.17.1", + "@tiptap/extension-list": "3.17.1", + "@tiptap/extension-placeholder": "3.17.1", + "@tiptap/extension-subscript": "3.17.1", + "@tiptap/extension-superscript": "3.17.1", + "@tiptap/extension-table": "3.17.1", + "@tiptap/extension-text": "3.17.1", + "@tiptap/extension-text-align": "3.17.1", + "@tiptap/extension-text-style": "3.17.1", + "@tiptap/extension-typography": "3.17.1", + "@tiptap/extension-unique-id": "^3.17.1", + "@tiptap/extension-youtube": "3.17.1", + "@tiptap/html": "3.17.1", + "@tiptap/pm": "3.17.1", + "@tiptap/react": "3.17.1", + "@tiptap/starter-kit": "3.17.1", + "@tiptap/suggestion": "3.17.1", "@types/qrcode": "^1.5.5", "bytes": "^3.1.2", "cross-env": "^7.0.3", "date-fns": "^4.1.0", "dompurify": "^3.2.6", "fractional-indexing-jittered": "^1.0.0", + "highlight.js": "^11.11.1", + "image-dimensions": "^2.5.0", "ioredis": "^5.4.1", "jszip": "^3.10.1", "linkifyjs": "^4.3.2", @@ -79,11 +74,12 @@ "uuid": "^11.1.0", "y-indexeddb": "^9.0.12", "y-prosemirror": "1.3.7", - "yjs": "^13.6.27" + "yjs": "^13.6.29" }, "devDependencies": { "@nx/js": "20.4.5", "@types/bytes": "^3.1.5", + "@types/turndown": "^5.0.6", "@types/uuid": "^10.0.0", "concurrently": "^9.1.2", "nx": "20.4.5", diff --git a/packages/editor-ext/.prettierrc b/packages/editor-ext/.prettierrc new file mode 100644 index 00000000..dcb72794 --- /dev/null +++ b/packages/editor-ext/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/packages/editor-ext/src/index.ts b/packages/editor-ext/src/index.ts index 3ff99083..24d0ac5f 100644 --- a/packages/editor-ext/src/index.ts +++ b/packages/editor-ext/src/index.ts @@ -23,3 +23,4 @@ export * from "./lib/subpages"; export * from "./lib/highlight"; export * from "./lib/heading/heading"; export * from "./lib/unique-id"; +export * from "./lib/shared-storage"; diff --git a/packages/editor-ext/src/lib/attachment/attachment-upload.ts b/packages/editor-ext/src/lib/attachment/attachment-upload.ts index 0d2ac6c7..a3446db9 100644 --- a/packages/editor-ext/src/lib/attachment/attachment-upload.ts +++ b/packages/editor-ext/src/lib/attachment/attachment-upload.ts @@ -1,126 +1,125 @@ -import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; -import { Decoration, DecorationSet } from "@tiptap/pm/view"; -import { - insertTrailingNode, - MediaUploadOptions, - UploadFn, -} from "../media-utils"; +import { Node } from "@tiptap/pm/model"; +import { MediaUploadOptions, UploadFn } from "../media-utils"; import { IAttachment } from "../types"; +import { generateNodeId } from "../utils"; +import { Command } from "@tiptap/core"; -const uploadKey = new PluginKey("attachment-upload"); +const findAttachmentNodeByPlaceholderId = ( + doc: Node, + placeholderId: string, +): { node: Node; pos: number } | null => { + let result: { node: Node; pos: number } | null = null; -export const AttachmentUploadPlugin = ({ - placeholderClass, -}: { - placeholderClass: string; -}) => - new Plugin({ - key: uploadKey, - state: { - init() { - return DecorationSet.empty; - }, - apply(tr, set) { - set = set.map(tr.mapping, tr.doc); - // See if the transaction adds or removes any placeholders - //@-ts-expect-error - not yet sure what the type I need here - const action = tr.getMeta(this); - if (action?.add) { - const { id, pos, fileName } = action.add; - - const placeholder = document.createElement("div"); - placeholder.setAttribute("class", placeholderClass); - - const uploadingText = document.createElement("span"); - uploadingText.setAttribute("class", "uploading-text"); - uploadingText.textContent = `Uploading ${fileName}`; - - placeholder.appendChild(uploadingText); - - const realPos = pos + 1; - const deco = Decoration.widget(realPos, placeholder, { - id, - }); - set = set.add(tr.doc, [deco]); - } else if (action?.remove) { - set = set.remove( - set.find( - undefined, - undefined, - (spec) => spec.id == action.remove.id, - ), - ); - } - return set; - }, - }, - props: { - decorations(state) { - return this.getState(state); - }, - }, + doc.descendants((node, pos) => { + if (result) return false; + if ( + node.type.name === "attachment" && + node.attrs.placeholder?.id === placeholderId + ) { + result = { node, pos }; + return false; + } + return true; }); -function findPlaceholder(state: EditorState, id: {}) { - const decos = uploadKey.getState(state) as DecorationSet; - const found = decos.find(undefined, undefined, (spec) => spec.id == id); - return found.length ? found[0]?.from : null; -} - -export const handleAttachmentUpload = + return result; +}; +const handleAttachmentUpload = ({ validateFn, onUpload }: MediaUploadOptions): UploadFn => - async (file, view, pos, pageId, allowMedia) => { + async (file, editor, pos, pageId, allowMedia) => { const validated = validateFn?.(file, allowMedia); // @ts-ignore if (!validated) return; - // A fresh object to act as the ID for this upload - const id = {}; - // Replace the selection with a placeholder - const tr = view.state.tr; - if (!tr.selection.empty) tr.deleteSelection(); + const placeholderId = generateNodeId(); - tr.setMeta(uploadKey, { - add: { - id, - pos, - fileName: file.name, - }, - }); + let placeholderInserted = false; - insertTrailingNode(tr, pos, view); - view.dispatch(tr); + const insertPlaceholder = (): Command => { + return ({ tr, state }) => { + const initialPlaceholderNode = state.schema.nodes.attachment?.create({ + placeholder: { + id: placeholderId, + }, + name: file.name, + size: file.size, + }); - await onUpload(file, pageId).then( - (attachment: IAttachment) => { - const { schema } = view.state; + if (!initialPlaceholderNode) return false; - const pos = findPlaceholder(view.state, id); + const { parent } = tr.doc.resolve(pos); + const isEmptyTextBlock = parent.isTextblock && !parent.childCount; - if (pos == null) return; + if (isEmptyTextBlock) { + tr.replaceRangeWith(pos - 1, pos + 1, initialPlaceholderNode); + } else { + tr.insert(pos, initialPlaceholderNode); + } - if (!attachment) return; + return true; + }; + }; + const replacePlaceholderWithAttachment = ( + attachment: IAttachment, + ): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findAttachmentNodeByPlaceholderId(tr.doc, placeholderId) || {}; - const node = schema.nodes.attachment?.create({ + // If the placeholder is not found or attachment is missing, abort the process + if (currentPos === null || !attachment) return false; + + // Update the placeholder node with the actual attachment data + tr.setNodeMarkup(currentPos, undefined, { url: `/api/files/${attachment.id}/${attachment.fileName}`, name: attachment.fileName, mime: attachment.mimeType, size: attachment.fileSize, attachmentId: attachment.id, }); - if (!node) return; - const transaction = view.state.tr - .replaceWith(pos, pos, node) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - () => { - // Deletes the placeholder on error - const transaction = view.state.tr - .delete(pos, pos) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - ); + return true; + }; + }; + const removePlaceholder = (): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findAttachmentNodeByPlaceholderId(tr.doc, placeholderId) || {}; + + if (currentPos === null) return false; + + tr.delete(currentPos, currentPos + 2); + + return true; + }; + }; + // Only show the placeholder if the upload takes more than 250ms + const insertPlaceholderTimeout = setTimeout(() => { + editor.commands.command(insertPlaceholder()); + placeholderInserted = true; + }, 250); + + try { + const attachment: IAttachment = await onUpload(file, pageId); + + clearTimeout(insertPlaceholderTimeout); + + if (placeholderInserted) { + setTimeout(() => { + editor.commands.command(replacePlaceholderWithAttachment(attachment)); + }, 100); + } else { + editor + .chain() + .command(insertPlaceholder()) + .command(replacePlaceholderWithAttachment(attachment)) + .run(); + } + } catch (error) { + clearTimeout(insertPlaceholderTimeout); + + editor.commands.command(removePlaceholder()); + } }; + +export { handleAttachmentUpload }; diff --git a/packages/editor-ext/src/lib/attachment/attachment.ts b/packages/editor-ext/src/lib/attachment/attachment.ts index 5231c897..0e37e014 100644 --- a/packages/editor-ext/src/lib/attachment/attachment.ts +++ b/packages/editor-ext/src/lib/attachment/attachment.ts @@ -1,6 +1,5 @@ import { Node, mergeAttributes } from "@tiptap/core"; import { ReactNodeViewRenderer } from "@tiptap/react"; -import { AttachmentUploadPlugin } from "./attachment-upload"; export interface AttachmentOptions { HTMLAttributes: Record; @@ -13,6 +12,7 @@ export interface AttachmentAttributes { mime?: string; // e.g. application/zip size?: number; attachmentId?: string; + placeholder?: string; } declare module "@tiptap/core" { @@ -75,6 +75,10 @@ export const Attachment = Node.create({ "data-attachment-id": attributes.attachmentId, }), }, + placeholder: { + default: null, + rendered: false, + }, }; }, @@ -120,14 +124,9 @@ export const Attachment = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, - - addProseMirrorPlugins() { - return [ - AttachmentUploadPlugin({ - placeholderClass: "attachment-placeholder", - }), - ]; - }, }); diff --git a/packages/editor-ext/src/lib/callout/callout.ts b/packages/editor-ext/src/lib/callout/callout.ts index 97c5dfcc..1dc4d800 100644 --- a/packages/editor-ext/src/lib/callout/callout.ts +++ b/packages/editor-ext/src/lib/callout/callout.ts @@ -87,7 +87,7 @@ export const Callout = Node.create({ mergeAttributes( { "data-type": this.name }, this.options.HTMLAttributes, - HTMLAttributes, + HTMLAttributes ), 0, ]; @@ -130,6 +130,9 @@ export const Callout = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, @@ -193,7 +196,7 @@ export const Callout = Node.create({ tr.delete(pos, pos + nodeSize); tr.setSelection( - TextSelection.near(tr.doc.resolve(previousPosition - 1)), + TextSelection.near(tr.doc.resolve(previousPosition - 1)) ); tr.insert(previousPosition - 1, content); diff --git a/packages/editor-ext/src/lib/custom-code-block.ts b/packages/editor-ext/src/lib/custom-code-block.ts deleted file mode 100644 index 702e98a9..00000000 --- a/packages/editor-ext/src/lib/custom-code-block.ts +++ /dev/null @@ -1,81 +0,0 @@ -import CodeBlockLowlight, { - CodeBlockLowlightOptions, -} from "@tiptap/extension-code-block-lowlight"; -import { ReactNodeViewRenderer } from "@tiptap/react"; - -export interface CustomCodeBlockOptions extends CodeBlockLowlightOptions { - view: any; -} - -const TAB_CHAR = "\u00A0\u00A0"; - -export const CustomCodeBlock = CodeBlockLowlight.extend( - { - selectable: true, - - addOptions() { - return { - ...this.parent?.(), - view: null, - }; - }, - - addKeyboardShortcuts() { - return { - ...this.parent?.(), - Tab: () => { - if (this.editor.isActive("codeBlock")) { - this.editor - .chain() - .command(({ tr }) => { - tr.insertText(TAB_CHAR); - return true; - }) - .run(); - return true; - } - }, - "Mod-a": () => { - if (this.editor.isActive("codeBlock")) { - const { state } = this.editor; - const { $from } = state.selection; - - let codeBlockNode = null; - let codeBlockPos = null; - let depth = 0; - - for (depth = $from.depth; depth > 0; depth--) { - const node = $from.node(depth); - if (node.type.name === "codeBlock") { - codeBlockNode = node; - codeBlockPos = $from.start(depth) - 1; - break; - } - } - - if (codeBlockNode && codeBlockPos !== null) { - const codeBlockStart = codeBlockPos; - const codeBlockEnd = codeBlockPos + codeBlockNode.nodeSize; - - const contentStart = codeBlockStart + 1; - const contentEnd = codeBlockEnd - 1; - - this.editor.commands.setTextSelection({ - from: contentStart, - to: contentEnd, - }); - - return true; - } - } - - return false; - }, - }; - }, - - addNodeView() { - return ReactNodeViewRenderer(this.options.view); - }, - } -); diff --git a/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts b/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts new file mode 100644 index 00000000..ba9fe9c1 --- /dev/null +++ b/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts @@ -0,0 +1,108 @@ +import type { CodeBlockOptions } from "@tiptap/extension-code-block"; +import CodeBlock from "@tiptap/extension-code-block"; + +import { LowlightPlugin } from "./lowlight-plugin.js"; +import { ReactNodeViewRenderer } from "@tiptap/react"; + +export interface CodeBlockLowlightOptions extends CodeBlockOptions { + /** + * The lowlight instance. + */ + lowlight: any; + view: any; +} + +const TAB_CHAR = "\u00A0\u00A0"; + +/** + * This extension allows you to highlight code blocks with lowlight. + * @see https://tiptap.dev/api/nodes/code-block-lowlight + */ +export const CustomCodeBlock = CodeBlock.extend({ + selectable: true, + + addOptions() { + return { + ...this.parent?.(), + lowlight: {}, + languageClassPrefix: "language-", + exitOnTripleEnter: true, + exitOnArrowDown: true, + defaultLanguage: null, + HTMLAttributes: {}, + view: null, + }; + }, + + addKeyboardShortcuts() { + return { + ...this.parent?.(), + Tab: () => { + if (this.editor.isActive("codeBlock")) { + this.editor + .chain() + .command(({ tr }) => { + tr.insertText(TAB_CHAR); + return true; + }) + .run(); + return true; + } + }, + "Mod-a": () => { + if (this.editor.isActive("codeBlock")) { + const { state } = this.editor; + const { $from } = state.selection; + + let codeBlockNode = null; + let codeBlockPos = null; + let depth = 0; + + for (depth = $from.depth; depth > 0; depth--) { + const node = $from.node(depth); + if (node.type.name === "codeBlock") { + codeBlockNode = node; + codeBlockPos = $from.start(depth) - 1; + break; + } + } + + if (codeBlockNode && codeBlockPos !== null) { + const codeBlockStart = codeBlockPos; + const codeBlockEnd = codeBlockPos + codeBlockNode.nodeSize; + + const contentStart = codeBlockStart + 1; + const contentEnd = codeBlockEnd - 1; + + this.editor.commands.setTextSelection({ + from: contentStart, + to: contentEnd, + }); + + return true; + } + } + + return false; + }, + }; + }, + + addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + + return ReactNodeViewRenderer(this.options.view); + }, + + addProseMirrorPlugins() { + return [ + ...(this.parent?.() || []), + LowlightPlugin({ + name: this.name, + lowlight: this.options.lowlight, + defaultLanguage: this.options.defaultLanguage, + }), + ]; + }, +}); diff --git a/packages/editor-ext/src/lib/custom-code-block/index.ts b/packages/editor-ext/src/lib/custom-code-block/index.ts new file mode 100644 index 00000000..f6e3470f --- /dev/null +++ b/packages/editor-ext/src/lib/custom-code-block/index.ts @@ -0,0 +1 @@ +export { CustomCodeBlock } from "./custom-code-block"; diff --git a/packages/editor-ext/src/lib/custom-code-block/lowlight-plugin.ts b/packages/editor-ext/src/lib/custom-code-block/lowlight-plugin.ts new file mode 100644 index 00000000..505b8f20 --- /dev/null +++ b/packages/editor-ext/src/lib/custom-code-block/lowlight-plugin.ts @@ -0,0 +1,159 @@ +import { findChildren } from '@tiptap/core' +import type { Node as ProsemirrorNode } from '@tiptap/pm/model' +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { Decoration, DecorationSet } from '@tiptap/pm/view' +// @ts-ignore +import highlight from 'highlight.js/lib/core' + +function parseNodes(nodes: any[], className: string[] = []): { text: string; classes: string[] }[] { + return nodes + .map(node => { + const classes = [...className, ...(node.properties ? node.properties.className : [])] + + if (node.children) { + return parseNodes(node.children, classes) + } + + return { + text: node.value, + classes, + } + }) + .flat() +} + +function getHighlightNodes(result: any) { + // `.value` for lowlight v1, `.children` for lowlight v2 + return result.value || result.children || [] +} + +function registered(aliasOrLanguage: string) { + return Boolean(highlight.getLanguage(aliasOrLanguage)) +} + +function getDecorations({ + doc, + name, + lowlight, + defaultLanguage, +}: { + doc: ProsemirrorNode + name: string + lowlight: any + defaultLanguage: string | null | undefined +}) { + const decorations: Decoration[] = [] + + findChildren(doc, node => node.type.name === name).forEach(block => { + let from = block.pos + 1 + const language = block.node.attrs.language || defaultLanguage + const languages = lowlight.listLanguages() + + const nodes = + language && (languages.includes(language) || registered(language) || lowlight.registered?.(language)) + ? getHighlightNodes(lowlight.highlight(language, block.node.textContent)) + : getHighlightNodes(lowlight.highlightAuto(block.node.textContent)) + + parseNodes(nodes).forEach(node => { + const to = from + node.text.length + + if (node.classes.length) { + const decoration = Decoration.inline(from, to, { + class: node.classes.join(' '), + }) + + decorations.push(decoration) + } + + from = to + }) + }) + + return DecorationSet.create(doc, decorations) +} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +function isFunction(param: any): param is Function { + return typeof param === 'function' +} + +export function LowlightPlugin({ + name, + lowlight, + defaultLanguage, +}: { + name: string + lowlight: any + defaultLanguage: string | null | undefined +}) { + if (!['highlight', 'highlightAuto', 'listLanguages'].every(api => isFunction(lowlight[api]))) { + throw Error('You should provide an instance of lowlight to use the code-block-lowlight extension') + } + + const lowlightPlugin: Plugin = new Plugin({ + key: new PluginKey('lowlight'), + + state: { + init: (_, { doc }) => + getDecorations({ + doc, + name, + lowlight, + defaultLanguage, + }), + apply: (transaction, decorationSet, oldState, newState) => { + const oldNodeName = oldState.selection.$head.parent.type.name + const newNodeName = newState.selection.$head.parent.type.name + const oldNodes = findChildren(oldState.doc, node => node.type.name === name) + const newNodes = findChildren(newState.doc, node => node.type.name === name) + + if ( + transaction.docChanged && + // Apply decorations if: + // selection includes named node, + ([oldNodeName, newNodeName].includes(name) || + // OR transaction adds/removes named node, + newNodes.length !== oldNodes.length || + // OR transaction has changes that completely encapsulte a node + // (for example, a transaction that affects the entire document). + // Such transactions can happen during collab syncing via y-prosemirror, for example. + transaction.steps.some(step => { + // @ts-ignore + return ( + // @ts-ignore + step.from !== undefined && + // @ts-ignore + step.to !== undefined && + oldNodes.some(node => { + // @ts-ignore + return ( + // @ts-ignore + node.pos >= step.from && + // @ts-ignore + node.pos + node.node.nodeSize <= step.to + ) + }) + ) + })) + ) { + return getDecorations({ + doc: transaction.doc, + name, + lowlight, + defaultLanguage, + }) + } + + return decorationSet.map(transaction.mapping, transaction.doc) + }, + }, + + props: { + decorations(state) { + return lowlightPlugin.getState(state) + }, + }, + }) + + return lowlightPlugin +} \ No newline at end of file diff --git a/packages/editor-ext/src/lib/details/details.ts b/packages/editor-ext/src/lib/details/details.ts index b28c4de7..41c66dca 100644 --- a/packages/editor-ext/src/lib/details/details.ts +++ b/packages/editor-ext/src/lib/details/details.ts @@ -27,6 +27,7 @@ export const Details = Node.create({ content: "detailsSummary detailsContent", defining: true, isolating: true, + // @ts-ignore allowGapCursor: false, addOptions() { return { diff --git a/packages/editor-ext/src/lib/drawio.ts b/packages/editor-ext/src/lib/drawio.ts index 319853b2..3cc041a2 100644 --- a/packages/editor-ext/src/lib/drawio.ts +++ b/packages/editor-ext/src/lib/drawio.ts @@ -41,45 +41,45 @@ export const Drawio = Node.create({ addAttributes() { return { src: { - default: '', - parseHTML: (element) => element.getAttribute('data-src'), + default: "", + parseHTML: (element) => element.getAttribute("data-src"), renderHTML: (attributes) => ({ - 'data-src': attributes.src, + "data-src": attributes.src, }), }, title: { default: undefined, - parseHTML: (element) => element.getAttribute('data-title'), + parseHTML: (element) => element.getAttribute("data-title"), renderHTML: (attributes: DrawioAttributes) => ({ - 'data-title': attributes.title, + "data-title": attributes.title, }), }, width: { - default: '100%', - parseHTML: (element) => element.getAttribute('data-width'), + default: "100%", + parseHTML: (element) => element.getAttribute("data-width"), renderHTML: (attributes: DrawioAttributes) => ({ - 'data-width': attributes.width, + "data-width": attributes.width, }), }, size: { default: null, - parseHTML: (element) => element.getAttribute('data-size'), + parseHTML: (element) => element.getAttribute("data-size"), renderHTML: (attributes: DrawioAttributes) => ({ - 'data-size': attributes.size, + "data-size": attributes.size, }), }, align: { - default: 'center', - parseHTML: (element) => element.getAttribute('data-align'), + default: "center", + parseHTML: (element) => element.getAttribute("data-align"), renderHTML: (attributes: DrawioAttributes) => ({ - 'data-align': attributes.align, + "data-align": attributes.align, }), }, attachmentId: { default: undefined, - parseHTML: (element) => element.getAttribute('data-attachment-id'), + parseHTML: (element) => element.getAttribute("data-attachment-id"), renderHTML: (attributes: DrawioAttributes) => ({ - 'data-attachment-id': attributes.attachmentId, + "data-attachment-id": attributes.attachmentId, }), }, }; @@ -95,13 +95,20 @@ export const Drawio = Node.create({ renderHTML({ HTMLAttributes }) { return [ - 'div', + "div", mergeAttributes( - { 'data-type': this.name }, + { "data-type": this.name }, this.options.HTMLAttributes, HTMLAttributes ), - ['img', { src: HTMLAttributes['data-src'], alt: HTMLAttributes['data-title'], width: HTMLAttributes['data-width'] }], + [ + "img", + { + src: HTMLAttributes["data-src"], + alt: HTMLAttributes["data-title"], + width: HTMLAttributes["data-width"], + }, + ], ]; }, @@ -119,6 +126,9 @@ export const Drawio = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, }); diff --git a/packages/editor-ext/src/lib/embed-provider.ts b/packages/editor-ext/src/lib/embed-provider.ts index 7a91ae9f..4c286a8b 100644 --- a/packages/editor-ext/src/lib/embed-provider.ts +++ b/packages/editor-ext/src/lib/embed-provider.ts @@ -99,7 +99,7 @@ export const embedProviders: IEmbedProvider[] = [ id: "gsheets", name: "Google Sheets", regex: - /^((?:https?:)?\/\/)?((?:www|m)\.)?(docs\.google\.com)\/spreadsheets\/d\/e\/([a-zA-Z0-9_-]+)\/.*$/, + /^((?:https?:)?\/\/)?((?:www|m)\.)?(docs\.google\.com)\/spreadsheets\/d\/([a-zA-Z0-9_-]+)\/.*$/, getEmbedUrl: (match, url: string) => { return url; }, diff --git a/packages/editor-ext/src/lib/embed.ts b/packages/editor-ext/src/lib/embed.ts index 47fc251e..a93648b1 100644 --- a/packages/editor-ext/src/lib/embed.ts +++ b/packages/editor-ext/src/lib/embed.ts @@ -1,6 +1,6 @@ -import { Node, mergeAttributes } from '@tiptap/core'; -import { ReactNodeViewRenderer } from '@tiptap/react'; -import { sanitizeUrl } from './utils'; +import { Node, mergeAttributes } from "@tiptap/core"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import { sanitizeUrl } from "./utils"; export interface EmbedOptions { HTMLAttributes: Record; @@ -14,7 +14,7 @@ export interface EmbedAttributes { height?: number; } -declare module '@tiptap/core' { +declare module "@tiptap/core" { interface Commands { embeds: { setEmbed: (attributes?: EmbedAttributes) => ReturnType; @@ -23,9 +23,9 @@ declare module '@tiptap/core' { } export const Embed = Node.create({ - name: 'embed', + name: "embed", inline: false, - group: 'block', + group: "block", isolating: true, atom: true, defining: true, @@ -40,41 +40,41 @@ export const Embed = Node.create({ addAttributes() { return { src: { - default: '', + default: "", parseHTML: (element) => { - const src = element.getAttribute('data-src'); + const src = element.getAttribute("data-src"); return sanitizeUrl(src); }, renderHTML: (attributes: EmbedAttributes) => ({ - 'data-src': sanitizeUrl(attributes.src), + "data-src": sanitizeUrl(attributes.src), }), }, provider: { - default: '', - parseHTML: (element) => element.getAttribute('data-provider'), + default: "", + parseHTML: (element) => element.getAttribute("data-provider"), renderHTML: (attributes: EmbedAttributes) => ({ - 'data-provider': attributes.provider, + "data-provider": attributes.provider, }), }, align: { - default: 'center', - parseHTML: (element) => element.getAttribute('data-align'), + default: "center", + parseHTML: (element) => element.getAttribute("data-align"), renderHTML: (attributes: EmbedAttributes) => ({ - 'data-align': attributes.align, + "data-align": attributes.align, }), }, width: { default: 640, - parseHTML: (element) => element.getAttribute('data-width'), + parseHTML: (element) => element.getAttribute("data-width"), renderHTML: (attributes: EmbedAttributes) => ({ - 'data-width': attributes.width, + "data-width": attributes.width, }), }, height: { default: 480, - parseHTML: (element) => element.getAttribute('data-height'), + parseHTML: (element) => element.getAttribute("data-height"), renderHTML: (attributes: EmbedAttributes) => ({ - 'data-height': attributes.height, + "data-height": attributes.height, }), }, }; @@ -91,13 +91,13 @@ export const Embed = Node.create({ renderHTML({ HTMLAttributes }) { const src = HTMLAttributes["data-src"]; const safeHref = sanitizeUrl(src); - + return [ "div", mergeAttributes( { "data-type": this.name }, this.options.HTMLAttributes, - HTMLAttributes, + HTMLAttributes ), [ "a", @@ -120,9 +120,9 @@ export const Embed = Node.create({ ...attrs, src: sanitizeUrl(attrs.src), }; - + return commands.insertContent({ - type: 'embed', + type: "embed", attrs: validatedAttrs, }); }, @@ -130,6 +130,9 @@ export const Embed = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, }); diff --git a/packages/editor-ext/src/lib/excalidraw.ts b/packages/editor-ext/src/lib/excalidraw.ts index a7e3a468..28b064e4 100644 --- a/packages/editor-ext/src/lib/excalidraw.ts +++ b/packages/editor-ext/src/lib/excalidraw.ts @@ -1,5 +1,5 @@ -import { Node, mergeAttributes } from '@tiptap/core'; -import { ReactNodeViewRenderer } from '@tiptap/react'; +import { Node, mergeAttributes } from "@tiptap/core"; +import { ReactNodeViewRenderer } from "@tiptap/react"; export interface ExcalidrawOptions { HTMLAttributes: Record; @@ -14,7 +14,7 @@ export interface ExcalidrawAttributes { attachmentId?: string; } -declare module '@tiptap/core' { +declare module "@tiptap/core" { interface Commands { excalidraw: { setExcalidraw: (attributes?: ExcalidrawAttributes) => ReturnType; @@ -23,9 +23,9 @@ declare module '@tiptap/core' { } export const Excalidraw = Node.create({ - name: 'excalidraw', + name: "excalidraw", inline: false, - group: 'block', + group: "block", isolating: true, atom: true, defining: true, @@ -40,45 +40,45 @@ export const Excalidraw = Node.create({ addAttributes() { return { src: { - default: '', - parseHTML: (element) => element.getAttribute('data-src'), + default: "", + parseHTML: (element) => element.getAttribute("data-src"), renderHTML: (attributes) => ({ - 'data-src': attributes.src, + "data-src": attributes.src, }), }, title: { default: undefined, - parseHTML: (element) => element.getAttribute('data-title'), + parseHTML: (element) => element.getAttribute("data-title"), renderHTML: (attributes: ExcalidrawAttributes) => ({ - 'data-title': attributes.title, + "data-title": attributes.title, }), }, width: { - default: '100%', - parseHTML: (element) => element.getAttribute('data-width'), + default: "100%", + parseHTML: (element) => element.getAttribute("data-width"), renderHTML: (attributes: ExcalidrawAttributes) => ({ - 'data-width': attributes.width, + "data-width": attributes.width, }), }, size: { default: null, - parseHTML: (element) => element.getAttribute('data-size'), + parseHTML: (element) => element.getAttribute("data-size"), renderHTML: (attributes: ExcalidrawAttributes) => ({ - 'data-size': attributes.size, + "data-size": attributes.size, }), }, align: { - default: 'center', - parseHTML: (element) => element.getAttribute('data-align'), + default: "center", + parseHTML: (element) => element.getAttribute("data-align"), renderHTML: (attributes: ExcalidrawAttributes) => ({ - 'data-align': attributes.align, + "data-align": attributes.align, }), }, attachmentId: { default: undefined, - parseHTML: (element) => element.getAttribute('data-attachment-id'), + parseHTML: (element) => element.getAttribute("data-attachment-id"), renderHTML: (attributes: ExcalidrawAttributes) => ({ - 'data-attachment-id': attributes.attachmentId, + "data-attachment-id": attributes.attachmentId, }), }, }; @@ -94,13 +94,20 @@ export const Excalidraw = Node.create({ renderHTML({ HTMLAttributes }) { return [ - 'div', + "div", mergeAttributes( - { 'data-type': this.name }, + { "data-type": this.name }, this.options.HTMLAttributes, HTMLAttributes ), - ['img', { src: HTMLAttributes['data-src'], alt: HTMLAttributes['data-title'], width: HTMLAttributes['data-width'] }], + [ + "img", + { + src: HTMLAttributes["data-src"], + alt: HTMLAttributes["data-title"], + width: HTMLAttributes["data-width"], + }, + ], ]; }, @@ -110,7 +117,7 @@ export const Excalidraw = Node.create({ (attrs: ExcalidrawAttributes) => ({ commands }) => { return commands.insertContent({ - type: 'excalidraw', + type: "excalidraw", attrs: attrs, }); }, @@ -118,6 +125,9 @@ export const Excalidraw = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, }); diff --git a/packages/editor-ext/src/lib/image/image-upload.ts b/packages/editor-ext/src/lib/image/image-upload.ts index 9a759903..d5acdcff 100644 --- a/packages/editor-ext/src/lib/image/image-upload.ts +++ b/packages/editor-ext/src/lib/image/image-upload.ts @@ -1,127 +1,145 @@ -import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; -import { Decoration, DecorationSet } from "@tiptap/pm/view"; -import { insertTrailingNode, MediaUploadOptions, UploadFn } from "../media-utils"; +import { imageDimensionsFromStream } from "image-dimensions"; +import { MediaUploadOptions, UploadFn } from "../media-utils"; import { IAttachment } from "../types"; +import { generateNodeId } from "../utils"; +import { Node } from "@tiptap/pm/model"; +import { Command } from "@tiptap/core"; -const uploadKey = new PluginKey("image-upload"); +const findImageNodeByPlaceholderId = ( + doc: Node, + placeholderId: string, +): { node: Node; pos: number } | null => { + let result: { node: Node; pos: number } | null = null; -export const ImageUploadPlugin = ({ - placeholderClass, -}: { - placeholderClass: string; -}) => - new Plugin({ - key: uploadKey, - state: { - init() { - return DecorationSet.empty; - }, - apply(tr, set) { - set = set.map(tr.mapping, tr.doc); - // See if the transaction adds or removes any placeholders - //@-ts-expect-error - not yet sure what the type I need here - const action = tr.getMeta(this); - if (action?.add) { - const { id, pos, src } = action.add; - - const placeholder = document.createElement("div"); - placeholder.setAttribute("class", "img-placeholder"); - const image = document.createElement("img"); - image.setAttribute("class", placeholderClass); - image.src = src; - placeholder.appendChild(image); - const deco = Decoration.widget(pos + 1, placeholder, { - id, - }); - set = set.add(tr.doc, [deco]); - } else if (action?.remove) { - set = set.remove( - set.find( - undefined, - undefined, - (spec) => spec.id == action.remove.id, - ), - ); - } - return set; - }, - }, - props: { - decorations(state) { - return this.getState(state); - }, - }, + doc.descendants((node, pos) => { + if (result) return false; + if ( + node.type.name === "image" && + node.attrs.placeholder?.id === placeholderId + ) { + result = { node, pos }; + return false; + } + return true; }); -function findPlaceholder(state: EditorState, id: {}) { - const decos = uploadKey.getState(state) as DecorationSet; - const found = decos.find(undefined, undefined, (spec) => spec.id == id); - return found.length ? found[0]?.from : null; -} - -export const handleImageUpload = + return result; +}; +const handleImageUpload = ({ validateFn, onUpload }: MediaUploadOptions): UploadFn => - async (file, view, pos, pageId) => { + async (file, editor, pos, pageId) => { // check if the file is an image const validated = validateFn?.(file); // @ts-ignore if (!validated) return; - // A fresh object to act as the ID for this upload - const id = {}; - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - const tr = view.state.tr; - // Replace the selection with a placeholder - if (!tr.selection.empty) tr.deleteSelection(); + const objectUrl = URL.createObjectURL(file); + const imageDimensions = await imageDimensionsFromStream(file.stream()); + const placeholderId = generateNodeId(); + const aspectRatio = imageDimensions + ? imageDimensions.width / imageDimensions.height + : undefined; - tr.setMeta(uploadKey, { - add: { - id, - pos, - src: reader.result, - }, - }); + let placeholderInserted = false; - insertTrailingNode(tr, pos, view); - view.dispatch(tr); + editor.storage.shared.imagePreviews = + editor.storage.shared.imagePreviews || {}; + editor.storage.shared.imagePreviews[placeholderId] = objectUrl; + + const insertPlaceholder = (): Command => { + return ({ tr, state }) => { + const initialPlaceholderNode = state.schema.nodes.image?.create({ + placeholder: { + id: placeholderId, + name: file.name, + }, + aspectRatio, + }); + + if (!initialPlaceholderNode) return false; + + const { parent } = tr.doc.resolve(pos); + const isEmptyTextBlock = parent.isTextblock && !parent.childCount; + + if (isEmptyTextBlock) { + // Replace e.g. empty paragraph with the image + tr.replaceRangeWith(pos - 1, pos + 1, initialPlaceholderNode); + } else { + tr.insert(pos, initialPlaceholderNode); + } + + return true; + }; }; + const replacePlaceholderWithImage = (attachment: IAttachment): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findImageNodeByPlaceholderId(tr.doc, placeholderId) || {}; - await onUpload(file, pageId).then( - (attachment: IAttachment) => { - const { schema } = view.state; + // If the placeholder is not found or attachment is missing, abort the process + if (currentPos === null || !attachment) return false; - const pos = findPlaceholder(view.state, id); - - // If the content around the placeholder has been deleted, drop - // the image - if (pos == null) return; - - // Otherwise, insert it at the placeholder's position, and remove - // the placeholder - - if (!attachment) return; - - const node = schema.nodes.image?.create({ + // Update the placeholder node with the actual image data + tr.setNodeMarkup(currentPos, undefined, { src: `/api/files/${attachment.id}/${attachment.fileName}`, attachmentId: attachment.id, - title: attachment.fileName, size: attachment.fileSize, + aspectRatio, }); - if (!node) return; - const transaction = view.state.tr - .replaceWith(pos, pos, node) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - () => { - // Deletes the image placeholder on error - const transaction = view.state.tr - .delete(pos, pos) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - ); + return true; + }; + }; + const removePlaceholder = (): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findImageNodeByPlaceholderId(tr.doc, placeholderId) || {}; + + if (currentPos === null) return false; + + // Remove the placeholder node + tr.delete(currentPos, currentPos + 2); + + return true; + }; + }; + // Only show the placeholder if the upload takes more than 250ms + const insertPlaceholderTimeout = setTimeout(() => { + editor.commands.command(insertPlaceholder()); + placeholderInserted = true; + }, 250); + const disposePreviewFile = () => { + URL.revokeObjectURL(objectUrl); + + if (editor.storage.shared.imagePreviews) { + delete editor.storage.shared.imagePreviews[placeholderId]; + } + }; + + try { + const attachment: IAttachment = await onUpload(file, pageId); + + clearTimeout(insertPlaceholderTimeout); + + if (placeholderInserted) { + setTimeout(() => { + editor.commands.command(replacePlaceholderWithImage(attachment)); + disposePreviewFile(); + }, 100); + } else { + editor + .chain() + .command(insertPlaceholder()) + .command(replacePlaceholderWithImage(attachment)) + .run(); + disposePreviewFile(); + } + } catch (error) { + clearTimeout(insertPlaceholderTimeout); + + editor.commands.command(removePlaceholder()); + disposePreviewFile(); + } }; + +export { handleImageUpload }; diff --git a/packages/editor-ext/src/lib/image/image.ts b/packages/editor-ext/src/lib/image/image.ts index 3f7683e4..e6426f23 100644 --- a/packages/editor-ext/src/lib/image/image.ts +++ b/packages/editor-ext/src/lib/image/image.ts @@ -1,7 +1,6 @@ import Image from "@tiptap/extension-image"; import { ImageOptions as DefaultImageOptions } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; -import { ImageUploadPlugin } from "./image-upload"; import { mergeAttributes, Range } from "@tiptap/core"; export interface ImageOptions extends DefaultImageOptions { @@ -10,11 +9,15 @@ export interface ImageOptions extends DefaultImageOptions { export interface ImageAttributes { src?: string; alt?: string; - title?: string; align?: string; attachmentId?: string; size?: number; width?: number; + aspectRatio?: number; + placeholder?: { + id: string; + name: string; + }; } declare module "@tiptap/core" { @@ -90,6 +93,17 @@ export const TiptapImage = Image.extend({ "data-size": attributes.size, }), }, + aspectRatio: { + default: null, + parseHTML: (element) => element.getAttribute("data-aspect-ratio"), + renderHTML: (attributes: ImageAttributes) => ({ + "data-aspect-ratio": attributes.aspectRatio, + }), + }, + placeholder: { + default: null, + rendered: false, + }, }; }, @@ -135,14 +149,9 @@ export const TiptapImage = Image.extend({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, - - addProseMirrorPlugins() { - return [ - ImageUploadPlugin({ - placeholderClass: "image-upload", - }), - ]; - }, }); diff --git a/packages/editor-ext/src/lib/markdown/index.ts b/packages/editor-ext/src/lib/markdown/index.ts index 96daf9c9..26eb5d48 100644 --- a/packages/editor-ext/src/lib/markdown/index.ts +++ b/packages/editor-ext/src/lib/markdown/index.ts @@ -1 +1,2 @@ export * from "./utils/marked.utils"; +export * from "./utils/turndown.utils"; diff --git a/packages/editor-ext/src/lib/markdown/utils/basename.ts b/packages/editor-ext/src/lib/markdown/utils/basename.ts new file mode 100644 index 00000000..503de941 --- /dev/null +++ b/packages/editor-ext/src/lib/markdown/utils/basename.ts @@ -0,0 +1,29 @@ +/** + * Flexible `basename` implementation for node and the browser + * @see https://stackoverflow.com/a/59907288/2228771 + */ +export function getBasename(path: string) { + // make sure the basename is not empty, if string ends with separator + let end = path.length - 1; + while (path[end] === '/' || path[end] === '\\') { + --end; + } + + // support mixing of Win + Unix path separators + const i1 = path.lastIndexOf('/', end); + const i2 = path.lastIndexOf('\\', end); + + let start: number; + if (i1 === -1) { + if (i2 === -1) { + // no separator in the whole thing + return path; + } + start = i2; + } else if (i2 === -1) { + start = i1; + } else { + start = Math.max(i1, i2); + } + return path.substring(start + 1, end + 1); +} diff --git a/packages/editor-ext/src/lib/markdown/utils/turndown.d.ts b/packages/editor-ext/src/lib/markdown/utils/turndown.d.ts new file mode 100644 index 00000000..0e8a9a2d --- /dev/null +++ b/packages/editor-ext/src/lib/markdown/utils/turndown.d.ts @@ -0,0 +1,12 @@ +// Map @joplin/turndown types to @types/turndown +declare module "@joplin/turndown" { + import TurndownService from "turndown"; + export = TurndownService; +} + +declare module "@joplin/turndown-plugin-gfm" { + import TurndownService from "turndown"; + export const tables: TurndownService.Plugin; + export const strikethrough: TurndownService.Plugin; + export const highlightedCodeBlock: TurndownService.Plugin; +} diff --git a/apps/server/src/integrations/export/turndown-utils.ts b/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts similarity index 69% rename from apps/server/src/integrations/export/turndown-utils.ts rename to packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts index b20e6733..71a2b512 100644 --- a/apps/server/src/integrations/export/turndown-utils.ts +++ b/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts @@ -1,22 +1,22 @@ -import * as TurndownService from '@joplin/turndown'; +import * as _TurndownService from '@joplin/turndown'; import * as TurndownPluginGfm from '@joplin/turndown-plugin-gfm'; -import * as path from 'path'; +import { getBasename } from './basename'; -export function turndown(html: string): string { +// CJS/ESM interop: .default exists in Vite, not in NestJS +const TurndownService = (_TurndownService as any).default || _TurndownService; + +export function htmlToMarkdown(html: string): string { const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', hr: '---', bulletListMarker: '-', }); - const tables = TurndownPluginGfm.tables; - const strikethrough = TurndownPluginGfm.strikethrough; - const highlightedCodeBlock = TurndownPluginGfm.highlightedCodeBlock; turndownService.use([ - tables, - strikethrough, - highlightedCodeBlock, + TurndownPluginGfm.tables, + TurndownPluginGfm.strikethrough, + TurndownPluginGfm.highlightedCodeBlock, taskList, callout, preserveDetail, @@ -29,34 +29,33 @@ export function turndown(html: string): string { return turndownService.turndown(html).replaceAll('
', ' '); } -function listParagraph(turndownService: TurndownService) { +function listParagraph(turndownService: _TurndownService) { turndownService.addRule('paragraph', { filter: ['p'], - replacement: (content: any, node: HTMLInputElement) => { + replacement: (content: string, node: HTMLInputElement) => { if (node.parentElement?.nodeName === 'LI') { return content; } - return `\n\n${content}\n\n`; }, }); } -function callout(turndownService: TurndownService) { +function callout(turndownService: _TurndownService) { turndownService.addRule('callout', { filter: function (node: HTMLInputElement) { return ( node.nodeName === 'DIV' && node.getAttribute('data-type') === 'callout' ); }, - replacement: function (content: any, node: HTMLInputElement) { + replacement: function (content: string, node: HTMLInputElement) { const calloutType = node.getAttribute('data-callout-type'); return `\n\n:::${calloutType}\n${content.trim()}\n:::\n\n`; }, }); } -function taskList(turndownService: TurndownService) { +function taskList(turndownService: _TurndownService) { turndownService.addRule('taskListItem', { filter: function (node: HTMLInputElement) { return ( @@ -64,32 +63,36 @@ function taskList(turndownService: TurndownService) { node.parentNode.nodeName === 'UL' ); }, - replacement: function (content: any, node: HTMLInputElement) { + replacement: function (content: string, node: HTMLInputElement) { const checkbox = node.querySelector( 'input[type="checkbox"]', ) as HTMLInputElement; const isChecked = checkbox.checked; - + // Process content like regular list items content = content .replace(/^\n+/, '') // remove leading newlines .replace(/\n+$/, '\n') // replace trailing newlines with just a single one .replace(/\n/gm, '\n '); // indent nested content with 2 spaces - + // Create the checkbox prefix const prefix = `- ${isChecked ? '[x]' : '[ ]'} `; - - return prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : ''); + + return ( + prefix + + content + + (node.nextSibling && !/\n$/.test(content) ? '\n' : '') + ); }, }); } -function preserveDetail(turndownService: TurndownService) { +function preserveDetail(turndownService: _TurndownService) { turndownService.addRule('preserveDetail', { filter: function (node: HTMLInputElement) { return node.nodeName === 'DETAILS'; }, - replacement: function (content: any, node: HTMLInputElement) { + replacement: function (_content: string, node: HTMLInputElement) { const summary = node.querySelector(':scope > summary'); let detailSummary = ''; @@ -111,7 +114,7 @@ function preserveDetail(turndownService: TurndownService) { }); } -function mathInline(turndownService: TurndownService) { +function mathInline(turndownService: _TurndownService) { turndownService.addRule('mathInline', { filter: function (node: HTMLInputElement) { return ( @@ -119,13 +122,13 @@ function mathInline(turndownService: TurndownService) { node.getAttribute('data-type') === 'mathInline' ); }, - replacement: function (content: any, node: HTMLInputElement) { + replacement: function (content: string) { return `$${content}$`; }, }); } -function mathBlock(turndownService: TurndownService) { +function mathBlock(turndownService: _TurndownService) { turndownService.addRule('mathBlock', { filter: function (node: HTMLInputElement) { return ( @@ -133,32 +136,32 @@ function mathBlock(turndownService: TurndownService) { node.getAttribute('data-type') === 'mathBlock' ); }, - replacement: function (content: any, node: HTMLInputElement) { + replacement: function (content: string) { return `\n$$\n${content}\n$$\n`; }, }); } -function iframeEmbed(turndownService: TurndownService) { +function iframeEmbed(turndownService: _TurndownService) { turndownService.addRule('iframeEmbed', { filter: function (node: HTMLInputElement) { return node.nodeName === 'IFRAME'; }, - replacement: function (content: any, node: HTMLInputElement) { + replacement: function (_content: string, node: HTMLInputElement) { const src = node.getAttribute('src'); return '[' + src + '](' + src + ')'; }, }); } -function video(turndownService: TurndownService) { +function video(turndownService: _TurndownService) { turndownService.addRule('video', { filter: function (node: HTMLInputElement) { return node.tagName === 'VIDEO'; }, - replacement: function (content: any, node: HTMLInputElement) { + replacement: function (_content: string, node: HTMLInputElement) { const src = node.getAttribute('src') || ''; - const name = path.basename(src); + const name = getBasename(src) || src; return '[' + name + '](' + src + ')'; }, }); diff --git a/packages/editor-ext/src/lib/math/math-block.ts b/packages/editor-ext/src/lib/math/math-block.ts index a580596b..cf11e8f8 100644 --- a/packages/editor-ext/src/lib/math/math-block.ts +++ b/packages/editor-ext/src/lib/math/math-block.ts @@ -63,6 +63,9 @@ export const MathBlock = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, diff --git a/packages/editor-ext/src/lib/math/math-inline.ts b/packages/editor-ext/src/lib/math/math-inline.ts index 39c1cd49..3de9d291 100644 --- a/packages/editor-ext/src/lib/math/math-inline.ts +++ b/packages/editor-ext/src/lib/math/math-inline.ts @@ -64,6 +64,9 @@ export const MathInline = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, diff --git a/packages/editor-ext/src/lib/media-utils.ts b/packages/editor-ext/src/lib/media-utils.ts index f05c4264..02a4a1d1 100644 --- a/packages/editor-ext/src/lib/media-utils.ts +++ b/packages/editor-ext/src/lib/media-utils.ts @@ -1,9 +1,8 @@ -import type { EditorView } from "@tiptap/pm/view"; -import { Transaction } from "@tiptap/pm/state"; +import { Editor } from "@tiptap/core"; export type UploadFn = ( file: File, - view: EditorView, + editor: Editor, pos: number, pageId: string, // only applicable to file attachments @@ -14,16 +13,3 @@ export interface MediaUploadOptions { validateFn?: (file: File, allowMedia?: boolean) => void; onUpload: (file: File, pageId: string) => Promise; } - -export function insertTrailingNode( - tr: Transaction, - pos: number, - view: EditorView, -) { - // create trailing node after decoration - // if decoration is at the last node - const currentDocSize = view.state.doc.content.size; - if (pos + 1 === currentDocSize) { - tr.insert(currentDocSize, view.state.schema.nodes.paragraph.create()); - } -} diff --git a/packages/editor-ext/src/lib/search-and-replace/search-and-replace.ts b/packages/editor-ext/src/lib/search-and-replace/search-and-replace.ts index ca66958f..1ed7632d 100644 --- a/packages/editor-ext/src/lib/search-and-replace/search-and-replace.ts +++ b/packages/editor-ext/src/lib/search-and-replace/search-and-replace.ts @@ -31,6 +31,9 @@ import { import { Node as PMNode, Mark } from "@tiptap/pm/model"; declare module "@tiptap/core" { + interface Storage { + searchAndReplace: SearchAndReplaceStorage; + } interface Commands { search: { /** @@ -184,21 +187,21 @@ const replace = ( if (dispatch) { const tr = state.tr; - + // Get all marks that span the text being replaced const marksSet = new Set(); state.doc.nodesBetween(from, to, (node) => { if (node.isText && node.marks) { - node.marks.forEach(mark => marksSet.add(mark)); + node.marks.forEach((mark) => marksSet.add(mark)); } }); - + const marks = Array.from(marksSet); - + // Delete the old text and insert new text with preserved marks tr.delete(from, to); tr.insert(from, state.schema.text(replaceTerm, marks)); - + dispatch(tr); } }; @@ -215,17 +218,17 @@ const replaceAll = ( // Process replacements in reverse order to avoid position shifting issues for (let i = resultsCopy.length - 1; i >= 0; i -= 1) { const { from, to } = resultsCopy[i]; - + // Get all marks that span the text being replaced const marksSet = new Set(); tr.doc.nodesBetween(from, to, (node) => { if (node.isText && node.marks) { - node.marks.forEach(mark => marksSet.add(mark)); + node.marks.forEach((mark) => marksSet.add(mark)); } }); - + const marks = Array.from(marksSet); - + // Delete and insert with preserved marks tr.delete(from, to); tr.insert(from, tr.doc.type.schema.text(replaceTerm, marks)); @@ -352,10 +355,17 @@ export const SearchAndReplace = Extension.create< // The results will be recalculated by the plugin, but we need to ensure // the index doesn't exceed the new bounds setTimeout(() => { - const newResultsLength = editor.storage.searchAndReplace.results.length; - if (newResultsLength > 0 && editor.storage.searchAndReplace.resultIndex >= newResultsLength) { + const newResultsLength = + editor.storage.searchAndReplace.results.length; + if ( + newResultsLength > 0 && + editor.storage.searchAndReplace.resultIndex >= newResultsLength + ) { // Keep the same position if possible, otherwise go to the last result - editor.storage.searchAndReplace.resultIndex = Math.min(resultIndex, newResultsLength - 1); + editor.storage.searchAndReplace.resultIndex = Math.min( + resultIndex, + newResultsLength - 1, + ); } }, 0); diff --git a/packages/editor-ext/src/lib/shared-storage/index.ts b/packages/editor-ext/src/lib/shared-storage/index.ts new file mode 100644 index 00000000..5b486420 --- /dev/null +++ b/packages/editor-ext/src/lib/shared-storage/index.ts @@ -0,0 +1 @@ +export { SharedStorage } from "./shared-storage"; diff --git a/packages/editor-ext/src/lib/shared-storage/shared-storage.ts b/packages/editor-ext/src/lib/shared-storage/shared-storage.ts new file mode 100644 index 00000000..aa008d45 --- /dev/null +++ b/packages/editor-ext/src/lib/shared-storage/shared-storage.ts @@ -0,0 +1,17 @@ +import { Extension } from "@tiptap/core"; + +declare module "@tiptap/core" { + interface Storage { + shared: Record; + } +} + +const SharedStorage = Extension.create({ + name: "shared", + + addStorage() { + return {}; + }, +}); + +export { SharedStorage }; diff --git a/packages/editor-ext/src/lib/subpages/subpages.ts b/packages/editor-ext/src/lib/subpages/subpages.ts index 59eb9896..617f43ce 100644 --- a/packages/editor-ext/src/lib/subpages/subpages.ts +++ b/packages/editor-ext/src/lib/subpages/subpages.ts @@ -44,7 +44,7 @@ export const Subpages = Node.create({ mergeAttributes( { "data-type": this.name }, this.options.HTMLAttributes, - HTMLAttributes, + HTMLAttributes ), ]; }, @@ -63,6 +63,9 @@ export const Subpages = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, }); diff --git a/packages/editor-ext/src/lib/table/cell.ts b/packages/editor-ext/src/lib/table/cell.ts index 63df7dcf..2f693573 100644 --- a/packages/editor-ext/src/lib/table/cell.ts +++ b/packages/editor-ext/src/lib/table/cell.ts @@ -1,4 +1,4 @@ -import { TableCell as TiptapTableCell } from "@tiptap/extension-table-cell"; +import { TableCell as TiptapTableCell } from "@tiptap/extension-table"; export const TableCell = TiptapTableCell.extend({ name: "tableCell", diff --git a/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts b/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts index b4ac2950..1ad57ec1 100644 --- a/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts +++ b/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts @@ -1,7 +1,12 @@ import { Editor, Extension } from "@tiptap/core"; import { PluginKey, Plugin, PluginSpec } from "@tiptap/pm/state"; import { EditorProps, EditorView } from "@tiptap/pm/view"; -import { DraggingDOMs, getDndRelatedDOMs, getHoveringCell, HoveringCellInfo } from "./utils"; +import { + DraggingDOMs, + getDndRelatedDOMs, + getHoveringCell, + HoveringCellInfo, +} from "./utils"; import { getDragOverColumn, getDragOverRow } from "./calc-drag-over"; import { moveColumn, moveRow } from "../utils"; import { PreviewController } from "./preview/preview-controller"; @@ -10,268 +15,302 @@ import { DragHandleController } from "./handle/drag-handle-controller"; import { EmptyImageController } from "./handle/empty-image-controller"; import { AutoScrollController } from "./auto-scroll-controller"; -export const TableDndKey = new PluginKey('table-drag-and-drop') +export const TableDndKey = new PluginKey("table-drag-and-drop"); class TableDragHandlePluginSpec implements PluginSpec { - key = TableDndKey - props: EditorProps> + key = TableDndKey; + props: EditorProps>; - private _colDragHandle: HTMLElement; - private _rowDragHandle: HTMLElement; - private _hoveringCell?: HoveringCellInfo; - private _disposables: (() => void)[] = []; - private _draggingCoords: { x: number; y: number } = { x: 0, y: 0 }; - private _dragging = false; - private _draggingDirection: 'col' | 'row' = 'col'; - private _draggingIndex = -1; - private _droppingIndex = -1; - private _draggingDOMs?: DraggingDOMs | undefined - private _startCoords: { x: number; y: number } = { x: 0, y: 0 }; - private _previewController: PreviewController; - private _dropIndicatorController: DropIndicatorController; - private _dragHandleController: DragHandleController; - private _emptyImageController: EmptyImageController; - private _autoScrollController: AutoScrollController; + private _colDragHandle: HTMLElement; + private _rowDragHandle: HTMLElement; + private _hoveringCell?: HoveringCellInfo; + private _disposables: (() => void)[] = []; + private _draggingCoords: { x: number; y: number } = { x: 0, y: 0 }; + private _dragging = false; + private _draggingDirection: "col" | "row" = "col"; + private _draggingIndex = -1; + private _droppingIndex = -1; + private _draggingDOMs?: DraggingDOMs | undefined; + private _startCoords: { x: number; y: number } = { x: 0, y: 0 }; + private _previewController: PreviewController; + private _dropIndicatorController: DropIndicatorController; + private _dragHandleController: DragHandleController; + private _emptyImageController: EmptyImageController; + private _autoScrollController: AutoScrollController; - constructor(public editor: Editor) { - this.props = { - handleDOMEvents: { - pointerover: this._pointerOver, - } - } + constructor(public editor: Editor) { + this.props = { + handleDOMEvents: { + pointerover: this._pointerOver, + }, + }; - this._dragHandleController = new DragHandleController(); - this._colDragHandle = this._dragHandleController.colDragHandle; - this._rowDragHandle = this._dragHandleController.rowDragHandle; + this._dragHandleController = new DragHandleController(); + this._colDragHandle = this._dragHandleController.colDragHandle; + this._rowDragHandle = this._dragHandleController.rowDragHandle; - this._previewController = new PreviewController(); - this._dropIndicatorController = new DropIndicatorController(); - this._emptyImageController = new EmptyImageController(); + this._previewController = new PreviewController(); + this._dropIndicatorController = new DropIndicatorController(); + this._emptyImageController = new EmptyImageController(); - this._autoScrollController = new AutoScrollController(); + this._autoScrollController = new AutoScrollController(); - this._bindDragEvents(); + this._bindDragEvents(); + } + + view = () => { + const wrapper = this.editor.options.element; + //@ts-ignore + wrapper.appendChild(this._colDragHandle); + //@ts-ignore + wrapper.appendChild(this._rowDragHandle); + //@ts-ignore + wrapper.appendChild(this._previewController.previewRoot); + //@ts-ignore + wrapper.appendChild(this._dropIndicatorController.dropIndicatorRoot); + + return { + update: this.update, + destroy: this.destroy, + }; + }; + + update = () => {}; + + destroy = () => { + if (!this.editor.isDestroyed) return; + this._dragHandleController.destroy(); + this._emptyImageController.destroy(); + this._previewController.destroy(); + this._dropIndicatorController.destroy(); + this._autoScrollController.stop(); + + this._disposables.forEach((disposable) => disposable()); + }; + + private _pointerOver = (view: EditorView, event: PointerEvent) => { + if (this._dragging) return; + + // Don't show drag handles in readonly mode + if (!this.editor.isEditable) { + this._dragHandleController.hide(); + return; } - view = () => { - const wrapper = this.editor.options.element; - wrapper.appendChild(this._colDragHandle) - wrapper.appendChild(this._rowDragHandle) - wrapper.appendChild(this._previewController.previewRoot) - wrapper.appendChild(this._dropIndicatorController.dropIndicatorRoot) + const hoveringCell = getHoveringCell(view, event); + this._hoveringCell = hoveringCell; + if (!hoveringCell) { + this._dragHandleController.hide(); + } else { + this._dragHandleController.show(this.editor, hoveringCell); + } + }; - return { - update: this.update, - destroy: this.destroy, - } + private _onDragColStart = (event: DragEvent) => { + this._onDragStart(event, "col"); + }; + + private _onDraggingCol = (event: DragEvent) => { + const draggingDOMs = this._draggingDOMs; + if (!draggingDOMs) return; + + this._draggingCoords = { x: event.clientX, y: event.clientY }; + this._previewController.onDragging( + draggingDOMs, + this._draggingCoords.x, + this._draggingCoords.y, + "col", + ); + + this._autoScrollController.checkXAutoScroll(event.clientX, draggingDOMs); + + const direction = + this._startCoords.x > this._draggingCoords.x ? "left" : "right"; + const dragOverColumn = getDragOverColumn( + draggingDOMs.table, + this._draggingCoords.x, + ); + if (!dragOverColumn) return; + + const [col, index] = dragOverColumn; + this._droppingIndex = index; + this._dropIndicatorController.onDragging(col, direction, "col"); + }; + + private _onDragRowStart = (event: DragEvent) => { + this._onDragStart(event, "row"); + }; + + private _onDraggingRow = (event: DragEvent) => { + const draggingDOMs = this._draggingDOMs; + if (!draggingDOMs) return; + + this._draggingCoords = { x: event.clientX, y: event.clientY }; + this._previewController.onDragging( + draggingDOMs, + this._draggingCoords.x, + this._draggingCoords.y, + "row", + ); + + this._autoScrollController.checkYAutoScroll(event.clientY); + + const direction = + this._startCoords.y > this._draggingCoords.y ? "up" : "down"; + const dragOverRow = getDragOverRow( + draggingDOMs.table, + this._draggingCoords.y, + ); + if (!dragOverRow) return; + + const [row, index] = dragOverRow; + this._droppingIndex = index; + this._dropIndicatorController.onDragging(row, direction, "row"); + }; + + private _onDragEnd = () => { + this._dragging = false; + this._draggingIndex = -1; + this._droppingIndex = -1; + this._startCoords = { x: 0, y: 0 }; + this._autoScrollController.stop(); + this._dropIndicatorController.onDragEnd(); + this._previewController.onDragEnd(); + }; + + private _bindDragEvents = () => { + this._colDragHandle.addEventListener("dragstart", this._onDragColStart); + this._disposables.push(() => { + this._colDragHandle.removeEventListener( + "dragstart", + this._onDragColStart, + ); + }); + + this._colDragHandle.addEventListener("dragend", this._onDragEnd); + this._disposables.push(() => { + this._colDragHandle.removeEventListener("dragend", this._onDragEnd); + }); + + this._rowDragHandle.addEventListener("dragstart", this._onDragRowStart); + this._disposables.push(() => { + this._rowDragHandle.removeEventListener( + "dragstart", + this._onDragRowStart, + ); + }); + + this._rowDragHandle.addEventListener("dragend", this._onDragEnd); + this._disposables.push(() => { + this._rowDragHandle.removeEventListener("dragend", this._onDragEnd); + }); + + const ownerDocument = this.editor.view.dom?.ownerDocument; + if (ownerDocument) { + // To make `drop` event work, we need to prevent the default behavior of the + // `dragover` event for drop zone. Here we set the whole document as the + // drop zone so that even the mouse moves outside the editor, the `drop` + // event will still be triggered. + ownerDocument.addEventListener("drop", this._onDrop); + ownerDocument.addEventListener("dragover", this._onDrag); + this._disposables.push(() => { + ownerDocument.removeEventListener("drop", this._onDrop); + ownerDocument.removeEventListener("dragover", this._onDrag); + }); + } + }; + + private _onDragStart = (event: DragEvent, type: "col" | "row") => { + const dataTransfer = event.dataTransfer; + if (dataTransfer) { + dataTransfer.effectAllowed = "move"; + this._emptyImageController.hideDragImage(dataTransfer); + } + this._dragging = true; + this._draggingDirection = type; + this._startCoords = { x: event.clientX, y: event.clientY }; + const draggingIndex = + (type === "col" + ? this._hoveringCell?.colIndex + : this._hoveringCell?.rowIndex) ?? 0; + + this._draggingIndex = draggingIndex; + + const relatedDoms = getDndRelatedDOMs( + this.editor.view, + this._hoveringCell?.cellPos, + draggingIndex, + type, + ); + this._draggingDOMs = relatedDoms; + + const index = + type === "col" + ? this._hoveringCell?.colIndex + : this._hoveringCell?.rowIndex; + + this._previewController.onDragStart(relatedDoms, index, type); + this._dropIndicatorController.onDragStart(relatedDoms, type); + }; + + private _onDrag = (event: DragEvent) => { + event.preventDefault(); + if (!this._dragging) return; + if (this._draggingDirection === "col") { + this._onDraggingCol(event); + } else { + this._onDraggingRow(event); + } + }; + + private _onDrop = () => { + if (!this._dragging) return; + const direction = this._draggingDirection; + const from = this._draggingIndex; + const to = this._droppingIndex; + const tr = this.editor.state.tr; + const pos = this.editor.state.selection.from; + + if (direction === "col") { + const canMove = moveColumn({ + tr, + originIndex: from, + targetIndex: to, + select: true, + pos, + }); + if (canMove) { + this.editor.view.dispatch(tr); + } + + return; } - update = () => {} + if (direction === "row") { + const canMove = moveRow({ + tr, + originIndex: from, + targetIndex: to, + select: true, + pos, + }); + if (canMove) { + this.editor.view.dispatch(tr); + } - destroy = () => { - if (!this.editor.isDestroyed) return; - this._dragHandleController.destroy(); - this._emptyImageController.destroy(); - this._previewController.destroy(); - this._dropIndicatorController.destroy(); - this._autoScrollController.stop(); - - this._disposables.forEach(disposable => disposable()); - } - - private _pointerOver = (view: EditorView, event: PointerEvent) => { - if (this._dragging) return; - - // Don't show drag handles in readonly mode - if (!this.editor.isEditable) { - this._dragHandleController.hide(); - return; - } - - const hoveringCell = getHoveringCell(view, event) - this._hoveringCell = hoveringCell; - if (!hoveringCell) { - this._dragHandleController.hide(); - } else { - this._dragHandleController.show(this.editor, hoveringCell); - } - } - - private _onDragColStart = (event: DragEvent) => { - this._onDragStart(event, 'col'); - } - - private _onDraggingCol = (event: DragEvent) => { - const draggingDOMs = this._draggingDOMs; - if (!draggingDOMs) return; - - this._draggingCoords = { x: event.clientX, y: event.clientY }; - this._previewController.onDragging(draggingDOMs, this._draggingCoords.x, this._draggingCoords.y, 'col'); - - this._autoScrollController.checkXAutoScroll(event.clientX, draggingDOMs); - - const direction = this._startCoords.x > this._draggingCoords.x ? 'left' : 'right'; - const dragOverColumn = getDragOverColumn(draggingDOMs.table, this._draggingCoords.x); - if (!dragOverColumn) return; - - const [col, index] = dragOverColumn; - this._droppingIndex = index; - this._dropIndicatorController.onDragging(col, direction, 'col'); - } - - private _onDragRowStart = (event: DragEvent) => { - this._onDragStart(event, 'row'); - } - - private _onDraggingRow = (event: DragEvent) => { - const draggingDOMs = this._draggingDOMs; - if (!draggingDOMs) return; - - this._draggingCoords = { x: event.clientX, y: event.clientY }; - this._previewController.onDragging(draggingDOMs, this._draggingCoords.x, this._draggingCoords.y, 'row'); - - this._autoScrollController.checkYAutoScroll(event.clientY); - - const direction = this._startCoords.y > this._draggingCoords.y ? 'up' : 'down'; - const dragOverRow = getDragOverRow(draggingDOMs.table, this._draggingCoords.y); - if (!dragOverRow) return; - - const [row, index] = dragOverRow; - this._droppingIndex = index; - this._dropIndicatorController.onDragging(row, direction, 'row'); - } - - private _onDragEnd = () => { - this._dragging = false; - this._draggingIndex = -1; - this._droppingIndex = -1; - this._startCoords = { x: 0, y: 0 }; - this._autoScrollController.stop(); - this._dropIndicatorController.onDragEnd(); - this._previewController.onDragEnd(); - } - - private _bindDragEvents = () => { - this._colDragHandle.addEventListener('dragstart', this._onDragColStart); - this._disposables.push(() => { - this._colDragHandle.removeEventListener('dragstart', this._onDragColStart); - }) - - this._colDragHandle.addEventListener('dragend', this._onDragEnd); - this._disposables.push(() => { - this._colDragHandle.removeEventListener('dragend', this._onDragEnd); - }) - - this._rowDragHandle.addEventListener('dragstart', this._onDragRowStart); - this._disposables.push(() => { - this._rowDragHandle.removeEventListener('dragstart', this._onDragRowStart); - }) - - this._rowDragHandle.addEventListener('dragend', this._onDragEnd); - this._disposables.push(() => { - this._rowDragHandle.removeEventListener('dragend', this._onDragEnd); - }) - - const ownerDocument = this.editor.view.dom?.ownerDocument - if (ownerDocument) { - // To make `drop` event work, we need to prevent the default behavior of the - // `dragover` event for drop zone. Here we set the whole document as the - // drop zone so that even the mouse moves outside the editor, the `drop` - // event will still be triggered. - ownerDocument.addEventListener('drop', this._onDrop); - ownerDocument.addEventListener('dragover', this._onDrag); - this._disposables.push(() => { - ownerDocument.removeEventListener('drop', this._onDrop); - ownerDocument.removeEventListener('dragover', this._onDrag); - }); - } - } - - private _onDragStart = (event: DragEvent, type: 'col' | 'row') => { - const dataTransfer = event.dataTransfer; - if (dataTransfer) { - dataTransfer.effectAllowed = 'move'; - this._emptyImageController.hideDragImage(dataTransfer); - } - this._dragging = true; - this._draggingDirection = type; - this._startCoords = { x: event.clientX, y: event.clientY }; - const draggingIndex = (type === 'col' ? this._hoveringCell?.colIndex : this._hoveringCell?.rowIndex) ?? 0; - - this._draggingIndex = draggingIndex; - - const relatedDoms = getDndRelatedDOMs( - this.editor.view, - this._hoveringCell?.cellPos, - draggingIndex, - type - ) - this._draggingDOMs = relatedDoms; - - const index = type === 'col' ? this._hoveringCell?.colIndex : this._hoveringCell?.rowIndex; - - this._previewController.onDragStart(relatedDoms, index, type); - this._dropIndicatorController.onDragStart(relatedDoms, type); - } - - private _onDrag = (event: DragEvent) => { - event.preventDefault() - if (!this._dragging) return; - if (this._draggingDirection === 'col') { - this._onDraggingCol(event); - } else { - this._onDraggingRow(event); - } - } - - private _onDrop = () => { - if (!this._dragging) return; - const direction = this._draggingDirection; - const from = this._draggingIndex; - const to = this._droppingIndex; - const tr = this.editor.state.tr; - const pos = this.editor.state.selection.from; - - if (direction === 'col') { - const canMove = moveColumn({ - tr, - originIndex: from, - targetIndex: to, - select: true, - pos, - }) - if (canMove) { - this.editor.view.dispatch(tr); - } - - return; - } - - if (direction === 'row') { - const canMove = moveRow({ - tr, - originIndex: from, - targetIndex: to, - select: true, - pos, - }) - if (canMove) { - this.editor.view.dispatch(tr); - } - - return; - } + return; } + }; } export const TableDndExtension = Extension.create({ - name: 'table-drag-and-drop', - addProseMirrorPlugins() { - const editor = this.editor + name: "table-drag-and-drop", + addProseMirrorPlugins() { + const editor = this.editor; - const dragHandlePluginSpec = new TableDragHandlePluginSpec(editor) - const dragHandlePlugin = new Plugin(dragHandlePluginSpec) + const dragHandlePluginSpec = new TableDragHandlePluginSpec(editor); + const dragHandlePlugin = new Plugin(dragHandlePluginSpec); - return [dragHandlePlugin] - } -}) + return [dragHandlePlugin]; + }, +}); diff --git a/packages/editor-ext/src/lib/table/header.ts b/packages/editor-ext/src/lib/table/header.ts index 501f089d..77ab02f1 100644 --- a/packages/editor-ext/src/lib/table/header.ts +++ b/packages/editor-ext/src/lib/table/header.ts @@ -1,4 +1,4 @@ -import { TableHeader as TiptapTableHeader } from "@tiptap/extension-table-header"; +import { TableHeader as TiptapTableHeader } from "@tiptap/extension-table"; export const TableHeader = TiptapTableHeader.extend({ name: "tableHeader", diff --git a/packages/editor-ext/src/lib/table/row.ts b/packages/editor-ext/src/lib/table/row.ts index 3aa67dcd..7839afdf 100644 --- a/packages/editor-ext/src/lib/table/row.ts +++ b/packages/editor-ext/src/lib/table/row.ts @@ -1,6 +1,5 @@ -import TiptapTableRow from "@tiptap/extension-table-row"; +import { TableRow as TiptapTableRow } from "@tiptap/extension-table"; export const TableRow = TiptapTableRow.extend({ - allowGapCursor: false, content: "(tableCell | tableHeader)*", }); diff --git a/packages/editor-ext/src/lib/table/table.ts b/packages/editor-ext/src/lib/table/table.ts index 87053832..f1436c28 100644 --- a/packages/editor-ext/src/lib/table/table.ts +++ b/packages/editor-ext/src/lib/table/table.ts @@ -1,4 +1,4 @@ -import Table from "@tiptap/extension-table"; +import { Table } from "@tiptap/extension-table"; import { Editor } from "@tiptap/core"; import { DOMOutputSpec } from "@tiptap/pm/model"; diff --git a/packages/editor-ext/src/lib/unique-id/helpers/findDuplicates.ts b/packages/editor-ext/src/lib/unique-id/helpers/findDuplicates.ts deleted file mode 100644 index d193e8b3..00000000 --- a/packages/editor-ext/src/lib/unique-id/helpers/findDuplicates.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { removeDuplicates } from './removeDuplicates.js' - -/** - * Returns a list of duplicated items within an array. - */ -export function findDuplicates(items: any[]): any[] { - const filtered = items.filter((el, index) => items.indexOf(el) !== index) - const duplicates = removeDuplicates(filtered) - - return duplicates -} diff --git a/packages/editor-ext/src/lib/unique-id/helpers/removeDuplicates.ts b/packages/editor-ext/src/lib/unique-id/helpers/removeDuplicates.ts deleted file mode 100644 index 2bae38fd..00000000 --- a/packages/editor-ext/src/lib/unique-id/helpers/removeDuplicates.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Removes duplicated values within an array. - * Supports numbers, strings and objects. - */ -export function removeDuplicates(array: T[], by = JSON.stringify): T[] { - const seen: Record = {} - - return array.filter(item => { - const key = by(item) - - return Object.prototype.hasOwnProperty.call(seen, key) - ? false - : (seen[key] = true) - }) -} diff --git a/packages/editor-ext/src/lib/unique-id/unique-id.ts b/packages/editor-ext/src/lib/unique-id/unique-id.ts index 6ecf15f0..8436cbd2 100644 --- a/packages/editor-ext/src/lib/unique-id/unique-id.ts +++ b/packages/editor-ext/src/lib/unique-id/unique-id.ts @@ -1,386 +1,11 @@ -import { - combineTransactionSteps, - Extension, - findChildren, - findChildrenInRange, - getChangedRanges, -} from "@tiptap/core"; -import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; -import { Fragment, Slice } from "@tiptap/pm/model"; -import type { Transaction } from "@tiptap/pm/state"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; - -import { findDuplicates } from "./helpers/findDuplicates.js"; import { generateNodeId } from "../utils"; +import { UniqueID as TiptapUniqueID } from "@tiptap/extension-unique-id"; -export type UniqueIDGenerationContext = { - node: ProseMirrorNode; - pos: number; -}; - -export interface UniqueIDOptions { - /** - * The name of the attribute to add the unique ID to. - * @default "id" - */ - attributeName: string; - /** - * The types of nodes to add unique IDs to. - * @default [] - */ - types: string[]; - /** - * The function that generates the unique ID. By default, a UUID v4 is - * generated. However, you can provide your own function to generate the - * unique ID based on the node type and the position. - */ - generateID: (ctx: UniqueIDGenerationContext) => any; - /** - * Ignore some mutations, for example applied from other users through the collaboration plugin. - * - * @default null - */ - filterTransaction: ((transaction: Transaction) => boolean) | null; - /** - * Whether to update the document by adding unique IDs to the nodes. Set this - * property to `false` if the document is in `readonly` mode, is immutable, or - * you don't want it to be modified. - * - * @default true - */ - updateDocument: boolean; -} - -export const UniqueID = Extension.create({ - name: "uniqueID", - - // we’ll set a very high priority to make sure this runs first - // and is compatible with `appendTransaction` hooks of other extensions - priority: 10000, - +export const UniqueID = TiptapUniqueID.extend({ addOptions() { return { - attributeName: "id", - types: [], + ...this.parent?.(), generateID: () => generateNodeId(), - filterTransaction: null, - updateDocument: true, }; }, - - addGlobalAttributes() { - return [ - { - types: this.options.types, - attributes: { - [this.options.attributeName]: { - default: null, - parseHTML: (element) => - element.getAttribute(`data-${this.options.attributeName}`), - renderHTML: (attributes) => { - if (!attributes[this.options.attributeName]) { - return {}; - } - - return { - [`data-${this.options.attributeName}`]: - attributes[this.options.attributeName], - }; - }, - }, - }, - }, - ]; - }, - - // check initial content for missing ids - onCreate() { - if (!this.options.updateDocument) { - return; - } - - const collaboration = this.editor.extensionManager.extensions.find( - (ext) => ext.name === "collaboration", - ); - const collaborationCursor = this.editor.extensionManager.extensions.find( - (ext) => ext.name === "collaborationCursor", - ); - - const collabExtensions = [collaboration, collaborationCursor].filter( - Boolean, - ); - const collab = collabExtensions.find((ext) => ext?.options?.provider); - const provider = collab?.options?.provider; - - const createIds = () => { - const { view, state } = this.editor; - const { tr, doc } = state; - const { types, attributeName, generateID } = this.options; - const nodesWithoutId = findChildren(doc, (node) => { - return ( - types.includes(node.type.name) && node.attrs[attributeName] === null - ); - }); - - nodesWithoutId.forEach(({ node, pos }) => { - tr.setNodeMarkup(pos, undefined, { - ...node.attrs, - [attributeName]: generateID({ node, pos }), - }); - }); - - tr.setMeta("addToHistory", false); - - view.dispatch(tr); - - if (provider) { - provider.off("synced", createIds); - } - }; - - /** - * We need to handle collaboration a bit different here - * because we can't automatically add IDs when the provider is not yet synced - * otherwise we end up with empty paragraphs - */ - if (collab) { - if (!provider) { - return createIds(); - } - - provider.on("synced", createIds); - } else { - return createIds(); - } - }, - - addProseMirrorPlugins() { - if (!this.options.updateDocument) { - return []; - } - - let dragSourceElement: Element | null = null; - let transformPasted = false; - - return [ - new Plugin({ - key: new PluginKey("uniqueID"), - - appendTransaction: (transactions, oldState, newState) => { - const hasDocChanges = - transactions.some((transaction) => transaction.docChanged) && - !oldState.doc.eq(newState.doc); - const filterTransactions = - this.options.filterTransaction && - transactions.some((tr) => !this.options.filterTransaction?.(tr)); - - const isCollabTransaction = transactions.find((tr) => - tr.getMeta("y-sync$"), - ); - - if (isCollabTransaction) { - return; - } - - if (!hasDocChanges || filterTransactions) { - return; - } - - const { tr } = newState; - - const { types, attributeName, generateID } = this.options; - const transform = combineTransactionSteps( - oldState.doc, - transactions as Transaction[], - ); - const { mapping } = transform; - - // get changed ranges based on the old state - const changes = getChangedRanges(transform); - - changes.forEach(({ newRange }) => { - const newNodes = findChildrenInRange( - newState.doc, - newRange, - (node) => { - return types.includes(node.type.name); - }, - ); - - const newIds = newNodes - .map(({ node }) => node.attrs[attributeName]) - .filter((id) => id !== null); - - newNodes.forEach(({ node, pos }, i) => { - // instead of checking `node.attrs[attributeName]` directly - // we look at the current state of the node within `tr.doc`. - // this helps to prevent adding new ids to the same node - // if the node changed multiple times within one transaction - const id = tr.doc.nodeAt(pos)?.attrs[attributeName]; - - if (id === null) { - tr.setNodeMarkup(pos, undefined, { - ...node.attrs, - [attributeName]: generateID({ node, pos }), - }); - - return; - } - - const nextNode = newNodes[i + 1]; - - if (nextNode && node.content.size === 0) { - tr.setNodeMarkup(nextNode.pos, undefined, { - ...nextNode.node.attrs, - [attributeName]: id, - }); - newIds[i + 1] = id; - - if (nextNode.node.attrs[attributeName]) { - return; - } - - const generatedId = generateID({ node, pos }); - - tr.setNodeMarkup(pos, undefined, { - ...node.attrs, - [attributeName]: generatedId, - }); - newIds[i] = generatedId; - - return tr; - } - - const duplicatedNewIds = findDuplicates(newIds); - - // check if the node doesn’t exist in the old state - const { deleted } = mapping.invert().mapResult(pos); - - const newNode = deleted && duplicatedNewIds.includes(id); - - if (newNode) { - tr.setNodeMarkup(pos, undefined, { - ...node.attrs, - [attributeName]: generateID({ node, pos }), - }); - } - }); - }); - - if (!tr.steps.length) { - return; - } - - // `tr.setNodeMarkup` resets the stored marks - // so we'll restore them if they exist - tr.setStoredMarks(newState.tr.storedMarks); - - // Mark this transaction as coming from UniqueID - // to prevent infinite loops with other extensions (e.g., TrailingNode) - tr.setMeta("__uniqueIDTransaction", true); - - return tr; - }, - - // we register a global drag handler to track the current drag source element - view(view) { - const handleDragstart = (event: DragEvent) => { - dragSourceElement = view.dom.parentElement?.contains( - event.target as Element, - ) - ? view.dom.parentElement - : null; - }; - - window.addEventListener("dragstart", handleDragstart); - - return { - destroy() { - window.removeEventListener("dragstart", handleDragstart); - }, - }; - }, - - props: { - // `handleDOMEvents` is called before `transformPasted` - // so we can do some checks before - handleDOMEvents: { - // only create new ids for dropped content - // or dropped content while holding `alt` - // or content is dragged from another editor - drop: (view, event) => { - if ( - dragSourceElement !== view.dom.parentElement || - event.dataTransfer?.effectAllowed === "copyMove" || - event.dataTransfer?.effectAllowed === "copy" - ) { - dragSourceElement = null; - transformPasted = true; - } - - return false; - }, - // always create new ids on pasted content - paste: () => { - transformPasted = true; - - return false; - }, - }, - - // we’ll remove ids for every pasted node - // so we can create a new one within `appendTransaction` - transformPasted: (slice) => { - if (!transformPasted) { - return slice; - } - - const { types, attributeName } = this.options; - const removeId = (fragment: Fragment): Fragment => { - const list: ProseMirrorNode[] = []; - - fragment.forEach((node) => { - // don’t touch text nodes - if (node.isText) { - list.push(node); - - return; - } - - // check for any other child nodes - if (!types.includes(node.type.name)) { - list.push(node.copy(removeId(node.content))); - - return; - } - - // remove id - const nodeWithoutId = node.type.create( - { - ...node.attrs, - [attributeName]: null, - }, - removeId(node.content), - node.marks, - ); - - list.push(nodeWithoutId); - }); - - return Fragment.from(list); - }; - - // reset check - transformPasted = false; - - return new Slice( - removeId(slice.content), - slice.openStart, - slice.openEnd, - ); - }, - }, - }), - ]; - }, }); diff --git a/packages/editor-ext/src/lib/utils.ts b/packages/editor-ext/src/lib/utils.ts index e4e7fda4..350ab3bb 100644 --- a/packages/editor-ext/src/lib/utils.ts +++ b/packages/editor-ext/src/lib/utils.ts @@ -1,6 +1,6 @@ -// @ts-nocheck import { Editor, findParentNode, isTextSelection } from "@tiptap/core"; -import { Selection, Transaction } from "@tiptap/pm/state"; +import { EditorState, Selection, Transaction } from "@tiptap/pm/state"; +import { EditorView } from "@tiptap/pm/view"; import { CellSelection, TableMap } from "@tiptap/pm/tables"; import { Node, ResolvedPos } from "@tiptap/pm/model"; import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url"; @@ -287,11 +287,7 @@ export const isColumnGripSelected = ({ const nodeDOM = view.nodeDOM(from) as HTMLElement; const node = nodeDOM || domAtPos; - if ( - !editor.isActive("table") || - !node || - isTableSelected(state.selection) - ) { + if (!editor.isActive("table") || !node || isTableSelected(state.selection)) { return false; } @@ -324,11 +320,7 @@ export const isRowGripSelected = ({ const nodeDOM = view.nodeDOM(from) as HTMLElement; const node = nodeDOM || domAtPos; - if ( - !editor.isActive(Table.name) || - !node || - isTableSelected(state.selection) - ) { + if (!editor.isActive("table") || !node || isTableSelected(state.selection)) { return false; } diff --git a/packages/editor-ext/src/lib/video/video-upload.ts b/packages/editor-ext/src/lib/video/video-upload.ts index 1e976ecc..404cf99e 100644 --- a/packages/editor-ext/src/lib/video/video-upload.ts +++ b/packages/editor-ext/src/lib/video/video-upload.ts @@ -1,132 +1,169 @@ -import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; -import { Decoration, DecorationSet } from "@tiptap/pm/view"; -import { - insertTrailingNode, - MediaUploadOptions, - UploadFn, -} from "../media-utils"; +import { MediaUploadOptions, UploadFn } from "../media-utils"; import { IAttachment } from "../types"; +import { generateNodeId } from "../utils"; +import { Node } from "@tiptap/pm/model"; +import { Command } from "@tiptap/core"; -const uploadKey = new PluginKey("video-upload"); +const findVideoNodeByPlaceholderId = ( + doc: Node, + placeholderId: string, +): { node: Node; pos: number } | null => { + let result: { node: Node; pos: number } | null = null; -export const VideoUploadPlugin = ({ - placeholderClass, -}: { - placeholderClass: string; -}) => - new Plugin({ - key: uploadKey, - state: { - init() { - return DecorationSet.empty; - }, - apply(tr, set) { - set = set.map(tr.mapping, tr.doc); - // See if the transaction adds or removes any placeholders - //@-ts-expect-error - not yet sure what the type I need here - const action = tr.getMeta(this); - if (action?.add) { - const { id, pos, src } = action.add; + doc.descendants((node, pos) => { + if (result) return false; - const placeholder = document.createElement("div"); - placeholder.setAttribute("class", "video-placeholder"); - const video = document.createElement("video"); - video.setAttribute("class", placeholderClass); - video.src = src; - placeholder.appendChild(video); - const deco = Decoration.widget(pos + 1, placeholder, { - id, - }); - set = set.add(tr.doc, [deco]); - } else if (action?.remove) { - set = set.remove( - set.find( - undefined, - undefined, - (spec) => spec.id == action.remove.id, - ), - ); - } - return set; - }, - }, - props: { - decorations(state) { - return this.getState(state); - }, - }, + if ( + node.type.name === "video" && + node.attrs.placeholder?.id === placeholderId + ) { + result = { node, pos }; + return false; + } + + return true; }); -function findPlaceholder(state: EditorState, id: {}) { - const decos = uploadKey.getState(state) as DecorationSet; - const found = decos.find(undefined, undefined, (spec) => spec.id == id); - return found.length ? found[0]?.from : null; -} + return result; +}; +const getVideoDimensions = ( + url: string, +): Promise< + { width: number; height: number; aspectRatio: number } | undefined +> => { + return new Promise< + { width: number; height: number; aspectRatio: number } | undefined + >((resolve) => { + const video = document.createElement("video"); -export const handleVideoUpload = + video.preload = "metadata"; + video.onloadedmetadata = () => { + const width = video.videoWidth; + const height = video.videoHeight; + const aspectRatio = height > 0 ? width / height : 1; + + resolve({ width, height, aspectRatio }); + }; + video.onerror = () => { + resolve(undefined); + }; + video.src = url; + }); +}; +const handleVideoUpload = ({ validateFn, onUpload }: MediaUploadOptions): UploadFn => - async (file, view, pos, pageId) => { - // check if the file is an image + async (file, editor, pos, pageId) => { + // check if the file is valid const validated = validateFn?.(file); // @ts-ignore if (!validated) return; - // A fresh object to act as the ID for this upload - const id = {}; - // Replace the selection with a placeholder + const objectUrl = URL.createObjectURL(file); + const videoDimensions = await getVideoDimensions(objectUrl); + const placeholderId = generateNodeId(); + const aspectRatio = videoDimensions.aspectRatio; - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - const tr = view.state.tr; - if (!tr.selection.empty) tr.deleteSelection(); + let placeholderInserted = false; - tr.setMeta(uploadKey, { - add: { - id, - pos, - src: reader.result, - }, - }); + editor.storage.shared.videoPreviews = + editor.storage.shared.videoPreviews || {}; + editor.storage.shared.videoPreviews[placeholderId] = objectUrl; - insertTrailingNode(tr, pos, view); - view.dispatch(tr); + const insertPlaceholder = (): Command => { + return ({ tr, state }) => { + const initialPlaceholderNode = state.schema.nodes.video?.create({ + placeholder: { + id: placeholderId, + name: file.name, + }, + aspectRatio, + }); + + if (!initialPlaceholderNode) return false; + + const { parent } = tr.doc.resolve(pos); + const isEmptyTextBlock = parent.isTextblock && !parent.childCount; + + if (isEmptyTextBlock) { + // Replace e.g. empty paragraph with the video + tr.replaceRangeWith(pos - 1, pos + 1, initialPlaceholderNode); + } else { + tr.insert(pos, initialPlaceholderNode); + } + + return true; + }; }; + const replacePlaceholderWithVideo = (attachment: IAttachment): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findVideoNodeByPlaceholderId(tr.doc, placeholderId) || {}; - await onUpload(file, pageId).then( - (attachment: IAttachment) => { - const { schema } = view.state; + // If the placeholder is not found or attachment is missing, abort the process + if (currentPos === null || !attachment) return; - const pos = findPlaceholder(view.state, id); - - // If the content around the placeholder has been deleted, drop - // the image - if (pos == null) return; - - // Otherwise, insert it at the placeholder's position, and remove - // the placeholder - - if (!attachment) return; - - const node = schema.nodes.video?.create({ + // Update the placeholder node with the actual video data + tr.setNodeMarkup(currentPos, undefined, { src: `/api/files/${attachment.id}/${attachment.fileName}`, attachmentId: attachment.id, title: attachment.fileName, size: attachment.fileSize, + aspectRatio, }); - if (!node) return; - const transaction = view.state.tr - .replaceWith(pos, pos, node) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - () => { - // Deletes the image placeholder on error - const transaction = view.state.tr - .delete(pos, pos) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - ); + return true; + }; + }; + const removePlaceholder = (): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findVideoNodeByPlaceholderId(tr.doc, placeholderId) || {}; + + if (currentPos === null) return false; + + tr.delete(currentPos, currentPos + 2); + + return true; + }; + }; + + // Only show the placeholder if the upload takes more than 250ms + const insertPlaceholderTimeout = setTimeout(() => { + editor.commands.command(insertPlaceholder()); + placeholderInserted = true; + }, 250); + const disposePreviewFile = () => { + URL.revokeObjectURL(objectUrl); + + if (editor.storage.shared.videoPreviews) { + delete editor.storage.shared.videoPreviews[placeholderId]; + } + }; + + try { + const attachment: IAttachment = await onUpload(file, pageId); + + clearTimeout(insertPlaceholderTimeout); + + if (placeholderInserted) { + setTimeout(() => { + editor.commands.command(replacePlaceholderWithVideo(attachment)); + disposePreviewFile(); + }, 100); + } else { + editor + .chain() + .command(insertPlaceholder()) + .command(replacePlaceholderWithVideo(attachment)) + .run(); + disposePreviewFile(); + } + } catch (error) { + clearTimeout(insertPlaceholderTimeout); + + editor.commands.command(removePlaceholder()); + disposePreviewFile(); + } }; + +export { handleVideoUpload }; diff --git a/packages/editor-ext/src/lib/video/video.ts b/packages/editor-ext/src/lib/video/video.ts index 6f28e7c0..31c68f89 100644 --- a/packages/editor-ext/src/lib/video/video.ts +++ b/packages/editor-ext/src/lib/video/video.ts @@ -1,6 +1,5 @@ import { ReactNodeViewRenderer } from "@tiptap/react"; -import { VideoUploadPlugin } from "./video-upload"; -import { mergeAttributes, Range, Node, nodeInputRule } from "@tiptap/core"; +import { Range, Node } from "@tiptap/core"; export interface VideoOptions { view: any; @@ -8,11 +7,15 @@ export interface VideoOptions { } export interface VideoAttributes { src?: string; - title?: string; align?: string; attachmentId?: string; size?: number; width?: number; + aspectRatio?: number; + placeholder?: { + id: string; + name: string; + }; } declare module "@tiptap/core" { @@ -81,15 +84,26 @@ export const TiptapVideo = Node.create({ "data-align": attributes.align, }), }, + aspectRatio: { + default: null, + parseHTML: (element) => element.getAttribute("data-aspect-ratio"), + renderHTML: (attributes: VideoAttributes) => ({ + "data-aspect-ratio": attributes.aspectRatio, + }), + }, + placeholder: { + default: null, + rendered: false, + }, }; }, parseHTML() { return [ { - tag: 'video', + tag: "video", }, - ] + ]; }, renderHTML({ HTMLAttributes }) { @@ -126,14 +140,9 @@ export const TiptapVideo = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, - - addProseMirrorPlugins() { - return [ - VideoUploadPlugin({ - placeholderClass: "video-upload", - }), - ]; - }, }); diff --git a/packages/editor-ext/tsconfig.json b/packages/editor-ext/tsconfig.json index efbfcd61..974fea06 100644 --- a/packages/editor-ext/tsconfig.json +++ b/packages/editor-ext/tsconfig.json @@ -8,6 +8,7 @@ "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "ES2022", + "jsx": "react-jsx", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbc34840..b5890b0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,8 +22,8 @@ importers: specifier: ^7.1.0 version: 7.1.0 '@casl/ability': - specifier: ^6.7.5 - version: 6.7.5 + specifier: 6.8.0 + version: 6.8.0 '@docmost/editor-ext': specifier: workspace:* version: link:packages/editor-ext @@ -31,17 +31,17 @@ importers: specifier: ^1.7.3 version: 1.7.3 '@hocuspocus/extension-redis': - specifier: ^2.15.3 - version: 2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + specifier: 3.4.3 + version: 3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) '@hocuspocus/provider': - specifier: ^2.15.3 - version: 2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + specifier: 3.4.3 + version: 3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) '@hocuspocus/server': - specifier: ^2.15.3 - version: 2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + specifier: 3.4.3 + version: 3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) '@hocuspocus/transformer': - specifier: ^2.15.3 - version: 2.15.3(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27) + specifier: 3.4.3 + version: 3.4.3(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29) '@joplin/turndown': specifier: ^4.0.74 version: 4.0.74 @@ -52,107 +52,86 @@ importers: specifier: 1.1.0 version: 1.1.0 '@tiptap/core': - specifier: 2.27.1 - version: 2.27.1(@tiptap/pm@2.27.1) + specifier: 3.17.1 + version: 3.17.1(@tiptap/pm@3.17.1) '@tiptap/extension-code-block': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-code-block-lowlight': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/extension-code-block@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(highlight.js@11.11.1)(lowlight@3.3.0) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-collaboration': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) - '@tiptap/extension-collaboration-cursor': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29) + '@tiptap/extension-collaboration-caret': + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29)) '@tiptap/extension-color': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/extension-text-style@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))) + specifier: 3.17.1 + version: 3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))) '@tiptap/extension-document': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-heading': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-highlight': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-history': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + specifier: 3.17.1 + version: 3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) '@tiptap/extension-image': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-link': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-list-item': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-list-keymap': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list': + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-placeholder': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + specifier: 3.17.1 + version: 3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) '@tiptap/extension-subscript': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-superscript': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-table': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-table-cell': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-table-header': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-table-row': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-task-item': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-task-list': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-text': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-text-align': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-text-style': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-typography': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-underline': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-unique-id': + specifier: ^3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-youtube': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/html': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0) '@tiptap/pm': - specifier: 2.27.1 - version: 2.27.1 + specifier: 3.17.1 + version: 3.17.1 '@tiptap/react': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 3.17.1 + version: 3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tiptap/starter-kit': - specifier: 2.27.1 - version: 2.27.1 + specifier: 3.17.1 + version: 3.17.1 '@tiptap/suggestion': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@types/qrcode': specifier: ^1.5.5 version: 1.5.5 @@ -171,6 +150,12 @@ importers: fractional-indexing-jittered: specifier: ^1.0.0 version: 1.0.0 + highlight.js: + specifier: ^11.11.1 + version: 11.11.1 + image-dimensions: + specifier: ^2.5.0 + version: 2.5.0 ioredis: specifier: ^5.4.1 version: 5.4.1 @@ -194,13 +179,13 @@ importers: version: 11.1.0 y-indexeddb: specifier: ^9.0.12 - version: 9.0.12(yjs@13.6.27) + version: 9.0.12(yjs@13.6.29) y-prosemirror: specifier: 1.3.7 - version: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + version: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) yjs: - specifier: ^13.6.27 - version: 13.6.27 + specifier: ^13.6.29 + version: 13.6.29 devDependencies: '@nx/js': specifier: 20.4.5 @@ -208,6 +193,9 @@ importers: '@types/bytes': specifier: ^3.1.5 version: 3.1.5 + '@types/turndown': + specifier: ^5.0.6 + version: 5.0.6 '@types/uuid': specifier: ^10.0.0 version: 10.0.0 @@ -225,7 +213,7 @@ importers: dependencies: '@casl/react': specifier: ^4.0.0 - version: 4.0.0(@casl/ability@6.7.5)(react@18.3.1) + version: 4.0.0(@casl/ability@6.8.0)(react@18.3.1) '@docmost/editor-ext': specifier: workspace:* version: link:../../packages/editor-ext @@ -265,9 +253,6 @@ importers: '@tanstack/react-query': specifier: ^5.90.17 version: 5.90.17(react@18.3.1) - '@tiptap/extension-character-count': - specifier: ^2.27.1 - version: 2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) alfaaz: specifier: ^1.1.0 version: 1.1.0 @@ -355,9 +340,6 @@ importers: socket.io-client: specifier: ^4.8.3 version: 4.8.3 - tippy.js: - specifier: ^6.3.7 - version: 6.3.7 tiptap-extension-global-drag-handle: specifier: ^0.1.18 version: 0.1.18 @@ -463,7 +445,7 @@ importers: version: 8.3.0 '@keyv/redis': specifier: ^5.1.5 - version: 5.1.5(keyv@5.5.5) + version: 5.1.6(keyv@5.6.0) '@langchain/core': specifier: 1.1.13 version: 1.1.13(@opentelemetry/api@1.9.0)(openai@6.2.0(ws@8.19.0)(zod@4.3.5)) @@ -478,7 +460,7 @@ importers: version: 11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(bullmq@5.65.0) '@nestjs/cache-manager': specifier: ^3.1.0 - version: 3.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(cache-manager@7.2.8)(keyv@5.5.5)(rxjs@7.8.2) + version: 3.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2) '@nestjs/common': specifier: ^11.1.11 version: 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -572,6 +554,9 @@ importers: kysely-migration-cli: specifier: ^0.4.2 version: 0.4.2 + kysely-postgres-js: + specifier: ^3.0.0 + version: 3.0.0(kysely@0.28.2)(postgres@3.4.8) ldapts: specifier: ^7.4.0 version: 7.4.0 @@ -611,9 +596,6 @@ importers: pdfjs-dist: specifier: ^5.4.394 version: 5.4.394 - pg: - specifier: ^8.16.3 - version: 8.16.3 pg-tsquery: specifier: ^8.4.2 version: 8.4.2 @@ -626,6 +608,9 @@ importers: pino-pretty: specifier: ^13.1.3 version: 13.1.3 + postgres: + specifier: ^3.4.8 + version: 3.4.8 postmark: specifier: ^4.0.5 version: 4.0.5 @@ -702,9 +687,6 @@ importers: '@types/passport-jwt': specifier: ^4.0.1 version: 4.0.1 - '@types/pg': - specifier: ^8.11.11 - version: 8.11.11 '@types/supertest': specifier: ^6.0.2 version: 6.0.2 @@ -1877,8 +1859,8 @@ packages: '@cacheable/utils@2.3.3': resolution: {integrity: sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A==} - '@casl/ability@6.7.5': - resolution: {integrity: sha512-NaOHPi9JMn8Kesh+GRkjNKAYkl4q8qMFAlqw7w2yrE+cBQZSbV9GkBGKvgzs3CdzEc5Yl1cn3JwDxxbBN5gjog==} + '@casl/ability@6.8.0': + resolution: {integrity: sha512-Ipt4mzI4gSgnomFdaPjaLgY2MWuXqAEZLrU6qqWBB7khGiBBuuEp6ytYDnq09bRXqcjaeeHiaCvCGFbBA2SpvA==} '@casl/react@4.0.0': resolution: {integrity: sha512-ovmI4JfNw7TfVVV+XhAJ//gXgMEkkPJU6YBWFVFZGa8Oikdh8Qxr/sdXcqj71QWEHAGN7aSKMtBE0MZylPUVsg==} @@ -2407,32 +2389,32 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@hocuspocus/common@2.15.3': - resolution: {integrity: sha512-Rzh1HF0a2o/tf90A3w2XNdXd9Ym3aQzMDfD3lAUONCX9B9QOdqdyiORrj6M25QEaJrEIbXFy8LtAFcL0wRdWzA==} + '@hocuspocus/common@3.4.3': + resolution: {integrity: sha512-wnBBO9sWcVAoUPEXN1qO+zk3HaEF9VTemxB6kjuuH6e1dHnD0v12m4P4X1wiZVhmMIX/PMl/fu3MGtYWQJz8gA==} - '@hocuspocus/extension-redis@2.15.3': - resolution: {integrity: sha512-gKeiiuQcAoRYb+QK9vyIczRrjNy8NW6ky+oyVv7raMcaizfFxeWP3TaAHPyC2pjGKfXsqN2m3YM0GbBGZfMiCg==} + '@hocuspocus/extension-redis@3.4.3': + resolution: {integrity: sha512-r64Vpgk6tt0VZaQPEo1dQuyur2ozr243ncDcDM+4gFPuV8ZRUjL1rvaJTidb2HCcAW2zjfwshNxw4+OixeksBA==} peerDependencies: y-protocols: ^1.0.6 yjs: ^13.6.8 - '@hocuspocus/provider@2.15.3': - resolution: {integrity: sha512-oadN05m+KL4ylNKVo5YspNG4MXkT2Y+FUFzrgigpQeTjQibkPUwCNmUnkUxMgrGRgxb+O0lJCfirFIJMxedctA==} + '@hocuspocus/provider@3.4.3': + resolution: {integrity: sha512-zt+UgVXGsEQrqnDZgavc2PT9yKJjmVjV+5YxvhlmFVFLVORqawT4l601aKmLPhvyK97un4ZApZ5rso8iO6crWg==} peerDependencies: y-protocols: ^1.0.6 yjs: ^13.6.8 - '@hocuspocus/server@2.15.3': - resolution: {integrity: sha512-Ju4ty4/7JtmvivcP7gKReOLf8KrFwN7Yx/5VhXYh4TRULy4kSo2fsDVUaluPp0neZa6PbVhizJuzlOim73IEbQ==} + '@hocuspocus/server@3.4.3': + resolution: {integrity: sha512-a9bqAXUMBo9YBeuzqNf9C3eVbu1RIWUrtmFMGq+ZssQr3Jugt/5PCkZskgqhJNvPkyTARHcUtN80j/SDLylZmg==} peerDependencies: y-protocols: ^1.0.6 yjs: ^13.6.8 - '@hocuspocus/transformer@2.15.3': - resolution: {integrity: sha512-01UU3iZA9MF+MmB2SweKyC70nBM/FkBt3veWiAMoXPiegUG47wY8QO2MksBD/ucnz7C5M/0oAsTjqrx+j0ynIw==} + '@hocuspocus/transformer@3.4.3': + resolution: {integrity: sha512-jQZiqFGCvGQJLgE0nHZ4TdpEJlI7WkM8CKA1wLcs0beVs0kNXg32lykGckjveJwwJuJ/hieMqIEqj9POxTWPEw==} peerDependencies: - '@tiptap/core': ^2.6.4 - '@tiptap/pm': ^2.6.4 + '@tiptap/core': ^3.0.1 + '@tiptap/pm': ^3.0.1 y-prosemirror: 1.3.7 yjs: ^13.6.8 @@ -2841,11 +2823,11 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@keyv/redis@5.1.5': - resolution: {integrity: sha512-8tD98mQoZTHHHFG7bcI2T4gkvAL9YsBI7TqCW4sXbx33WmAaPQ+JofxdN+rCu5LKxiboapfS8swZWJ7Tl/XbQg==} + '@keyv/redis@5.1.6': + resolution: {integrity: sha512-eKvW6pspvVaU5dxigaIDZr635/Uw6urTXL3gNbY9WTR8d3QigZQT+r8gxYSEOsw4+1cCBsC4s7T2ptR0WC9LfQ==} engines: {node: '>= 18'} peerDependencies: - keyv: ^5.5.5 + keyv: ^5.6.0 '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} @@ -3416,9 +3398,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@popperjs/core@2.11.8': - resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} @@ -3933,6 +3912,12 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@sesamecare-oss/redlock@1.4.0': + resolution: {integrity: sha512-2z589R+yxKLN4CgKxP1oN4dsg6Y548SE4bVYam/R0kHk7Q9VrQ9l66q+k1ehhSLLY4or9hcchuF9/MhuuZdjJg==} + engines: {node: '>=16'} + peerDependencies: + ioredis: '>=5' + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -4268,271 +4253,261 @@ packages: peerDependencies: react: ^18 || ^19 - '@tiptap/core@2.27.1': - resolution: {integrity: sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==} + '@tiptap/core@3.17.1': + resolution: {integrity: sha512-f8hB9MzXqsuXoF9qXEDEH5Fb3VgwhEFMBMfk9EKN88l5adri6oM8mt2XOWVxVVssjpEW0177zXSLPKWzoS/vrw==} peerDependencies: - '@tiptap/pm': ^2.7.0 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-blockquote@2.27.1': - resolution: {integrity: sha512-QrUX3muElDrNjKM3nqCSAtm3H3pT33c6ON8kwRiQboOAjT/9D57Cs7XEVY7r6rMaJPeKztrRUrNVF9w/w/6B0A==} + '@tiptap/extension-blockquote@3.17.1': + resolution: {integrity: sha512-X4jU/fllJQ8QbjCHUafU4QIHBobyXP3yGBoOcXxUaKlWbLvUs0SQTREM3n6/86m2YyAxwTPG1cn3Xypf42DMAQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-bold@2.27.1': - resolution: {integrity: sha512-g4l4p892x/r7mhea8syp3fNYODxsDrimgouQ+q4DKXIgQmm5+uNhyuEPexP3I8TFNXqQ4DlMNFoM9yCqk97etQ==} + '@tiptap/extension-bold@3.17.1': + resolution: {integrity: sha512-PZmrljcVBziJkQDXT/QJv4ESxVVQ0iRH+ruTzPda56Kk4h2310cSXGjI33W7rlCikGPoBAAjY/inujm46YB4bw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-bubble-menu@2.27.1': - resolution: {integrity: sha512-ki1R27VsSvY2tT9Q2DIlcATwLOoEjf5DsN+5sExarQ8S/ZxT/tvIjRxB8Dx7lb2a818W5f/NER26YchGtmHfpg==} + '@tiptap/extension-bubble-menu@3.17.1': + resolution: {integrity: sha512-z3E8biLiWlzZJwNHnB6j/ZyBdFrJmpl1lqKHc72JqahUHZvidZHdCOYssvR3fc6IaI7MXV13XY1DXUdFbatnaw==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-bullet-list@2.27.1': - resolution: {integrity: sha512-5FmnfXkJ76wN4EbJNzBhAlmQxho8yEMIJLchTGmXdsD/n/tsyVVtewnQYaIOj/Z7naaGySTGDmjVtLgTuQ+Sxw==} + '@tiptap/extension-bullet-list@3.17.1': + resolution: {integrity: sha512-2zw17XHruOJQK7ntLVq0PmOLajFhvQ+U4/qTfJnV3VOsHkm+2GPAksFe7I7+X0XmSmDru0pcT339Yywx/6Aykw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extension-list': ^3.17.1 - '@tiptap/extension-character-count@2.27.2': - resolution: {integrity: sha512-EcQRIvbLbMDDzo7uFqXYgh1CfgedS9sYX4BllktY2OlXLPdNpwo9t8WMK/a7soESNv0Le3WZ5pNvnNhv7Z2YdA==} + '@tiptap/extension-code-block@3.17.1': + resolution: {integrity: sha512-h4i+Y/cN7nMi0Tmlp6V1w4dI7NTqrUFSr1W/vMqnq4vn+c6jvm35KubKU5ry/1qQp8KfndDA02BtVQiMx6DmpA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-code-block-lowlight@2.27.1': - resolution: {integrity: sha512-Ijg9724uX/l4LXLELEeztZIgg+bDE/jJCkgS1+mavkRA/qtidpQkHo7L/Ry22fmj/ktCtZLjPXE5JAPAoRU6zA==} + '@tiptap/extension-code@3.17.1': + resolution: {integrity: sha512-4W0x1ZZqSnIVzQV0/b5VR0bktef2HykH5I/Czzir9yqoZ5zV2cLrMVuLvdFNgRIckU60tQLmHrfKWLF50OY0ew==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/extension-code-block': ^2.7.0 - '@tiptap/pm': ^2.7.0 - highlight.js: ^11 - lowlight: ^2 || ^3 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-code-block@2.27.1': - resolution: {integrity: sha512-wCI5VIOfSAdkenCWFvh4m8FFCJ51EOK+CUmOC/PWUjyo2Dgn8QC8HMi015q8XF7886T0KvYVVoqxmxJSUDAYNg==} + '@tiptap/extension-collaboration-caret@3.17.1': + resolution: {integrity: sha512-tYzujG4ABacSbjd8QOqMt1IP3QdCmAEBHP2faF4SeFauaP6Nto88JvTiZVCHad0BBwiNrj4UPGZSujcNQiLjTA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 + '@tiptap/y-tiptap': ^3.0.2 - '@tiptap/extension-code@2.27.1': - resolution: {integrity: sha512-i65wUGJevzBTIIUBHBc1ggVa27bgemvGl/tY1/89fEuS/0Xmre+OQjw8rCtSLevoHSiYYLgLRlvjtUSUhE4kgg==} + '@tiptap/extension-collaboration@3.17.1': + resolution: {integrity: sha512-4ehZ5LL7M3nFfcogCG7bWRHIR/8366i1vz5i0PaaoArJga2N5sXnWcuBGXG7ykC8owbgrfL3agFxjHlhTl4sNw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 + '@tiptap/y-tiptap': ^3.0.2 + yjs: ^13 - '@tiptap/extension-collaboration-cursor@2.27.1': - resolution: {integrity: sha512-k4vLA1QeGM4FfO9BMKw8O0Nxv2zDrsUpnP7wKAJp/zmr2lHbQX86cO+SGEy+kcRtPeIp6Y4Phytp6F+1HMjbLA==} + '@tiptap/extension-color@3.17.1': + resolution: {integrity: sha512-QVlzpzGB+QcZgHgvFMRPckZutpkOLzNmZzhupNA7G2CMeeoCwZOJeZkyd3zvtAnRZkf7FrQBO123On30pJt7TA==} peerDependencies: - '@tiptap/core': ^2.7.0 - y-prosemirror: 1.3.7 + '@tiptap/extension-text-style': ^3.17.1 - '@tiptap/extension-collaboration@2.27.1': - resolution: {integrity: sha512-fR35dIYDHM9870zl2sHaA2ytSVcjASv8Nfnb1Mgslt/F3Lqsu9TOv/oJWi9nYBvjjrfK0RNaoGFVH7p2z7FR3w==} + '@tiptap/extension-document@3.17.1': + resolution: {integrity: sha512-F7Q5HoAU383HWFa6AXZQ5N6t6lTJzVjYM8z93XrtH/2GzDFwy1UmDSrsXqvgznedBLAOgCNVTNh9PjXpLoOUbg==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - y-prosemirror: 1.3.7 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-color@2.27.1': - resolution: {integrity: sha512-raYRsdG2tZvVvY1LV/VTZnDG44Y0xRBwo5CZEat0OUqdx34dfvCtYm8HIOTyWBwr7OOW+yR4O1Vc2zFkmfthZw==} + '@tiptap/extension-dropcursor@3.17.1': + resolution: {integrity: sha512-EKJYPb7OSk3p9mX1SmHt4ccw89w1P1d55hC8aPtZJ6jxAUd5MSuVwvEEVz7LGldUZD9HZz9WFQ0Sv9U73Bpkmw==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/extension-text-style': ^2.7.0 + '@tiptap/extensions': ^3.17.1 - '@tiptap/extension-document@2.27.1': - resolution: {integrity: sha512-NtJzJY7Q/6XWjpOm5OXKrnEaofrcc1XOTYlo/SaTwl8k2bZo918Vl0IDBWhPVDsUN7kx767uHwbtuQZ+9I82hA==} + '@tiptap/extension-floating-menu@3.17.1': + resolution: {integrity: sha512-zYkoYsxp+cZ8tBDODm4E8hnSaMTdDWKJuCQWY2Ep14oMPkAkSJr8sCLL1tOnNSAnhGwLJQtRLkZ41nvUEP6xKA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-dropcursor@2.27.1': - resolution: {integrity: sha512-3MBQRGHHZ0by3OT0CWbLKS7J3PH9PpobrXjmIR7kr0nde7+bHqxXiVNuuIf501oKU9rnEUSedipSHkLYGkmfsA==} + '@tiptap/extension-gapcursor@3.17.1': + resolution: {integrity: sha512-xItmJZTi+Z6UbLBhpBBL9RZDNbDXf+ntWVgblAmxtpyEyNh5k5tkM6IP9SJRhk92uVfnFpH9qkGo66a537I8QA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/extensions': ^3.17.1 - '@tiptap/extension-floating-menu@2.27.1': - resolution: {integrity: sha512-nUk/8DbiXO69l6FDwkWso94BTf52IBoWALo+YGWT6o+FO6cI9LbUGghEX2CdmQYXCvSvwvISF2jXeLQWNZvPZQ==} + '@tiptap/extension-hard-break@3.17.1': + resolution: {integrity: sha512-28FZPUho1Q2AB3ka5SVEVib5f9dMKbE1kewLZeRIOQ5FuFNholGIPL5X1tKcwGW7G3A7Y0fGxeNmIZJ3hrqhzA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-gapcursor@2.27.1': - resolution: {integrity: sha512-A9e1jr+jGhDWzNSXtIO6PYVYhf5j/udjbZwMja+wCE/3KvZU9V3IrnGKz1xNW+2Q2BDOe1QO7j5uVL9ElR6nTA==} + '@tiptap/extension-heading@3.17.1': + resolution: {integrity: sha512-rT+Su/YnHdlikg8f78t6RXlc1sVSfp7B0fdJdtFgS2e6BBYJQoDMp5L9nt54RR9Yy953aDW2sko7NArUCb8log==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-hard-break@2.27.1': - resolution: {integrity: sha512-W4hHa4Io6QCTwpyTlN6UAvqMIQ7t56kIUByZhyY9EWrg/+JpbfpxE1kXFLPB4ZGgwBknFOw+e4bJ1j3oAbTJFw==} + '@tiptap/extension-highlight@3.17.1': + resolution: {integrity: sha512-I4EdBhPVzJd4ECMI9kP0NE4aG4Numd46jy/AqeZyf3dqVgCxRyAbSyU7oy4aXUnsojYODrKKG6+djm07KgOGoQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-heading@2.27.1': - resolution: {integrity: sha512-6xoC7igZlW1EmnQ5WVH9IL7P1nCQb3bBUaIDLvk7LbweEogcTUECI4Xg1vxMOVmj9tlDe1I4BsgfcKpB5KEsZw==} + '@tiptap/extension-history@3.17.1': + resolution: {integrity: sha512-YHW4HP9ovZ/zqc1u3+cDdAY/LITaMQNRnX5foLsDFLV5FU+zqonYo2CqDkVwaQs9UfCp9PM0ehZzxMI8hc58oA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extensions': ^3.17.1 - '@tiptap/extension-highlight@2.27.1': - resolution: {integrity: sha512-ntuYX09tvHQE/R/8WbTOxbFuQhRr2jhTkKz/gLwDD2o8IhccSy3f0nm+mVmVamKQnbsBBbLohojd5IGOnX9f1A==} + '@tiptap/extension-horizontal-rule@3.17.1': + resolution: {integrity: sha512-CHG6LBtxV+3qj5EcCRVlpvSW5udKD6KbnXIGhP+Tvy+OabLGzO4HNxz3+duDE0pMR4eKX1libsnqffj0vq7mnQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-history@2.27.1': - resolution: {integrity: sha512-K8PHC9gegSAt0wzSlsd4aUpoEyIJYOmVVeyniHr1P1mIblW1KYEDbRGbDlrLALTyUEfMcBhdIm8zrB9X2Nihvg==} + '@tiptap/extension-image@3.17.1': + resolution: {integrity: sha512-VbSSZ//5qijm8F0lQQ6K+DGnZgjLKYQY2c+O56QNEoN8BaCFrJlsVgF1ttrSRUmoG4XBNIMlAS07kZXvMZQr0g==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-horizontal-rule@2.27.1': - resolution: {integrity: sha512-WxXWGEEsqDmGIF2o9av+3r9Qje4CKrqrpeQY6aRO5bxvWX9AabQCfasepayBok6uwtvNzh3Xpsn9zbbSk09dNA==} + '@tiptap/extension-italic@3.17.1': + resolution: {integrity: sha512-unfRLmvf680Y0UkBToUcrDkSEKO/wAjd3nQ7CNPMfAc8m+ZMReXkcgLpeVvnDEiHNsJ0PlYSW7a45tnQD9HQdg==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-image@2.27.1': - resolution: {integrity: sha512-wu3vMKDYWJwKS6Hrw5PPCKBO2RxyHNeFLiA/uDErEV7axzNpievK/U9DyaDXmtK3K/h1XzJAJz19X+2d/pY68w==} + '@tiptap/extension-link@3.17.1': + resolution: {integrity: sha512-5kdN7vms5hMXtjiophUkgvzy8dNGvGSmol1Sawh30TEPrgXc93Ayj7YyGZlbimInKZcD8q+Od/FFc+wkrof3nA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-italic@2.27.1': - resolution: {integrity: sha512-rcm0GyniWW0UhcNI9+1eIK64GqWQLyIIrWGINslvqSUoBc+WkfocLvv4CMpRkzKlfsAxwVIBuH2eLxHKDtAREA==} + '@tiptap/extension-list-item@3.17.1': + resolution: {integrity: sha512-Qjj4oIa44cTX0E6aw/4+wleqX21t5jMDxeSqP5uQ8Q3IdD1GoR5+yo+41XAHELaeZOXLHLkAIbzIxik3pOqO8w==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extension-list': ^3.17.1 - '@tiptap/extension-link@2.27.1': - resolution: {integrity: sha512-cCwWPZsnVh9MXnGOqSIRXPPuUixRDK8eMN2TvqwbxUBb1TU7b/HtNvfMU4tAOqAuMRJ0aJkFuf3eB0Gi8LVb1g==} + '@tiptap/extension-list-keymap@3.17.1': + resolution: {integrity: sha512-zRidxbkJNe/j3nZpOGLnPeVdyciUM8MM+NHhxcjVKoNDA+/zEBfjXJ1dKC4UBsnSr4AS/3SCWBYHGXOoSqdUaA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/extension-list': ^3.17.1 - '@tiptap/extension-list-item@2.27.1': - resolution: {integrity: sha512-dtsxvtzxfwOJP6dKGf0vb2MJAoDF2NxoiWzpq0XTvo7NGGYUHfuHjX07Zp0dYqb4seaDXjwsi5BIQUOp3+WMFQ==} + '@tiptap/extension-list@3.17.1': + resolution: {integrity: sha512-LHKIxmXe5Me+vJZKhiwMBGHlApaBIAduNMRUpm5mkY7ER/m96zKR0VqrJd4LjVVH2iDvck5h1Ka4396MHWlKNg==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-list-keymap@2.27.1': - resolution: {integrity: sha512-k7+Ulz9B1NjqwU6NEFYkJh4rGGT/iRVaCBa8OL9YYrVS3H44LgEqUCEbRu6TeEq4XXrLwueQpkkyl4Evi15lAQ==} + '@tiptap/extension-ordered-list@3.17.1': + resolution: {integrity: sha512-pahAXbVajqX0Y51Zge9jKZlCtPV1oiq5Fbzs7gHF80KICIKf44i/AsUvfdJyT2N5/8kZrAMQHEiU/UgTMrhM3w==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extension-list': ^3.17.1 - '@tiptap/extension-ordered-list@2.27.1': - resolution: {integrity: sha512-U1/sWxc2TciozQsZjH35temyidYUjvroHj3PUPzPyh19w2fwKh1NSbFybWuoYs6jS3XnMSwnM2vF52tOwvfEmA==} + '@tiptap/extension-paragraph@3.17.1': + resolution: {integrity: sha512-Vl+xAlINaPtX8XTPvPmeveYMEIMLs8gA7ItcKpyyo4cCzAfVCY3DKuWzOkQGUf7DKrhyJQZhpgLNMaq+h5sTSw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-paragraph@2.27.1': - resolution: {integrity: sha512-R3QdrHcUdFAsdsn2UAIvhY0yWyHjqGyP/Rv8RRdN0OyFiTKtwTPqreKMHKJOflgX4sMJl/OpHTpNG1Kaf7Lo2A==} + '@tiptap/extension-placeholder@3.17.1': + resolution: {integrity: sha512-cE8Rij5/1t4KnWE7GaDewhBek9DKNB+97yrxyggMegILg6v195hOmOkRZkyfnFMYZoBDlrfSAtX9wBvbZBqIsg==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extensions': ^3.17.1 - '@tiptap/extension-placeholder@2.27.1': - resolution: {integrity: sha512-UbXaibHHFE+lOTlw/vs3jPzBoj1sAfbXuTAhXChjgYIcTTY5Cr6yxwcymLcimbQ79gf04Xkua2FCN3YsJxIFmw==} + '@tiptap/extension-strike@3.17.1': + resolution: {integrity: sha512-c6fS6YIhxoU55etlJgM0Xqker+jn7I1KC7GVu6ljmda8I00K3/lOLZgvFUNPmgp8EJWtyTctj+3D3D+PaZaFAA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-strike@2.27.1': - resolution: {integrity: sha512-S9I//K8KPgfFTC5I5lorClzXk0g4lrAv9y5qHzHO5EOWt7AFl0YTg2oN8NKSIBK4bHRnPIrjJJKv+dDFnUp5jQ==} + '@tiptap/extension-subscript@3.17.1': + resolution: {integrity: sha512-+y/sl1d+TcecX2n1r6ZTjBmY3D6cfqAW86iKsvudCFSpp9SQk85RaumPzELOXWOjz9g0mtfUnXifrLYF3dS+vA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-subscript@2.27.1': - resolution: {integrity: sha512-n2jTaYriewwz3ES1o6Wt/OwREvPwi97n+yEsJ7i31wiuxGTdCP31eAuppC6DvixEvDt3/rZMZcNp8Ah9crlbnw==} + '@tiptap/extension-superscript@3.17.1': + resolution: {integrity: sha512-FKt+lI1ocFRW0EFla9EuO71aLQINpkC/wt9zxWnJJnfPIWfxYlsTSFJLjLkVungTmwfeCnoCVcXnZ0dSKDnoGg==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-superscript@2.27.1': - resolution: {integrity: sha512-zTYOD7k3txm21rjeYHsf/VIpBe9IvVfNHSNayyY/JOgyQ/fW40cgX0gADNoT2ayAtRes4TvpcUYdgF9vC5bkJw==} + '@tiptap/extension-table@3.17.1': + resolution: {integrity: sha512-FuAMdmM330tHJUYT5IV2ooFRqtXf+0D8llcE9nIQQCXKL4J0pfGSOIm40LVpunYgx2pV8SSCL51qTBuEmR84tQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-table-cell@2.27.1': - resolution: {integrity: sha512-VowNmz1kub2qfntWkU8jGA6DoCl9xjJBWSypuQIeiN/IRId3BMrJodT26pTNJ3ChDMtYaanWaUvYqckRxgTC2A==} + '@tiptap/extension-text-align@3.17.1': + resolution: {integrity: sha512-CyJbZf823dqPZ/1zwRsza5pk/NQwFZwILdFYLVkV88I4+Ua9YVztI9kmwTB6dJyuKT4kTc7nhQHdaa957alGZQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-table-header@2.27.1': - resolution: {integrity: sha512-lSbGB6kBp/sTVzAWl4v7v7ztL5XU3aTdlS7FhfGjpdsxd4zPKYG8kx+Uxgq25W9/BlCbnqHnO0poAMfOlspDQw==} + '@tiptap/extension-text-style@3.17.1': + resolution: {integrity: sha512-TCMsEU92r/TfZkN8AKo/WIcJ1uNq/5NiZxloq5drF1HXxDDjwliurgwBw3OTGUlKQmer0N9hV0AAePY/G+5Akw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-table-row@2.27.1': - resolution: {integrity: sha512-3xtlmZ6NWDi5a42gK0qQQTeBUpJ2j1o7qyXTFkhQaJAeIFEqsemgSRhgXZxbwSmQQZsPJ/86KWBNVkT0FaRFDw==} + '@tiptap/extension-text@3.17.1': + resolution: {integrity: sha512-rGml96vokQbvPB+w6L3+WKyYJWwqELaLdFUr1WMgg+py5uNYGJYAExYNAbDb5biWJBrX9GgMlCaNeiJj849L1w==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-table@2.27.1': - resolution: {integrity: sha512-iOoOo0vYFzAogAZlw36DgmFfNM5vOkLqnApm81soO/YWpqtKAvBn+TMY4ss4OMDsOefUzBa6xqOJ0gJR5ZygjA==} + '@tiptap/extension-typography@3.17.1': + resolution: {integrity: sha512-bEocTrK/gryk3VtthC9Ca03p2kutVIIFnDkVW6iOG8PgQWEspuQRgqE8yPnHxY8pBBDWxiaBzcGTSrp+3U9d5A==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-task-item@2.27.1': - resolution: {integrity: sha512-vaEtdos+9jApD6yRfD6F/xShikiZFHi7I0nswAmGKT/kE1wmHCUxme8OFMe7642e2OK0lqgHsUaOLxP/0nZJ5A==} + '@tiptap/extension-underline@3.17.1': + resolution: {integrity: sha512-6RdBzmkg6DYs0EqPyoqLGkISXzCnPqM/q3A6nh3EmFmORcIDfuNmcidvA6EImebK8KQGmtZKsRhQSnK4CNQ39g==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-task-list@2.27.1': - resolution: {integrity: sha512-KRlYOZ6kdURvAspUrLVsC7mLkVW2DYhpj+7QxH7gVDZuAuoPUEmpJVcBVPq7GhPF9PccaRLru+n1Ege5VqvZ+Q==} + '@tiptap/extension-unique-id@3.17.1': + resolution: {integrity: sha512-R+lXBIaEmJ23rJMMLs6dPIVMhpv+TU8vEFVtpbMoOl/yfoc9Pvr6Q0EgLnRDX6l4yAekenem4KmGeG9CmuoskA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-text-align@2.27.1': - resolution: {integrity: sha512-D7dLPk7y5mDn9ZNANQ4K2gCq4vy+Emm5AdeWOGzNeqJsYrBotiQYXd9rb1QYjdup2kzAoKduMTUXV92ujo5cEg==} + '@tiptap/extension-youtube@3.17.1': + resolution: {integrity: sha512-AarpN4vI/S6jPMuLuFGEFLgdoasGiUW+rGLj+jH/0Of6l27nKRN00MTm/fD/62qjR6At3Rd7Xsue/GuXdmDUWw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-text-style@2.27.1': - resolution: {integrity: sha512-NagQ9qLk0Ril83gfrk+C65SvTqPjL3WVnLF2arsEVnCrxcx3uDOvdJW67f/K5HEwEHsoqJ4Zq9Irco/koXrOXA==} + '@tiptap/extensions@3.17.1': + resolution: {integrity: sha512-aQ4WA5bdRpv9yPQ6rRdiqwlMZ1eJw1HyEaNPQhOr2HVhQ0EqSDIOEXF4ymCveGAHxXbxNvtQ+4t1ymQEikGfXA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-text@2.27.1': - resolution: {integrity: sha512-a4GCT+GZ9tUwl82F4CEum9/+WsuW0/De9Be/NqrMmi7eNfAwbUTbLCTFU0gEvv25WMHCoUzaeNk/qGmzeVPJ1Q==} + '@tiptap/html@3.17.1': + resolution: {integrity: sha512-fLb2fo8+3oQ+5FTx5IGZvLI5+VLgN9BM6pHaO1+IrwqQ5w2RBFIGp8M946asBPkxJ74EtzHqFKJpVFtaY2CcpA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 + happy-dom: ^20.0.2 - '@tiptap/extension-typography@2.27.1': - resolution: {integrity: sha512-jAZU5IuWH9CtZlolQ1gRhV+bT75s19SXjadQwkk18gMMiapcaIVVTxUDWY6ycv9ge4cjRoaP3lqBviW3cGqhOA==} + '@tiptap/pm@3.17.1': + resolution: {integrity: sha512-UyVLkN8axV/zop6Se2DCBJRu5DM21X0XEQvwEC5P/vk8eC9OcQZ3FLtxeYy2ZjpAZUzBGLw0/BGsmEip/n7olw==} + + '@tiptap/react@3.17.1': + resolution: {integrity: sha512-Hn/pIP3HG9xYnhI3iGrfVhgQhfIdOaEBSxOFzJ37patqSOlIoP5aZH/b2HZ4vgo5DdRlV56q7WtRC+vLIw4Neg==} peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-underline@2.27.1': - resolution: {integrity: sha512-fPTmfJFAQWg1O/os1pYSPVdtvly6eW/w5sDofG7pre+bdQUN+8s1cZYelSuj/ltNVioRaB2Ws7tvNgnHL0aAJQ==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-youtube@2.27.1': - resolution: {integrity: sha512-HjBBgE0Zbch/S2UP0YYQXervfoBd4Trw0dYmlZbX9cXJcZv+QFx0vsPGmjAGlqzXf9Y8ZioWm8fso4u6AsUfTw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/html@2.27.1': - resolution: {integrity: sha512-5iPo36g4nbBVoEVBQb6my4KNpNzu38gtCFXIIlAJdAZQvPs+XC8TkrnGK/G4UGpwBXCuQjSQm0iyn4znmQPDsw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/pm@2.27.1': - resolution: {integrity: sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==} - - '@tiptap/react@2.27.1': - resolution: {integrity: sha512-leJximSjYJuhLJQv9azOP9R7w6zuxVgKOHYT4w83Gte7GhWMpNL6xRWzld280vyq/YW/cSYjPb/8ESEOgKNBdQ==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tiptap/starter-kit@2.27.1': - resolution: {integrity: sha512-uQQlP0Nmn9eq19qm8YoOeloEfmcGbPpB1cujq54Q6nPgxaBozR7rE7tXbFTinxRW2+Hr7XyNWhpjB7DMNkdU2Q==} + '@tiptap/starter-kit@3.17.1': + resolution: {integrity: sha512-3vBGqag9mwuQoWTrfQlULtHeoFs7k/2Q8CREf3Y79hv2fqAXTvTOKlWYPSgZhiGVMp6Dti7BDiE9Y1QpvAat2g==} - '@tiptap/suggestion@2.27.1': - resolution: {integrity: sha512-yTy75ZMYgVWM18cl7YxLqMJ7TorQTGysSd1aKmBA9qd8uzYlvLMmHKE9qBDxM9HXODBz1DA/BLLm9esv2enmFw==} + '@tiptap/suggestion@3.17.1': + resolution: {integrity: sha512-a188uVYjlLsUiwK3Ki7KsaWVWC0u28KsqGEAqCk9ECYmtVY99Hrb+rcAwGpMjA7tn8WAwThOxiLISoMdpuqXwg==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 + + '@tiptap/y-tiptap@3.0.1': + resolution: {integrity: sha512-F3hj5X77ckmyIywbCQpKgyX3xKra2/acJPWaV5R9wqp0cUPBmm62FYbkQ6HaqxH1VhCkUhhAZcDSQjbjj7tnWw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.5.38 '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} @@ -4829,9 +4804,6 @@ packages: '@types/passport@1.0.17': resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} - '@types/pg@8.11.11': - resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==} - '@types/prop-types@15.7.11': resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} @@ -4871,6 +4843,9 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/turndown@5.0.6': + resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -5300,6 +5275,9 @@ packages: async-lock@1.4.1: resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} @@ -5406,9 +5384,6 @@ packages: bluebird@3.4.7: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -6117,10 +6092,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - denque@1.5.1: - resolution: {integrity: sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==} - engines: {node: '>=0.10'} - denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -6300,10 +6271,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@5.0.0: - resolution: {integrity: sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==} - engines: {node: '>=0.12'} - entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -6535,6 +6502,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.3.4: + resolution: {integrity: sha512-d+yU9iNQbbC098NOuMlAIth/g+owbpX/uuOkH/DQcC2fMMyjOlX292Op29DrUKq388m4UUyOdWakUH/msGypOg==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -6966,6 +6937,11 @@ packages: image-blob-reduce@3.0.1: resolution: {integrity: sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q==} + image-dimensions@2.5.0: + resolution: {integrity: sha512-CKZPHjAEtSg9lBV9eER0bhNn/yrY7cFEQEhkwjLhqLY+Na8lcP1pEyWsaGMGc8t2qbKWA/tuqbhFQpOKGN72Yw==} + engines: {node: '>=18'} + hasBin: true + image-size@0.5.5: resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} @@ -7018,10 +6994,6 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ioredis@4.28.5: - resolution: {integrity: sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==} - engines: {node: '>=6'} - ioredis@5.4.1: resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} engines: {node: '>=12.22.0'} @@ -7532,8 +7504,8 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - keyv@5.5.5: - resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==} + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} @@ -7592,6 +7564,16 @@ packages: resolution: {integrity: sha512-904MSUdzkdxl+k3C67ogvP6ogPOEr0D6ZZDtxAmDeIHEJxZAA+eC+TLAcJt3HQABTPetwsW3pj6y1MPmaveQUg==} hasBin: true + kysely-postgres-js@3.0.0: + resolution: {integrity: sha512-o2t/xNSYJQDW6rVGGFPXKmZ0BEz2dGn66c2B+cO/k9ZNcU2qPWPycQPQ+B+P2MBXbKYq0xV9BZmFIvkUrmFWAQ==} + engines: {bun: '>=1.2', node: '>=20'} + peerDependencies: + kysely: '>= 0.24.0 < 1' + postgres: ^3.4.0 + peerDependenciesMeta: + postgres: + optional: true + kysely@0.28.2: resolution: {integrity: sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==} engines: {node: '>=18.0.0'} @@ -7643,11 +7625,6 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lib0@0.2.108: - resolution: {integrity: sha512-+3eK/B0SqYoZiQu9fNk4VEc6EX8cb0Li96tPGKgugzoGj/OdRdREtuTLvUW+mtinoB2mFiJjSqOJBIaMkAGhxQ==} - engines: {node: '>=16'} - hasBin: true - lib0@0.2.114: resolution: {integrity: sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==} engines: {node: '>=16'} @@ -7712,9 +7689,6 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - lodash.flatten@4.4.0: - resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} - lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -8238,9 +8212,6 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} - obuf@1.1.2: - resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - oidc-token-hash@5.0.3: resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} engines: {node: ^10.13.0 || >=12.0.0} @@ -8333,10 +8304,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-map@2.1.0: - resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} - engines: {node: '>=6'} - p-queue@6.6.2: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} @@ -8469,10 +8436,6 @@ packages: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - pg-numeric@1.0.2: - resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} - engines: {node: '>=4'} - pg-pool@3.10.1: resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} peerDependencies: @@ -8481,9 +8444,6 @@ packages: pg-protocol@1.10.3: resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} - pg-protocol@1.7.0: - resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} - pg-tsquery@8.4.2: resolution: {integrity: sha512-waJSlBIKE+shDhuDpuQglTH6dG5zakDhnrnxu8XB8V5c7yoDSuy4pOxY6t2dyoxTjaKMcMmlByJN7n9jx9eqMA==} engines: {node: '>=10'} @@ -8492,10 +8452,6 @@ packages: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg-types@4.0.2: - resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} - engines: {node: '>=10'} - pg@8.16.3: resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} engines: {node: '>= 16.0.0'} @@ -8653,37 +8609,22 @@ packages: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} - postgres-array@3.0.2: - resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} - engines: {node: '>=12'} - postgres-bytea@1.0.0: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} engines: {node: '>=0.10.0'} - postgres-bytea@3.0.0: - resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} - engines: {node: '>= 6'} - postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} engines: {node: '>=0.10.0'} - postgres-date@2.1.0: - resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} - engines: {node: '>=12'} - postgres-interval@1.2.0: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - postgres-interval@3.0.0: - resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + postgres@3.4.8: + resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} engines: {node: '>=12'} - postgres-range@1.1.4: - resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} - posthog-js@1.255.1: resolution: {integrity: sha512-KMh0o9MhORhEZVjXpktXB5rJ8PfDk+poqBoTSoLzWgNjhJf6D8jcyB9jUMA6vVPfn4YeepVX5NuclDRqOwr5Mw==} peerDependencies: @@ -8779,8 +8720,8 @@ packages: prosemirror-schema-basic@1.2.3: resolution: {integrity: sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==} - prosemirror-schema-list@1.4.1: - resolution: {integrity: sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==} + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} prosemirror-state@1.4.3: resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} @@ -9050,9 +8991,6 @@ packages: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} - redis-commands@1.7.0: - resolution: {integrity: sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==} - redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -9061,10 +8999,6 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} - redlock@4.2.0: - resolution: {integrity: sha512-j+oQlG+dOwcetUt2WJWttu4CZVeRzUrcVcISFmEmfyuwCVSJ93rDT7YSgg7H7rnxwoRyk/jU46kycVka5tW7jA==} - engines: {node: '>=8.0.0'} - redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} @@ -9620,9 +9554,6 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tippy.js@6.3.7: - resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} - tiptap-extension-global-drag-handle@0.1.18: resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==} @@ -9972,6 +9903,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utf8-byte-length@1.0.4: resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==} @@ -10357,8 +10293,8 @@ packages: resolution: {integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==} engines: {node: '>=12'} - yjs@13.6.27: - resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==} + yjs@13.6.29: + resolution: {integrity: sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} yn@3.1.1: @@ -10377,10 +10313,6 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} - zeed-dom@0.15.1: - resolution: {integrity: sha512-dtZ0aQSFyZmoJS0m06/xBN1SazUBPL5HpzlAcs/KcRW0rzadYw12deQBjeMhGKMMeGEp7bA9vmikMLaO4exBcg==} - engines: {node: '>=14.13.1'} - zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -12297,15 +12229,15 @@ snapshots: '@cacheable/utils@2.3.3': dependencies: hashery: 1.4.0 - keyv: 5.5.5 + keyv: 5.6.0 - '@casl/ability@6.7.5': + '@casl/ability@6.8.0': dependencies: '@ucast/mongo2js': 1.3.4 - '@casl/react@4.0.0(@casl/ability@6.7.5)(react@18.3.1)': + '@casl/react@4.0.0(@casl/ability@6.8.0)(react@18.3.1)': dependencies: - '@casl/ability': 6.7.5 + '@casl/ability': 6.8.0 react: 18.3.1 '@cfworker/json-schema@4.1.1': {} @@ -12762,58 +12694,57 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@hocuspocus/common@2.15.3': + '@hocuspocus/common@3.4.3': dependencies: lib0: 0.2.114 - '@hocuspocus/extension-redis@2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)': + '@hocuspocus/extension-redis@3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@hocuspocus/server': 2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) - ioredis: 4.28.5 + '@hocuspocus/server': 3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) + '@sesamecare-oss/redlock': 1.4.0(ioredis@5.8.2) + ioredis: 5.8.2 kleur: 4.1.5 lodash.debounce: 4.0.8 - redlock: 4.2.0 - uuid: 11.1.0 - y-protocols: 1.0.6(yjs@13.6.27) - yjs: 13.6.27 + y-protocols: 1.0.6(yjs@13.6.29) + yjs: 13.6.29 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@hocuspocus/provider@2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)': + '@hocuspocus/provider@3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@hocuspocus/common': 2.15.3 + '@hocuspocus/common': 3.4.3 '@lifeomic/attempt': 3.0.3 lib0: 0.2.114 ws: 8.19.0 - y-protocols: 1.0.6(yjs@13.6.27) - yjs: 13.6.27 + y-protocols: 1.0.6(yjs@13.6.29) + yjs: 13.6.29 transitivePeerDependencies: - bufferutil - utf-8-validate - '@hocuspocus/server@2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)': + '@hocuspocus/server@3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@hocuspocus/common': 2.15.3 + '@hocuspocus/common': 3.4.3 async-lock: 1.4.1 + async-mutex: 0.5.0 kleur: 4.1.5 lib0: 0.2.114 - uuid: 11.1.0 ws: 8.19.0 - y-protocols: 1.0.6(yjs@13.6.27) - yjs: 13.6.27 + y-protocols: 1.0.6(yjs@13.6.29) + yjs: 13.6.29 transitivePeerDependencies: - bufferutil - utf-8-validate - '@hocuspocus/transformer@2.15.3(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27)': + '@hocuspocus/transformer@3.4.3(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - '@tiptap/starter-kit': 2.27.1 - y-prosemirror: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) - yjs: 13.6.27 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + '@tiptap/starter-kit': 3.17.1 + y-prosemirror: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) + yjs: 13.6.29 '@humanfs/core@0.19.1': {} @@ -13303,12 +13234,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - '@keyv/redis@5.1.5(keyv@5.5.5)': + '@keyv/redis@5.1.6(keyv@5.6.0)': dependencies: '@redis/client': 5.10.0 cluster-key-slot: 1.1.2 hookified: 1.15.0 - keyv: 5.5.5 + keyv: 5.6.0 '@keyv/serialize@1.1.1': {} @@ -13495,12 +13426,12 @@ snapshots: bullmq: 5.65.0 tslib: 2.8.1 - '@nestjs/cache-manager@3.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(cache-manager@7.2.8)(keyv@5.5.5)(rxjs@7.8.2)': + '@nestjs/cache-manager@3.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) cache-manager: 7.2.8 - keyv: 5.5.5 + keyv: 5.6.0 rxjs: 7.8.2 '@nestjs/cli@11.0.4(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.13.4)': @@ -13849,8 +13780,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@popperjs/core@2.11.8': {} - '@radix-ui/primitive@1.1.1': {} '@radix-ui/react-arrow@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -14278,6 +14207,10 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 + '@sesamecare-oss/redlock@1.4.0(ioredis@5.8.2)': + dependencies: + ioredis: 5.8.2 + '@sinclair/typebox@0.27.8': {} '@sindresorhus/slugify@1.1.0': @@ -14728,212 +14661,195 @@ snapshots: '@tanstack/query-core': 5.90.17 react: 18.3.1 - '@tiptap/core@2.27.1(@tiptap/pm@2.27.1)': + '@tiptap/core@3.17.1(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/pm': 2.27.1 + '@tiptap/pm': 3.17.1 - '@tiptap/extension-blockquote@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-blockquote@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-bold@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-bold@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-bubble-menu@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-bubble-menu@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - tippy.js: 6.3.7 + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + optional: true - '@tiptap/extension-bullet-list@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-bullet-list@3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-character-count@2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-code-block@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-code-block-lowlight@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/extension-code-block@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(highlight.js@11.11.1)(lowlight@3.3.0)': + '@tiptap/extension-code@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/extension-code-block': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - highlight.js: 11.11.1 - lowlight: 3.3.0 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-code-block@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-collaboration-caret@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + '@tiptap/y-tiptap': 3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) - '@tiptap/extension-code@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-collaboration@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + '@tiptap/y-tiptap': 3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) + yjs: 13.6.29 - '@tiptap/extension-collaboration-cursor@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': + '@tiptap/extension-color@3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - y-prosemirror: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + '@tiptap/extension-text-style': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-collaboration@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': + '@tiptap/extension-document@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - y-prosemirror: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-color@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/extension-text-style@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)))': + '@tiptap/extension-dropcursor@3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/extension-text-style': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extensions': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-document@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-floating-menu@3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@floating-ui/dom': 1.7.3 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + optional: true - '@tiptap/extension-dropcursor@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-gapcursor@3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/extensions': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-floating-menu@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-hard-break@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - tippy.js: 6.3.7 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-gapcursor@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-heading@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-hard-break@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-highlight@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-heading@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-history@3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extensions': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-highlight@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-horizontal-rule@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-history@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-image@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-horizontal-rule@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-italic@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-image@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-link@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - - '@tiptap/extension-italic@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': - dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - - '@tiptap/extension-link@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': - dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 linkifyjs: 4.3.2 - '@tiptap/extension-list-item@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-list-item@3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-list-keymap@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-list-keymap@3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-ordered-list@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-paragraph@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-ordered-list@3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-placeholder@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-paragraph@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-strike@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-placeholder@3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extensions': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-subscript@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-strike@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-superscript@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-subscript@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-table-cell@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-superscript@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-table-header@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-table@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-table-row@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-text-align@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-table@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-task-item@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-text@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-task-list@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-typography@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-text-align@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-underline@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-text-style@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-unique-id@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + uuid: 10.0.0 - '@tiptap/extension-text@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-youtube@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-typography@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-underline@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/html@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + happy-dom: 20.1.0 - '@tiptap/extension-youtube@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': - dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - - '@tiptap/html@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': - dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - zeed-dom: 0.15.1 - - '@tiptap/pm@2.27.1': + '@tiptap/pm@3.17.1': dependencies: prosemirror-changeset: 2.3.1 prosemirror-collab: 1.3.1 @@ -14947,53 +14863,70 @@ snapshots: prosemirror-menu: 1.2.4 prosemirror-model: 1.25.1 prosemirror-schema-basic: 1.2.3 - prosemirror-schema-list: 1.4.1 + prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.3 prosemirror-tables: 1.7.1 prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0) prosemirror-transform: 1.10.4 prosemirror-view: 1.40.0 - '@tiptap/react@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tiptap/react@3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/extension-bubble-menu': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-floating-menu': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 '@types/use-sync-external-store': 0.0.6 - fast-deep-equal: 3.1.3 + fast-equals: 5.3.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - use-sync-external-store: 1.2.2(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-floating-menu': 3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + transitivePeerDependencies: + - '@floating-ui/dom' - '@tiptap/starter-kit@2.27.1': + '@tiptap/starter-kit@3.17.1': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/extension-blockquote': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-bold': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-bullet-list': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-code': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-code-block': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-document': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-dropcursor': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-gapcursor': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-hard-break': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-heading': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-history': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-horizontal-rule': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-italic': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-list-item': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-ordered-list': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-paragraph': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-strike': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-text': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-text-style': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/extension-blockquote': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-bold': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-bullet-list': 3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-code': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-code-block': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-document': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-dropcursor': 3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-gapcursor': 3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-hard-break': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-heading': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-horizontal-rule': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-italic': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-link': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list-item': 3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-list-keymap': 3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-ordered-list': 3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-paragraph': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-strike': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-text': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-underline': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extensions': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/suggestion@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/suggestion@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + + '@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29)': + dependencies: + lib0: 0.2.114 + prosemirror-model: 1.25.1 + prosemirror-state: 1.4.3 + prosemirror-view: 1.40.0 + y-protocols: 1.0.6(yjs@13.6.29) + yjs: 13.6.29 '@tokenizer/inflate@0.4.1': dependencies: @@ -15353,12 +15286,6 @@ snapshots: dependencies: '@types/express': 4.17.23 - '@types/pg@8.11.11': - dependencies: - '@types/node': 22.19.1 - pg-protocol: 1.7.0 - pg-types: 4.0.2 - '@types/prop-types@15.7.11': {} '@types/qrcode@1.5.5': @@ -15407,6 +15334,8 @@ snapshots: '@types/trusted-types@2.0.7': optional: true + '@types/turndown@5.0.6': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.2': {} @@ -15926,6 +15855,10 @@ snapshots: async-lock@1.4.1: {} + async-mutex@0.5.0: + dependencies: + tslib: 2.8.1 + async@3.2.5: {} asynckit@0.4.0: {} @@ -16108,8 +16041,6 @@ snapshots: bluebird@3.4.7: {} - bluebird@3.7.2: {} - boolbase@1.0.0: {} bowser@2.11.0: {} @@ -16200,7 +16131,7 @@ snapshots: cache-manager@7.2.8: dependencies: '@cacheable/utils': 2.3.3 - keyv: 5.5.5 + keyv: 5.6.0 call-bind-apply-helpers@1.0.2: dependencies: @@ -16856,8 +16787,6 @@ snapshots: delayed-stream@1.0.0: {} - denque@1.5.1: {} - denque@2.1.0: {} depd@2.0.0: {} @@ -17045,8 +16974,6 @@ snapshots: entities@4.5.0: {} - entities@5.0.0: {} - entities@6.0.1: {} env-paths@2.2.1: {} @@ -17434,6 +17361,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.3.4: {} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -17918,6 +17847,8 @@ snapshots: dependencies: pica: 7.1.1 + image-dimensions@2.5.0: {} + image-size@0.5.5: optional: true @@ -17962,22 +17893,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - ioredis@4.28.5: - dependencies: - cluster-key-slot: 1.1.2 - debug: 4.4.1 - denque: 1.5.1 - lodash.defaults: 4.2.0 - lodash.flatten: 4.4.0 - lodash.isarguments: 3.1.0 - p-map: 2.1.0 - redis-commands: 1.7.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - ioredis@5.4.1: dependencies: '@ioredis/commands': 1.2.0 @@ -18725,7 +18640,7 @@ snapshots: dependencies: json-buffer: 3.0.1 - keyv@5.5.5: + keyv@5.6.0: dependencies: '@keyv/serialize': 1.1.1 @@ -18761,6 +18676,12 @@ snapshots: '@commander-js/extra-typings': 11.1.0(commander@11.1.0) commander: 11.1.0 + kysely-postgres-js@3.0.0(kysely@0.28.2)(postgres@3.4.8): + dependencies: + kysely: 0.28.2 + optionalDependencies: + postgres: 3.4.8 + kysely@0.28.2: {} langium@3.3.1: @@ -18822,10 +18743,6 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lib0@0.2.108: - dependencies: - isomorphic.js: 0.2.5 - lib0@0.2.114: dependencies: isomorphic.js: 0.2.5 @@ -18882,8 +18799,6 @@ snapshots: lodash.defaults@4.2.0: {} - lodash.flatten@4.4.0: {} - lodash.includes@4.3.0: {} lodash.isarguments@3.1.0: {} @@ -19522,8 +19437,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 - obuf@1.1.2: {} - oidc-token-hash@5.0.3: {} ollama@0.6.3: @@ -19627,8 +19540,6 @@ snapshots: dependencies: p-limit: 3.1.0 - p-map@2.1.0: {} - p-queue@6.6.2: dependencies: eventemitter3: 4.0.7 @@ -19750,19 +19661,19 @@ snapshots: pg-cloudflare@1.2.7: optional: true - pg-connection-string@2.9.1: {} + pg-connection-string@2.9.1: + optional: true - pg-int8@1.0.1: {} - - pg-numeric@1.0.2: {} + pg-int8@1.0.1: + optional: true pg-pool@3.10.1(pg@8.16.3): dependencies: pg: 8.16.3 + optional: true - pg-protocol@1.10.3: {} - - pg-protocol@1.7.0: {} + pg-protocol@1.10.3: + optional: true pg-tsquery@8.4.2: {} @@ -19773,16 +19684,7 @@ snapshots: postgres-bytea: 1.0.0 postgres-date: 1.0.7 postgres-interval: 1.2.0 - - pg-types@4.0.2: - dependencies: - pg-int8: 1.0.1 - pg-numeric: 1.0.2 - postgres-array: 3.0.2 - postgres-bytea: 3.0.0 - postgres-date: 2.1.0 - postgres-interval: 3.0.0 - postgres-range: 1.1.4 + optional: true pg@8.16.3: dependencies: @@ -19793,10 +19695,12 @@ snapshots: pgpass: 1.0.5 optionalDependencies: pg-cloudflare: 1.2.7 + optional: true pgpass@1.0.5: dependencies: split2: 4.2.0 + optional: true pgvector@0.2.1: {} @@ -19965,27 +19869,21 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postgres-array@2.0.0: {} + postgres-array@2.0.0: + optional: true - postgres-array@3.0.2: {} + postgres-bytea@1.0.0: + optional: true - postgres-bytea@1.0.0: {} - - postgres-bytea@3.0.0: - dependencies: - obuf: 1.1.2 - - postgres-date@1.0.7: {} - - postgres-date@2.1.0: {} + postgres-date@1.0.7: + optional: true postgres-interval@1.2.0: dependencies: xtend: 4.0.2 + optional: true - postgres-interval@3.0.0: {} - - postgres-range@1.1.4: {} + postgres@3.4.8: {} posthog-js@1.255.1: dependencies: @@ -20100,7 +19998,7 @@ snapshots: dependencies: prosemirror-model: 1.25.1 - prosemirror-schema-list@1.4.1: + prosemirror-schema-list@1.5.1: dependencies: prosemirror-model: 1.25.1 prosemirror-state: 1.4.3 @@ -20405,18 +20303,12 @@ snapshots: dependencies: resolve: 1.22.8 - redis-commands@1.7.0: {} - redis-errors@1.2.0: {} redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 - redlock@4.2.0: - dependencies: - bluebird: 3.7.2 - redux@4.2.1: dependencies: '@babel/runtime': 7.25.6 @@ -21060,10 +20952,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tippy.js@6.3.7: - dependencies: - '@popperjs/core': 2.11.8 - tiptap-extension-global-drag-handle@0.1.18: {} tldts-core@6.1.72: {} @@ -21404,6 +21292,10 @@ snapshots: dependencies: react: 18.3.1 + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + utf8-byte-length@1.0.4: {} util-deprecate@1.0.2: {} @@ -21678,26 +21570,27 @@ snapshots: xpath@0.0.34: {} - xtend@4.0.2: {} + xtend@4.0.2: + optional: true - y-indexeddb@9.0.12(yjs@13.6.27): + y-indexeddb@9.0.12(yjs@13.6.29): dependencies: lib0: 0.2.88 - yjs: 13.6.27 + yjs: 13.6.29 - y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27): + y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29): dependencies: lib0: 0.2.114 prosemirror-model: 1.25.1 prosemirror-state: 1.4.3 prosemirror-view: 1.40.0 - y-protocols: 1.0.6(yjs@13.6.27) - yjs: 13.6.27 + y-protocols: 1.0.6(yjs@13.6.29) + yjs: 13.6.29 - y-protocols@1.0.6(yjs@13.6.27): + y-protocols@1.0.6(yjs@13.6.29): dependencies: lib0: 0.2.114 - yjs: 13.6.27 + yjs: 13.6.29 y18n@4.0.3: {} @@ -21747,9 +21640,9 @@ snapshots: buffer-crc32: 0.2.13 pend: 1.2.0 - yjs@13.6.27: + yjs@13.6.29: dependencies: - lib0: 0.2.108 + lib0: 0.2.114 yn@3.1.1: {} @@ -21759,11 +21652,6 @@ snapshots: yoctocolors-cjs@2.1.2: {} - zeed-dom@0.15.1: - dependencies: - css-what: 6.1.0 - entities: 5.0.0 - zod@3.25.76: {} zod@4.3.5: {}