mirror of
https://github.com/docmost/docmost.git
synced 2026-05-09 07:43:06 +08:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe3732add9 | |||
| 9db9eb852c | |||
| 3007060ac4 | |||
| ba9e58ede9 | |||
| f2bc0b5049 | |||
| eba5ae2eb3 | |||
| b6344f2e08 | |||
| db52b6036e | |||
| fe5b236d41 | |||
| b3c5ca6d5f | |||
| 483e39db1c | |||
| 56f476649c | |||
| 1b82959859 | |||
| 4d322e9157 | |||
| 20a7acfccc | |||
| 040ad04a27 | |||
| d3ca1ed72c | |||
| 207b1b593a | |||
| 6c664a366f | |||
| 4873f7b9ff | |||
| 718ca2b674 | |||
| e189d01ce0 | |||
| edd3754e46 | |||
| b2b147f1bd | |||
| 129e21f728 | |||
| 70124475ab | |||
| 7d7decb459 | |||
| 1eba6e93cc | |||
| cd52acc415 | |||
| a09e35ba8f | |||
| 8e2cf9bb02 | |||
| cff4ba8e50 | |||
| f44bb3f121 | |||
| 669ff0435f |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.25.1",
|
"version": "0.25.0-beta.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "Seite",
|
"page": "Seite",
|
||||||
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
||||||
"Page history": "Seitengeschichte",
|
"Page history": "Seitengeschichte",
|
||||||
"Select version": "Version auswählen",
|
|
||||||
"Highlight changes": "Änderungen hervorheben",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.",
|
"Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.",
|
||||||
"Pages": "Seiten",
|
"Pages": "Seiten",
|
||||||
"pages": "Seiten",
|
"pages": "Seiten",
|
||||||
|
|||||||
@@ -123,7 +123,10 @@
|
|||||||
"page": "page",
|
"page": "page",
|
||||||
"Page deleted successfully": "Page deleted successfully",
|
"Page deleted successfully": "Page deleted successfully",
|
||||||
"Page history": "Page history",
|
"Page history": "Page history",
|
||||||
|
"Version history for": "Version history for",
|
||||||
|
"document": "document",
|
||||||
"Select version": "Select version",
|
"Select version": "Select version",
|
||||||
|
"Close": "Close",
|
||||||
"Highlight changes": "Highlight changes",
|
"Highlight changes": "Highlight changes",
|
||||||
"Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.",
|
"Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.",
|
||||||
"Pages": "Pages",
|
"Pages": "Pages",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "página",
|
"page": "página",
|
||||||
"Page deleted successfully": "Página eliminada con éxito",
|
"Page deleted successfully": "Página eliminada con éxito",
|
||||||
"Page history": "Historial de la página",
|
"Page history": "Historial de la página",
|
||||||
"Select version": "Seleccionar versión",
|
|
||||||
"Highlight changes": "Resaltar cambios",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "La importación de la página está en curso. Por favor, no cierre esta pestaña.",
|
"Page import is in progress. Please do not close this tab.": "La importación de la página está en curso. Por favor, no cierre esta pestaña.",
|
||||||
"Pages": "Páginas",
|
"Pages": "Páginas",
|
||||||
"pages": "páginas",
|
"pages": "páginas",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "page",
|
"page": "page",
|
||||||
"Page deleted successfully": "Page supprimée avec succès",
|
"Page deleted successfully": "Page supprimée avec succès",
|
||||||
"Page history": "Historique de la page",
|
"Page history": "Historique de la page",
|
||||||
"Select version": "Sélectionner la version",
|
|
||||||
"Highlight changes": "Mettre en évidence les changements",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "L'importation de la page est en cours. Veuillez ne pas fermer cet onglet.",
|
"Page import is in progress. Please do not close this tab.": "L'importation de la page est en cours. Veuillez ne pas fermer cet onglet.",
|
||||||
"Pages": "Pages",
|
"Pages": "Pages",
|
||||||
"pages": "pages",
|
"pages": "pages",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "pagina",
|
"page": "pagina",
|
||||||
"Page deleted successfully": "Pagina eliminata con successo",
|
"Page deleted successfully": "Pagina eliminata con successo",
|
||||||
"Page history": "Cronologia della pagina",
|
"Page history": "Cronologia della pagina",
|
||||||
"Select version": "Seleziona versione",
|
|
||||||
"Highlight changes": "Evidenzia modifiche",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "L'importazione della pagina è in corso. Si prega di non chiudere questa scheda.",
|
"Page import is in progress. Please do not close this tab.": "L'importazione della pagina è in corso. Si prega di non chiudere questa scheda.",
|
||||||
"Pages": "Pagine",
|
"Pages": "Pagine",
|
||||||
"pages": "pagine",
|
"pages": "pagine",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "ページ",
|
"page": "ページ",
|
||||||
"Page deleted successfully": "ページを削除しました",
|
"Page deleted successfully": "ページを削除しました",
|
||||||
"Page history": "ページ履歴",
|
"Page history": "ページ履歴",
|
||||||
"Select version": "バージョンを選択",
|
|
||||||
"Highlight changes": "変更を強調表示",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "ページをインポート中です。このタブを閉じないでください",
|
"Page import is in progress. Please do not close this tab.": "ページをインポート中です。このタブを閉じないでください",
|
||||||
"Pages": "ページ",
|
"Pages": "ページ",
|
||||||
"pages": "ページ",
|
"pages": "ページ",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "페이지",
|
"page": "페이지",
|
||||||
"Page deleted successfully": "페이지 삭제 완료",
|
"Page deleted successfully": "페이지 삭제 완료",
|
||||||
"Page history": "페이지 기록",
|
"Page history": "페이지 기록",
|
||||||
"Select version": "버전 선택",
|
|
||||||
"Highlight changes": "변경 사항 강조",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "페이지 가져오기가 진행 중입니다. 이 탭을 닫지 마세요.",
|
"Page import is in progress. Please do not close this tab.": "페이지 가져오기가 진행 중입니다. 이 탭을 닫지 마세요.",
|
||||||
"Pages": "페이지",
|
"Pages": "페이지",
|
||||||
"pages": "페이지",
|
"pages": "페이지",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "pagina",
|
"page": "pagina",
|
||||||
"Page deleted successfully": "Pagina succesvol verwijderd",
|
"Page deleted successfully": "Pagina succesvol verwijderd",
|
||||||
"Page history": "Pagina geschiedenis",
|
"Page history": "Pagina geschiedenis",
|
||||||
"Select version": "Selecteer versie",
|
|
||||||
"Highlight changes": "Wijzigingen markeren",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "Importeren van pagina's is bezig. Sluit dit tabblad niet.",
|
"Page import is in progress. Please do not close this tab.": "Importeren van pagina's is bezig. Sluit dit tabblad niet.",
|
||||||
"Pages": "Pagina's",
|
"Pages": "Pagina's",
|
||||||
"pages": "pagina's",
|
"pages": "pagina's",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "página",
|
"page": "página",
|
||||||
"Page deleted successfully": "Página excluída com sucesso",
|
"Page deleted successfully": "Página excluída com sucesso",
|
||||||
"Page history": "Histórico da página",
|
"Page history": "Histórico da página",
|
||||||
"Select version": "Selecionar versão",
|
|
||||||
"Highlight changes": "Destacar alterações",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "A importação da página está em andamento. Por favor, não feche esta aba.",
|
"Page import is in progress. Please do not close this tab.": "A importação da página está em andamento. Por favor, não feche esta aba.",
|
||||||
"Pages": "Páginas",
|
"Pages": "Páginas",
|
||||||
"pages": "páginas",
|
"pages": "páginas",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "страница",
|
"page": "страница",
|
||||||
"Page deleted successfully": "Страница успешно удалена",
|
"Page deleted successfully": "Страница успешно удалена",
|
||||||
"Page history": "История страницы",
|
"Page history": "История страницы",
|
||||||
"Select version": "Выбрать версию",
|
|
||||||
"Highlight changes": "Выделить изменения",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "Импорт страницы в процессе. Пожалуйста, не закрывайте эту вкладку.",
|
"Page import is in progress. Please do not close this tab.": "Импорт страницы в процессе. Пожалуйста, не закрывайте эту вкладку.",
|
||||||
"Pages": "Страницы",
|
"Pages": "Страницы",
|
||||||
"pages": "страницы",
|
"pages": "страницы",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "сторінка",
|
"page": "сторінка",
|
||||||
"Page deleted successfully": "Сторінку успішно видалено",
|
"Page deleted successfully": "Сторінку успішно видалено",
|
||||||
"Page history": "Історія сторінки",
|
"Page history": "Історія сторінки",
|
||||||
"Select version": "Вибрати версію",
|
|
||||||
"Highlight changes": "Підсвітити зміни",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "Імпорт сторінки в процесі. Будь ласка, не закривайте цю вкладку.",
|
"Page import is in progress. Please do not close this tab.": "Імпорт сторінки в процесі. Будь ласка, не закривайте цю вкладку.",
|
||||||
"Pages": "Сторінки",
|
"Pages": "Сторінки",
|
||||||
"pages": "сторінки",
|
"pages": "сторінки",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "个页面",
|
"page": "个页面",
|
||||||
"Page deleted successfully": "页面已成功删除",
|
"Page deleted successfully": "页面已成功删除",
|
||||||
"Page history": "页面历史",
|
"Page history": "页面历史",
|
||||||
"Select version": "选择版本",
|
|
||||||
"Highlight changes": "突出显示更改",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。",
|
"Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。",
|
||||||
"Pages": "页面",
|
"Pages": "页面",
|
||||||
"pages": "个页面",
|
"pages": "个页面",
|
||||||
|
|||||||
@@ -170,8 +170,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "image/*";
|
input.accept = "image/*";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
input.style.display = "none";
|
|
||||||
document.body.appendChild(input);
|
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
for (const file of input.files) {
|
for (const file of input.files) {
|
||||||
@@ -181,7 +179,8 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input.remove();
|
// Reset the input value to allow uploading the same file again if needed
|
||||||
|
input.value = "";
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
},
|
},
|
||||||
@@ -203,8 +202,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "video/*";
|
input.accept = "video/*";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
input.style.display = "none";
|
|
||||||
document.body.appendChild(input);
|
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
for (const file of input.files) {
|
for (const file of input.files) {
|
||||||
@@ -214,7 +211,8 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input.remove();
|
// Reset the input value to allow uploading the same file again if needed
|
||||||
|
input.value = "";
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
},
|
},
|
||||||
@@ -236,8 +234,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "";
|
input.accept = "";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
input.style.display = "none";
|
|
||||||
document.body.appendChild(input);
|
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
for (const file of input.files) {
|
for (const file of input.files) {
|
||||||
@@ -247,7 +243,8 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input.remove();
|
// Reset the input value to allow uploading the same file again if needed
|
||||||
|
input.value = "";
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|||||||
import { EditorProvider } from "@tiptap/react";
|
import { EditorProvider } from "@tiptap/react";
|
||||||
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
||||||
import { Document } from "@tiptap/extension-document";
|
import { Document } from "@tiptap/extension-document";
|
||||||
import { Heading, UniqueID } from "@docmost/editor-ext";
|
import { Heading, generateNodeId, UniqueID } from "@docmost/editor-ext";
|
||||||
import { Text } from "@tiptap/extension-text";
|
import { Text } from "@tiptap/extension-text";
|
||||||
import { Placeholder } from "@tiptap/extension-placeholder";
|
import { Placeholder } from "@tiptap/extension-placeholder";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mantine-AppShell-main {
|
.mantine-AppShell-main {
|
||||||
padding: 0 !important;
|
padding-top: 0 !important;
|
||||||
min-height: auto !important;
|
min-height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,9 +157,7 @@ export function TitleEditor({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// guard against Cannot access view['hasFocus'] error
|
titleEditor?.commands.focus("end");
|
||||||
if (!titleEditor?.isInitialized) return;
|
|
||||||
titleEditor?.commands?.focus("end");
|
|
||||||
}, 500);
|
}, 500);
|
||||||
}, [titleEditor]);
|
}, [titleEditor]);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
.diffSummary {
|
||||||
|
border: rem(1px) solid
|
||||||
|
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||||
|
border-radius: rem(10px);
|
||||||
|
padding: rem(12px);
|
||||||
|
background: light-dark(
|
||||||
|
var(--mantine-color-gray-0),
|
||||||
|
var(--mantine-color-dark-7)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-added) {
|
||||||
|
background: light-dark(#e1f3f2, #01654a) !important;
|
||||||
|
color: light-dark(#007b69, #cafff7) !important;
|
||||||
|
-webkit-box-decoration-break: clone;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-deleted) {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: light-dark(var(--mantine-color-red-7), var(--mantine-color-red-4));
|
||||||
|
background: light-dark(var(--mantine-color-red-0), rgba(255, 0, 0, 0.1));
|
||||||
|
border-radius: rem(2px);
|
||||||
|
padding: 0 rem(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-node-added) {
|
||||||
|
outline: rem(2px) solid light-dark(var(--mantine-color-teal-5), var(--mantine-color-teal-7));
|
||||||
|
outline-offset: rem(2px);
|
||||||
|
border-radius: rem(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-node-deleted) {
|
||||||
|
opacity: 0.5;
|
||||||
|
outline: rem(2px) dashed light-dark(var(--mantine-color-red-4), var(--mantine-color-red-6));
|
||||||
|
outline-offset: rem(4px);
|
||||||
|
border-radius: rem(4px);
|
||||||
|
}
|
||||||
@@ -16,36 +16,6 @@
|
|||||||
:global(.ProseMirror) {
|
:global(.ProseMirror) {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
& :global(.history-diff-added) {
|
|
||||||
background: light-dark(#e1f3f2, #01654a) !important;
|
|
||||||
color: light-dark(#007b69, #cafff7) !important;
|
|
||||||
-webkit-box-decoration-break: clone;
|
|
||||||
box-decoration-break: clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
& :global(.history-diff-deleted) {
|
|
||||||
text-decoration: line-through;
|
|
||||||
color: light-dark(var(--mantine-color-red-7), var(--mantine-color-red-4));
|
|
||||||
background: light-dark(var(--mantine-color-red-0), rgba(255, 0, 0, 0.1));
|
|
||||||
border-radius: rem(2px);
|
|
||||||
padding: 0 rem(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
& :global(.history-diff-node-added) {
|
|
||||||
outline: rem(2px) solid
|
|
||||||
light-dark(var(--mantine-color-teal-5), var(--mantine-color-teal-7));
|
|
||||||
outline-offset: rem(2px);
|
|
||||||
border-radius: rem(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
& :global(.history-diff-node-deleted) {
|
|
||||||
opacity: 0.5;
|
|
||||||
outline: rem(2px) dashed
|
|
||||||
light-dark(var(--mantine-color-red-4), var(--mantine-color-red-6));
|
|
||||||
outline-offset: rem(4px);
|
|
||||||
border-radius: rem(4px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import "@/features/editor/styles/index.css";
|
import "@/features/editor/styles/index.css";
|
||||||
|
import "./css/history-diff.module.css";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { EditorContent, useEditor } from "@tiptap/react";
|
import { EditorContent, useEditor } from "@tiptap/react";
|
||||||
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
||||||
@@ -43,21 +44,21 @@ export function HistoryEditor({
|
|||||||
if (previousContent) {
|
if (previousContent) {
|
||||||
try {
|
try {
|
||||||
const schema = editor.schema;
|
const schema = editor.schema;
|
||||||
const oldContent = Node.fromJSON(schema, previousContent);
|
const docOld = Node.fromJSON(schema, previousContent);
|
||||||
const newContent = Node.fromJSON(schema, content);
|
const docNew = Node.fromJSON(schema, content);
|
||||||
|
|
||||||
const tr = recreateTransform(oldContent, newContent, {
|
const tr = recreateTransform(docOld, docNew, {
|
||||||
complexSteps: false,
|
complexSteps: true,
|
||||||
wordDiffs: true,
|
wordDiffs: true,
|
||||||
simplifyDiff: true,
|
simplifyDiff: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeSet = ChangeSet.create(oldContent).addSteps(
|
const changeSet = ChangeSet.create(docOld).addSteps(
|
||||||
tr.doc,
|
tr.doc,
|
||||||
tr.mapping.maps,
|
tr.mapping.maps,
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const changes = simplifyChanges(changeSet.changes, newContent);
|
const changes = simplifyChanges(changeSet.changes, docNew);
|
||||||
|
|
||||||
editor.commands.setContent(content);
|
editor.commands.setContent(content);
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ export function HistoryEditor({
|
|||||||
changeIndex++;
|
changeIndex++;
|
||||||
const currentIndex = changeIndex;
|
const currentIndex = changeIndex;
|
||||||
let foundSpecialNode: { node: Node; pos: number } | null = null;
|
let foundSpecialNode: { node: Node; pos: number } | null = null;
|
||||||
newContent.nodesBetween(change.fromB, change.toB, (node, pos) => {
|
docNew.nodesBetween(change.fromB, change.toB, (node, pos) => {
|
||||||
if (specialNodeTypes.has(node.type.name)) {
|
if (specialNodeTypes.has(node.type.name)) {
|
||||||
const nodeEnd = pos + node.nodeSize;
|
const nodeEnd = pos + node.nodeSize;
|
||||||
if (change.fromB <= pos && change.toB >= nodeEnd) {
|
if (change.fromB <= pos && change.toB >= nodeEnd) {
|
||||||
@@ -116,7 +117,7 @@ export function HistoryEditor({
|
|||||||
changeIndex++;
|
changeIndex++;
|
||||||
const currentIndex = changeIndex;
|
const currentIndex = changeIndex;
|
||||||
let foundDeletedNode: { node: Node; pos: number } | null = null;
|
let foundDeletedNode: { node: Node; pos: number } | null = null;
|
||||||
oldContent.nodesBetween(change.fromA, change.toA, (node, pos) => {
|
docOld.nodesBetween(change.fromA, change.toA, (node, pos) => {
|
||||||
if (specialNodeTypes.has(node.type.name)) {
|
if (specialNodeTypes.has(node.type.name)) {
|
||||||
const nodeEnd = pos + node.nodeSize;
|
const nodeEnd = pos + node.nodeSize;
|
||||||
if (change.fromA <= pos && change.toA >= nodeEnd) {
|
if (change.fromA <= pos && change.toA >= nodeEnd) {
|
||||||
@@ -139,7 +140,7 @@ export function HistoryEditor({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const deletedText = oldContent.textBetween(
|
const deletedText = docOld.textBetween(
|
||||||
change.fromA,
|
change.fromA,
|
||||||
change.toA,
|
change.toA,
|
||||||
"",
|
"",
|
||||||
@@ -160,7 +161,7 @@ export function HistoryEditor({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
decorationSet = DecorationSet.create(newContent, decorations);
|
decorationSet = DecorationSet.create(docNew, decorations);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("History diff failed:", e);
|
console.error("History diff failed:", e);
|
||||||
editor.commands.setContent(content);
|
editor.commands.setContent(content);
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ function HistoryList({ pageId }: Props) {
|
|||||||
size="compact-md"
|
size="compact-md"
|
||||||
onClick={() => setHistoryModalOpen(false)}
|
onClick={() => setHistoryModalOpen(false)}
|
||||||
>
|
>
|
||||||
{t("Cancel")}
|
{t("Close")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="compact-md" onClick={confirmRestore}>
|
<Button size="compact-md" onClick={confirmRestore}>
|
||||||
{t("Restore")}
|
{t("Restore")}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export default function HistoryModalMobile({ pageId, pageTitle }: Props) {
|
|||||||
{canRestore && (
|
{canRestore && (
|
||||||
<Group className={classes.actionButtons} justify="flex-end" gap="sm">
|
<Group className={classes.actionButtons} justify="flex-end" gap="sm">
|
||||||
<Button variant="default" onClick={() => setHistoryModalOpen(false)}>
|
<Button variant="default" onClick={() => setHistoryModalOpen(false)}>
|
||||||
{t("Cancel")}
|
{t("Close")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={confirmRestore}>{t("Restore")}</Button>
|
<Button onClick={confirmRestore}>{t("Restore")}</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
+11
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.25.1",
|
"version": "0.25.0-beta.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -33,28 +33,28 @@
|
|||||||
"@ai-sdk/google": "^3.0.9",
|
"@ai-sdk/google": "^3.0.9",
|
||||||
"@ai-sdk/openai": "^3.0.11",
|
"@ai-sdk/openai": "^3.0.11",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.12",
|
"@ai-sdk/openai-compatible": "^2.0.12",
|
||||||
"@aws-sdk/client-s3": "3.982.0",
|
"@aws-sdk/client-s3": "3.701.0",
|
||||||
"@aws-sdk/lib-storage": "3.982.0",
|
"@aws-sdk/lib-storage": "3.701.0",
|
||||||
"@aws-sdk/s3-request-presigner": "3.982.0",
|
"@aws-sdk/s3-request-presigner": "3.701.0",
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/multipart": "^9.4.0",
|
"@fastify/multipart": "^9.3.0",
|
||||||
"@fastify/static": "^9.0.0",
|
"@fastify/static": "^8.3.0",
|
||||||
"@langchain/core": "1.1.18",
|
"@langchain/core": "1.1.13",
|
||||||
"@langchain/textsplitters": "1.0.1",
|
"@langchain/textsplitters": "1.0.1",
|
||||||
"@nestjs-labs/nestjs-ioredis": "^11.0.4",
|
"@nestjs-labs/nestjs-ioredis": "^11.0.4",
|
||||||
"@nestjs/bullmq": "^11.0.4",
|
"@nestjs/bullmq": "^11.0.4",
|
||||||
"@nestjs/common": "^11.1.11",
|
"@nestjs/common": "^11.1.11",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.1.13",
|
"@nestjs/core": "^11.1.11",
|
||||||
"@nestjs/event-emitter": "^3.0.1",
|
"@nestjs/event-emitter": "^3.0.1",
|
||||||
"@nestjs/jwt": "11.0.0",
|
"@nestjs/jwt": "11.0.0",
|
||||||
"@nestjs/mapped-types": "^2.1.0",
|
"@nestjs/mapped-types": "^2.1.0",
|
||||||
"@nestjs/passport": "^11.0.5",
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-fastify": "^11.1.13",
|
"@nestjs/platform-fastify": "^11.1.11",
|
||||||
"@nestjs/platform-socket.io": "^11.1.13",
|
"@nestjs/platform-socket.io": "^11.1.11",
|
||||||
"@nestjs/schedule": "^6.1.0",
|
"@nestjs/schedule": "^6.1.0",
|
||||||
"@nestjs/terminus": "^11.0.0",
|
"@nestjs/terminus": "^11.0.0",
|
||||||
"@nestjs/websockets": "^11.1.13",
|
"@nestjs/websockets": "^11.1.11",
|
||||||
"@node-saml/passport-saml": "^5.1.0",
|
"@node-saml/passport-saml": "^5.1.0",
|
||||||
"@react-email/components": "0.0.28",
|
"@react-email/components": "0.0.28",
|
||||||
"@react-email/render": "1.0.2",
|
"@react-email/render": "1.0.2",
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ export class HistoryListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastHistory = await this.pageHistoryRepo.findPageLastHistory(page.id, {
|
const lastHistory = await this.pageHistoryRepo.findPageLastHistory(page.id);
|
||||||
includeContent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!lastHistory ||
|
!lastHistory ||
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import {
|
|||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AttachmentService } from './services/attachment.service';
|
import { AttachmentService } from './services/attachment.service';
|
||||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
import { FastifyReply } from 'fastify';
|
||||||
import { FileInterceptor } from '../../common/interceptors/file.interceptor';
|
import { FileInterceptor } from '../../common/interceptors/file.interceptor';
|
||||||
import * as bytes from 'bytes';
|
import * as bytes from 'bytes';
|
||||||
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
||||||
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { Attachment, User, Workspace } from '@docmost/db/types/entity.types';
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
import { StorageService } from '../../integrations/storage/storage.service';
|
import { StorageService } from '../../integrations/storage/storage.service';
|
||||||
import {
|
import {
|
||||||
getAttachmentFolderPath,
|
getAttachmentFolderPath,
|
||||||
@@ -151,7 +151,6 @@ export class AttachmentController {
|
|||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get('/files/:fileId/:fileName')
|
@Get('/files/:fileId/:fileName')
|
||||||
async getFile(
|
async getFile(
|
||||||
@Req() req: FastifyRequest,
|
|
||||||
@Res() res: FastifyReply,
|
@Res() res: FastifyReply,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@@ -182,7 +181,22 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.sendFileResponse(req, res, attachment, 'private');
|
const fileStream = await this.storageService.readStream(
|
||||||
|
attachment.filePath,
|
||||||
|
);
|
||||||
|
res.headers({
|
||||||
|
'Content-Type': attachment.mimeType,
|
||||||
|
'Cache-Control': 'private, max-age=3600',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!inlineFileExtensions.includes(attachment.fileExt)) {
|
||||||
|
res.header(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send(fileStream);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
throw new NotFoundException('File not found');
|
throw new NotFoundException('File not found');
|
||||||
@@ -191,7 +205,6 @@ export class AttachmentController {
|
|||||||
|
|
||||||
@Get('/files/public/:fileId/:fileName')
|
@Get('/files/public/:fileId/:fileName')
|
||||||
async getPublicFile(
|
async getPublicFile(
|
||||||
@Req() req: FastifyRequest,
|
|
||||||
@Res() res: FastifyReply,
|
@Res() res: FastifyReply,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@Param('fileId') fileId: string,
|
@Param('fileId') fileId: string,
|
||||||
@@ -230,7 +243,22 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.sendFileResponse(req, res, attachment, 'public');
|
const fileStream = await this.storageService.readStream(
|
||||||
|
attachment.filePath,
|
||||||
|
);
|
||||||
|
res.headers({
|
||||||
|
'Content-Type': attachment.mimeType,
|
||||||
|
'Cache-Control': 'public, max-age=3600',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!inlineFileExtensions.includes(attachment.fileExt)) {
|
||||||
|
res.header(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send(fileStream);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
throw new NotFoundException('File not found');
|
throw new NotFoundException('File not found');
|
||||||
@@ -405,69 +433,4 @@ export class AttachmentController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendFileResponse(
|
|
||||||
req: FastifyRequest,
|
|
||||||
res: FastifyReply,
|
|
||||||
attachment: Attachment,
|
|
||||||
cacheScope: 'private' | 'public',
|
|
||||||
) {
|
|
||||||
const fileSize = Number(attachment.fileSize);
|
|
||||||
const rangeHeader = req.headers.range;
|
|
||||||
|
|
||||||
res.header('Accept-Ranges', 'bytes');
|
|
||||||
|
|
||||||
if (!inlineFileExtensions.includes(attachment.fileExt)) {
|
|
||||||
res.header(
|
|
||||||
'Content-Disposition',
|
|
||||||
`attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rangeHeader && fileSize) {
|
|
||||||
const match = rangeHeader.match(/bytes=(\d+)-(\d*)/);
|
|
||||||
if (match) {
|
|
||||||
const start = parseInt(match[1], 10);
|
|
||||||
const end = match[2]
|
|
||||||
? Math.min(parseInt(match[2], 10), fileSize - 1)
|
|
||||||
: fileSize - 1;
|
|
||||||
|
|
||||||
if (start >= fileSize || start > end) {
|
|
||||||
res.status(416);
|
|
||||||
res.header('Content-Range', `bytes */${fileSize}`);
|
|
||||||
return res.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileStream = await this.storageService.readRangeStream(
|
|
||||||
attachment.filePath,
|
|
||||||
{ start, end },
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(206);
|
|
||||||
res.headers({
|
|
||||||
'Content-Type': attachment.mimeType,
|
|
||||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
|
||||||
'Content-Length': end - start + 1,
|
|
||||||
'Cache-Control': `${cacheScope}, max-age=3600`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.send(fileStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileStream = await this.storageService.readStream(
|
|
||||||
attachment.filePath,
|
|
||||||
);
|
|
||||||
|
|
||||||
res.headers({
|
|
||||||
'Content-Type': attachment.mimeType,
|
|
||||||
'Cache-Control': `${cacheScope}, max-age=3600`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fileSize) {
|
|
||||||
res.header('Content-Length', fileSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send(fileStream);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ export class PageController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: scope to workspaces
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/history')
|
@Post('/history')
|
||||||
async getPageHistory(
|
async getPageHistory(
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ export class PageHistoryService {
|
|||||||
constructor(private pageHistoryRepo: PageHistoryRepo) {}
|
constructor(private pageHistoryRepo: PageHistoryRepo) {}
|
||||||
|
|
||||||
async findById(historyId: string): Promise<PageHistory> {
|
async findById(historyId: string): Promise<PageHistory> {
|
||||||
return await this.pageHistoryRepo.findById(historyId, {
|
return await this.pageHistoryRepo.findById(historyId);
|
||||||
includeContent: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findHistoryByPageId(
|
async findHistoryByPageId(
|
||||||
|
|||||||
@@ -17,32 +17,15 @@ import { DB } from '@docmost/db/types/db';
|
|||||||
export class PageHistoryRepo {
|
export class PageHistoryRepo {
|
||||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||||
|
|
||||||
private baseFields: Array<keyof PageHistory> = [
|
|
||||||
'id',
|
|
||||||
'pageId',
|
|
||||||
'slugId',
|
|
||||||
'title',
|
|
||||||
'icon',
|
|
||||||
'coverPhoto',
|
|
||||||
'lastUpdatedById',
|
|
||||||
'spaceId',
|
|
||||||
'workspaceId',
|
|
||||||
'createdAt',
|
|
||||||
];
|
|
||||||
|
|
||||||
async findById(
|
async findById(
|
||||||
pageHistoryId: string,
|
pageHistoryId: string,
|
||||||
opts?: {
|
trx?: KyselyTransaction,
|
||||||
includeContent?: boolean;
|
|
||||||
trx?: KyselyTransaction;
|
|
||||||
},
|
|
||||||
): Promise<PageHistory> {
|
): Promise<PageHistory> {
|
||||||
const db = dbOrTx(this.db, opts?.trx);
|
const db = dbOrTx(this.db, trx);
|
||||||
|
|
||||||
return await db
|
return await db
|
||||||
.selectFrom('pageHistory')
|
.selectFrom('pageHistory')
|
||||||
.select(this.baseFields)
|
.selectAll()
|
||||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
|
||||||
.select((eb) => this.withLastUpdatedBy(eb))
|
.select((eb) => this.withLastUpdatedBy(eb))
|
||||||
.where('id', '=', pageHistoryId)
|
.where('id', '=', pageHistoryId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
@@ -80,7 +63,7 @@ export class PageHistoryRepo {
|
|||||||
async findPageHistoryByPageId(pageId: string, pagination: PaginationOptions) {
|
async findPageHistoryByPageId(pageId: string, pagination: PaginationOptions) {
|
||||||
const query = this.db
|
const query = this.db
|
||||||
.selectFrom('pageHistory')
|
.selectFrom('pageHistory')
|
||||||
.select(this.baseFields)
|
.selectAll()
|
||||||
.select((eb) => this.withLastUpdatedBy(eb))
|
.select((eb) => this.withLastUpdatedBy(eb))
|
||||||
.where('pageId', '=', pageId);
|
.where('pageId', '=', pageId);
|
||||||
|
|
||||||
@@ -93,19 +76,12 @@ export class PageHistoryRepo {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findPageLastHistory(
|
async findPageLastHistory(pageId: string, trx?: KyselyTransaction) {
|
||||||
pageId: string,
|
const db = dbOrTx(this.db, trx);
|
||||||
opts?: {
|
|
||||||
includeContent?: boolean;
|
|
||||||
trx?: KyselyTransaction;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const db = dbOrTx(this.db, opts?.trx);
|
|
||||||
|
|
||||||
return await db
|
return await db
|
||||||
.selectFrom('pageHistory')
|
.selectFrom('pageHistory')
|
||||||
.select(this.baseFields)
|
.selectAll()
|
||||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
|
||||||
.where('pageId', '=', pageId)
|
.where('pageId', '=', pageId)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.orderBy('createdAt', 'desc')
|
.orderBy('createdAt', 'desc')
|
||||||
|
|||||||
@@ -73,20 +73,6 @@ export class LocalDriver implements StorageDriver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readRangeStream(
|
|
||||||
filePath: string,
|
|
||||||
range: { start: number; end: number },
|
|
||||||
): Promise<Readable> {
|
|
||||||
try {
|
|
||||||
return createReadStream(this._fullPath(filePath), {
|
|
||||||
start: range.start,
|
|
||||||
end: range.end,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Failed to read file: ${(err as Error).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async exists(filePath: string): Promise<boolean> {
|
async exists(filePath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
return await fs.pathExists(this._fullPath(filePath));
|
return await fs.pathExists(this._fullPath(filePath));
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { Readable } from 'stream';
|
|||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||||
import { getMimeType } from '../../../common/helpers';
|
import { getMimeType } from '../../../common/helpers';
|
||||||
import { Upload } from '@aws-sdk/lib-storage';
|
import { Upload } from '@aws-sdk/lib-storage';
|
||||||
import { Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
export class S3Driver implements StorageDriver {
|
export class S3Driver implements StorageDriver {
|
||||||
private readonly s3Client: S3Client;
|
private readonly s3Client: S3Client;
|
||||||
@@ -40,7 +39,6 @@ export class S3Driver implements StorageDriver {
|
|||||||
|
|
||||||
await upload.done();
|
await upload.done();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(err);
|
|
||||||
throw new Error(`Failed to upload file: ${(err as Error).message}`);
|
throw new Error(`Failed to upload file: ${(err as Error).message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +73,6 @@ export class S3Driver implements StorageDriver {
|
|||||||
|
|
||||||
await upload.done();
|
await upload.done();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(err);
|
|
||||||
throw new Error(`Failed to upload file: ${(err as Error).message}`);
|
throw new Error(`Failed to upload file: ${(err as Error).message}`);
|
||||||
} finally {
|
} finally {
|
||||||
if (shouldDestroyClient && clientToUse) {
|
if (shouldDestroyClient && clientToUse) {
|
||||||
@@ -130,25 +127,6 @@ export class S3Driver implements StorageDriver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readRangeStream(
|
|
||||||
filePath: string,
|
|
||||||
range: { start: number; end: number },
|
|
||||||
): Promise<Readable> {
|
|
||||||
try {
|
|
||||||
const command = new GetObjectCommand({
|
|
||||||
Bucket: this.config.bucket,
|
|
||||||
Key: filePath,
|
|
||||||
Range: `bytes=${range.start}-${range.end}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await this.s3Client.send(command);
|
|
||||||
|
|
||||||
return response.Body as Readable;
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Failed to read file from S3: ${(err as Error).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async exists(filePath: string): Promise<boolean> {
|
async exists(filePath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const command = new HeadObjectCommand({
|
const command = new HeadObjectCommand({
|
||||||
|
|||||||
@@ -11,11 +11,6 @@ export interface StorageDriver {
|
|||||||
|
|
||||||
readStream(filePath: string): Promise<Readable>;
|
readStream(filePath: string): Promise<Readable>;
|
||||||
|
|
||||||
readRangeStream(
|
|
||||||
filePath: string,
|
|
||||||
range: { start: number; end: number },
|
|
||||||
): Promise<Readable>;
|
|
||||||
|
|
||||||
exists(filePath: string): Promise<boolean>;
|
exists(filePath: string): Promise<boolean>;
|
||||||
|
|
||||||
getUrl(filePath: string): string;
|
getUrl(filePath: string): string;
|
||||||
|
|||||||
@@ -33,13 +33,6 @@ export class StorageService {
|
|||||||
return this.storageDriver.readStream(filePath);
|
return this.storageDriver.readStream(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async readRangeStream(
|
|
||||||
filePath: string,
|
|
||||||
range: { start: number; end: number },
|
|
||||||
): Promise<Readable> {
|
|
||||||
return this.storageDriver.readRangeStream(filePath, range);
|
|
||||||
}
|
|
||||||
|
|
||||||
async exists(filePath: string): Promise<boolean> {
|
async exists(filePath: string): Promise<boolean> {
|
||||||
return this.storageDriver.exists(filePath);
|
return this.storageDriver.exists(filePath);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.25.1",
|
"version": "0.25.0-beta.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
"@casl/ability": "6.8.0",
|
"@casl/ability": "6.8.0",
|
||||||
"@docmost/editor-ext": "workspace:*",
|
"@docmost/editor-ext": "workspace:*",
|
||||||
"@floating-ui/dom": "^1.7.3",
|
"@floating-ui/dom": "^1.7.3",
|
||||||
"@hocuspocus/provider": "3.4.4",
|
"@hocuspocus/provider": "3.4.3",
|
||||||
"@hocuspocus/server": "3.4.4",
|
"@hocuspocus/server": "3.4.3",
|
||||||
"@hocuspocus/transformer": "3.4.4",
|
"@hocuspocus/transformer": "3.4.3",
|
||||||
"@joplin/turndown": "^4.0.74",
|
"@joplin/turndown": "^4.0.74",
|
||||||
"@joplin/turndown-plugin-gfm": "^1.0.56",
|
"@joplin/turndown-plugin-gfm": "^1.0.56",
|
||||||
"@sindresorhus/slugify": "1.1.0",
|
"@sindresorhus/slugify": "1.1.0",
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# prosemirror-changeset
|
||||||
|
|
||||||
|
This is a helper module that can turn a sequence of document changes
|
||||||
|
into a set of insertions and deletions, for example to display them in
|
||||||
|
a change-tracking interface. Such a set can be built up incrementally,
|
||||||
|
in order to do such change tracking in a halfway performant way during
|
||||||
|
live editing.
|
||||||
|
|
||||||
|
This code is licensed under an [MIT
|
||||||
|
licence](https://github.com/ProseMirror/prosemirror-changeset/blob/master/LICENSE).
|
||||||
|
|
||||||
|
## Programming interface
|
||||||
|
|
||||||
|
Insertions and deletions are represented as ‘spans’—ranges in the
|
||||||
|
document. The deleted spans refer to the original document, whereas
|
||||||
|
the inserted ones point into the current document.
|
||||||
|
|
||||||
|
It is possible to associate arbitrary data values with such spans, for
|
||||||
|
example to track the user that made the change, the timestamp at which
|
||||||
|
it was made, or the step data necessary to invert it again.
|
||||||
|
|
||||||
|
### class Change`<Data = any>`
|
||||||
|
|
||||||
|
A replaced range with metadata associated with it.
|
||||||
|
|
||||||
|
* **`fromA`**`: number`\
|
||||||
|
The start of the range deleted/replaced in the old document.
|
||||||
|
|
||||||
|
* **`toA`**`: number`\
|
||||||
|
The end of the range in the old document.
|
||||||
|
|
||||||
|
* **`fromB`**`: number`\
|
||||||
|
The start of the range inserted in the new document.
|
||||||
|
|
||||||
|
* **`toB`**`: number`\
|
||||||
|
The end of the range in the new document.
|
||||||
|
|
||||||
|
* **`deleted`**`: readonly Span[]`\
|
||||||
|
Data associated with the deleted content. The length of these
|
||||||
|
spans adds up to `this.toA - this.fromA`.
|
||||||
|
|
||||||
|
* **`inserted`**`: readonly Span[]`\
|
||||||
|
Data associated with the inserted content. Length adds up to
|
||||||
|
`this.toB - this.fromB`.
|
||||||
|
|
||||||
|
* `static `**`merge`**`<Data>(x: readonly Change[], y: readonly Change[], combine: fn(dataA: Data, dataB: Data) → Data) → readonly Change[]`\
|
||||||
|
This merges two changesets (the end document of x should be the
|
||||||
|
start document of y) into a single one spanning the start of x to
|
||||||
|
the end of y.
|
||||||
|
|
||||||
|
|
||||||
|
### class Span`<Data = any>`
|
||||||
|
|
||||||
|
Stores metadata for a part of a change.
|
||||||
|
|
||||||
|
* **`length`**`: number`\
|
||||||
|
The length of this span.
|
||||||
|
|
||||||
|
* **`data`**`: Data`\
|
||||||
|
The data associated with this span.
|
||||||
|
|
||||||
|
|
||||||
|
### class ChangeSet`<Data = any>`
|
||||||
|
|
||||||
|
A change set tracks the changes to a document from a given point
|
||||||
|
in the past. It condenses a number of step maps down to a flat
|
||||||
|
sequence of replacements, and simplifies replacments that
|
||||||
|
partially undo themselves by comparing their content.
|
||||||
|
|
||||||
|
* **`changes`**`: readonly Change[]`\
|
||||||
|
Replaced regions.
|
||||||
|
|
||||||
|
* **`addSteps`**`(newDoc: Node, maps: readonly StepMap[], data: Data | readonly Data[]) → ChangeSet`\
|
||||||
|
Computes a new changeset by adding the given step maps and
|
||||||
|
metadata (either as an array, per-map, or as a single value to be
|
||||||
|
associated with all maps) to the current set. Will not mutate the
|
||||||
|
old set.
|
||||||
|
|
||||||
|
Note that due to simplification that happens after each add,
|
||||||
|
incrementally adding steps might create a different final set
|
||||||
|
than adding all those changes at once, since different document
|
||||||
|
tokens might be matched during simplification depending on the
|
||||||
|
boundaries of the current changed ranges.
|
||||||
|
|
||||||
|
* **`startDoc`**`: Node`\
|
||||||
|
The starting document of the change set.
|
||||||
|
|
||||||
|
* **`map`**`(f: fn(range: Span) → Data) → ChangeSet`\
|
||||||
|
Map the span's data values in the given set through a function
|
||||||
|
and construct a new set with the resulting data.
|
||||||
|
|
||||||
|
* **`changedRange`**`(b: ChangeSet, maps?: readonly StepMap[]) → {from: number, to: number}`\
|
||||||
|
Compare two changesets and return the range in which they are
|
||||||
|
changed, if any. If the document changed between the maps, pass
|
||||||
|
the maps for the steps that changed it as second argument, and
|
||||||
|
make sure the method is called on the old set and passed the new
|
||||||
|
set. The returned positions will be in new document coordinates.
|
||||||
|
|
||||||
|
* `static `**`create`**`<Data = any>(doc: Node, combine?: fn(dataA: Data, dataB: Data) → Data = (a, b) => a === b ? a : null as any, tokenEncoder?: TokenEncoder = DefaultEncoder) → ChangeSet`\
|
||||||
|
Create a changeset with the given base object and configuration.
|
||||||
|
|
||||||
|
The `combine` function is used to compare and combine metadata—it
|
||||||
|
should return null when metadata isn't compatible, and a combined
|
||||||
|
version for a merged range when it is.
|
||||||
|
|
||||||
|
When given, a token encoder determines how document tokens are
|
||||||
|
serialized and compared when diffing the content produced by
|
||||||
|
changes. The default is to just compare nodes by name and text
|
||||||
|
by character, ignoring marks and attributes.
|
||||||
|
|
||||||
|
|
||||||
|
* **`simplifyChanges`**`(changes: readonly Change[], doc: Node) → Change[]`\
|
||||||
|
Simplifies a set of changes for presentation. This makes the
|
||||||
|
assumption that having both insertions and deletions within a word
|
||||||
|
is confusing, and, when such changes occur without a word boundary
|
||||||
|
between them, they should be expanded to cover the entire set of
|
||||||
|
words (in the new document) they touch. An exception is made for
|
||||||
|
single-character replacements.
|
||||||
|
|
||||||
|
|
||||||
|
### interface TokenEncoder`<T>`
|
||||||
|
|
||||||
|
A token encoder can be passed when creating a `ChangeSet` in order
|
||||||
|
to influence the way the library runs its diffing algorithm. The
|
||||||
|
encoder determines how document tokens (such as nodes and
|
||||||
|
characters) are encoded and compared.
|
||||||
|
|
||||||
|
Note that both the encoding and the comparison may run a lot, and
|
||||||
|
doing non-trivial work in these functions could impact
|
||||||
|
performance.
|
||||||
|
|
||||||
|
* **`encodeCharacter`**`(char: number, marks: readonly Mark[]) → T`\
|
||||||
|
Encode a given character, with the given marks applied.
|
||||||
|
|
||||||
|
* **`encodeNodeStart`**`(node: Node) → T`\
|
||||||
|
Encode the start of a node or, if this is a leaf node, the
|
||||||
|
entire node.
|
||||||
|
|
||||||
|
* **`encodeNodeEnd`**`(node: Node) → T`\
|
||||||
|
Encode the end token for the given node. It is valid to encode
|
||||||
|
every end token in the same way.
|
||||||
|
|
||||||
|
* **`compareTokens`**`(a: T, b: T) → boolean`\
|
||||||
|
Compare the given tokens. Should return true when they count as
|
||||||
|
equal.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# prosemirror-recreate-transform
|
||||||
|
|
||||||
|
> reduced and modified fork of https://gitlab.com/mpapp-public/prosemirror-recreate-steps
|
||||||
|
|
||||||
|
This is a non-core module of [ProseMirror](http://prosemirror.net).
|
||||||
|
ProseMirror is a well-behaved rich semantic content editor based on
|
||||||
|
contentEditable, with support for collaborative editing and custom
|
||||||
|
document schemas.
|
||||||
|
|
||||||
|
Every change to the document is recorded by ProseMirror as a step.
|
||||||
|
This module allows recreating the steps needed to go from document
|
||||||
|
A to B should these not be available otherwise. Recreating steps
|
||||||
|
can be interesting for example in order to show the changes between
|
||||||
|
two document versions without having access to the original steps.
|
||||||
|
|
||||||
|
Recreating a `Transform` works this way:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { recreateTransform } from "@technik-sde/prosemirror-recreate-transform";
|
||||||
|
|
||||||
|
let tr = recreateTransform(
|
||||||
|
startDoc,
|
||||||
|
endDoc,
|
||||||
|
{
|
||||||
|
complexSteps: true, // Whether step types other than ReplaceStep are allowed.
|
||||||
|
wordDiffs: false, // Whether diffs in text nodes should cover entire words.
|
||||||
|
simplifyDiffs: true // Whether steps should be merged, where possible
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { imageDimensionsFromData } from 'image-dimensions';
|
import { imageDimensionsFromStream } from "image-dimensions";
|
||||||
import { MediaUploadOptions, UploadFn } from '../media-utils';
|
import { MediaUploadOptions, UploadFn } from "../media-utils";
|
||||||
import { IAttachment } from '../types';
|
import { IAttachment } from "../types";
|
||||||
import { generateNodeId } from '../utils';
|
import { generateNodeId } from "../utils";
|
||||||
import { Node } from '@tiptap/pm/model';
|
import { Node } from "@tiptap/pm/model";
|
||||||
import { Command } from '@tiptap/core';
|
import { Command } from "@tiptap/core";
|
||||||
|
|
||||||
const findImageNodeByPlaceholderId = (
|
const findImageNodeByPlaceholderId = (
|
||||||
doc: Node,
|
doc: Node,
|
||||||
@@ -14,7 +14,7 @@ const findImageNodeByPlaceholderId = (
|
|||||||
doc.descendants((node, pos) => {
|
doc.descendants((node, pos) => {
|
||||||
if (result) return false;
|
if (result) return false;
|
||||||
if (
|
if (
|
||||||
node.type.name === 'image' &&
|
node.type.name === "image" &&
|
||||||
node.attrs.placeholder?.id === placeholderId
|
node.attrs.placeholder?.id === placeholderId
|
||||||
) {
|
) {
|
||||||
result = { node, pos };
|
result = { node, pos };
|
||||||
@@ -34,11 +34,7 @@ const handleImageUpload =
|
|||||||
if (!validated) return;
|
if (!validated) return;
|
||||||
|
|
||||||
const objectUrl = URL.createObjectURL(file);
|
const objectUrl = URL.createObjectURL(file);
|
||||||
|
const imageDimensions = await imageDimensionsFromStream(file.stream());
|
||||||
const imageDimensions = imageDimensionsFromData(
|
|
||||||
new Uint8Array(await file.arrayBuffer()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const placeholderId = generateNodeId();
|
const placeholderId = generateNodeId();
|
||||||
const aspectRatio = imageDimensions
|
const aspectRatio = imageDimensions
|
||||||
? imageDimensions.width / imageDimensions.height
|
? imageDimensions.width / imageDimensions.height
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// https://gitlab.com/mpapp-public/prosemirror-recreate-steps - MIT
|
// https://gitlab.com/mpapp-public/prosemirror-recreate-steps
|
||||||
// https://github.com/sueddeutsche/prosemirror-recreate-transform - MIT
|
// https://github.com/sueddeutsche/prosemirror-recreate-transform
|
||||||
export { recreateTransform, RecreateTransform } from "./recreateTransform";
|
export { recreateTransform, RecreateTransform } from "./recreateTransform";
|
||||||
export type { Options } from "./recreateTransform";
|
export type { Options } from "./recreateTransform";
|
||||||
|
|||||||
Generated
+1083
-1094
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user