mirror of
https://github.com/docmost/docmost.git
synced 2025-11-18 06:21:10 +10:00
Merge branch 'main' into ai-vector
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.23.1",
|
"version": "0.23.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconExternalLink, IconWorld } from "@tabler/icons-react";
|
import { IconExternalLink, IconWorld, IconLock } from "@tabler/icons-react";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
useCreateShareMutation,
|
useCreateShareMutation,
|
||||||
@ -18,23 +18,27 @@ import {
|
|||||||
useShareForPageQuery,
|
useShareForPageQuery,
|
||||||
useUpdateShareMutation,
|
useUpdateShareMutation,
|
||||||
} from "@/features/share/queries/share-query.ts";
|
} from "@/features/share/queries/share-query.ts";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
import { extractPageSlugId, getPageIcon } from "@/lib";
|
import { extractPageSlugId, getPageIcon } from "@/lib";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CopyTextButton from "@/components/common/copy.tsx";
|
import CopyTextButton from "@/components/common/copy.tsx";
|
||||||
import { getAppUrl } from "@/lib/config.ts";
|
import { getAppUrl, isCloud } from "@/lib/config.ts";
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
import classes from "@/features/share/components/share.module.css";
|
import classes from "@/features/share/components/share.module.css";
|
||||||
|
import useTrial from "@/ee/hooks/use-trial.tsx";
|
||||||
|
import { getCheckoutLink } from "@/ee/billing/services/billing-service.ts";
|
||||||
|
|
||||||
interface ShareModalProps {
|
interface ShareModalProps {
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
}
|
}
|
||||||
export default function ShareModal({ readOnly }: ShareModalProps) {
|
export default function ShareModal({ readOnly }: ShareModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { pageSlug } = useParams();
|
const { pageSlug } = useParams();
|
||||||
const pageId = extractPageSlugId(pageSlug);
|
const pageId = extractPageSlugId(pageSlug);
|
||||||
const { data: share } = useShareForPageQuery(pageId);
|
const { data: share } = useShareForPageQuery(pageId);
|
||||||
const { spaceSlug } = useParams();
|
const { spaceSlug } = useParams();
|
||||||
|
const { isTrial } = useTrial();
|
||||||
const createShareMutation = useCreateShareMutation();
|
const createShareMutation = useCreateShareMutation();
|
||||||
const updateShareMutation = useUpdateShareMutation();
|
const updateShareMutation = useUpdateShareMutation();
|
||||||
const deleteShareMutation = useDeleteShareMutation();
|
const deleteShareMutation = useDeleteShareMutation();
|
||||||
@ -61,7 +65,7 @@ export default function ShareModal({ readOnly }: ShareModalProps) {
|
|||||||
createShareMutation.mutateAsync({
|
createShareMutation.mutateAsync({
|
||||||
pageId: pageId,
|
pageId: pageId,
|
||||||
includeSubPages: true,
|
includeSubPages: true,
|
||||||
searchIndexing: true,
|
searchIndexing: false,
|
||||||
});
|
});
|
||||||
setIsPagePublic(value);
|
setIsPagePublic(value);
|
||||||
} else {
|
} else {
|
||||||
@ -92,26 +96,29 @@ export default function ShareModal({ readOnly }: ShareModalProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const shareLink = useMemo(() => (
|
const shareLink = useMemo(
|
||||||
<Group my="sm" gap={4} wrap="nowrap">
|
() => (
|
||||||
<TextInput
|
<Group my="sm" gap={4} wrap="nowrap">
|
||||||
variant="filled"
|
<TextInput
|
||||||
value={publicLink}
|
variant="filled"
|
||||||
readOnly
|
value={publicLink}
|
||||||
rightSection={<CopyTextButton text={publicLink} />}
|
readOnly
|
||||||
style={{ width: "100%" }}
|
rightSection={<CopyTextButton text={publicLink} />}
|
||||||
/>
|
style={{ width: "100%" }}
|
||||||
<ActionIcon
|
/>
|
||||||
component="a"
|
<ActionIcon
|
||||||
variant="default"
|
component="a"
|
||||||
target="_blank"
|
variant="default"
|
||||||
href={publicLink}
|
target="_blank"
|
||||||
size="sm"
|
href={publicLink}
|
||||||
>
|
size="sm"
|
||||||
<IconExternalLink size={16} />
|
>
|
||||||
</ActionIcon>
|
<IconExternalLink size={16} />
|
||||||
</Group>
|
</ActionIcon>
|
||||||
), [publicLink]);
|
</Group>
|
||||||
|
),
|
||||||
|
[publicLink],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover width={350} position="bottom" withArrow shadow="md">
|
<Popover width={350} position="bottom" withArrow shadow="md">
|
||||||
@ -135,7 +142,28 @@ export default function ShareModal({ readOnly }: ShareModalProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown style={{ userSelect: "none" }}>
|
<Popover.Dropdown style={{ userSelect: "none" }}>
|
||||||
{isDescendantShared ? (
|
{isCloud() && isTrial ? (
|
||||||
|
<>
|
||||||
|
<Group justify="center" mb="sm">
|
||||||
|
<IconLock size={20} stroke={1.5} />
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" ta="center" fw={500} mb="xs">
|
||||||
|
{t("Upgrade to share pages")}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" ta="center" mb="sm">
|
||||||
|
{t(
|
||||||
|
"Page sharing is available on paid plans. Upgrade to share your pages publicly.",
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
onClick={() => navigate("/settings/billing")}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{t("Upgrade Plan")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : isDescendantShared ? (
|
||||||
<>
|
<>
|
||||||
<Text size="sm">{t("Inherits public sharing from")}</Text>
|
<Text size="sm">{t("Inherits public sharing from")}</Text>
|
||||||
<Anchor
|
<Anchor
|
||||||
|
|||||||
@ -152,13 +152,36 @@ export function useDeleteSpaceMutation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const spaces = queryClient.getQueryData(["spaces"]) as any;
|
// Remove space-specific queries
|
||||||
|
if (variables.id) {
|
||||||
|
queryClient.removeQueries({
|
||||||
|
queryKey: ["space", variables.id],
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalidate recent changes
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["recent-changes"],
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["recent-changes", variables.id],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update spaces list cache
|
||||||
|
/* const spaces = queryClient.getQueryData(["spaces"]) as any;
|
||||||
if (spaces) {
|
if (spaces) {
|
||||||
spaces.items = spaces.items?.filter(
|
spaces.items = spaces.items?.filter(
|
||||||
(space: ISpace) => space.id !== variables.id,
|
(space: ISpace) => space.id !== variables.id,
|
||||||
);
|
);
|
||||||
queryClient.setQueryData(["spaces"], spaces);
|
queryClient.setQueryData(["spaces"], spaces);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
// Invalidate all spaces queries to refresh lists
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
predicate: (item) => ["spaces"].includes(item.queryKey[0] as string),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error["response"]?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.23.1",
|
"version": "0.23.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
Submodule apps/server/src/ee updated: fd34d4183a...3af21def15
@ -47,15 +47,23 @@ export class FileTaskProcessor extends WorkerHost implements OnModuleDestroy {
|
|||||||
await this.handleFailedJob(job);
|
await this.handleFailedJob(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnWorkerEvent('stalled')
|
@OnWorkerEvent('completed')
|
||||||
async onStalled(job: Job) {
|
async onCompleted(job: Job) {
|
||||||
this.logger.error(
|
this.logger.log(
|
||||||
`Job ${job.name} stalled. . Import Task ID: ${job.data.fileTaskId}.. Job ID: ${job.id}`,
|
`Completed ${job.name} job for File task ID ${job.data.fileTaskId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set failedReason for stalled jobs since it's not automatically set
|
try {
|
||||||
job.failedReason = 'Job stalled and was marked as failed';
|
const fileTask = await this.fileTaskService.getFileTask(
|
||||||
await this.handleFailedJob(job);
|
job.data.fileTaskId,
|
||||||
|
);
|
||||||
|
if (fileTask) {
|
||||||
|
await this.storageService.delete(fileTask.filePath);
|
||||||
|
this.logger.debug(`Deleted imported zip file: ${fileTask.filePath}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`Failed to delete imported zip file:`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleFailedJob(job: Job) {
|
private async handleFailedJob(job: Job) {
|
||||||
@ -78,25 +86,6 @@ export class FileTaskProcessor extends WorkerHost implements OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnWorkerEvent('completed')
|
|
||||||
async onCompleted(job: Job) {
|
|
||||||
this.logger.log(
|
|
||||||
`Completed ${job.name} job for File task ID ${job.data.fileTaskId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fileTask = await this.fileTaskService.getFileTask(
|
|
||||||
job.data.fileTaskId,
|
|
||||||
);
|
|
||||||
if (fileTask) {
|
|
||||||
await this.storageService.delete(fileTask.filePath);
|
|
||||||
this.logger.debug(`Deleted imported zip file: ${fileTask.filePath}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(`Failed to delete imported zip file:`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async onModuleDestroy(): Promise<void> {
|
async onModuleDestroy(): Promise<void> {
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
await this.worker.close();
|
await this.worker.close();
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import { formatImportHtml } from '../utils/import-formatter';
|
|||||||
import {
|
import {
|
||||||
buildAttachmentCandidates,
|
buildAttachmentCandidates,
|
||||||
collectMarkdownAndHtmlFiles,
|
collectMarkdownAndHtmlFiles,
|
||||||
|
stripNotionID,
|
||||||
} from '../utils/import.utils';
|
} from '../utils/import.utils';
|
||||||
import { executeTx } from '@docmost/db/utils';
|
import { executeTx } from '@docmost/db/utils';
|
||||||
import { BacklinkRepo } from '@docmost/db/repos/backlink/backlink.repo';
|
import { BacklinkRepo } from '@docmost/db/repos/backlink/backlink.repo';
|
||||||
@ -159,17 +160,12 @@ export class FileImportTaskService {
|
|||||||
.split(path.sep)
|
.split(path.sep)
|
||||||
.join('/'); // normalize to forward-slashes
|
.join('/'); // normalize to forward-slashes
|
||||||
const ext = path.extname(relPath).toLowerCase();
|
const ext = path.extname(relPath).toLowerCase();
|
||||||
let content = await fs.readFile(absPath, 'utf-8');
|
|
||||||
|
|
||||||
if (ext.toLowerCase() === '.md') {
|
|
||||||
content = await markdownToHtml(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
pagesMap.set(relPath, {
|
pagesMap.set(relPath, {
|
||||||
id: v7(),
|
id: v7(),
|
||||||
slugId: generateSlugId(),
|
slugId: generateSlugId(),
|
||||||
name: path.basename(relPath, ext),
|
name: stripNotionID(path.basename(relPath, ext)),
|
||||||
content,
|
content: '',
|
||||||
parentPageId: null,
|
parentPageId: null,
|
||||||
fileExtension: ext,
|
fileExtension: ext,
|
||||||
filePath: relPath,
|
filePath: relPath,
|
||||||
@ -254,71 +250,160 @@ export class FileImportTaskService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageResults = await Promise.all(
|
// Group pages by level (topological sort for parent-child relationships)
|
||||||
Array.from(pagesMap.values()).map(async (page) => {
|
const pagesByLevel = new Map<number, Array<[string, ImportPageNode]>>();
|
||||||
const htmlContent =
|
const pageLevel = new Map<string, number>();
|
||||||
await this.importAttachmentService.processAttachments({
|
|
||||||
html: page.content,
|
|
||||||
pageRelativePath: page.filePath,
|
|
||||||
extractDir,
|
|
||||||
pageId: page.id,
|
|
||||||
fileTask,
|
|
||||||
attachmentCandidates,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { html, backlinks, pageIcon } = await formatImportHtml({
|
// Calculate levels using BFS
|
||||||
html: htmlContent,
|
const calculateLevels = () => {
|
||||||
currentFilePath: page.filePath,
|
const queue: Array<{ filePath: string; level: number }> = [];
|
||||||
filePathToPageMetaMap: filePathToPageMetaMap,
|
|
||||||
creatorId: fileTask.creatorId,
|
|
||||||
sourcePageId: page.id,
|
|
||||||
workspaceId: fileTask.workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pmState = getProsemirrorContent(
|
// Start with root pages (no parent)
|
||||||
await this.importService.processHTML(html),
|
for (const [filePath, page] of pagesMap.entries()) {
|
||||||
|
if (!page.parentPageId) {
|
||||||
|
queue.push({ filePath, level: 0 });
|
||||||
|
pageLevel.set(filePath, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BFS to assign levels
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const { filePath, level } = queue.shift()!;
|
||||||
|
const currentPage = pagesMap.get(filePath)!;
|
||||||
|
|
||||||
|
// Find children of current page
|
||||||
|
for (const [childFilePath, childPage] of pagesMap.entries()) {
|
||||||
|
if (
|
||||||
|
childPage.parentPageId === currentPage.id &&
|
||||||
|
!pageLevel.has(childFilePath)
|
||||||
|
) {
|
||||||
|
pageLevel.set(childFilePath, level + 1);
|
||||||
|
queue.push({ filePath: childFilePath, level: level + 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group pages by level
|
||||||
|
for (const [filePath, page] of pagesMap.entries()) {
|
||||||
|
const level = pageLevel.get(filePath) || 0;
|
||||||
|
if (!pagesByLevel.has(level)) {
|
||||||
|
pagesByLevel.set(level, []);
|
||||||
|
}
|
||||||
|
pagesByLevel.get(level)!.push([filePath, page]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateLevels();
|
||||||
|
|
||||||
|
if (pagesMap.size < 1) return;
|
||||||
|
|
||||||
|
// Process pages level by level sequentially to respect foreign key constraints
|
||||||
|
const allBacklinks: any[] = [];
|
||||||
|
const validPageIds = new Set<string>();
|
||||||
|
let totalPagesProcessed = 0;
|
||||||
|
|
||||||
|
// Sort levels to process in order
|
||||||
|
const sortedLevels = Array.from(pagesByLevel.keys()).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await executeTx(this.db, async (trx) => {
|
||||||
|
// Process pages level by level sequentially within the transaction
|
||||||
|
for (const level of sortedLevels) {
|
||||||
|
const levelPages = pagesByLevel.get(level)!;
|
||||||
|
|
||||||
|
for (const [filePath, page] of levelPages) {
|
||||||
|
const absPath = path.join(extractDir, filePath);
|
||||||
|
let content = await fs.readFile(absPath, 'utf-8');
|
||||||
|
|
||||||
|
if (page.fileExtension.toLowerCase() === '.md') {
|
||||||
|
content = await markdownToHtml(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlContent =
|
||||||
|
await this.importAttachmentService.processAttachments({
|
||||||
|
html: content,
|
||||||
|
pageRelativePath: page.filePath,
|
||||||
|
extractDir,
|
||||||
|
pageId: page.id,
|
||||||
|
fileTask,
|
||||||
|
attachmentCandidates,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { html, backlinks, pageIcon } = await formatImportHtml({
|
||||||
|
html: htmlContent,
|
||||||
|
currentFilePath: page.filePath,
|
||||||
|
filePathToPageMetaMap: filePathToPageMetaMap,
|
||||||
|
creatorId: fileTask.creatorId,
|
||||||
|
sourcePageId: page.id,
|
||||||
|
workspaceId: fileTask.workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pmState = getProsemirrorContent(
|
||||||
|
await this.importService.processHTML(html),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { title, prosemirrorJson } =
|
||||||
|
this.importService.extractTitleAndRemoveHeading(pmState);
|
||||||
|
|
||||||
|
const insertablePage: InsertablePage = {
|
||||||
|
id: page.id,
|
||||||
|
slugId: page.slugId,
|
||||||
|
title: title || page.name,
|
||||||
|
icon: pageIcon || null,
|
||||||
|
content: prosemirrorJson,
|
||||||
|
textContent: jsonToText(prosemirrorJson),
|
||||||
|
ydoc: await this.importService.createYdoc(prosemirrorJson),
|
||||||
|
position: page.position!,
|
||||||
|
spaceId: fileTask.spaceId,
|
||||||
|
workspaceId: fileTask.workspaceId,
|
||||||
|
creatorId: fileTask.creatorId,
|
||||||
|
lastUpdatedById: fileTask.creatorId,
|
||||||
|
parentPageId: page.parentPageId,
|
||||||
|
};
|
||||||
|
|
||||||
|
await trx.insertInto('pages').values(insertablePage).execute();
|
||||||
|
|
||||||
|
// Track valid page IDs and collect backlinks
|
||||||
|
validPageIds.add(insertablePage.id);
|
||||||
|
allBacklinks.push(...backlinks);
|
||||||
|
totalPagesProcessed++;
|
||||||
|
|
||||||
|
// Log progress periodically
|
||||||
|
if (totalPagesProcessed % 50 === 0) {
|
||||||
|
this.logger.debug(`Processed ${totalPagesProcessed} pages...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredBacklinks = allBacklinks.filter(
|
||||||
|
({ sourcePageId, targetPageId }) =>
|
||||||
|
validPageIds.has(sourcePageId) && validPageIds.has(targetPageId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { title, prosemirrorJson } =
|
// Insert backlinks in batches
|
||||||
this.importService.extractTitleAndRemoveHeading(pmState);
|
if (filteredBacklinks.length > 0) {
|
||||||
|
const BACKLINK_BATCH_SIZE = 100;
|
||||||
|
for (
|
||||||
|
let i = 0;
|
||||||
|
i < filteredBacklinks.length;
|
||||||
|
i += BACKLINK_BATCH_SIZE
|
||||||
|
) {
|
||||||
|
const backlinkChunk = filteredBacklinks.slice(
|
||||||
|
i,
|
||||||
|
Math.min(i + BACKLINK_BATCH_SIZE, filteredBacklinks.length),
|
||||||
|
);
|
||||||
|
await this.backlinkRepo.insertBacklink(backlinkChunk, trx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const insertablePage: InsertablePage = {
|
this.logger.log(
|
||||||
id: page.id,
|
`Successfully imported ${totalPagesProcessed} pages with ${filteredBacklinks.length} backlinks`,
|
||||||
slugId: page.slugId,
|
);
|
||||||
title: title || page.name,
|
});
|
||||||
icon: pageIcon || null,
|
} catch (error) {
|
||||||
content: prosemirrorJson,
|
this.logger.error('Failed to import files:', error);
|
||||||
textContent: jsonToText(prosemirrorJson),
|
throw new Error(`File import failed: ${error?.['message']}`);
|
||||||
ydoc: await this.importService.createYdoc(prosemirrorJson),
|
}
|
||||||
position: page.position!,
|
|
||||||
spaceId: fileTask.spaceId,
|
|
||||||
workspaceId: fileTask.workspaceId,
|
|
||||||
creatorId: fileTask.creatorId,
|
|
||||||
lastUpdatedById: fileTask.creatorId,
|
|
||||||
parentPageId: page.parentPageId,
|
|
||||||
};
|
|
||||||
|
|
||||||
return { insertablePage, backlinks };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const insertablePages = pageResults.map((r) => r.insertablePage);
|
|
||||||
const insertableBacklinks = pageResults.flatMap((r) => r.backlinks);
|
|
||||||
|
|
||||||
if (insertablePages.length < 1) return;
|
|
||||||
const validPageIds = new Set(insertablePages.map((row) => row.id));
|
|
||||||
const filteredBacklinks = insertableBacklinks.filter(
|
|
||||||
({ sourcePageId, targetPageId }) =>
|
|
||||||
validPageIds.has(sourcePageId) && validPageIds.has(targetPageId),
|
|
||||||
);
|
|
||||||
|
|
||||||
await executeTx(this.db, async (trx) => {
|
|
||||||
await trx.insertInto('pages').values(insertablePages).execute();
|
|
||||||
|
|
||||||
if (filteredBacklinks.length > 0) {
|
|
||||||
await this.backlinkRepo.insertBacklink(filteredBacklinks, trx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFileTask(fileTaskId: string) {
|
async getFileTask(fileTaskId: string) {
|
||||||
|
|||||||
@ -35,7 +35,7 @@ interface DrawioPair {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImportAttachmentService {
|
export class ImportAttachmentService {
|
||||||
private readonly logger = new Logger(ImportAttachmentService.name);
|
private readonly logger = new Logger(ImportAttachmentService.name);
|
||||||
private readonly CONCURRENT_UPLOADS = 1;
|
private readonly CONCURRENT_UPLOADS = 3;
|
||||||
private readonly MAX_RETRIES = 2;
|
private readonly MAX_RETRIES = 2;
|
||||||
private readonly RETRY_DELAY = 2000;
|
private readonly RETRY_DELAY = 2000;
|
||||||
|
|
||||||
|
|||||||
@ -222,17 +222,40 @@ export function notionFormatter($: CheerioAPI, $root: Cheerio<any>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function unwrapFromParagraph($: CheerioAPI, $node: Cheerio<any>) {
|
export function unwrapFromParagraph($: CheerioAPI, $node: Cheerio<any>) {
|
||||||
// find the nearest <p> or <a> ancestor
|
// Keep track of processed wrappers to avoid infinite loops
|
||||||
let $wrapper = $node.closest('p, a');
|
const processedWrappers = new Set<any>();
|
||||||
|
|
||||||
|
let $wrapper = $node.closest('p, a');
|
||||||
while ($wrapper.length) {
|
while ($wrapper.length) {
|
||||||
// if the wrapper has only our node inside, replace it entirely
|
const wrapperElement = $wrapper.get(0);
|
||||||
if ($wrapper.contents().length === 1) {
|
|
||||||
|
// If we've already processed this wrapper, break to avoid infinite loop
|
||||||
|
if (processedWrappers.has(wrapperElement)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
processedWrappers.add(wrapperElement);
|
||||||
|
|
||||||
|
// Check if the wrapper contains only whitespace and our target node
|
||||||
|
const hasOnlyTargetNode =
|
||||||
|
$wrapper.contents().filter((_, el) => {
|
||||||
|
const $el = $(el);
|
||||||
|
// Skip whitespace-only text nodes. NodeType 3 = text node
|
||||||
|
if (el.nodeType === 3 && !$el.text().trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Return true if this is not our target node
|
||||||
|
return !$el.is($node) && !$node.is($el);
|
||||||
|
}).length === 0;
|
||||||
|
|
||||||
|
if (hasOnlyTargetNode) {
|
||||||
|
// Replace the wrapper entirely with our node
|
||||||
$wrapper.replaceWith($node);
|
$wrapper.replaceWith($node);
|
||||||
} else {
|
} else {
|
||||||
// otherwise just move the node to before the wrapper
|
// Move the node to before the wrapper, preserving other content
|
||||||
$wrapper.before($node);
|
$wrapper.before($node);
|
||||||
}
|
}
|
||||||
|
|
||||||
// look again for any new wrapper around $node
|
// look again for any new wrapper around $node
|
||||||
$wrapper = $node.closest('p, a');
|
$wrapper = $node.closest('p, a');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,3 +64,9 @@ export async function collectMarkdownAndHtmlFiles(
|
|||||||
await walk(dir);
|
await walk(dir);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stripNotionID(fileName: string): string {
|
||||||
|
// Handle optional separator (space or dash) + 32 alphanumeric chars at end
|
||||||
|
const notionIdPattern = /[ -]?[a-z0-9]{32}$/i;
|
||||||
|
return fileName.replace(notionIdPattern, '').trim();
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.23.1",
|
"version": "0.23.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
|
|||||||
@ -2,33 +2,39 @@ import { TableCell as TiptapTableCell } from "@tiptap/extension-table-cell";
|
|||||||
|
|
||||||
export const TableCell = TiptapTableCell.extend({
|
export const TableCell = TiptapTableCell.extend({
|
||||||
name: "tableCell",
|
name: "tableCell",
|
||||||
content: "(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+",
|
content:
|
||||||
|
"(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+",
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
...this.parent?.(),
|
...this.parent?.(),
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
default: null,
|
default: null,
|
||||||
parseHTML: (element) => element.style.backgroundColor || null,
|
parseHTML: (element) =>
|
||||||
|
element.style.backgroundColor ||
|
||||||
|
element.getAttribute("data-background-color") ||
|
||||||
|
null,
|
||||||
renderHTML: (attributes) => {
|
renderHTML: (attributes) => {
|
||||||
if (!attributes.backgroundColor) {
|
if (!attributes.backgroundColor) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
style: `background-color: ${attributes.backgroundColor}`,
|
style: `background-color: ${attributes.backgroundColor}`,
|
||||||
'data-background-color': attributes.backgroundColor,
|
"data-background-color": attributes.backgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
backgroundColorName: {
|
backgroundColorName: {
|
||||||
default: null,
|
default: null,
|
||||||
parseHTML: (element) => element.getAttribute('data-background-color-name') || null,
|
parseHTML: (element) =>
|
||||||
|
element.getAttribute("data-background-color-name") || null,
|
||||||
renderHTML: (attributes) => {
|
renderHTML: (attributes) => {
|
||||||
if (!attributes.backgroundColorName) {
|
if (!attributes.backgroundColorName) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'data-background-color-name': attributes.backgroundColorName.toLowerCase(),
|
"data-background-color-name":
|
||||||
|
attributes.backgroundColorName.toLowerCase(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,36 +2,42 @@ import { TableHeader as TiptapTableHeader } from "@tiptap/extension-table-header
|
|||||||
|
|
||||||
export const TableHeader = TiptapTableHeader.extend({
|
export const TableHeader = TiptapTableHeader.extend({
|
||||||
name: "tableHeader",
|
name: "tableHeader",
|
||||||
content: "(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+",
|
content:
|
||||||
|
"(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+",
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
...this.parent?.(),
|
...this.parent?.(),
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
default: null,
|
default: null,
|
||||||
parseHTML: (element) => element.style.backgroundColor || null,
|
parseHTML: (element) =>
|
||||||
|
element.style.backgroundColor ||
|
||||||
|
element.getAttribute("data-background-color") ||
|
||||||
|
null,
|
||||||
renderHTML: (attributes) => {
|
renderHTML: (attributes) => {
|
||||||
if (!attributes.backgroundColor) {
|
if (!attributes.backgroundColor) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
style: `background-color: ${attributes.backgroundColor}`,
|
style: `background-color: ${attributes.backgroundColor}`,
|
||||||
'data-background-color': attributes.backgroundColor,
|
"data-background-color": attributes.backgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
backgroundColorName: {
|
backgroundColorName: {
|
||||||
default: null,
|
default: null,
|
||||||
parseHTML: (element) => element.getAttribute('data-background-color-name') || null,
|
parseHTML: (element) =>
|
||||||
|
element.getAttribute("data-background-color-name") || null,
|
||||||
renderHTML: (attributes) => {
|
renderHTML: (attributes) => {
|
||||||
if (!attributes.backgroundColorName) {
|
if (!attributes.backgroundColorName) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'data-background-color-name': attributes.backgroundColorName.toLowerCase(),
|
"data-background-color-name":
|
||||||
|
attributes.backgroundColorName.toLowerCase(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user