mirror of
https://github.com/docmost/docmost.git
synced 2026-05-10 16:24:05 +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",
|
||||
"private": true,
|
||||
"version": "0.25.1",
|
||||
"version": "0.25.0-beta.1",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
"page": "Seite",
|
||||
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
||||
"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.",
|
||||
"Pages": "Seiten",
|
||||
"pages": "Seiten",
|
||||
|
||||
@@ -123,7 +123,10 @@
|
||||
"page": "page",
|
||||
"Page deleted successfully": "Page deleted successfully",
|
||||
"Page history": "Page history",
|
||||
"Version history for": "Version history for",
|
||||
"document": "document",
|
||||
"Select version": "Select version",
|
||||
"Close": "Close",
|
||||
"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.",
|
||||
"Pages": "Pages",
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
"page": "página",
|
||||
"Page deleted successfully": "Página eliminada con éxito",
|
||||
"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.",
|
||||
"Pages": "Páginas",
|
||||
"pages": "páginas",
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
"page": "page",
|
||||
"Page deleted successfully": "Page supprimée avec succès",
|
||||
"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.",
|
||||
"Pages": "Pages",
|
||||
"pages": "pages",
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
"page": "pagina",
|
||||
"Page deleted successfully": "Pagina eliminata con successo",
|
||||
"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.",
|
||||
"Pages": "Pagine",
|
||||
"pages": "pagine",
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
"page": "ページ",
|
||||
"Page deleted successfully": "ページを削除しました",
|
||||
"Page history": "ページ履歴",
|
||||
"Select version": "バージョンを選択",
|
||||
"Highlight changes": "変更を強調表示",
|
||||
"Page import is in progress. Please do not close this tab.": "ページをインポート中です。このタブを閉じないでください",
|
||||
"Pages": "ページ",
|
||||
"pages": "ページ",
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
"page": "페이지",
|
||||
"Page deleted successfully": "페이지 삭제 완료",
|
||||
"Page history": "페이지 기록",
|
||||
"Select version": "버전 선택",
|
||||
"Highlight changes": "변경 사항 강조",
|
||||
"Page import is in progress. Please do not close this tab.": "페이지 가져오기가 진행 중입니다. 이 탭을 닫지 마세요.",
|
||||
"Pages": "페이지",
|
||||
"pages": "페이지",
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
"page": "pagina",
|
||||
"Page deleted successfully": "Pagina succesvol verwijderd",
|
||||
"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.",
|
||||
"Pages": "Pagina's",
|
||||
"pages": "pagina's",
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
"page": "página",
|
||||
"Page deleted successfully": "Página excluída com sucesso",
|
||||
"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.",
|
||||
"Pages": "Páginas",
|
||||
"pages": "páginas",
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
"page": "страница",
|
||||
"Page deleted successfully": "Страница успешно удалена",
|
||||
"Page history": "История страницы",
|
||||
"Select version": "Выбрать версию",
|
||||
"Highlight changes": "Выделить изменения",
|
||||
"Page import is in progress. Please do not close this tab.": "Импорт страницы в процессе. Пожалуйста, не закрывайте эту вкладку.",
|
||||
"Pages": "Страницы",
|
||||
"pages": "страницы",
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
"page": "сторінка",
|
||||
"Page deleted successfully": "Сторінку успішно видалено",
|
||||
"Page history": "Історія сторінки",
|
||||
"Select version": "Вибрати версію",
|
||||
"Highlight changes": "Підсвітити зміни",
|
||||
"Page import is in progress. Please do not close this tab.": "Імпорт сторінки в процесі. Будь ласка, не закривайте цю вкладку.",
|
||||
"Pages": "Сторінки",
|
||||
"pages": "сторінки",
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
"page": "个页面",
|
||||
"Page deleted successfully": "页面已成功删除",
|
||||
"Page history": "页面历史",
|
||||
"Select version": "选择版本",
|
||||
"Highlight changes": "突出显示更改",
|
||||
"Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。",
|
||||
"Pages": "页面",
|
||||
"pages": "个页面",
|
||||
|
||||
@@ -8,5 +8,3 @@ export const titleEditorAtom = atom<Editor | null>(null);
|
||||
export const readOnlyEditorAtom = atom<Editor | null>(null);
|
||||
|
||||
export const yjsConnectionStatusAtom = atom<string>("");
|
||||
|
||||
export const showAiMenuAtom = atom(false);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
.aiMenu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 2.25rem;
|
||||
}
|
||||
.menuItemSelected {
|
||||
background-color: var(--mantine-color-gray-1);
|
||||
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
|
||||
.resultPreviewWrapper {
|
||||
*:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
*:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { ActionIcon, TextInput, Tooltip } from "@mantine/core";
|
||||
import { useDebouncedCallback, useMediaQuery } from "@mantine/hooks";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useAtom } from "jotai";
|
||||
import { IconSend } from "@tabler/icons-react";
|
||||
import { showAiMenuAtom } from "@/features/editor/atoms/editor-atoms";
|
||||
import { useAiGenerateStreamMutation } from "@/ee/ai/queries/ai-query";
|
||||
import { AiAction } from "@/ee/ai/types/ai.types";
|
||||
import { CommandItem, commandItems, CommandSet } from "./command-items";
|
||||
import { CommandSelector } from "./command-selector";
|
||||
import { ResultPreview } from "./result-preview";
|
||||
import classes from "./ai-menu.module.css";
|
||||
import { marked } from "marked";
|
||||
|
||||
interface EditorAiMenuProps {
|
||||
editor: Editor | null;
|
||||
}
|
||||
|
||||
const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
|
||||
const aiGenerateStreamMutation = useAiGenerateStreamMutation();
|
||||
const isSmBreakpoint = useMediaQuery("(max-width: 48em)");
|
||||
const [showAiMenu, setShowAiMenu] = useAtom(showAiMenuAtom);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [output, setOutput] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [activeCommandSet, setActiveCommandSet] = useState<CommandSet>("main");
|
||||
const [lastAction, setLastAction] = useState<CommandItem | null>(null);
|
||||
const [menuPlacement, setMenuPlacement] = useState<{
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
}>({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
});
|
||||
const currentItems = useMemo(() => {
|
||||
return commandItems[activeCommandSet].filter((item) => {
|
||||
return item.name.toLowerCase().includes(prompt.toLowerCase());
|
||||
});
|
||||
}, [prompt, output, activeCommandSet]);
|
||||
const updateMenuPlacement = useCallback(() => {
|
||||
if (!editor || !showAiMenu) return;
|
||||
|
||||
const { view } = editor;
|
||||
const { to } = editor.state.selection;
|
||||
const editorRect = view.dom.getBoundingClientRect();
|
||||
const cursorCoords = view.coordsAtPos(to);
|
||||
const topOffset = 8;
|
||||
const editorPadding = isSmBreakpoint ? 16 : 48;
|
||||
|
||||
setMenuPlacement({
|
||||
top: cursorCoords.bottom + topOffset,
|
||||
left: editorRect.left + editorPadding,
|
||||
width: editorRect.width - editorPadding * 2,
|
||||
});
|
||||
}, [editor, showAiMenu, isSmBreakpoint]);
|
||||
const resetMenu = useCallback(() => {
|
||||
setPrompt("");
|
||||
setOutput("");
|
||||
setActiveCommandSet("main");
|
||||
setLastAction(null);
|
||||
aiGenerateStreamMutation.reset();
|
||||
}, [aiGenerateStreamMutation.reset]);
|
||||
const debouncedUpdateMenuPlacement = useDebouncedCallback(
|
||||
updateMenuPlacement,
|
||||
60,
|
||||
);
|
||||
const handleGenerate = useCallback(
|
||||
(item?: CommandItem) => {
|
||||
if (!editor || isLoading) return;
|
||||
|
||||
let command: CommandItem | null = item || null;
|
||||
|
||||
if (!command) {
|
||||
if (!prompt) return;
|
||||
|
||||
command = {
|
||||
id: "custom",
|
||||
name: "Custom",
|
||||
action: AiAction.CUSTOM,
|
||||
prompt,
|
||||
};
|
||||
}
|
||||
|
||||
const { from, to } = editor.state.selection;
|
||||
const content = editor.state.doc.textBetween(from, to);
|
||||
|
||||
setOutput("");
|
||||
setIsLoading(true);
|
||||
aiGenerateStreamMutation.mutate({
|
||||
action: command.action,
|
||||
prompt: command.prompt,
|
||||
content,
|
||||
onChunk: (chunk) => {
|
||||
setOutput((output) => output + chunk.content);
|
||||
},
|
||||
onComplete: () => {
|
||||
setIsLoading(false);
|
||||
setActiveCommandSet("result");
|
||||
},
|
||||
onError: () => {
|
||||
setIsLoading(false);
|
||||
resetMenu();
|
||||
},
|
||||
});
|
||||
setLastAction(command);
|
||||
},
|
||||
[
|
||||
editor,
|
||||
prompt,
|
||||
isLoading,
|
||||
aiGenerateStreamMutation.mutateAsync,
|
||||
resetMenu,
|
||||
],
|
||||
);
|
||||
const handleCommand = useCallback(
|
||||
(item?: CommandItem) => {
|
||||
setPrompt("");
|
||||
|
||||
if (!item) {
|
||||
return handleGenerate();
|
||||
}
|
||||
if (item.id === "back") {
|
||||
return setActiveCommandSet("main");
|
||||
}
|
||||
if (item.id === "result-replace") {
|
||||
const chain = editor.chain().focus();
|
||||
|
||||
if (lastAction.action === AiAction.CONTINUE_WRITING) {
|
||||
chain.setTextSelection(editor.state.selection.to);
|
||||
}
|
||||
|
||||
chain.insertContent(marked.parse(output)).run();
|
||||
|
||||
return setShowAiMenu(false);
|
||||
}
|
||||
if (item.id === "result-insert-below") {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.setTextSelection(editor.state.selection.to)
|
||||
.insertContent(marked.parse(output))
|
||||
.run();
|
||||
|
||||
return setShowAiMenu(false);
|
||||
}
|
||||
if (item.id === "result-copy") {
|
||||
navigator.clipboard.writeText(output);
|
||||
|
||||
return setShowAiMenu(false);
|
||||
}
|
||||
if (item.id === "result-discard") {
|
||||
setOutput("");
|
||||
|
||||
return resetMenu();
|
||||
}
|
||||
if (item.id === "result-try-again" && lastAction) {
|
||||
return handleGenerate(lastAction);
|
||||
}
|
||||
if (item.subCommandSet) {
|
||||
return setActiveCommandSet(item.subCommandSet);
|
||||
}
|
||||
|
||||
return handleGenerate(item);
|
||||
},
|
||||
[editor, output, lastAction, handleGenerate, resetMenu],
|
||||
);
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const totalItems = currentItems.length;
|
||||
const cycleSize = totalItems + 1;
|
||||
|
||||
if (event.key === "Escape") {
|
||||
return setShowAiMenu(false);
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
|
||||
return setSelectedIndex((selectedIndex) => {
|
||||
const direction = event.key === "ArrowDown" ? 1 : -1;
|
||||
const newIndex = selectedIndex + direction;
|
||||
|
||||
if (newIndex < -1) return cycleSize - 1;
|
||||
if (newIndex >= cycleSize) return 0;
|
||||
|
||||
return newIndex;
|
||||
});
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
|
||||
return handleCommand(currentItems[selectedIndex]);
|
||||
}
|
||||
},
|
||||
[currentItems, selectedIndex],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
|
||||
const handleClose = () => setShowAiMenu(false);
|
||||
const observer = new ResizeObserver(() => {
|
||||
debouncedUpdateMenuPlacement();
|
||||
});
|
||||
|
||||
updateMenuPlacement();
|
||||
editor.on("focus", handleClose);
|
||||
editor.on("blur", handleClose);
|
||||
window.addEventListener("resize", debouncedUpdateMenuPlacement);
|
||||
window.addEventListener("scroll", debouncedUpdateMenuPlacement, true);
|
||||
observer.observe(editor.view.dom);
|
||||
|
||||
return () => {
|
||||
editor.off("focus", handleClose);
|
||||
editor.off("blur", handleClose);
|
||||
window.removeEventListener("resize", debouncedUpdateMenuPlacement);
|
||||
window.removeEventListener("scroll", debouncedUpdateMenuPlacement, true);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [editor, updateMenuPlacement, debouncedUpdateMenuPlacement]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showAiMenu) {
|
||||
resetMenu();
|
||||
}
|
||||
}, [showAiMenu, resetMenu]);
|
||||
useEffect(() => {
|
||||
// Focus input when menu opens or command set changes
|
||||
requestAnimationFrame(() => {
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
}, [showAiMenu, isLoading, currentItems]);
|
||||
useEffect(() => {
|
||||
if (!currentItems.length) {
|
||||
setSelectedIndex(-1);
|
||||
}
|
||||
setSelectedIndex(prompt || activeCommandSet !== "main" ? 0 : -1);
|
||||
}, [prompt, activeCommandSet, currentItems]);
|
||||
|
||||
if (!showAiMenu) return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
style={{
|
||||
zIndex: 200,
|
||||
position: "fixed",
|
||||
top: menuPlacement.top,
|
||||
left: menuPlacement.left,
|
||||
width: menuPlacement.width,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classes.aiMenu}
|
||||
style={{ pointerEvents: "auto" }}
|
||||
tabIndex={0}
|
||||
ref={containerRef}
|
||||
>
|
||||
<ResultPreview output={output} isLoading={isLoading} />
|
||||
<CommandSelector
|
||||
selectedIndex={selectedIndex}
|
||||
isLoading={isLoading}
|
||||
output={output}
|
||||
currentItems={currentItems}
|
||||
handleCommand={handleCommand}
|
||||
>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Ask AI..."
|
||||
data-autofocus
|
||||
value={prompt}
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setPrompt(e.currentTarget.value)}
|
||||
rightSection={
|
||||
<Tooltip label="Ask AI">
|
||||
<ActionIcon
|
||||
disabled={!prompt || isLoading}
|
||||
variant="transparent"
|
||||
onClick={() => handleGenerate()}
|
||||
>
|
||||
<IconSend size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</CommandSelector>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
|
||||
export { EditorAiMenu };
|
||||
@@ -1,164 +0,0 @@
|
||||
import { AiAction } from "@/ee/ai/types/ai.types";
|
||||
import {
|
||||
IconSparkles,
|
||||
IconArrowsMaximize,
|
||||
IconArrowsMinimize,
|
||||
IconWriting,
|
||||
IconHelp,
|
||||
IconList,
|
||||
IconMoodSmile,
|
||||
IconLanguage,
|
||||
IconTrash,
|
||||
IconRefresh,
|
||||
IconChevronLeft,
|
||||
IconCheck,
|
||||
IconArrowDownLeft,
|
||||
IconCopy,
|
||||
IconTextPlus,
|
||||
IconAlignJustified,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
interface CommandItem {
|
||||
name: string;
|
||||
id: string;
|
||||
icon?: typeof IconSparkles;
|
||||
action?: AiAction;
|
||||
prompt?: string;
|
||||
subCommandSet?: CommandSet;
|
||||
}
|
||||
|
||||
type CommandSet = "main" | "tone" | "translate" | "result";
|
||||
|
||||
const mainItems: CommandItem[] = [
|
||||
{
|
||||
id: "improve-writing",
|
||||
name: "Improve writing",
|
||||
icon: IconSparkles,
|
||||
action: AiAction.IMPROVE_WRITING,
|
||||
},
|
||||
{
|
||||
id: "fix-spelling-grammar",
|
||||
name: "Fix spelling & grammar",
|
||||
icon: IconCheck,
|
||||
action: AiAction.FIX_SPELLING_GRAMMAR,
|
||||
},
|
||||
{
|
||||
id: "make-longer",
|
||||
name: "Make longer",
|
||||
icon: IconTextPlus,
|
||||
action: AiAction.MAKE_LONGER,
|
||||
},
|
||||
{
|
||||
id: "make-shorter",
|
||||
name: "Make shorter",
|
||||
icon: IconAlignJustified,
|
||||
action: AiAction.MAKE_SHORTER,
|
||||
},
|
||||
{
|
||||
id: "continue-writing",
|
||||
name: "Continue writing",
|
||||
icon: IconWriting,
|
||||
action: AiAction.CONTINUE_WRITING,
|
||||
},
|
||||
{
|
||||
id: "explain",
|
||||
name: "Explain",
|
||||
icon: IconHelp,
|
||||
action: AiAction.CUSTOM,
|
||||
prompt: "Explain this text",
|
||||
},
|
||||
{
|
||||
id: "summarize",
|
||||
name: "Summarize",
|
||||
icon: IconList,
|
||||
action: AiAction.SUMMARIZE,
|
||||
},
|
||||
{
|
||||
id: "change-tone",
|
||||
name: "Change tone",
|
||||
icon: IconMoodSmile,
|
||||
subCommandSet: "tone",
|
||||
},
|
||||
{
|
||||
id: "translate",
|
||||
name: "Translate",
|
||||
icon: IconLanguage,
|
||||
subCommandSet: "translate",
|
||||
},
|
||||
];
|
||||
const toneItems: CommandItem[] = [
|
||||
{
|
||||
id: "back",
|
||||
name: "Back",
|
||||
icon: IconChevronLeft,
|
||||
},
|
||||
{
|
||||
id: "tone-professional",
|
||||
name: "Professional",
|
||||
icon: IconMoodSmile,
|
||||
action: AiAction.CHANGE_TONE,
|
||||
prompt: "Professional",
|
||||
},
|
||||
{
|
||||
id: "tone-casual",
|
||||
name: "Casual",
|
||||
icon: IconMoodSmile,
|
||||
action: AiAction.CHANGE_TONE,
|
||||
prompt: "Casual",
|
||||
},
|
||||
{
|
||||
id: "tone-friendly",
|
||||
name: "Friendly",
|
||||
icon: IconMoodSmile,
|
||||
action: AiAction.CHANGE_TONE,
|
||||
prompt: "Friendly",
|
||||
},
|
||||
];
|
||||
const translateItems: CommandItem[] = [
|
||||
{
|
||||
id: "back",
|
||||
name: "Back",
|
||||
icon: IconChevronLeft,
|
||||
},
|
||||
{
|
||||
id: "translate-english",
|
||||
name: "English",
|
||||
icon: IconLanguage,
|
||||
action: AiAction.TRANSLATE,
|
||||
prompt: "English",
|
||||
},
|
||||
{
|
||||
id: "translate-french",
|
||||
name: "French",
|
||||
icon: IconLanguage,
|
||||
action: AiAction.TRANSLATE,
|
||||
prompt: "French",
|
||||
},
|
||||
{
|
||||
id: "translate-german",
|
||||
name: "German",
|
||||
icon: IconLanguage,
|
||||
action: AiAction.TRANSLATE,
|
||||
prompt: "German",
|
||||
},
|
||||
];
|
||||
const resultItems: CommandItem[] = [
|
||||
{ id: "result-replace", name: "Replace", icon: IconCheck },
|
||||
{ id: "result-insert-below", name: "Insert below", icon: IconArrowDownLeft },
|
||||
{ id: "result-copy", name: "Copy", icon: IconCopy },
|
||||
{ id: "result-discard", name: "Discard", icon: IconTrash },
|
||||
{
|
||||
id: "result-try-again",
|
||||
name: "Try again",
|
||||
icon: IconRefresh,
|
||||
},
|
||||
];
|
||||
const commandItems: Record<CommandSet, CommandItem[]> = {
|
||||
main: mainItems,
|
||||
tone: toneItems,
|
||||
translate: translateItems,
|
||||
result: resultItems,
|
||||
};
|
||||
|
||||
export type { CommandItem, CommandSet };
|
||||
export { commandItems };
|
||||
@@ -1,71 +0,0 @@
|
||||
import { Loader, Menu, ScrollArea } from "@mantine/core";
|
||||
import { IconChevronRight } from "@tabler/icons-react";
|
||||
import { ReactNode } from "react";
|
||||
import { CommandItem } from "./command-items";
|
||||
import classes from "./ai-menu.module.css";
|
||||
|
||||
interface CommandSelectorProps {
|
||||
selectedIndex: number;
|
||||
|
||||
isLoading: boolean;
|
||||
output: string;
|
||||
currentItems: CommandItem[];
|
||||
children: ReactNode;
|
||||
handleCommand(item: CommandItem): void;
|
||||
}
|
||||
|
||||
const CommandSelector = ({
|
||||
selectedIndex,
|
||||
children,
|
||||
isLoading,
|
||||
output,
|
||||
currentItems,
|
||||
handleCommand,
|
||||
}: CommandSelectorProps) => {
|
||||
return (
|
||||
<Menu
|
||||
opened={!isLoading && currentItems.length > 0}
|
||||
position="bottom-start"
|
||||
offset={4}
|
||||
width={250}
|
||||
trapFocus={false}
|
||||
shadow="lg"
|
||||
>
|
||||
<Menu.Target>{children}</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<ScrollArea.Autosize type="scroll" scrollbarSize={5} mah={300}>
|
||||
{currentItems.map((item, index) => {
|
||||
const isSelected = selectedIndex === index;
|
||||
const showLoader =
|
||||
isLoading && output === "" && !item.subCommandSet;
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
key={item.id}
|
||||
className={isSelected ? classes.menuItemSelected : undefined}
|
||||
leftSection={
|
||||
showLoader ? (
|
||||
<Loader size={14} />
|
||||
) : item.icon ? (
|
||||
<item.icon size={16} />
|
||||
) : undefined
|
||||
}
|
||||
rightSection={
|
||||
item.subCommandSet ? (
|
||||
<IconChevronRight size={14} />
|
||||
) : undefined
|
||||
}
|
||||
onClick={() => handleCommand(item)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{item.name}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</ScrollArea.Autosize>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export { CommandSelector };
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Loader, Paper, Text } from "@mantine/core";
|
||||
import DOMPurify from "dompurify";
|
||||
import { marked } from "marked";
|
||||
import { memo } from "react";
|
||||
import classes from "./ai-menu.module.css";
|
||||
|
||||
interface ResultPreviewProps {
|
||||
output: string;
|
||||
isLoading: boolean;
|
||||
}
|
||||
const ResultPreview = memo(({ output, isLoading }: ResultPreviewProps) => {
|
||||
if (!output && !isLoading) return;
|
||||
|
||||
const parsedOutput = `${marked.parse(output)}`;
|
||||
|
||||
return (
|
||||
<Paper p="sm" mb={4} shadow="lg" withBorder>
|
||||
<Text size="sm" component="div">
|
||||
{parsedOutput && (
|
||||
<div
|
||||
className={classes.resultPreviewWrapper}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(parsedOutput) }}
|
||||
/>
|
||||
)}
|
||||
{isLoading && <Loader size={12} ml="xs" display="inline-block" />}
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
|
||||
export { ResultPreview };
|
||||
@@ -9,11 +9,10 @@ import {
|
||||
IconStrikethrough,
|
||||
IconUnderline,
|
||||
IconMessage,
|
||||
IconSparkles,
|
||||
} from "@tabler/icons-react";
|
||||
import clsx from "clsx";
|
||||
import classes from "./bubble-menu.module.css";
|
||||
import { ActionIcon, Button, rem, Tooltip } from "@mantine/core";
|
||||
import { ActionIcon, rem, Tooltip } from "@mantine/core";
|
||||
import { ColorSelector } from "./color-selector";
|
||||
import { NodeSelector } from "./node-selector";
|
||||
import { TextAlignmentSelector } from "./text-alignment-selector";
|
||||
@@ -26,7 +25,6 @@ import { v7 as uuid7 } from "uuid";
|
||||
import { isCellSelection, isTextSelected } from "@docmost/editor-ext";
|
||||
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { showAiMenuAtom } from "@/features/editor/atoms/editor-atoms";
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
||||
@@ -41,20 +39,14 @@ type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
|
||||
|
||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [showAiMenu, setShowAiMenu] = useAtom(showAiMenuAtom);
|
||||
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
||||
const showCommentPopupRef = useRef(showCommentPopup);
|
||||
const showAiMenuRef = useRef(showAiMenu);
|
||||
|
||||
useEffect(() => {
|
||||
showCommentPopupRef.current = showCommentPopup;
|
||||
}, [showCommentPopup]);
|
||||
|
||||
useEffect(() => {
|
||||
showAiMenuRef.current = showAiMenu;
|
||||
}, [showAiMenu]);
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor: props.editor,
|
||||
selector: (ctx) => {
|
||||
@@ -131,7 +123,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
empty ||
|
||||
isNodeSelection(selection) ||
|
||||
isCellSelection(selection) ||
|
||||
showAiMenuRef.current ||
|
||||
showCommentPopupRef?.current
|
||||
) {
|
||||
return false;
|
||||
@@ -155,26 +146,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
||||
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
||||
|
||||
// Hide the bubble menu immediately when AI menu is shown
|
||||
if (showAiMenu) return;
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
{...bubbleMenuProps}
|
||||
style={{ zIndex: 200, position: "relative" }}
|
||||
>
|
||||
<BubbleMenu {...bubbleMenuProps} style={{ zIndex: 200, position: "relative"}}>
|
||||
<div className={classes.bubbleMenu}>
|
||||
<Button
|
||||
variant="default"
|
||||
style={{ border: "none", height: "34px" }}
|
||||
radius="0"
|
||||
rightSection={<IconSparkles size={16} />}
|
||||
onClick={() => {
|
||||
setShowAiMenu(true);
|
||||
}}
|
||||
>
|
||||
Ask AI
|
||||
</Button>
|
||||
<NodeSelector
|
||||
editor={props.editor}
|
||||
isOpen={isNodeSelectorOpen}
|
||||
|
||||
@@ -66,7 +66,6 @@ import { PageEditMode } from "@/features/user/types/user.types.ts";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { searchSpotlight } from "@/features/search/constants.ts";
|
||||
import { useEditorScroll } from "./hooks/use-editor-scroll";
|
||||
import { EditorAiMenu } from "./components/ai-menu/ai-menu";
|
||||
|
||||
interface PageEditorProps {
|
||||
pageId: string;
|
||||
@@ -406,7 +405,6 @@ export default function PageEditor({
|
||||
|
||||
{editor && editorIsEditable && (
|
||||
<div>
|
||||
<EditorAiMenu editor={editor} />
|
||||
<EditorBubbleMenu editor={editor} />
|
||||
<TableMenu editor={editor} />
|
||||
<TableCellMenu editor={editor} appendTo={menuContainerRef} />
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { EditorProvider } from "@tiptap/react";
|
||||
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
||||
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 { Placeholder } from "@tiptap/extension-placeholder";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
.mantine-AppShell-main {
|
||||
padding: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
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 {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "@/features/editor/styles/index.css";
|
||||
import "./css/history-diff.module.css";
|
||||
import { useEffect } from "react";
|
||||
import { EditorContent, useEditor } from "@tiptap/react";
|
||||
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
||||
@@ -43,21 +44,21 @@ export function HistoryEditor({
|
||||
if (previousContent) {
|
||||
try {
|
||||
const schema = editor.schema;
|
||||
const oldContent = Node.fromJSON(schema, previousContent);
|
||||
const newContent = Node.fromJSON(schema, content);
|
||||
const docOld = Node.fromJSON(schema, previousContent);
|
||||
const docNew = Node.fromJSON(schema, content);
|
||||
|
||||
const tr = recreateTransform(oldContent, newContent, {
|
||||
complexSteps: false,
|
||||
const tr = recreateTransform(docOld, docNew, {
|
||||
complexSteps: true,
|
||||
wordDiffs: true,
|
||||
simplifyDiff: true,
|
||||
});
|
||||
|
||||
const changeSet = ChangeSet.create(oldContent).addSteps(
|
||||
const changeSet = ChangeSet.create(docOld).addSteps(
|
||||
tr.doc,
|
||||
tr.mapping.maps,
|
||||
[],
|
||||
);
|
||||
const changes = simplifyChanges(changeSet.changes, newContent);
|
||||
const changes = simplifyChanges(changeSet.changes, docNew);
|
||||
|
||||
editor.commands.setContent(content);
|
||||
|
||||
@@ -83,7 +84,7 @@ export function HistoryEditor({
|
||||
changeIndex++;
|
||||
const currentIndex = changeIndex;
|
||||
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)) {
|
||||
const nodeEnd = pos + node.nodeSize;
|
||||
if (change.fromB <= pos && change.toB >= nodeEnd) {
|
||||
@@ -116,7 +117,7 @@ export function HistoryEditor({
|
||||
changeIndex++;
|
||||
const currentIndex = changeIndex;
|
||||
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)) {
|
||||
const nodeEnd = pos + node.nodeSize;
|
||||
if (change.fromA <= pos && change.toA >= nodeEnd) {
|
||||
@@ -139,7 +140,7 @@ export function HistoryEditor({
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const deletedText = oldContent.textBetween(
|
||||
const deletedText = docOld.textBetween(
|
||||
change.fromA,
|
||||
change.toA,
|
||||
"",
|
||||
@@ -160,7 +161,7 @@ export function HistoryEditor({
|
||||
}
|
||||
}
|
||||
|
||||
decorationSet = DecorationSet.create(newContent, decorations);
|
||||
decorationSet = DecorationSet.create(docNew, decorations);
|
||||
} catch (e) {
|
||||
console.error("History diff failed:", e);
|
||||
editor.commands.setContent(content);
|
||||
|
||||
@@ -157,7 +157,7 @@ function HistoryList({ pageId }: Props) {
|
||||
size="compact-md"
|
||||
onClick={() => setHistoryModalOpen(false)}
|
||||
>
|
||||
{t("Cancel")}
|
||||
{t("Close")}
|
||||
</Button>
|
||||
<Button size="compact-md" onClick={confirmRestore}>
|
||||
{t("Restore")}
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function HistoryModalMobile({ pageId, pageTitle }: Props) {
|
||||
{canRestore && (
|
||||
<Group className={classes.actionButtons} justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={() => setHistoryModalOpen(false)}>
|
||||
{t("Cancel")}
|
||||
{t("Close")}
|
||||
</Button>
|
||||
<Button onClick={confirmRestore}>{t("Restore")}</Button>
|
||||
</Group>
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.25.1",
|
||||
"version": "0.25.0-beta.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -33,28 +33,28 @@
|
||||
"@ai-sdk/google": "^3.0.9",
|
||||
"@ai-sdk/openai": "^3.0.11",
|
||||
"@ai-sdk/openai-compatible": "^2.0.12",
|
||||
"@aws-sdk/client-s3": "3.982.0",
|
||||
"@aws-sdk/lib-storage": "3.982.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.982.0",
|
||||
"@aws-sdk/client-s3": "3.701.0",
|
||||
"@aws-sdk/lib-storage": "3.701.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.701.0",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/multipart": "^9.4.0",
|
||||
"@fastify/static": "^9.0.0",
|
||||
"@langchain/core": "1.1.18",
|
||||
"@fastify/multipart": "^9.3.0",
|
||||
"@fastify/static": "^8.3.0",
|
||||
"@langchain/core": "1.1.13",
|
||||
"@langchain/textsplitters": "1.0.1",
|
||||
"@nestjs-labs/nestjs-ioredis": "^11.0.4",
|
||||
"@nestjs/bullmq": "^11.0.4",
|
||||
"@nestjs/common": "^11.1.11",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.13",
|
||||
"@nestjs/core": "^11.1.11",
|
||||
"@nestjs/event-emitter": "^3.0.1",
|
||||
"@nestjs/jwt": "11.0.0",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-fastify": "^11.1.13",
|
||||
"@nestjs/platform-socket.io": "^11.1.13",
|
||||
"@nestjs/platform-fastify": "^11.1.11",
|
||||
"@nestjs/platform-socket.io": "^11.1.11",
|
||||
"@nestjs/schedule": "^6.1.0",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/websockets": "^11.1.13",
|
||||
"@nestjs/websockets": "^11.1.11",
|
||||
"@node-saml/passport-saml": "^5.1.0",
|
||||
"@react-email/components": "0.0.28",
|
||||
"@react-email/render": "1.0.2",
|
||||
|
||||
@@ -32,9 +32,7 @@ export class HistoryListener {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastHistory = await this.pageHistoryRepo.findPageLastHistory(page.id, {
|
||||
includeContent: true,
|
||||
});
|
||||
const lastHistory = await this.pageHistoryRepo.findPageLastHistory(page.id);
|
||||
|
||||
if (
|
||||
!lastHistory ||
|
||||
|
||||
@@ -215,6 +215,7 @@ export class PageController {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: scope to workspaces
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/history')
|
||||
async getPageHistory(
|
||||
|
||||
@@ -9,9 +9,7 @@ export class PageHistoryService {
|
||||
constructor(private pageHistoryRepo: PageHistoryRepo) {}
|
||||
|
||||
async findById(historyId: string): Promise<PageHistory> {
|
||||
return await this.pageHistoryRepo.findById(historyId, {
|
||||
includeContent: true,
|
||||
});
|
||||
return await this.pageHistoryRepo.findById(historyId);
|
||||
}
|
||||
|
||||
async findHistoryByPageId(
|
||||
|
||||
@@ -17,32 +17,15 @@ import { DB } from '@docmost/db/types/db';
|
||||
export class PageHistoryRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
private baseFields: Array<keyof PageHistory> = [
|
||||
'id',
|
||||
'pageId',
|
||||
'slugId',
|
||||
'title',
|
||||
'icon',
|
||||
'coverPhoto',
|
||||
'lastUpdatedById',
|
||||
'spaceId',
|
||||
'workspaceId',
|
||||
'createdAt',
|
||||
];
|
||||
|
||||
async findById(
|
||||
pageHistoryId: string,
|
||||
opts?: {
|
||||
includeContent?: boolean;
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<PageHistory> {
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
const db = dbOrTx(this.db, trx);
|
||||
|
||||
return await db
|
||||
.selectFrom('pageHistory')
|
||||
.select(this.baseFields)
|
||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
||||
.selectAll()
|
||||
.select((eb) => this.withLastUpdatedBy(eb))
|
||||
.where('id', '=', pageHistoryId)
|
||||
.executeTakeFirst();
|
||||
@@ -80,7 +63,7 @@ export class PageHistoryRepo {
|
||||
async findPageHistoryByPageId(pageId: string, pagination: PaginationOptions) {
|
||||
const query = this.db
|
||||
.selectFrom('pageHistory')
|
||||
.select(this.baseFields)
|
||||
.selectAll()
|
||||
.select((eb) => this.withLastUpdatedBy(eb))
|
||||
.where('pageId', '=', pageId);
|
||||
|
||||
@@ -93,19 +76,12 @@ export class PageHistoryRepo {
|
||||
});
|
||||
}
|
||||
|
||||
async findPageLastHistory(
|
||||
pageId: string,
|
||||
opts?: {
|
||||
includeContent?: boolean;
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
) {
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
async findPageLastHistory(pageId: string, trx?: KyselyTransaction) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
|
||||
return await db
|
||||
.selectFrom('pageHistory')
|
||||
.select(this.baseFields)
|
||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
||||
.selectAll()
|
||||
.where('pageId', '=', pageId)
|
||||
.limit(1)
|
||||
.orderBy('createdAt', 'desc')
|
||||
|
||||
+1
-1
Submodule apps/server/src/ee updated: 3a4b47ec30...6d3eb76d4e
@@ -13,7 +13,6 @@ import { Readable } from 'stream';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { getMimeType } from '../../../common/helpers';
|
||||
import { Upload } from '@aws-sdk/lib-storage';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
export class S3Driver implements StorageDriver {
|
||||
private readonly s3Client: S3Client;
|
||||
@@ -40,7 +39,6 @@ export class S3Driver implements StorageDriver {
|
||||
|
||||
await upload.done();
|
||||
} catch (err) {
|
||||
Logger.error(err);
|
||||
throw new Error(`Failed to upload file: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +73,6 @@ export class S3Driver implements StorageDriver {
|
||||
|
||||
await upload.done();
|
||||
} catch (err) {
|
||||
Logger.error(err);
|
||||
throw new Error(`Failed to upload file: ${(err as Error).message}`);
|
||||
} finally {
|
||||
if (shouldDestroyClient && clientToUse) {
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
],
|
||||
"cache": true
|
||||
},
|
||||
"start:dev": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"cache": true
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "docmost",
|
||||
"homepage": "https://docmost.com",
|
||||
"version": "0.25.1",
|
||||
"version": "0.25.0-beta.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nx run-many -t build",
|
||||
@@ -23,9 +23,9 @@
|
||||
"@casl/ability": "6.8.0",
|
||||
"@docmost/editor-ext": "workspace:*",
|
||||
"@floating-ui/dom": "^1.7.3",
|
||||
"@hocuspocus/provider": "3.4.4",
|
||||
"@hocuspocus/server": "3.4.4",
|
||||
"@hocuspocus/transformer": "3.4.4",
|
||||
"@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",
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
// https://gitlab.com/mpapp-public/prosemirror-recreate-steps - MIT
|
||||
// https://github.com/sueddeutsche/prosemirror-recreate-transform - MIT
|
||||
// https://gitlab.com/mpapp-public/prosemirror-recreate-steps
|
||||
// https://github.com/sueddeutsche/prosemirror-recreate-transform
|
||||
export { recreateTransform, RecreateTransform } 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