mirror of
https://github.com/docmost/docmost.git
synced 2026-05-08 23:33:09 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2b8d2077a | |||
| d7d14c2acf | |||
| 6e5efc3757 | |||
| bf692e8b08 | |||
| ff01355ec3 | |||
| 78c3839ae7 | |||
| 73ed0c54e5 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.70.3",
|
||||
"version": "0.70.1",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
|
||||
@@ -10,5 +10,3 @@ export const readOnlyEditorAtom = atom<Editor | null>(null);
|
||||
export const yjsConnectionStatusAtom = atom<string>("");
|
||||
|
||||
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 { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
||||
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";
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
@@ -49,8 +49,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
||||
const showCommentPopupRef = useRef(showCommentPopup);
|
||||
const showAiMenuRef = useRef(showAiMenu);
|
||||
const [showLinkMenu] = useAtom(showLinkMenuAtom);
|
||||
const showLinkMenuRef = useRef(showLinkMenu);
|
||||
|
||||
useEffect(() => {
|
||||
showCommentPopupRef.current = showCommentPopup;
|
||||
@@ -60,10 +58,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
showAiMenuRef.current = showAiMenu;
|
||||
}, [showAiMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
showLinkMenuRef.current = showLinkMenu;
|
||||
}, [showLinkMenu]);
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor: props.editor,
|
||||
selector: (ctx) => {
|
||||
@@ -141,7 +135,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
isNodeSelection(selection) ||
|
||||
isCellSelection(selection) ||
|
||||
showAiMenuRef.current ||
|
||||
showLinkMenuRef.current ||
|
||||
showCommentPopupRef?.current
|
||||
) {
|
||||
return false;
|
||||
@@ -154,6 +147,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
onHide: () => {
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
},
|
||||
},
|
||||
@@ -161,10 +155,11 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
|
||||
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
||||
const [isTextAlignmentSelectorOpen, setIsTextAlignmentOpen] = useState(false);
|
||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
||||
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
||||
|
||||
// Hide the bubble menu immediately when AI menu is shown
|
||||
if (showAiMenu || showLinkMenu) return;
|
||||
if (showAiMenu) return;
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
@@ -194,6 +189,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
setIsOpen={() => {
|
||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
@@ -204,6 +200,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
setIsOpen={() => {
|
||||
setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen);
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
@@ -227,7 +224,16 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
))}
|
||||
</ActionIcon.Group>
|
||||
|
||||
<LinkSelector />
|
||||
<LinkSelector
|
||||
editor={props.editor}
|
||||
isOpen={isLinkSelectorOpen}
|
||||
setIsOpen={(value) => {
|
||||
setIsLinkSelectorOpen(value);
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ColorSelector
|
||||
editor={props.editor}
|
||||
@@ -236,6 +242,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
setIsColorSelectorOpen(!isColorSelectorOpen);
|
||||
setIsNodeSelectorOpen(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 { ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { ActionIcon, Popover, Tooltip } from "@mantine/core";
|
||||
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 { 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 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 (
|
||||
<Tooltip label={t("Add link")} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="0"
|
||||
style={{ border: "none" }}
|
||||
onClick={() => setShowLinkMenu(true)}
|
||||
>
|
||||
<IconLink size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
width={320}
|
||||
opened={isOpen}
|
||||
trapFocus
|
||||
offset={{ mainAxis: 35, crossAxis: 0 }}
|
||||
withArrow
|
||||
shadow="md"
|
||||
>
|
||||
<Popover.Target>
|
||||
<Tooltip label={t("Add link")} withArrow>
|
||||
<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,
|
||||
includePages: true,
|
||||
spaceId: space?.id,
|
||||
limit: state.isSearchQuery ? 10 : 3,
|
||||
limit: state.isSearchQuery ? 10 : 5,
|
||||
preload: true,
|
||||
});
|
||||
|
||||
@@ -105,7 +105,6 @@ export const LinkEditorPanel = ({
|
||||
value={state.url}
|
||||
onChange={state.onChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
data-autofocus
|
||||
autoFocus
|
||||
/>
|
||||
</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 { useEditorScroll } from "./hooks/use-editor-scroll";
|
||||
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";
|
||||
|
||||
interface PageEditorProps {
|
||||
@@ -408,7 +407,6 @@ export default function PageEditor({
|
||||
{editor && editorIsEditable && (
|
||||
<div>
|
||||
<EditorAiMenu editor={editor} />
|
||||
<EditorLinkMenu editor={editor} />
|
||||
<EditorBubbleMenu editor={editor} />
|
||||
<TableMenu editor={editor} />
|
||||
<TableCellMenu editor={editor} appendTo={menuContainerRef} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.70.3",
|
||||
"version": "0.70.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -91,15 +91,9 @@ export class SearchService {
|
||||
return { items: [] };
|
||||
}
|
||||
|
||||
const isRestricted =
|
||||
await this.pagePermissionRepo.hasRestrictedAncestor(share.pageId);
|
||||
if (isRestricted) {
|
||||
return { items: [] };
|
||||
}
|
||||
|
||||
const pageIdsToSearch = [];
|
||||
if (share.includeSubPages) {
|
||||
const pageList = await this.pageRepo.getPageAndDescendantsExcludingRestricted(
|
||||
const pageList = await this.pageRepo.getPageAndDescendants(
|
||||
share.pageId,
|
||||
{
|
||||
includeContent: false,
|
||||
|
||||
+1
-1
Submodule apps/server/src/ee updated: 0b5c8646e6...8b21c6e32e
@@ -25,7 +25,6 @@ import {
|
||||
buildAttachmentCandidates,
|
||||
collectMarkdownAndHtmlFiles,
|
||||
encodeFilePath,
|
||||
extractNotionPartialId,
|
||||
readDocmostMetadata,
|
||||
stripNotionID,
|
||||
} from '../utils/import.utils';
|
||||
@@ -161,7 +160,6 @@ export class FileImportTaskService {
|
||||
fileTask: FileTask;
|
||||
}): Promise<void> {
|
||||
const { extractDir, fileTask } = opts;
|
||||
const isNotion = fileTask.source === FileImportSource.Notion;
|
||||
const allFiles = await collectMarkdownAndHtmlFiles(extractDir);
|
||||
const attachmentCandidates = await buildAttachmentCandidates(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
|
||||
// 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) => {
|
||||
foldersWithContent.forEach((folderPath) => {
|
||||
if (
|
||||
skipRootFolder &&
|
||||
folderPath?.toLowerCase() === skipRootFolder?.toLowerCase()
|
||||
@@ -255,54 +243,18 @@ export class FileImportTaskService {
|
||||
|
||||
if (!pagesMap.has(mdPath) && !pagesMap.has(htmlPath)) {
|
||||
const folderName = path.basename(folderPath);
|
||||
const parentDir = path.dirname(folderPath);
|
||||
|
||||
// Notion no longer adds UUIDs to folder names, but still adds them to files.
|
||||
// For duplicate names, Notion adds a partial UUID "{first4}-{last4}" to the folder.
|
||||
let matched = false;
|
||||
if (isNotion) {
|
||||
const partialId = extractNotionPartialId(folderName);
|
||||
const strippedFolderName = stripNotionID(folderName);
|
||||
const isSameDir = (fileDir: string) =>
|
||||
fileDir === parentDir || (parentDir === '.' && !fileDir.includes('/'));
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
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 { Logger } from '@nestjs/common';
|
||||
import * as path from 'path';
|
||||
import { v7 } from 'uuid';
|
||||
import { InsertableBacklink } from '@docmost/db/types/entity.types';
|
||||
import { Cheerio, CheerioAPI, load } from 'cheerio';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
@@ -345,35 +344,14 @@ export async function rewriteInternalLinksToMentionHtml(
|
||||
const meta = filePathToPageMetaMap.get(resolved);
|
||||
if (!meta) return;
|
||||
|
||||
const linkText = $a.text().trim();
|
||||
const titleMatch =
|
||||
linkText === meta.title ||
|
||||
linkText === meta.title?.trim();
|
||||
const titleSlug = slugify(meta.title?.substring(0, 70) || 'untitled');
|
||||
const pageSlug = `${titleSlug}-${meta.slugId}`;
|
||||
const internalHref = spaceSlug
|
||||
? `/s/${spaceSlug}/p/${pageSlug}`
|
||||
: `/p/${pageSlug}`;
|
||||
|
||||
if (titleMatch) {
|
||||
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');
|
||||
}
|
||||
$a.attr('href', internalHref);
|
||||
$a.attr('data-internal', 'true');
|
||||
|
||||
backlinks.push({ sourcePageId, targetPageId: meta.id, workspaceId });
|
||||
});
|
||||
|
||||
@@ -81,25 +81,7 @@ export async function collectMarkdownAndHtmlFiles(
|
||||
export function stripNotionID(fileName: string): string {
|
||||
// Handle optional separator (space or dash) + 32 alphanumeric chars at end
|
||||
const notionIdPattern = /[ -]?[a-z0-9]{32}$/i;
|
||||
// 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() };
|
||||
return fileName.replace(notionIdPattern, '').trim();
|
||||
}
|
||||
|
||||
export function encodeFilePath(filePath: string): string {
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "docmost",
|
||||
"homepage": "https://docmost.com",
|
||||
"version": "0.70.3",
|
||||
"version": "0.70.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nx run-many -t build",
|
||||
@@ -62,7 +62,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"diff": "8.0.3",
|
||||
"dompurify": "^3.3.3",
|
||||
"dompurify": "^3.3.1",
|
||||
"fractional-indexing-jittered": "^1.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"image-dimensions": "^2.5.0",
|
||||
@@ -109,7 +109,7 @@
|
||||
"lodash": "4.17.23",
|
||||
"ws": "8.19.0",
|
||||
"cross-spawn": "7.0.5",
|
||||
"dompurify": "3.3.3",
|
||||
"dompurify": "3.3.1",
|
||||
"tmp": "0.2.5",
|
||||
"lodash-es": "4.17.23",
|
||||
"markdown-it": "14.1.1",
|
||||
|
||||
Generated
+8
-8
@@ -14,7 +14,7 @@ overrides:
|
||||
lodash: 4.17.23
|
||||
ws: 8.19.0
|
||||
cross-spawn: 7.0.5
|
||||
dompurify: 3.3.3
|
||||
dompurify: 3.3.1
|
||||
tmp: 0.2.5
|
||||
lodash-es: 4.17.23
|
||||
markdown-it: 14.1.1
|
||||
@@ -170,8 +170,8 @@ importers:
|
||||
specifier: 8.0.3
|
||||
version: 8.0.3
|
||||
dompurify:
|
||||
specifier: 3.3.3
|
||||
version: 3.3.3
|
||||
specifier: 3.3.1
|
||||
version: 3.3.1
|
||||
fractional-indexing-jittered:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
@@ -6510,8 +6510,8 @@ packages:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
dompurify@3.3.3:
|
||||
resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==}
|
||||
dompurify@3.3.1:
|
||||
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
|
||||
|
||||
domutils@3.2.2:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
@@ -17205,7 +17205,7 @@ snapshots:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
dompurify@3.3.3:
|
||||
dompurify@3.3.1:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
@@ -19251,7 +19251,7 @@ snapshots:
|
||||
d3-sankey: 0.12.3
|
||||
dagre-d3-es: 7.0.13
|
||||
dayjs: 1.11.19
|
||||
dompurify: 3.3.3
|
||||
dompurify: 3.3.1
|
||||
katex: 0.16.27
|
||||
khroma: 2.1.0
|
||||
lodash-es: 4.17.23
|
||||
@@ -20004,7 +20004,7 @@ snapshots:
|
||||
'@posthog/core': 1.22.0
|
||||
'@posthog/types': 1.345.5
|
||||
core-js: 3.43.0
|
||||
dompurify: 3.3.3
|
||||
dompurify: 3.3.1
|
||||
fflate: 0.4.8
|
||||
preact: 10.28.3
|
||||
query-selector-shadow-dom: 1.0.1
|
||||
|
||||
Reference in New Issue
Block a user