mirror of
https://github.com/docmost/docmost.git
synced 2026-05-09 07:43:06 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2b8d2077a | |||
| d7d14c2acf | |||
| 6e5efc3757 | |||
| bf692e8b08 | |||
| ff01355ec3 | |||
| 78c3839ae7 | |||
| 73ed0c54e5 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.70.3",
|
"version": "0.70.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
|||||||
@@ -10,5 +10,3 @@ 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, showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms";
|
import { showAiMenuAtom } 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,8 +49,6 @@ 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;
|
||||||
@@ -60,10 +58,6 @@ 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) => {
|
||||||
@@ -141,7 +135,6 @@ 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;
|
||||||
@@ -154,6 +147,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
onHide: () => {
|
onHide: () => {
|
||||||
setIsNodeSelectorOpen(false);
|
setIsNodeSelectorOpen(false);
|
||||||
setIsTextAlignmentOpen(false);
|
setIsTextAlignmentOpen(false);
|
||||||
|
setIsLinkSelectorOpen(false);
|
||||||
setIsColorSelectorOpen(false);
|
setIsColorSelectorOpen(false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -161,10 +155,11 @@ 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 || showLinkMenu) return;
|
if (showAiMenu) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
@@ -194,6 +189,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
setIsOpen={() => {
|
setIsOpen={() => {
|
||||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||||
setIsTextAlignmentOpen(false);
|
setIsTextAlignmentOpen(false);
|
||||||
|
setIsLinkSelectorOpen(false);
|
||||||
setIsColorSelectorOpen(false);
|
setIsColorSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -204,6 +200,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
setIsOpen={() => {
|
setIsOpen={() => {
|
||||||
setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen);
|
setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen);
|
||||||
setIsNodeSelectorOpen(false);
|
setIsNodeSelectorOpen(false);
|
||||||
|
setIsLinkSelectorOpen(false);
|
||||||
setIsColorSelectorOpen(false);
|
setIsColorSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -227,7 +224,16 @@ 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}
|
||||||
@@ -236,6 +242,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
setIsColorSelectorOpen(!isColorSelectorOpen);
|
setIsColorSelectorOpen(!isColorSelectorOpen);
|
||||||
setIsNodeSelectorOpen(false);
|
setIsNodeSelectorOpen(false);
|
||||||
setIsTextAlignmentOpen(false);
|
setIsTextAlignmentOpen(false);
|
||||||
|
setIsLinkSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,68 @@
|
|||||||
import { FC } from "react";
|
import { Dispatch, FC, SetStateAction, useCallback } from "react";
|
||||||
import { IconLink } from "@tabler/icons-react";
|
import { IconLink } from "@tabler/icons-react";
|
||||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
import { ActionIcon, Popover, Tooltip } from "@mantine/core";
|
||||||
import { useSetAtom } from "jotai";
|
import { useEditor } from "@tiptap/react";
|
||||||
|
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";
|
|
||||||
|
|
||||||
export const LinkSelector: FC = () => {
|
interface LinkSelectorProps {
|
||||||
|
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 setShowLinkMenu = useSetAtom(showLinkMenuAtom);
|
const onLink = useCallback(
|
||||||
|
(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 (
|
||||||
<Tooltip label={t("Add link")} withArrow>
|
<Popover
|
||||||
<ActionIcon
|
width={320}
|
||||||
variant="default"
|
opened={isOpen}
|
||||||
size="lg"
|
trapFocus
|
||||||
radius="0"
|
offset={{ mainAxis: 35, crossAxis: 0 }}
|
||||||
style={{ border: "none" }}
|
withArrow
|
||||||
onClick={() => setShowLinkMenu(true)}
|
shadow="md"
|
||||||
>
|
>
|
||||||
<IconLink size={16} />
|
<Popover.Target>
|
||||||
</ActionIcon>
|
<Tooltip label={t("Add link")} withArrow>
|
||||||
</Tooltip>
|
<ActionIcon
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 : 3,
|
limit: state.isSearchQuery ? 10 : 5,
|
||||||
preload: true,
|
preload: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,7 +105,6 @@ 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>
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
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 "@/features/editor/components/link/link-view";
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -66,7 +66,6 @@ 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 {
|
||||||
@@ -408,7 +407,6 @@ 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} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.70.3",
|
"version": "0.70.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -91,15 +91,9 @@ 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.getPageAndDescendantsExcludingRestricted(
|
const pageList = await this.pageRepo.getPageAndDescendants(
|
||||||
share.pageId,
|
share.pageId,
|
||||||
{
|
{
|
||||||
includeContent: false,
|
includeContent: false,
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 0b5c8646e6...8b21c6e32e
@@ -25,7 +25,6 @@ import {
|
|||||||
buildAttachmentCandidates,
|
buildAttachmentCandidates,
|
||||||
collectMarkdownAndHtmlFiles,
|
collectMarkdownAndHtmlFiles,
|
||||||
encodeFilePath,
|
encodeFilePath,
|
||||||
extractNotionPartialId,
|
|
||||||
readDocmostMetadata,
|
readDocmostMetadata,
|
||||||
stripNotionID,
|
stripNotionID,
|
||||||
} from '../utils/import.utils';
|
} from '../utils/import.utils';
|
||||||
@@ -161,7 +160,6 @@ 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);
|
||||||
@@ -232,17 +230,7 @@ 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
|
||||||
// Process folders with partial UUIDs first so they claim their specific files
|
foldersWithContent.forEach((folderPath) => {
|
||||||
// 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()
|
||||||
@@ -255,54 +243,18 @@ 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 parentDir = path.dirname(folderPath);
|
const encodedMdPath = encodeFilePath(mdPath);
|
||||||
|
const placeholderMetadata = docmostMetadata?.pages[encodedMdPath];
|
||||||
// Notion no longer adds UUIDs to folder names, but still adds them to files.
|
pagesMap.set(mdPath, {
|
||||||
// For duplicate names, Notion adds a partial UUID "{first4}-{last4}" to the folder.
|
id: v7(),
|
||||||
let matched = false;
|
slugId: generateSlugId(),
|
||||||
if (isNotion) {
|
name: stripNotionID(folderName),
|
||||||
const partialId = extractNotionPartialId(folderName);
|
content: '',
|
||||||
const strippedFolderName = stripNotionID(folderName);
|
parentPageId: null,
|
||||||
const isSameDir = (fileDir: string) =>
|
fileExtension: '.md',
|
||||||
fileDir === parentDir || (parentDir === '.' && !fileDir.includes('/'));
|
filePath: mdPath,
|
||||||
|
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,7 +1,6 @@
|
|||||||
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
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
@@ -345,35 +344,14 @@ export async function rewriteInternalLinksToMentionHtml(
|
|||||||
const meta = filePathToPageMetaMap.get(resolved);
|
const meta = filePathToPageMetaMap.get(resolved);
|
||||||
if (!meta) return;
|
if (!meta) return;
|
||||||
|
|
||||||
const linkText = $a.text().trim();
|
const titleSlug = slugify(meta.title?.substring(0, 70) || 'untitled');
|
||||||
const titleMatch =
|
const pageSlug = `${titleSlug}-${meta.slugId}`;
|
||||||
linkText === meta.title ||
|
const internalHref = spaceSlug
|
||||||
linkText === meta.title?.trim();
|
? `/s/${spaceSlug}/p/${pageSlug}`
|
||||||
|
: `/p/${pageSlug}`;
|
||||||
|
|
||||||
if (titleMatch) {
|
$a.attr('href', internalHref);
|
||||||
const mentionId = v7();
|
$a.attr('data-internal', 'true');
|
||||||
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,25 +81,7 @@ 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;
|
||||||
// Handle partial UUID format used for duplicate names: "Name abcd-ef12"
|
return fileName.replace(notionIdPattern, '').trim();
|
||||||
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 {
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.70.3",
|
"version": "0.70.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"diff": "8.0.3",
|
"diff": "8.0.3",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.1",
|
||||||
"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",
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
"lodash": "4.17.23",
|
"lodash": "4.17.23",
|
||||||
"ws": "8.19.0",
|
"ws": "8.19.0",
|
||||||
"cross-spawn": "7.0.5",
|
"cross-spawn": "7.0.5",
|
||||||
"dompurify": "3.3.3",
|
"dompurify": "3.3.1",
|
||||||
"tmp": "0.2.5",
|
"tmp": "0.2.5",
|
||||||
"lodash-es": "4.17.23",
|
"lodash-es": "4.17.23",
|
||||||
"markdown-it": "14.1.1",
|
"markdown-it": "14.1.1",
|
||||||
|
|||||||
Generated
+8
-8
@@ -14,7 +14,7 @@ overrides:
|
|||||||
lodash: 4.17.23
|
lodash: 4.17.23
|
||||||
ws: 8.19.0
|
ws: 8.19.0
|
||||||
cross-spawn: 7.0.5
|
cross-spawn: 7.0.5
|
||||||
dompurify: 3.3.3
|
dompurify: 3.3.1
|
||||||
tmp: 0.2.5
|
tmp: 0.2.5
|
||||||
lodash-es: 4.17.23
|
lodash-es: 4.17.23
|
||||||
markdown-it: 14.1.1
|
markdown-it: 14.1.1
|
||||||
@@ -170,8 +170,8 @@ importers:
|
|||||||
specifier: 8.0.3
|
specifier: 8.0.3
|
||||||
version: 8.0.3
|
version: 8.0.3
|
||||||
dompurify:
|
dompurify:
|
||||||
specifier: 3.3.3
|
specifier: 3.3.1
|
||||||
version: 3.3.3
|
version: 3.3.1
|
||||||
fractional-indexing-jittered:
|
fractional-indexing-jittered:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@@ -6510,8 +6510,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
dompurify@3.3.3:
|
dompurify@3.3.1:
|
||||||
resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==}
|
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
|
||||||
|
|
||||||
domutils@3.2.2:
|
domutils@3.2.2:
|
||||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||||
@@ -17205,7 +17205,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
domelementtype: 2.3.0
|
domelementtype: 2.3.0
|
||||||
|
|
||||||
dompurify@3.3.3:
|
dompurify@3.3.1:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/trusted-types': 2.0.7
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
@@ -19251,7 +19251,7 @@ snapshots:
|
|||||||
d3-sankey: 0.12.3
|
d3-sankey: 0.12.3
|
||||||
dagre-d3-es: 7.0.13
|
dagre-d3-es: 7.0.13
|
||||||
dayjs: 1.11.19
|
dayjs: 1.11.19
|
||||||
dompurify: 3.3.3
|
dompurify: 3.3.1
|
||||||
katex: 0.16.27
|
katex: 0.16.27
|
||||||
khroma: 2.1.0
|
khroma: 2.1.0
|
||||||
lodash-es: 4.17.23
|
lodash-es: 4.17.23
|
||||||
@@ -20004,7 +20004,7 @@ snapshots:
|
|||||||
'@posthog/core': 1.22.0
|
'@posthog/core': 1.22.0
|
||||||
'@posthog/types': 1.345.5
|
'@posthog/types': 1.345.5
|
||||||
core-js: 3.43.0
|
core-js: 3.43.0
|
||||||
dompurify: 3.3.3
|
dompurify: 3.3.1
|
||||||
fflate: 0.4.8
|
fflate: 0.4.8
|
||||||
preact: 10.28.3
|
preact: 10.28.3
|
||||||
query-selector-shadow-dom: 1.0.1
|
query-selector-shadow-dom: 1.0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user