mirror of
https://github.com/docmost/docmost.git
synced 2026-05-12 18:04:06 +08:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9aea1a9e0 | |||
| 536dbf5e49 | |||
| 3881d62b6b | |||
| 6bdb0516b2 | |||
| 4730dc2fb9 | |||
| f7a9e82037 | |||
| aca63b7185 | |||
| 5557759f0b | |||
| 08986e701f | |||
| 9abbf12864 | |||
| 6683c515cf | |||
| cc5c800238 | |||
| cfaee93af9 | |||
| 74eddb0638 | |||
| 7c83a9d4f0 | |||
| 2678c4e279 | |||
| b0bde4b375 | |||
| 724e37d5b7 | |||
| 33184e9d8d | |||
| 7520c329d0 | |||
| d7a5fda53c |
+39
-39
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.70.1",
|
"version": "0.70.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
@@ -10,76 +10,76 @@
|
|||||||
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\""
|
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/react": "^4.0.0",
|
"@casl/react": "^5.0.1",
|
||||||
"@docmost/editor-ext": "workspace:*",
|
"@docmost/editor-ext": "workspace:*",
|
||||||
"@emoji-mart/data": "^1.2.1",
|
"@emoji-mart/data": "^1.2.1",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@excalidraw/excalidraw": "0.18.0-3a5ef40",
|
"@excalidraw/excalidraw": "0.18.0-3a5ef40",
|
||||||
"@mantine/core": "^8.3.14",
|
"@mantine/core": "^8.3.18",
|
||||||
"@mantine/dates": "^8.3.14",
|
"@mantine/dates": "^8.3.18",
|
||||||
"@mantine/form": "^8.3.14",
|
"@mantine/form": "^8.3.18",
|
||||||
"@mantine/hooks": "^8.3.14",
|
"@mantine/hooks": "^8.3.18",
|
||||||
"@mantine/modals": "^8.3.14",
|
"@mantine/modals": "^8.3.18",
|
||||||
"@mantine/notifications": "^8.3.14",
|
"@mantine/notifications": "^8.3.18",
|
||||||
"@mantine/spotlight": "^8.3.14",
|
"@mantine/spotlight": "^8.3.18",
|
||||||
"@tabler/icons-react": "^3.36.1",
|
"@tabler/icons-react": "^3.40.0",
|
||||||
"@tanstack/react-query": "^5.90.17",
|
"@tanstack/react-query": "5.90.17",
|
||||||
"alfaaz": "^1.1.0",
|
"alfaaz": "^1.1.0",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.6",
|
||||||
"blueimp-load-image": "^5.16.0",
|
"blueimp-load-image": "^5.16.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"emoji-mart": "^5.6.0",
|
"emoji-mart": "^5.6.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"highlightjs-sap-abap": "^0.3.0",
|
"highlightjs-sap-abap": "^0.3.0",
|
||||||
"i18next": "^23.16.8",
|
"i18next": "^25.10.1",
|
||||||
"i18next-http-backend": "^2.7.3",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"jotai": "^2.16.2",
|
"jotai": "^2.18.1",
|
||||||
"jotai-optics": "^0.4.0",
|
"jotai-optics": "^0.4.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"katex": "0.16.27",
|
"katex": "0.16.40",
|
||||||
"lowlight": "^3.3.0",
|
"lowlight": "^3.3.0",
|
||||||
"mantine-form-zod-resolver": "^1.3.0",
|
"mantine-form-zod-resolver": "^1.3.0",
|
||||||
"mermaid": "^11.12.2",
|
"mermaid": "^11.13.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"posthog-js": "1.345.5",
|
"posthog-js": "1.363.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-arborist": "3.4.0",
|
"react-arborist": "3.4.0",
|
||||||
"react-clear-modal": "^2.0.17",
|
"react-clear-modal": "^2.0.18",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-drawio": "^1.0.7",
|
"react-drawio": "^1.0.7",
|
||||||
"react-error-boundary": "^4.1.2",
|
"react-error-boundary": "^6.1.1",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^3.0.0",
|
||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^16.5.8",
|
||||||
"react-router-dom": "^7.12.0",
|
"react-router-dom": "^7.13.1",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.4",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"tiptap-extension-global-drag-handle": "^0.1.18",
|
"tiptap-extension-global-drag-handle": "^0.1.18",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.16.0",
|
"@eslint/js": "^9.28.0",
|
||||||
"@tanstack/eslint-plugin-query": "^5.62.1",
|
"@tanstack/eslint-plugin-query": "^5.94.4",
|
||||||
"@types/blueimp-load-image": "^5.16.0",
|
"@types/blueimp-load-image": "^5.16.6",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/katex": "^0.16.7",
|
"@types/katex": "^0.16.8",
|
||||||
"@types/node": "22.19.1",
|
"@types/node": "22.19.1",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^6.0.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.28.0",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.16",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^15.13.0",
|
"globals": "^15.13.0",
|
||||||
"optics-ts": "^2.4.1",
|
"optics-ts": "^2.4.1",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.5.8",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.17.0",
|
"typescript-eslint": "^8.57.1",
|
||||||
"vite": "^7.2.4"
|
"vite": "^8.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ export const readOnlyEditorAtom = atom<Editor | null>(null);
|
|||||||
export const yjsConnectionStatusAtom = atom<string>("");
|
export const yjsConnectionStatusAtom = atom<string>("");
|
||||||
|
|
||||||
export const showAiMenuAtom = atom(false);
|
export const showAiMenuAtom = atom(false);
|
||||||
|
|
||||||
|
export const showLinkMenuAtom = atom(false);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { v7 as uuid7 } from "uuid";
|
|||||||
import { isCellSelection, isTextSelected } from "@docmost/editor-ext";
|
import { isCellSelection, isTextSelected } from "@docmost/editor-ext";
|
||||||
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { showAiMenuAtom } from "@/features/editor/atoms/editor-atoms";
|
import { showAiMenuAtom, showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms";
|
||||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
|
import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
|
||||||
|
|
||||||
export interface BubbleMenuItem {
|
export interface BubbleMenuItem {
|
||||||
@@ -49,6 +49,8 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
||||||
const showCommentPopupRef = useRef(showCommentPopup);
|
const showCommentPopupRef = useRef(showCommentPopup);
|
||||||
const showAiMenuRef = useRef(showAiMenu);
|
const showAiMenuRef = useRef(showAiMenu);
|
||||||
|
const [showLinkMenu] = useAtom(showLinkMenuAtom);
|
||||||
|
const showLinkMenuRef = useRef(showLinkMenu);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
showCommentPopupRef.current = showCommentPopup;
|
showCommentPopupRef.current = showCommentPopup;
|
||||||
@@ -58,6 +60,10 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
showAiMenuRef.current = showAiMenu;
|
showAiMenuRef.current = showAiMenu;
|
||||||
}, [showAiMenu]);
|
}, [showAiMenu]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
showLinkMenuRef.current = showLinkMenu;
|
||||||
|
}, [showLinkMenu]);
|
||||||
|
|
||||||
const editorState = useEditorState({
|
const editorState = useEditorState({
|
||||||
editor: props.editor,
|
editor: props.editor,
|
||||||
selector: (ctx) => {
|
selector: (ctx) => {
|
||||||
@@ -135,6 +141,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
isNodeSelection(selection) ||
|
isNodeSelection(selection) ||
|
||||||
isCellSelection(selection) ||
|
isCellSelection(selection) ||
|
||||||
showAiMenuRef.current ||
|
showAiMenuRef.current ||
|
||||||
|
showLinkMenuRef.current ||
|
||||||
showCommentPopupRef?.current
|
showCommentPopupRef?.current
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
@@ -147,7 +154,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
onHide: () => {
|
onHide: () => {
|
||||||
setIsNodeSelectorOpen(false);
|
setIsNodeSelectorOpen(false);
|
||||||
setIsTextAlignmentOpen(false);
|
setIsTextAlignmentOpen(false);
|
||||||
setIsLinkSelectorOpen(false);
|
|
||||||
setIsColorSelectorOpen(false);
|
setIsColorSelectorOpen(false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -155,11 +161,10 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
|
|
||||||
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
||||||
const [isTextAlignmentSelectorOpen, setIsTextAlignmentOpen] = useState(false);
|
const [isTextAlignmentSelectorOpen, setIsTextAlignmentOpen] = useState(false);
|
||||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
|
||||||
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
||||||
|
|
||||||
// Hide the bubble menu immediately when AI menu is shown
|
// Hide the bubble menu immediately when AI menu is shown
|
||||||
if (showAiMenu) return;
|
if (showAiMenu || showLinkMenu) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
@@ -189,7 +194,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
setIsOpen={() => {
|
setIsOpen={() => {
|
||||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||||
setIsTextAlignmentOpen(false);
|
setIsTextAlignmentOpen(false);
|
||||||
setIsLinkSelectorOpen(false);
|
|
||||||
setIsColorSelectorOpen(false);
|
setIsColorSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -200,7 +204,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
setIsOpen={() => {
|
setIsOpen={() => {
|
||||||
setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen);
|
setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen);
|
||||||
setIsNodeSelectorOpen(false);
|
setIsNodeSelectorOpen(false);
|
||||||
setIsLinkSelectorOpen(false);
|
|
||||||
setIsColorSelectorOpen(false);
|
setIsColorSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -224,16 +227,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
))}
|
))}
|
||||||
</ActionIcon.Group>
|
</ActionIcon.Group>
|
||||||
|
|
||||||
<LinkSelector
|
<LinkSelector />
|
||||||
editor={props.editor}
|
|
||||||
isOpen={isLinkSelectorOpen}
|
|
||||||
setIsOpen={(value) => {
|
|
||||||
setIsLinkSelectorOpen(value);
|
|
||||||
setIsNodeSelectorOpen(false);
|
|
||||||
setIsTextAlignmentOpen(false);
|
|
||||||
setIsColorSelectorOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ColorSelector
|
<ColorSelector
|
||||||
editor={props.editor}
|
editor={props.editor}
|
||||||
@@ -242,7 +236,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
setIsColorSelectorOpen(!isColorSelectorOpen);
|
setIsColorSelectorOpen(!isColorSelectorOpen);
|
||||||
setIsNodeSelectorOpen(false);
|
setIsNodeSelectorOpen(false);
|
||||||
setIsTextAlignmentOpen(false);
|
setIsTextAlignmentOpen(false);
|
||||||
setIsLinkSelectorOpen(false);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +1,25 @@
|
|||||||
import { Dispatch, FC, SetStateAction, useCallback } from "react";
|
import { FC } from "react";
|
||||||
import { IconLink } from "@tabler/icons-react";
|
import { IconLink } from "@tabler/icons-react";
|
||||||
import { ActionIcon, Popover, Tooltip } from "@mantine/core";
|
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||||
import { useEditor } from "@tiptap/react";
|
import { useSetAtom } from "jotai";
|
||||||
import { TextSelection } from "@tiptap/pm/state";
|
|
||||||
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
|
|
||||||
import { normalizeUrl } from "@/features/editor/components/link/link-view";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms";
|
||||||
|
|
||||||
interface LinkSelectorProps {
|
export const LinkSelector: FC = () => {
|
||||||
editor: ReturnType<typeof useEditor>;
|
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LinkSelector: FC<LinkSelectorProps> = ({
|
|
||||||
editor,
|
|
||||||
isOpen,
|
|
||||||
setIsOpen,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const onLink = useCallback(
|
const setShowLinkMenu = useSetAtom(showLinkMenuAtom);
|
||||||
(url: string, internal?: boolean) => {
|
|
||||||
setIsOpen(false);
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.setLink({ href: internal ? url : normalizeUrl(url), internal: !!internal } as any)
|
|
||||||
.command(({ tr }) => {
|
|
||||||
tr.setSelection(TextSelection.create(tr.doc, tr.selection.to));
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
},
|
|
||||||
[editor, setIsOpen],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Tooltip label={t("Add link")} withArrow>
|
||||||
width={320}
|
<ActionIcon
|
||||||
opened={isOpen}
|
variant="default"
|
||||||
trapFocus
|
size="lg"
|
||||||
offset={{ mainAxis: 35, crossAxis: 0 }}
|
radius="0"
|
||||||
withArrow
|
style={{ border: "none" }}
|
||||||
shadow="md"
|
onClick={() => setShowLinkMenu(true)}
|
||||||
>
|
>
|
||||||
<Popover.Target>
|
<IconLink size={16} />
|
||||||
<Tooltip label={t("Add link")} withArrow>
|
</ActionIcon>
|
||||||
<ActionIcon
|
</Tooltip>
|
||||||
variant="default"
|
|
||||||
size="lg"
|
|
||||||
radius="0"
|
|
||||||
style={{
|
|
||||||
border: "none",
|
|
||||||
}}
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
>
|
|
||||||
<IconLink size={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Popover.Target>
|
|
||||||
|
|
||||||
<Popover.Dropdown p="sm">
|
|
||||||
<LinkEditorPanel onSetLink={onLink} />
|
|
||||||
</Popover.Dropdown>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@/features/editor/components/table/types/types.ts";
|
} from "@/features/editor/components/table/types/types.ts";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
|
LoadingOverlay,
|
||||||
Modal,
|
Modal,
|
||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -46,6 +47,8 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
const computedColorScheme = useComputedColorScheme();
|
const computedColorScheme = useComputedColorScheme();
|
||||||
const isDirtyRef = useRef(false);
|
const isDirtyRef = useRef(false);
|
||||||
const isSavingRef = useRef(false);
|
const isSavingRef = useRef(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const editorState = useEditorState({
|
const editorState = useEditorState({
|
||||||
editor,
|
editor,
|
||||||
@@ -140,6 +143,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
if (isSavingRef.current) return;
|
if (isSavingRef.current) return;
|
||||||
|
|
||||||
isSavingRef.current = true;
|
isSavingRef.current = true;
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const svgString = decodeBase64ToSvgString(svgXml);
|
const svgString = decodeBase64ToSvgString(svgXml);
|
||||||
@@ -167,6 +171,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
isDirtyRef.current = false;
|
isDirtyRef.current = false;
|
||||||
} finally {
|
} finally {
|
||||||
isSavingRef.current = false;
|
isSavingRef.current = false;
|
||||||
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}, [editor, editorState?.attachmentId]);
|
}, [editor, editorState?.attachmentId]);
|
||||||
|
|
||||||
@@ -196,6 +201,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
const handleOpen = useCallback(async () => {
|
const handleOpen = useCallback(async () => {
|
||||||
if (!editorState?.src) return;
|
if (!editorState?.src) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const url = getFileUrl(editorState.src);
|
const url = getFileUrl(editorState.src);
|
||||||
const request = await fetch(url, {
|
const request = await fetch(url, {
|
||||||
@@ -213,6 +219,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
isDirtyRef.current = false;
|
isDirtyRef.current = false;
|
||||||
open();
|
open();
|
||||||
}
|
}
|
||||||
@@ -307,6 +314,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Edit")}
|
aria-label={t("Edit")}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
<IconEdit size={18} />
|
<IconEdit size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -339,7 +347,8 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
<Modal.Root opened={opened} onClose={handleClose} fullScreen closeOnEscape={false}>
|
<Modal.Root opened={opened} onClose={handleClose} fullScreen closeOnEscape={false}>
|
||||||
<Modal.Overlay />
|
<Modal.Overlay />
|
||||||
<Modal.Content style={{ overflow: "hidden" }}>
|
<Modal.Content style={{ overflow: "hidden" }}>
|
||||||
<Modal.Body>
|
<Modal.Body pos="relative">
|
||||||
|
<LoadingOverlay visible={isSaving} />
|
||||||
<div style={{ height: "100vh" }}>
|
<div style={{ height: "100vh" }}>
|
||||||
<DrawIoEmbed
|
<DrawIoEmbed
|
||||||
ref={drawioRef}
|
ref={drawioRef}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
|||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Card,
|
Card,
|
||||||
|
LoadingOverlay,
|
||||||
Modal,
|
Modal,
|
||||||
Text,
|
Text,
|
||||||
useComputedColorScheme,
|
useComputedColorScheme,
|
||||||
@@ -34,6 +35,7 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
const computedColorScheme = useComputedColorScheme();
|
const computedColorScheme = useComputedColorScheme();
|
||||||
const isDirtyRef = useRef(false);
|
const isDirtyRef = useRef(false);
|
||||||
const isSavingRef = useRef(false);
|
const isSavingRef = useRef(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
const handleOpen = async () => {
|
const handleOpen = async () => {
|
||||||
if (!editor.isEditable) {
|
if (!editor.isEditable) {
|
||||||
@@ -47,6 +49,7 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
if (isSavingRef.current) return;
|
if (isSavingRef.current) return;
|
||||||
|
|
||||||
isSavingRef.current = true;
|
isSavingRef.current = true;
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const svgString = decodeBase64ToSvgString(svgXml);
|
const svgString = decodeBase64ToSvgString(svgXml);
|
||||||
@@ -79,6 +82,7 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
isDirtyRef.current = false;
|
isDirtyRef.current = false;
|
||||||
} finally {
|
} finally {
|
||||||
isSavingRef.current = false;
|
isSavingRef.current = false;
|
||||||
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,7 +140,8 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
<Modal.Root opened={opened} onClose={handleClose} fullScreen closeOnEscape={false}>
|
<Modal.Root opened={opened} onClose={handleClose} fullScreen closeOnEscape={false}>
|
||||||
<Modal.Overlay />
|
<Modal.Overlay />
|
||||||
<Modal.Content style={{ overflow: "hidden" }}>
|
<Modal.Content style={{ overflow: "hidden" }}>
|
||||||
<Modal.Body>
|
<Modal.Body pos="relative">
|
||||||
|
<LoadingOverlay visible={isSaving} />
|
||||||
<div style={{ height: "100vh" }}>
|
<div style={{ height: "100vh" }}>
|
||||||
<DrawIoEmbed
|
<DrawIoEmbed
|
||||||
ref={drawioRef}
|
ref={drawioRef}
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
const computedColorScheme = useComputedColorScheme();
|
const computedColorScheme = useComputedColorScheme();
|
||||||
const isDirtyRef = useRef(false);
|
const isDirtyRef = useRef(false);
|
||||||
const isSavingRef = useRef(false);
|
const isSavingRef = useRef(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const isInitialLoadRef = useRef(true);
|
const isInitialLoadRef = useRef(true);
|
||||||
const lastFingerprintRef = useRef("");
|
const lastFingerprintRef = useRef("");
|
||||||
|
|
||||||
@@ -153,6 +155,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
const handleOpen = useCallback(async () => {
|
const handleOpen = useCallback(async () => {
|
||||||
if (!editorState?.src) return;
|
if (!editorState?.src) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const url = getFileUrl(editorState.src);
|
const url = getFileUrl(editorState.src);
|
||||||
const request = await fetch(url, {
|
const request = await fetch(url, {
|
||||||
@@ -166,6 +169,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
isDirtyRef.current = false;
|
isDirtyRef.current = false;
|
||||||
isInitialLoadRef.current = true;
|
isInitialLoadRef.current = true;
|
||||||
open();
|
open();
|
||||||
@@ -178,6 +182,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSavingRef.current = true;
|
isSavingRef.current = true;
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { exportToSvg } = await import("@excalidraw/excalidraw");
|
const { exportToSvg } = await import("@excalidraw/excalidraw");
|
||||||
@@ -223,6 +228,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
isDirtyRef.current = false;
|
isDirtyRef.current = false;
|
||||||
} finally {
|
} finally {
|
||||||
isSavingRef.current = false;
|
isSavingRef.current = false;
|
||||||
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}, [editor, excalidrawAPI, editorState?.attachmentId]);
|
}, [editor, excalidrawAPI, editorState?.attachmentId]);
|
||||||
|
|
||||||
@@ -339,6 +345,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Edit")}
|
aria-label={t("Edit")}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
<IconEdit size={18} />
|
<IconEdit size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -390,7 +397,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
bg="var(--mantine-color-body)"
|
bg="var(--mantine-color-body)"
|
||||||
p="xs"
|
p="xs"
|
||||||
>
|
>
|
||||||
<Button onClick={handleSaveAndExit} size={"compact-sm"}>
|
<Button onClick={handleSaveAndExit} size={"compact-sm"} loading={isSaving}>
|
||||||
{t("Save & Exit")}
|
{t("Save & Exit")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleClose} color="red" size={"compact-sm"}>
|
<Button onClick={handleClose} color="red" size={"compact-sm"}>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
|
|
||||||
const isDirtyRef = useRef(false);
|
const isDirtyRef = useRef(false);
|
||||||
const isSavingRef = useRef(false);
|
const isSavingRef = useRef(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const isInitialLoadRef = useRef(true);
|
const isInitialLoadRef = useRef(true);
|
||||||
const lastFingerprintRef = useRef("");
|
const lastFingerprintRef = useRef("");
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSavingRef.current = true;
|
isSavingRef.current = true;
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { exportToSvg } = await import("@excalidraw/excalidraw");
|
const { exportToSvg } = await import("@excalidraw/excalidraw");
|
||||||
@@ -120,6 +122,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
isDirtyRef.current = false;
|
isDirtyRef.current = false;
|
||||||
} finally {
|
} finally {
|
||||||
isSavingRef.current = false;
|
isSavingRef.current = false;
|
||||||
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}, [excalidrawAPI, editor, attachmentId, updateAttributes]);
|
}, [excalidrawAPI, editor, attachmentId, updateAttributes]);
|
||||||
|
|
||||||
@@ -191,7 +194,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
bg="var(--mantine-color-body)"
|
bg="var(--mantine-color-body)"
|
||||||
p="xs"
|
p="xs"
|
||||||
>
|
>
|
||||||
<Button onClick={handleSaveAndExit} size={"compact-sm"}>
|
<Button onClick={handleSaveAndExit} size={"compact-sm"} loading={isSaving}>
|
||||||
{t("Save & Exit")}
|
{t("Save & Exit")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleClose} color="red" size={"compact-sm"}>
|
<Button onClick={handleClose} color="red" size={"compact-sm"}>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const LinkEditorPanel = ({
|
|||||||
includeUsers: false,
|
includeUsers: false,
|
||||||
includePages: true,
|
includePages: true,
|
||||||
spaceId: space?.id,
|
spaceId: space?.id,
|
||||||
limit: state.isSearchQuery ? 10 : 5,
|
limit: state.isSearchQuery ? 10 : 3,
|
||||||
preload: true,
|
preload: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,6 +105,7 @@ export const LinkEditorPanel = ({
|
|||||||
value={state.url}
|
value={state.url}
|
||||||
onChange={state.onChange}
|
onChange={state.onChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
data-autofocus
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { FC, useCallback, useEffect, useRef } from "react";
|
||||||
|
import { BubbleMenu } from "@tiptap/react/menus";
|
||||||
|
import type { Editor } from "@tiptap/react";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { isTextSelected } from "@docmost/editor-ext";
|
||||||
|
import { showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms";
|
||||||
|
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel";
|
||||||
|
import { normalizeUrl } from "@/lib/utils";
|
||||||
|
import { TextSelection } from "@tiptap/pm/state";
|
||||||
|
import { Paper } from "@mantine/core";
|
||||||
|
|
||||||
|
type EditorLinkMenuProps = {
|
||||||
|
editor: Editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditorLinkMenu: FC<EditorLinkMenuProps> = ({ editor }) => {
|
||||||
|
const [showLinkMenu, setShowLinkMenu] = useAtom(showLinkMenuAtom);
|
||||||
|
const showLinkMenuRef = useRef(showLinkMenu);
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
showLinkMenuRef.current = showLinkMenu;
|
||||||
|
if (showLinkMenu) {
|
||||||
|
editor.commands.focus();
|
||||||
|
}
|
||||||
|
}, [showLinkMenu, editor]);
|
||||||
|
|
||||||
|
const focusInput = useCallback(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
containerRef.current
|
||||||
|
?.querySelector<HTMLInputElement>("input")
|
||||||
|
?.focus({ preventScroll: true });
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSetLink = useCallback(
|
||||||
|
(url: string, internal?: boolean) => {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.setLink({
|
||||||
|
href: internal ? url : normalizeUrl(url),
|
||||||
|
internal: !!internal,
|
||||||
|
} as any)
|
||||||
|
.command(({ tr }) => {
|
||||||
|
tr.setSelection(TextSelection.create(tr.doc, tr.selection.to));
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
setShowLinkMenu(false);
|
||||||
|
},
|
||||||
|
[editor, setShowLinkMenu],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showLinkMenu) return;
|
||||||
|
|
||||||
|
const dismiss = () => {
|
||||||
|
setShowLinkMenu(false);
|
||||||
|
editor.commands.focus();
|
||||||
|
editor.commands.setTextSelection(editor.state.selection.to);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
document.addEventListener("mousedown", handleMouseDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
document.removeEventListener("mousedown", handleMouseDown);
|
||||||
|
};
|
||||||
|
}, [showLinkMenu, setShowLinkMenu]);
|
||||||
|
|
||||||
|
if (!showLinkMenu) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BubbleMenu
|
||||||
|
editor={editor}
|
||||||
|
shouldShow={({ editor, state }) => {
|
||||||
|
const { empty } = state.selection;
|
||||||
|
return (
|
||||||
|
showLinkMenuRef.current &&
|
||||||
|
editor.isEditable &&
|
||||||
|
!empty &&
|
||||||
|
isTextSelected(editor)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
placement: "bottom",
|
||||||
|
offset: 8,
|
||||||
|
onShow: focusInput,
|
||||||
|
onHide: () => {
|
||||||
|
setShowLinkMenu(false);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
style={{ zIndex: 198, position: "relative" }}
|
||||||
|
>
|
||||||
|
<Paper ref={containerRef} w={320} p="sm" shadow="md" radius={6} withBorder>
|
||||||
|
<LinkEditorPanel onSetLink={onSetLink} />
|
||||||
|
</Paper>
|
||||||
|
</BubbleMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -29,12 +29,7 @@ import { useSharePageQuery } from "@/features/share/queries/share-query.ts";
|
|||||||
import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
|
import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
|
||||||
import { extractPageSlugId } from "@/lib";
|
import { extractPageSlugId } from "@/lib";
|
||||||
import { sanitizeUrl, copyToClipboard } from "@docmost/editor-ext";
|
import { sanitizeUrl, copyToClipboard } from "@docmost/editor-ext";
|
||||||
|
import { normalizeUrl } from "@/lib/utils";
|
||||||
export const normalizeUrl = (url: string): string => {
|
|
||||||
if (!url) return url;
|
|
||||||
if (url.startsWith("/") || /^(\S+):(\/\/)?\S+$/.test(url)) return url;
|
|
||||||
return `https://${url}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseInternalLink = (
|
const parseInternalLink = (
|
||||||
href: string,
|
href: string,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import { jwtDecode } from "jwt-decode";
|
|||||||
import { searchSpotlight } from "@/features/search/constants.ts";
|
import { searchSpotlight } from "@/features/search/constants.ts";
|
||||||
import { useEditorScroll } from "./hooks/use-editor-scroll";
|
import { useEditorScroll } from "./hooks/use-editor-scroll";
|
||||||
import { EditorAiMenu } from "@/ee/ai/components/editor/ai-menu/ai-menu";
|
import { EditorAiMenu } from "@/ee/ai/components/editor/ai-menu/ai-menu";
|
||||||
|
import { EditorLinkMenu } from "@/features/editor/components/link/link-menu";
|
||||||
import ColumnsMenu from "@/features/editor/components/columns/columns-menu.tsx";
|
import ColumnsMenu from "@/features/editor/components/columns/columns-menu.tsx";
|
||||||
|
|
||||||
interface PageEditorProps {
|
interface PageEditorProps {
|
||||||
@@ -407,6 +408,7 @@ export default function PageEditor({
|
|||||||
{editor && editorIsEditable && (
|
{editor && editorIsEditable && (
|
||||||
<div>
|
<div>
|
||||||
<EditorAiMenu editor={editor} />
|
<EditorAiMenu editor={editor} />
|
||||||
|
<EditorLinkMenu editor={editor} />
|
||||||
<EditorBubbleMenu editor={editor} />
|
<EditorBubbleMenu editor={editor} />
|
||||||
<TableMenu editor={editor} />
|
<TableMenu editor={editor} />
|
||||||
<TableCellMenu editor={editor} appendTo={menuContainerRef} />
|
<TableCellMenu editor={editor} appendTo={menuContainerRef} />
|
||||||
|
|||||||
@@ -110,15 +110,7 @@ export function useUpdatePageMutation() {
|
|||||||
return useMutation<IPage, Error, Partial<IPageInput>>({
|
return useMutation<IPage, Error, Partial<IPageInput>>({
|
||||||
mutationFn: (data) => updatePage(data),
|
mutationFn: (data) => updatePage(data),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
updatePage(data);
|
updatePageData(data);
|
||||||
|
|
||||||
invalidateOnUpdatePage(
|
|
||||||
data.spaceId,
|
|
||||||
data.parentPageId,
|
|
||||||
data.id,
|
|
||||||
data.title,
|
|
||||||
data.icon,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,12 @@ export function getPageIcon(icon: string, size = 18): string | ReactNode {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const normalizeUrl = (url: string): string => {
|
||||||
|
if (!url) return url;
|
||||||
|
if (url.startsWith("/") || /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) return url;
|
||||||
|
return `https://${url}`;
|
||||||
|
};
|
||||||
|
|
||||||
export function castToBoolean(value: unknown): boolean {
|
export function castToBoolean(value: unknown): boolean {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { defineConfig, loadEnv } from "vite";
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
export const envPath = path.resolve(process.cwd(), "..", "..");
|
const envPath = path.resolve(process.cwd(), "..", "..");
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const {
|
const {
|
||||||
@@ -35,6 +35,20 @@ export default defineConfig(({ mode }) => {
|
|||||||
APP_VERSION: JSON.stringify(process.env.npm_package_version),
|
APP_VERSION: JSON.stringify(process.env.npm_package_version),
|
||||||
},
|
},
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
rolldownOptions: {
|
||||||
|
output: {
|
||||||
|
codeSplitting: {
|
||||||
|
groups: [
|
||||||
|
{ name: "vendor-mantine", test: /@mantine/ },
|
||||||
|
{ name: "vendor-mermaid", test: /mermaid|cytoscape|elkjs/ },
|
||||||
|
{ name: "vendor-excalidraw", test: /excalidraw/ },
|
||||||
|
{ name: "vendor-katex", test: /katex/ },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": "/src",
|
"@": "/src",
|
||||||
|
|||||||
+55
-55
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.70.1",
|
"version": "0.70.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -30,123 +30,123 @@
|
|||||||
"test:e2e": "jest --config test/jest-e2e.json"
|
"test:e2e": "jest --config test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/google": "^3.0.29",
|
"@ai-sdk/google": "^3.0.52",
|
||||||
"@ai-sdk/openai": "^3.0.29",
|
"@ai-sdk/openai": "^3.0.47",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
"@ai-sdk/openai-compatible": "^2.0.37",
|
||||||
"@aws-sdk/client-s3": "3.1000.0",
|
"@aws-sdk/client-s3": "3.1014.0",
|
||||||
"@aws-sdk/lib-storage": "3.1000.0",
|
"@aws-sdk/lib-storage": "3.1014.0",
|
||||||
"@aws-sdk/s3-request-presigner": "3.1000.0",
|
"@aws-sdk/s3-request-presigner": "3.1014.0",
|
||||||
"@clickhouse/client": "^1.17.0",
|
"@clickhouse/client": "^1.18.2",
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/multipart": "^9.4.0",
|
"@fastify/multipart": "^9.4.0",
|
||||||
"@fastify/static": "^9.0.0",
|
"@fastify/static": "^9.0.0",
|
||||||
"@keyv/redis": "^5.1.6",
|
"@keyv/redis": "^5.1.6",
|
||||||
"@langchain/core": "1.1.29",
|
"@langchain/core": "1.1.34",
|
||||||
"@langchain/textsplitters": "1.0.1",
|
"@langchain/textsplitters": "1.0.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
"@modelcontextprotocol/sdk": "^1.27.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/cache-manager": "^3.1.0",
|
"@nestjs/cache-manager": "^3.1.0",
|
||||||
"@nestjs/common": "^11.1.14",
|
"@nestjs/common": "^11.1.17",
|
||||||
"@nestjs/config": "^4.0.3",
|
"@nestjs/config": "^4.0.3",
|
||||||
"@nestjs/core": "^11.1.14",
|
"@nestjs/core": "^11.1.17",
|
||||||
"@nestjs/event-emitter": "^3.0.1",
|
"@nestjs/event-emitter": "^3.0.1",
|
||||||
"@nestjs/jwt": "11.0.0",
|
"@nestjs/jwt": "11.0.2",
|
||||||
"@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.14",
|
"@nestjs/platform-fastify": "^11.1.17",
|
||||||
"@nestjs/platform-socket.io": "^11.1.14",
|
"@nestjs/platform-socket.io": "^11.1.17",
|
||||||
"@nestjs/schedule": "^6.1.1",
|
"@nestjs/schedule": "^6.1.1",
|
||||||
"@nestjs/terminus": "^11.1.1",
|
"@nestjs/terminus": "^11.1.1",
|
||||||
"@nestjs/websockets": "^11.1.14",
|
"@nestjs/websockets": "^11.1.17",
|
||||||
"@node-saml/passport-saml": "^5.1.0",
|
"@node-saml/passport-saml": "^5.1.0",
|
||||||
"@react-email/components": "1.0.7",
|
"@react-email/components": "1.0.10",
|
||||||
"@react-email/render": "2.0.4",
|
"@react-email/render": "2.0.4",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"ai": "^6.0.86",
|
"ai": "^6.0.134",
|
||||||
"ai-sdk-ollama": "^3.7.0",
|
"ai-sdk-ollama": "^3.8.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bullmq": "^5.70.1",
|
"bullmq": "^5.71.0",
|
||||||
"cache-manager": "^7.2.8",
|
"cache-manager": "^7.2.8",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.15.1",
|
"class-validator": "^0.15.1",
|
||||||
"cookie": "^1.1.1",
|
"cookie": "^1.1.1",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.4",
|
||||||
"happy-dom": "20.1.0",
|
"happy-dom": "20.8.4",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.10.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"kysely": "^0.28.2",
|
"kysely": "^0.28.14",
|
||||||
"kysely-migration-cli": "^0.4.2",
|
"kysely-migration-cli": "^0.4.2",
|
||||||
"kysely-postgres-js": "^3.0.0",
|
"kysely-postgres-js": "^3.0.0",
|
||||||
"ldapts": "^7.4.0",
|
"ldapts": "^8.1.7",
|
||||||
"lib0": "^0.2.117",
|
"lib0": "^0.2.117",
|
||||||
"mammoth": "^1.11.0",
|
"mammoth": "^1.12.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^3.0.2",
|
||||||
"msgpackr": "^1.11.8",
|
"msgpackr": "^1.11.9",
|
||||||
"nanoid": "3.3.11",
|
"nanoid": "5.1.7",
|
||||||
"nestjs-cls": "^6.2.0",
|
"nestjs-cls": "^6.2.0",
|
||||||
"nestjs-kysely": "^1.2.0",
|
"nestjs-kysely": "^3.1.2",
|
||||||
"nestjs-pino": "^4.5.0",
|
"nestjs-pino": "^4.6.1",
|
||||||
"nodemailer": "^7.0.12",
|
"nodemailer": "^8.0.3",
|
||||||
"openid-client": "^5.7.1",
|
"openid-client": "^6.8.2",
|
||||||
"otpauth": "^9.4.1",
|
"otpauth": "^9.5.0",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^7.3.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pdfjs-dist": "^5.4.394",
|
"pdfjs-dist": "^5.5.207",
|
||||||
"pg-tsquery": "^8.4.2",
|
"pg-tsquery": "^8.4.2",
|
||||||
"pgvector": "^0.2.1",
|
"pgvector": "^0.2.1",
|
||||||
"pino-http": "^11.0.0",
|
"pino-http": "^11.0.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"postgres": "^3.4.8",
|
"postgres": "^3.4.8",
|
||||||
"postmark": "^4.0.5",
|
"postmark": "^4.0.7",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"sanitize-filename-ts": "1.0.2",
|
"sanitize-filename-ts": "1.0.2",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"stripe": "^17.5.0",
|
"stripe": "^17.7.0",
|
||||||
"tlds": "^1.261.0",
|
"tlds": "^1.261.0",
|
||||||
"tmp-promise": "^3.0.3",
|
"tmp-promise": "^3.0.3",
|
||||||
"tseep": "^1.3.1",
|
"tseep": "^1.3.1",
|
||||||
"typesense": "^2.1.0",
|
"typesense": "^3.0.3",
|
||||||
"ws": "^8.19.0",
|
"ws": "^8.19.0",
|
||||||
"yauzl": "^3.2.0",
|
"yauzl": "^3.2.1",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.20.0",
|
"@eslint/js": "^9.28.0",
|
||||||
"@nestjs/cli": "^11.0.16",
|
"@nestjs/cli": "^11.0.16",
|
||||||
"@nestjs/schematics": "^11.0.1",
|
"@nestjs/schematics": "^11.0.9",
|
||||||
"@nestjs/testing": "^11.0.10",
|
"@nestjs/testing": "^11.1.17",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/debounce": "^1.2.4",
|
"@types/debounce": "^1.2.4",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^3.0.1",
|
||||||
"@types/node": "^22.13.4",
|
"@types/node": "^25.5.0",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^7.0.11",
|
||||||
"@types/passport-google-oauth20": "^2.0.16",
|
"@types/passport-google-oauth20": "^2.0.17",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.3",
|
"@types/supertest": "^6.0.3",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@types/yauzl": "^2.10.3",
|
"@types/yauzl": "^2.10.3",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.28.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"globals": "^15.15.0",
|
"globals": "^17.4.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.3.0",
|
||||||
"kysely-codegen": "^0.20.0",
|
"kysely-codegen": "^0.20.0",
|
||||||
"prettier": "^3.5.1",
|
"prettier": "^3.8.1",
|
||||||
"react-email": "5.2.8",
|
"react-email": "5.2.10",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.2.2",
|
"supertest": "^7.2.2",
|
||||||
"ts-jest": "^29.4.6",
|
"ts-jest": "^29.4.6",
|
||||||
"ts-loader": "^9.5.4",
|
"ts-loader": "^9.5.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.24.1"
|
"typescript-eslint": "^8.57.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export class CollaborationGateway {
|
|||||||
|
|
||||||
// Forward close events
|
// Forward close events
|
||||||
client.on('close', (code: number, reason: Buffer) => {
|
client.on('close', (code: number, reason: Buffer) => {
|
||||||
this.redisSync!.onSocketClose(socketId, code, reason);
|
this.redisSync!.onSocketClose(socketId, code, reason.buffer as ArrayBuffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Forward pong events for keepalive
|
// Forward pong events for keepalive
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import type { StringValue } from 'ms';
|
||||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||||
import {
|
import {
|
||||||
JwtApiKeyPayload,
|
JwtApiKeyPayload,
|
||||||
@@ -96,7 +97,7 @@ export class TokenService {
|
|||||||
apiKeyId: string;
|
apiKeyId: string;
|
||||||
user: User;
|
user: User;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
expiresIn?: string | number;
|
expiresIn?: StringValue | number;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const { apiKeyId, user, workspaceId, expiresIn } = opts;
|
const { apiKeyId, user, workspaceId, expiresIn } = opts;
|
||||||
if (isUserDisabled(user)) {
|
if (isUserDisabled(user)) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import type { StringValue } from 'ms';
|
||||||
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
||||||
import { TokenService } from './services/token.service';
|
import { TokenService } from './services/token.service';
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ import { TokenService } from './services/token.service';
|
|||||||
return {
|
return {
|
||||||
secret: environmentService.getAppSecret(),
|
secret: environmentService.getAppSecret(),
|
||||||
signOptions: {
|
signOptions: {
|
||||||
expiresIn: environmentService.getJwtTokenExpiresIn(),
|
expiresIn: environmentService.getJwtTokenExpiresIn() as StringValue,
|
||||||
issuer: 'Docmost',
|
issuer: 'Docmost',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -91,9 +91,15 @@ export class SearchService {
|
|||||||
return { items: [] };
|
return { items: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRestricted =
|
||||||
|
await this.pagePermissionRepo.hasRestrictedAncestor(share.pageId);
|
||||||
|
if (isRestricted) {
|
||||||
|
return { items: [] };
|
||||||
|
}
|
||||||
|
|
||||||
const pageIdsToSearch = [];
|
const pageIdsToSearch = [];
|
||||||
if (share.includeSubPages) {
|
if (share.includeSubPages) {
|
||||||
const pageList = await this.pageRepo.getPageAndDescendants(
|
const pageList = await this.pageRepo.getPageAndDescendantsExcludingRestricted(
|
||||||
share.pageId,
|
share.pageId,
|
||||||
{
|
{
|
||||||
includeContent: false,
|
includeContent: false,
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 8b21c6e32e...c2755be37c
@@ -28,8 +28,7 @@ import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
|||||||
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
||||||
import { Node } from '@tiptap/pm/model';
|
import { Node } from '@tiptap/pm/model';
|
||||||
import { EditorState } from '@tiptap/pm/state';
|
import { EditorState } from '@tiptap/pm/state';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
import slugify from '@sindresorhus/slugify';
|
||||||
import slugify = require('@sindresorhus/slugify');
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
const packageJson = require('../../../package.json');
|
const packageJson = require('../../../package.json');
|
||||||
import { EnvironmentService } from '../environment/environment.service';
|
import { EnvironmentService } from '../environment/environment.service';
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
buildAttachmentCandidates,
|
buildAttachmentCandidates,
|
||||||
collectMarkdownAndHtmlFiles,
|
collectMarkdownAndHtmlFiles,
|
||||||
encodeFilePath,
|
encodeFilePath,
|
||||||
|
extractNotionPartialId,
|
||||||
readDocmostMetadata,
|
readDocmostMetadata,
|
||||||
stripNotionID,
|
stripNotionID,
|
||||||
} from '../utils/import.utils';
|
} from '../utils/import.utils';
|
||||||
@@ -160,6 +161,7 @@ export class FileImportTaskService {
|
|||||||
fileTask: FileTask;
|
fileTask: FileTask;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { extractDir, fileTask } = opts;
|
const { extractDir, fileTask } = opts;
|
||||||
|
const isNotion = fileTask.source === FileImportSource.Notion;
|
||||||
const allFiles = await collectMarkdownAndHtmlFiles(extractDir);
|
const allFiles = await collectMarkdownAndHtmlFiles(extractDir);
|
||||||
const attachmentCandidates = await buildAttachmentCandidates(extractDir);
|
const attachmentCandidates = await buildAttachmentCandidates(extractDir);
|
||||||
const docmostMetadata = await readDocmostMetadata(extractDir);
|
const docmostMetadata = await readDocmostMetadata(extractDir);
|
||||||
@@ -230,7 +232,17 @@ export class FileImportTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For each folder with content, create a placeholder page if no corresponding .md or .html exists
|
// For each folder with content, create a placeholder page if no corresponding .md or .html exists
|
||||||
foldersWithContent.forEach((folderPath) => {
|
// Process folders with partial UUIDs first so they claim their specific files
|
||||||
|
// before plain folders (without partial UUIDs) take whatever remains.
|
||||||
|
const sortedFolders = isNotion
|
||||||
|
? [...foldersWithContent].sort((a, b) => {
|
||||||
|
const aHasPartial = extractNotionPartialId(path.basename(a)) ? 0 : 1;
|
||||||
|
const bHasPartial = extractNotionPartialId(path.basename(b)) ? 0 : 1;
|
||||||
|
return aHasPartial - bHasPartial;
|
||||||
|
})
|
||||||
|
: [...foldersWithContent];
|
||||||
|
|
||||||
|
sortedFolders.forEach((folderPath) => {
|
||||||
if (
|
if (
|
||||||
skipRootFolder &&
|
skipRootFolder &&
|
||||||
folderPath?.toLowerCase() === skipRootFolder?.toLowerCase()
|
folderPath?.toLowerCase() === skipRootFolder?.toLowerCase()
|
||||||
@@ -243,18 +255,54 @@ export class FileImportTaskService {
|
|||||||
|
|
||||||
if (!pagesMap.has(mdPath) && !pagesMap.has(htmlPath)) {
|
if (!pagesMap.has(mdPath) && !pagesMap.has(htmlPath)) {
|
||||||
const folderName = path.basename(folderPath);
|
const folderName = path.basename(folderPath);
|
||||||
const encodedMdPath = encodeFilePath(mdPath);
|
const parentDir = path.dirname(folderPath);
|
||||||
const placeholderMetadata = docmostMetadata?.pages[encodedMdPath];
|
|
||||||
pagesMap.set(mdPath, {
|
// Notion no longer adds UUIDs to folder names, but still adds them to files.
|
||||||
id: v7(),
|
// For duplicate names, Notion adds a partial UUID "{first4}-{last4}" to the folder.
|
||||||
slugId: generateSlugId(),
|
let matched = false;
|
||||||
name: stripNotionID(folderName),
|
if (isNotion) {
|
||||||
content: '',
|
const partialId = extractNotionPartialId(folderName);
|
||||||
parentPageId: null,
|
const strippedFolderName = stripNotionID(folderName);
|
||||||
fileExtension: '.md',
|
const isSameDir = (fileDir: string) =>
|
||||||
filePath: mdPath,
|
fileDir === parentDir || (parentDir === '.' && !fileDir.includes('/'));
|
||||||
icon: placeholderMetadata?.icon ?? null,
|
|
||||||
});
|
for (const [filePath, page] of pagesMap.entries()) {
|
||||||
|
if (!isSameDir(path.dirname(filePath))) continue;
|
||||||
|
if (page.name !== strippedFolderName) continue;
|
||||||
|
|
||||||
|
if (partialId) {
|
||||||
|
// Match partial UUID against the full UUID in the filename
|
||||||
|
const fileBase = path.basename(filePath, path.extname(filePath));
|
||||||
|
const fullIdMatch = fileBase.match(/[a-f0-9]{32}$/i);
|
||||||
|
if (!fullIdMatch) continue;
|
||||||
|
const fullId = fullIdMatch[0].toLowerCase();
|
||||||
|
if (!fullId.startsWith(partialId.prefix) || !fullId.endsWith(partialId.suffix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pagesMap.delete(filePath);
|
||||||
|
page.filePath = mdPath;
|
||||||
|
pagesMap.set(mdPath, page);
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matched) {
|
||||||
|
const encodedMdPath = encodeFilePath(mdPath);
|
||||||
|
const placeholderMetadata = docmostMetadata?.pages[encodedMdPath];
|
||||||
|
pagesMap.set(mdPath, {
|
||||||
|
id: v7(),
|
||||||
|
slugId: generateSlugId(),
|
||||||
|
name: stripNotionID(folderName),
|
||||||
|
content: '',
|
||||||
|
parentPageId: null,
|
||||||
|
fileExtension: '.md',
|
||||||
|
filePath: mdPath,
|
||||||
|
icon: placeholderMetadata?.icon ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { getEmbedUrlAndProvider } from '@docmost/editor-ext';
|
import { getEmbedUrlAndProvider } from '@docmost/editor-ext';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { v7 } from 'uuid';
|
||||||
import { InsertableBacklink } from '@docmost/db/types/entity.types';
|
import { InsertableBacklink } from '@docmost/db/types/entity.types';
|
||||||
import { Cheerio, CheerioAPI, load } from 'cheerio';
|
import { Cheerio, CheerioAPI, load } from 'cheerio';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
import slugify from '@sindresorhus/slugify';
|
||||||
import slugify = require('@sindresorhus/slugify');
|
|
||||||
|
|
||||||
// Check if text contains Unicode characters (for emojis/icons)
|
// Check if text contains Unicode characters (for emojis/icons)
|
||||||
function isUnicodeCharacter(text: string): boolean {
|
function isUnicodeCharacter(text: string): boolean {
|
||||||
@@ -344,14 +344,35 @@ export async function rewriteInternalLinksToMentionHtml(
|
|||||||
const meta = filePathToPageMetaMap.get(resolved);
|
const meta = filePathToPageMetaMap.get(resolved);
|
||||||
if (!meta) return;
|
if (!meta) return;
|
||||||
|
|
||||||
const titleSlug = slugify(meta.title?.substring(0, 70) || 'untitled');
|
const linkText = $a.text().trim();
|
||||||
const pageSlug = `${titleSlug}-${meta.slugId}`;
|
const titleMatch =
|
||||||
const internalHref = spaceSlug
|
linkText === meta.title ||
|
||||||
? `/s/${spaceSlug}/p/${pageSlug}`
|
linkText === meta.title?.trim();
|
||||||
: `/p/${pageSlug}`;
|
|
||||||
|
|
||||||
$a.attr('href', internalHref);
|
if (titleMatch) {
|
||||||
$a.attr('data-internal', 'true');
|
const mentionId = v7();
|
||||||
|
const $mention = $('<span>')
|
||||||
|
.attr({
|
||||||
|
'data-type': 'mention',
|
||||||
|
'data-id': mentionId,
|
||||||
|
'data-entity-type': 'page',
|
||||||
|
'data-entity-id': meta.id,
|
||||||
|
'data-label': meta.title,
|
||||||
|
'data-slug-id': meta.slugId,
|
||||||
|
'data-creator-id': creatorId,
|
||||||
|
})
|
||||||
|
.text(meta.title);
|
||||||
|
$a.replaceWith($mention);
|
||||||
|
} else {
|
||||||
|
const titleSlug = slugify(meta.title?.substring(0, 70) || 'untitled');
|
||||||
|
const pageSlug = `${titleSlug}-${meta.slugId}`;
|
||||||
|
const internalHref = spaceSlug
|
||||||
|
? `/s/${spaceSlug}/p/${pageSlug}`
|
||||||
|
: `/p/${pageSlug}`;
|
||||||
|
|
||||||
|
$a.attr('href', internalHref);
|
||||||
|
$a.attr('data-internal', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
backlinks.push({ sourcePageId, targetPageId: meta.id, workspaceId });
|
backlinks.push({ sourcePageId, targetPageId: meta.id, workspaceId });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -81,7 +81,25 @@ export async function collectMarkdownAndHtmlFiles(
|
|||||||
export function stripNotionID(fileName: string): string {
|
export function stripNotionID(fileName: string): string {
|
||||||
// Handle optional separator (space or dash) + 32 alphanumeric chars at end
|
// Handle optional separator (space or dash) + 32 alphanumeric chars at end
|
||||||
const notionIdPattern = /[ -]?[a-z0-9]{32}$/i;
|
const notionIdPattern = /[ -]?[a-z0-9]{32}$/i;
|
||||||
return fileName.replace(notionIdPattern, '').trim();
|
// Handle partial UUID format used for duplicate names: "Name abcd-ef12"
|
||||||
|
const partialIdPattern = / [a-f0-9]{4}-[a-f0-9]{4}$/i;
|
||||||
|
return fileName
|
||||||
|
.replace(notionIdPattern, '')
|
||||||
|
.replace(partialIdPattern, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a partial Notion UUID suffix from a folder name.
|
||||||
|
* Notion adds "{first4}-{last4}" when multiple pages share the same title.
|
||||||
|
* e.g. "Cool 324d-35ab" → { prefix: "324d", suffix: "35ab" }
|
||||||
|
*/
|
||||||
|
export function extractNotionPartialId(
|
||||||
|
folderName: string,
|
||||||
|
): { prefix: string; suffix: string } | null {
|
||||||
|
const match = folderName.match(/ ([a-f0-9]{4})-([a-f0-9]{4})$/i);
|
||||||
|
if (!match) return null;
|
||||||
|
return { prefix: match[1].toLowerCase(), suffix: match[2].toLowerCase() };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encodeFilePath(filePath: string): string {
|
export function encodeFilePath(filePath: string): string {
|
||||||
|
|||||||
@@ -17,5 +17,6 @@
|
|||||||
},
|
},
|
||||||
"affected": {
|
"affected": {
|
||||||
"defaultBase": "main"
|
"defaultBase": "main"
|
||||||
}
|
},
|
||||||
}
|
"analytics": false
|
||||||
|
}
|
||||||
+62
-64
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.70.1",
|
"version": "0.70.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
@@ -19,73 +19,71 @@
|
|||||||
"clean": "rm -rf apps/*/dist packages/*/dist apps/client/node_modules/.vite"
|
"clean": "rm -rf apps/*/dist packages/*/dist apps/client/node_modules/.vite"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "^7.1.0",
|
"@braintree/sanitize-url": "^7.1.2",
|
||||||
"@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.4",
|
||||||
"@hocuspocus/server": "3.4.4",
|
"@hocuspocus/server": "3.4.4",
|
||||||
"@hocuspocus/transformer": "3.4.4",
|
"@hocuspocus/transformer": "3.4.4",
|
||||||
"@joplin/turndown": "^4.0.74",
|
"@joplin/turndown": "^4.0.82",
|
||||||
"@joplin/turndown-plugin-gfm": "^1.0.56",
|
"@joplin/turndown-plugin-gfm": "^1.0.64",
|
||||||
"@sindresorhus/slugify": "1.1.0",
|
"@sindresorhus/slugify": "3.0.0",
|
||||||
"@tiptap/core": "3.17.1",
|
"@tiptap/core": "3.20.4",
|
||||||
"@tiptap/extension-code-block": "3.17.1",
|
"@tiptap/extension-code-block": "3.20.4",
|
||||||
"@tiptap/extension-collaboration": "3.17.1",
|
"@tiptap/extension-collaboration": "3.20.4",
|
||||||
"@tiptap/extension-collaboration-caret": "3.17.1",
|
"@tiptap/extension-collaboration-caret": "3.20.4",
|
||||||
"@tiptap/extension-color": "3.17.1",
|
"@tiptap/extension-color": "3.20.4",
|
||||||
"@tiptap/extension-document": "3.17.1",
|
"@tiptap/extension-document": "3.20.4",
|
||||||
"@tiptap/extension-heading": "3.17.1",
|
"@tiptap/extension-heading": "3.20.4",
|
||||||
"@tiptap/extension-highlight": "3.17.1",
|
"@tiptap/extension-highlight": "3.20.4",
|
||||||
"@tiptap/extension-history": "3.17.1",
|
"@tiptap/extension-history": "3.20.4",
|
||||||
"@tiptap/extension-image": "3.17.1",
|
"@tiptap/extension-image": "3.20.4",
|
||||||
"@tiptap/extension-link": "3.17.1",
|
"@tiptap/extension-link": "3.20.4",
|
||||||
"@tiptap/extension-list": "3.17.1",
|
"@tiptap/extension-list": "3.20.4",
|
||||||
"@tiptap/extension-placeholder": "3.17.1",
|
"@tiptap/extension-placeholder": "3.20.4",
|
||||||
"@tiptap/extension-subscript": "3.17.1",
|
"@tiptap/extension-subscript": "3.20.4",
|
||||||
"@tiptap/extension-superscript": "3.17.1",
|
"@tiptap/extension-superscript": "3.20.4",
|
||||||
"@tiptap/extension-table": "3.17.1",
|
"@tiptap/extension-table": "3.20.4",
|
||||||
"@tiptap/extension-text": "3.17.1",
|
"@tiptap/extension-text": "3.20.4",
|
||||||
"@tiptap/extension-text-align": "3.17.1",
|
"@tiptap/extension-text-align": "3.20.4",
|
||||||
"@tiptap/extension-text-style": "3.17.1",
|
"@tiptap/extension-text-style": "3.20.4",
|
||||||
"@tiptap/extension-typography": "3.17.1",
|
"@tiptap/extension-typography": "3.20.4",
|
||||||
"@tiptap/extension-unique-id": "^3.17.1",
|
"@tiptap/extension-unique-id": "3.20.4",
|
||||||
"@tiptap/extension-youtube": "3.17.1",
|
"@tiptap/extension-youtube": "3.20.4",
|
||||||
"@tiptap/html": "3.17.1",
|
"@tiptap/html": "3.20.4",
|
||||||
"@tiptap/pm": "3.17.1",
|
"@tiptap/pm": "3.20.4",
|
||||||
"@tiptap/react": "3.17.1",
|
"@tiptap/react": "3.20.4",
|
||||||
"@tiptap/starter-kit": "3.17.1",
|
"@tiptap/starter-kit": "3.20.4",
|
||||||
"@tiptap/suggestion": "3.17.1",
|
"@tiptap/suggestion": "3.20.4",
|
||||||
"@tiptap/y-tiptap": "^3.0.2",
|
"@tiptap/y-tiptap": "3.0.2",
|
||||||
"@types/qrcode": "^1.5.5",
|
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^10.1.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"diff": "8.0.3",
|
"diff": "8.0.3",
|
||||||
"dompurify": "^3.3.1",
|
"dompurify": "^3.3.3",
|
||||||
"fractional-indexing-jittered": "^1.0.0",
|
"fractional-indexing-jittered": "^1.0.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"image-dimensions": "^2.5.0",
|
"image-dimensions": "^2.5.0",
|
||||||
"ioredis": "^5.4.1",
|
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"linkifyjs": "^4.3.2",
|
"linkifyjs": "^4.3.2",
|
||||||
"marked": "13.0.3",
|
"marked": "17.0.5",
|
||||||
"ms": "3.0.0-canary.1",
|
"ms": "3.0.0-canary.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"rfc6902": "5.1.2",
|
"rfc6902": "5.2.0",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^13.0.0",
|
||||||
"y-indexeddb": "^9.0.12",
|
"y-indexeddb": "^9.0.12",
|
||||||
"y-prosemirror": "1.3.7",
|
"y-prosemirror": "1.3.7",
|
||||||
"yjs": "^13.6.29"
|
"yjs": "^13.6.30"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nx/js": "22.5.3",
|
"@nx/js": "22.6.1",
|
||||||
"@types/bytes": "^3.1.5",
|
"@types/bytes": "^3.1.5",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/turndown": "^5.0.6",
|
"@types/turndown": "^5.0.6",
|
||||||
"@types/uuid": "^10.0.0",
|
"concurrently": "^9.2.1",
|
||||||
"concurrently": "^9.1.2",
|
"nx": "22.6.1",
|
||||||
"nx": "22.5.3",
|
"tsx": "^4.21.0"
|
||||||
"tsx": "^4.19.3"
|
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
@@ -100,28 +98,28 @@
|
|||||||
"@tiptap/core": "patches/@tiptap__core.patch"
|
"@tiptap/core": "patches/@tiptap__core.patch"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"jsdom": "25.0.1",
|
"prosemirror-changeset": "2.4.0",
|
||||||
"jsonwebtoken": "9.0.3",
|
|
||||||
"prosemirror-changeset": "2.3.1",
|
|
||||||
"y-prosemirror": "1.3.7",
|
"y-prosemirror": "1.3.7",
|
||||||
"qs": "6.14.2",
|
"glob": "13.0.6",
|
||||||
"glob": "10.5.0",
|
|
||||||
"lodash": "4.17.23",
|
|
||||||
"ws": "8.19.0",
|
"ws": "8.19.0",
|
||||||
"cross-spawn": "7.0.5",
|
"dompurify": "3.3.3",
|
||||||
"dompurify": "3.3.1",
|
|
||||||
"tmp": "0.2.5",
|
"tmp": "0.2.5",
|
||||||
|
"hono": "4.12.8",
|
||||||
|
"mermaid": "11.13.0",
|
||||||
|
"nanoid@^3": "3.3.8",
|
||||||
|
"socket.io-parser": "4.2.6",
|
||||||
|
"serialize-javascript": "7.0.3",
|
||||||
"lodash-es": "4.17.23",
|
"lodash-es": "4.17.23",
|
||||||
"markdown-it": "14.1.1",
|
"@hono/node-server": "1.19.10",
|
||||||
"@tiptap/core": "3.17.1",
|
"undici": "7.24.0",
|
||||||
"@tiptap/pm": "3.17.1",
|
"ajv@^6": "6.14.0",
|
||||||
"@tiptap/starter-kit": "3.17.1",
|
"ajv@^8": "8.18.0",
|
||||||
"@tiptap/extension-blockquote": "3.17.1",
|
"underscore": "1.13.8",
|
||||||
"@tiptap/extension-bold": "3.17.0",
|
"immutable": "4.3.8",
|
||||||
"@tiptap/extension-bubble-menu": "3.17.1",
|
"express-rate-limit": "8.2.2",
|
||||||
"@tiptap/extension-bullet-list": "3.17.1",
|
"minimatch@^3": "3.1.5",
|
||||||
"@tiptap/extension-list": "3.17.1",
|
"minimatch@^5": "5.1.8",
|
||||||
"@tiptap/extension-code": "3.17.1"
|
"flatted": "3.4.2"
|
||||||
},
|
},
|
||||||
"neverBuiltDependencies": []
|
"neverBuiltDependencies": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +1,111 @@
|
|||||||
import { findChildren } from '@tiptap/core'
|
import { findChildren } from '@tiptap/core';
|
||||||
import type { Node as ProsemirrorNode } from '@tiptap/pm/model'
|
import type { Node as ProsemirrorNode } from '@tiptap/pm/model';
|
||||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import highlight from 'highlight.js/lib/core'
|
import highlight from 'highlight.js/lib/core';
|
||||||
|
|
||||||
function parseNodes(nodes: any[], className: string[] = []): { text: string; classes: string[] }[] {
|
function parseNodes(
|
||||||
|
nodes: any[],
|
||||||
|
className: string[] = [],
|
||||||
|
): { text: string; classes: string[] }[] {
|
||||||
return nodes
|
return nodes
|
||||||
.map(node => {
|
.map((node) => {
|
||||||
const classes = [...className, ...(node.properties ? node.properties.className : [])]
|
const classes = [
|
||||||
|
...className,
|
||||||
|
...(node.properties ? node.properties.className : []),
|
||||||
|
];
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
return parseNodes(node.children, classes)
|
return parseNodes(node.children, classes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: node.value,
|
text: node.value,
|
||||||
classes,
|
classes,
|
||||||
}
|
};
|
||||||
})
|
})
|
||||||
.flat()
|
.flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHighlightNodes(result: any) {
|
function getHighlightNodes(result: any) {
|
||||||
// `.value` for lowlight v1, `.children` for lowlight v2
|
// `.value` for lowlight v1, `.children` for lowlight v2
|
||||||
return result.value || result.children || []
|
return result.value || result.children || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function registered(aliasOrLanguage: string) {
|
function registered(aliasOrLanguage: string) {
|
||||||
return Boolean(highlight.getLanguage(aliasOrLanguage))
|
return Boolean(highlight.getLanguage(aliasOrLanguage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Max characters to sample for auto-detection to avoid performance issues with large code blocks
|
||||||
|
const AUTO_DETECT_SAMPLE_SIZE = 3000;
|
||||||
|
|
||||||
function getDecorations({
|
function getDecorations({
|
||||||
doc,
|
doc,
|
||||||
name,
|
name,
|
||||||
lowlight,
|
lowlight,
|
||||||
defaultLanguage,
|
defaultLanguage,
|
||||||
}: {
|
}: {
|
||||||
doc: ProsemirrorNode
|
doc: ProsemirrorNode;
|
||||||
name: string
|
name: string;
|
||||||
lowlight: any
|
lowlight: any;
|
||||||
defaultLanguage: string | null | undefined
|
defaultLanguage: string | null | undefined;
|
||||||
}) {
|
}) {
|
||||||
const decorations: Decoration[] = []
|
const decorations: Decoration[] = [];
|
||||||
|
|
||||||
findChildren(doc, node => node.type.name === name).forEach(block => {
|
findChildren(doc, (node) => node.type.name === name).forEach((block) => {
|
||||||
let from = block.pos + 1
|
let from = block.pos + 1;
|
||||||
const language = block.node.attrs.language || defaultLanguage
|
const language = block.node.attrs.language || defaultLanguage;
|
||||||
const languages = lowlight.listLanguages()
|
const languages = lowlight.listLanguages();
|
||||||
|
const textContent = block.node.textContent;
|
||||||
|
|
||||||
const nodes =
|
let nodes;
|
||||||
language && (languages.includes(language) || registered(language) || lowlight.registered?.(language))
|
if (
|
||||||
? getHighlightNodes(lowlight.highlight(language, block.node.textContent))
|
language &&
|
||||||
: getHighlightNodes(lowlight.highlightAuto(block.node.textContent))
|
(languages.includes(language) ||
|
||||||
|
registered(language) ||
|
||||||
|
lowlight.registered?.(language))
|
||||||
|
) {
|
||||||
|
nodes = getHighlightNodes(lowlight.highlight(language, textContent));
|
||||||
|
} else {
|
||||||
|
// For auto-detection, sample a limited portion to detect the language,
|
||||||
|
// then highlight the full content with the detected language
|
||||||
|
const sample =
|
||||||
|
textContent.length > AUTO_DETECT_SAMPLE_SIZE
|
||||||
|
? textContent.slice(0, AUTO_DETECT_SAMPLE_SIZE)
|
||||||
|
: textContent;
|
||||||
|
const autoResult = lowlight.highlightAuto(sample);
|
||||||
|
const detectedLanguage = autoResult.data?.language;
|
||||||
|
if (detectedLanguage && textContent.length > AUTO_DETECT_SAMPLE_SIZE) {
|
||||||
|
nodes = getHighlightNodes(
|
||||||
|
lowlight.highlight(detectedLanguage, textContent),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
nodes = getHighlightNodes(autoResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
parseNodes(nodes).forEach(node => {
|
parseNodes(nodes).forEach((node) => {
|
||||||
const to = from + node.text.length
|
const to = from + node.text.length;
|
||||||
|
|
||||||
if (node.classes.length) {
|
if (node.classes.length) {
|
||||||
const decoration = Decoration.inline(from, to, {
|
const decoration = Decoration.inline(from, to, {
|
||||||
class: node.classes.join(' '),
|
class: node.classes.join(' '),
|
||||||
})
|
});
|
||||||
|
|
||||||
decorations.push(decoration)
|
decorations.push(decoration);
|
||||||
}
|
}
|
||||||
|
|
||||||
from = to
|
from = to;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
return DecorationSet.create(doc, decorations)
|
return DecorationSet.create(doc, decorations);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
function isFunction(param: any): param is Function {
|
function isFunction(param: any): param is Function {
|
||||||
return typeof param === 'function'
|
return typeof param === 'function';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LowlightPlugin({
|
export function LowlightPlugin({
|
||||||
@@ -82,12 +113,18 @@ export function LowlightPlugin({
|
|||||||
lowlight,
|
lowlight,
|
||||||
defaultLanguage,
|
defaultLanguage,
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string;
|
||||||
lowlight: any
|
lowlight: any;
|
||||||
defaultLanguage: string | null | undefined
|
defaultLanguage: string | null | undefined;
|
||||||
}) {
|
}) {
|
||||||
if (!['highlight', 'highlightAuto', 'listLanguages'].every(api => isFunction(lowlight[api]))) {
|
if (
|
||||||
throw Error('You should provide an instance of lowlight to use the code-block-lowlight extension')
|
!['highlight', 'highlightAuto', 'listLanguages'].every((api) =>
|
||||||
|
isFunction(lowlight[api]),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw Error(
|
||||||
|
'You should provide an instance of lowlight to use the code-block-lowlight extension',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lowlightPlugin: Plugin<any> = new Plugin({
|
const lowlightPlugin: Plugin<any> = new Plugin({
|
||||||
@@ -102,10 +139,16 @@ export function LowlightPlugin({
|
|||||||
defaultLanguage,
|
defaultLanguage,
|
||||||
}),
|
}),
|
||||||
apply: (transaction, decorationSet, oldState, newState) => {
|
apply: (transaction, decorationSet, oldState, newState) => {
|
||||||
const oldNodeName = oldState.selection.$head.parent.type.name
|
const oldNodeName = oldState.selection.$head.parent.type.name;
|
||||||
const newNodeName = newState.selection.$head.parent.type.name
|
const newNodeName = newState.selection.$head.parent.type.name;
|
||||||
const oldNodes = findChildren(oldState.doc, node => node.type.name === name)
|
const oldNodes = findChildren(
|
||||||
const newNodes = findChildren(newState.doc, node => node.type.name === name)
|
oldState.doc,
|
||||||
|
(node) => node.type.name === name,
|
||||||
|
);
|
||||||
|
const newNodes = findChildren(
|
||||||
|
newState.doc,
|
||||||
|
(node) => node.type.name === name,
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
transaction.docChanged &&
|
transaction.docChanged &&
|
||||||
@@ -117,23 +160,23 @@ export function LowlightPlugin({
|
|||||||
// OR transaction has changes that completely encapsulte a node
|
// OR transaction has changes that completely encapsulte a node
|
||||||
// (for example, a transaction that affects the entire document).
|
// (for example, a transaction that affects the entire document).
|
||||||
// Such transactions can happen during collab syncing via y-prosemirror, for example.
|
// Such transactions can happen during collab syncing via y-prosemirror, for example.
|
||||||
transaction.steps.some(step => {
|
transaction.steps.some((step) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
step.from !== undefined &&
|
step.from !== undefined &&
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
step.to !== undefined &&
|
step.to !== undefined &&
|
||||||
oldNodes.some(node => {
|
oldNodes.some((node) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
node.pos >= step.from &&
|
node.pos >= step.from &&
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
node.pos + node.node.nodeSize <= step.to
|
node.pos + node.node.nodeSize <= step.to
|
||||||
)
|
);
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
}))
|
}))
|
||||||
) {
|
) {
|
||||||
return getDecorations({
|
return getDecorations({
|
||||||
@@ -141,19 +184,19 @@ export function LowlightPlugin({
|
|||||||
name,
|
name,
|
||||||
lowlight,
|
lowlight,
|
||||||
defaultLanguage,
|
defaultLanguage,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return decorationSet.map(transaction.mapping, transaction.doc)
|
return decorationSet.map(transaction.mapping, transaction.doc);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
decorations(state) {
|
decorations(state) {
|
||||||
return lowlightPlugin.getState(state)
|
return lowlightPlugin.getState(state);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return lowlightPlugin
|
return lowlightPlugin;
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4264
-3776
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user