mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 22:53:08 +08:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a66e7fbe3 | |||
| 46923e19f5 | |||
| 35b408c076 | |||
| b915e37c0f | |||
| cd2f57ceb5 | |||
| ede69499ea | |||
| b963758d22 | |||
| 98a878b44b | |||
| 0cf1247f36 | |||
| 77ce9b5cd4 | |||
| 0549f3ef9a | |||
| e167804174 | |||
| d05f89da45 | |||
| 524f7a4c62 | |||
| 5e6cf2af4b | |||
| 8a20a9ea0d | |||
| 7a88c58036 | |||
| 4acdbedabd | |||
| 61395d1334 | |||
| e46a7f1c06 | |||
| ccfaa6f7e7 |
@@ -54,6 +54,7 @@
|
|||||||
"react-router-dom": "^7.13.1",
|
"react-router-dom": "^7.13.1",
|
||||||
"semver": "^7.7.4",
|
"semver": "^7.7.4",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
|
"tiptap-extension-global-drag-handle": "^0.1.18",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -900,17 +900,5 @@
|
|||||||
"SCIM tokens": "SCIM tokens",
|
"SCIM tokens": "SCIM tokens",
|
||||||
"This action cannot be undone. Your identity provider will stop syncing immediately.": "This action cannot be undone. Your identity provider will stop syncing immediately.",
|
"This action cannot be undone. Your identity provider will stop syncing immediately.": "This action cannot be undone. Your identity provider will stop syncing immediately.",
|
||||||
"Toggle SCIM provisioning": "Toggle SCIM provisioning",
|
"Toggle SCIM provisioning": "Toggle SCIM provisioning",
|
||||||
"Token": "Token",
|
"Token": "Token"
|
||||||
"Sync block": "Sync block",
|
|
||||||
"Create a block that stays in sync across pages.": "Create a block that stays in sync across pages.",
|
|
||||||
"Sync block name": "Sync block name",
|
|
||||||
"Editing original": "Editing original",
|
|
||||||
"Copy synced block": "Copy synced block",
|
|
||||||
"Unsync": "Unsync",
|
|
||||||
"Delete sync block": "Delete sync block",
|
|
||||||
"Synced to {{count}} other page_one": "Synced to {{count}} other page",
|
|
||||||
"Synced to {{count}} other page_other": "Synced to {{count}} other pages",
|
|
||||||
"ORIGINAL": "ORIGINAL",
|
|
||||||
"THIS PAGE": "THIS PAGE",
|
|
||||||
"No pages": "No pages"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export const Feature = {
|
|||||||
AI: 'ai',
|
AI: 'ai',
|
||||||
CONFLUENCE_IMPORT: 'import:confluence',
|
CONFLUENCE_IMPORT: 'import:confluence',
|
||||||
DOCX_IMPORT: 'import:docx',
|
DOCX_IMPORT: 'import:docx',
|
||||||
PDF_IMPORT: 'import:pdf',
|
|
||||||
ATTACHMENT_INDEXING: 'attachment:indexing',
|
ATTACHMENT_INDEXING: 'attachment:indexing',
|
||||||
SECURITY_SETTINGS: 'security:settings',
|
SECURITY_SETTINGS: 'security:settings',
|
||||||
MCP: 'mcp',
|
MCP: 'mcp',
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconList,
|
IconList,
|
||||||
IconListNumbers,
|
IconListNumbers,
|
||||||
IconQuote,
|
|
||||||
IconTypography,
|
IconTypography,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { Popover, Button, ScrollArea, Tooltip } from "@mantine/core";
|
import { Popover, Button, ScrollArea, Tooltip } from "@mantine/core";
|
||||||
@@ -60,7 +59,6 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
|||||||
isCodeBlock: ctx.editor.isActive("codeBlock"),
|
isCodeBlock: ctx.editor.isActive("codeBlock"),
|
||||||
isCallout: ctx.editor.isActive("callout"),
|
isCallout: ctx.editor.isActive("callout"),
|
||||||
isDetails: ctx.editor.isActive("details"),
|
isDetails: ctx.editor.isActive("details"),
|
||||||
isTransclusion: ctx.editor.isActive("transclusion"),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -142,12 +140,6 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
|||||||
command: () => editor.chain().focus().setDetails().run(),
|
command: () => editor.chain().focus().setDetails().run(),
|
||||||
isActive: () => editorState?.isDetails,
|
isActive: () => editorState?.isDetails,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Sync block",
|
|
||||||
icon: IconQuote,
|
|
||||||
command: () => editor.chain().focus().toggleTransclusion().run(),
|
|
||||||
isActive: () => editorState?.isTransclusion,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const activeItem = items.filter((item) => item.isActive()).pop() ?? {
|
const activeItem = items.filter((item) => item.isActive()).pop() ?? {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
IconColumns3,
|
IconColumns3,
|
||||||
IconColumns2,
|
IconColumns2,
|
||||||
IconTag,
|
IconTag,
|
||||||
IconRotate2,
|
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import {
|
import {
|
||||||
CommandProps,
|
CommandProps,
|
||||||
@@ -478,23 +477,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
editor.chain().focus().deleteRange(range).insertSubpages().run();
|
editor.chain().focus().deleteRange(range).insertSubpages().run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Sync block",
|
|
||||||
description: "Create a block that stays in sync across pages.",
|
|
||||||
searchTerms: [
|
|
||||||
"sync",
|
|
||||||
"synced",
|
|
||||||
"sync block",
|
|
||||||
"excerpt",
|
|
||||||
"transclusion",
|
|
||||||
"reusable",
|
|
||||||
"snippet",
|
|
||||||
],
|
|
||||||
icon: IconRotate2,
|
|
||||||
command: ({ editor, range }: CommandProps) => {
|
|
||||||
editor.chain().focus().deleteRange(range).insertTransclusion().run();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "2 Columns",
|
title: "2 Columns",
|
||||||
description: "Split content into two columns.",
|
description: "Split content into two columns.",
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import classes from "./transclusion.module.css";
|
|
||||||
|
|
||||||
export default function ErrorPlaceholder() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<div className={classes.placeholder}>
|
|
||||||
<IconAlertTriangle
|
|
||||||
size={20}
|
|
||||||
stroke={1.5}
|
|
||||||
className={classes.placeholderIcon}
|
|
||||||
/>
|
|
||||||
<div className={classes.placeholderTitle}>
|
|
||||||
{t("Failed to load transclusion")}
|
|
||||||
</div>
|
|
||||||
<div className={classes.placeholderSubtext}>
|
|
||||||
{t("An error occurred while rendering this reference")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { IconEyeOff } from "@tabler/icons-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import classes from "./transclusion.module.css";
|
|
||||||
|
|
||||||
export default function NoAccessPlaceholder() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<div className={classes.placeholder}>
|
|
||||||
<IconEyeOff size={20} stroke={1.5} className={classes.placeholderIcon} />
|
|
||||||
<div className={classes.placeholderTitle}>{t("No access")}</div>
|
|
||||||
<div className={classes.placeholderSubtext}>
|
|
||||||
{t("You don't have access to this content")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { IconQuestionMark } from "@tabler/icons-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import classes from "./transclusion.module.css";
|
|
||||||
|
|
||||||
export default function NotFoundPlaceholder() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<div className={classes.placeholder}>
|
|
||||||
<IconQuestionMark
|
|
||||||
size={20}
|
|
||||||
stroke={1.5}
|
|
||||||
className={classes.placeholderIcon}
|
|
||||||
/>
|
|
||||||
<div className={classes.placeholderTitle}>
|
|
||||||
{t("Synced block unavailable")}
|
|
||||||
</div>
|
|
||||||
<div className={classes.placeholderSubtext}>
|
|
||||||
{t(
|
|
||||||
"The source may have been removed, or embedding it here would create a loop.",
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { EditorProvider } from "@tiptap/react";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
|
||||||
import { UniqueID } from "@docmost/editor-ext";
|
|
||||||
import { TransclusionLookupProvider } from "./transclusion-lookup-context";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
hostPageId: string;
|
|
||||||
content: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TransclusionContent({ hostPageId, content }: Props) {
|
|
||||||
const extensions = useMemo(() => {
|
|
||||||
const filtered = mainExtensions.filter(
|
|
||||||
(e: any) => e.name !== "uniqueID" && e.name !== "globalDragHandle",
|
|
||||||
);
|
|
||||||
return [
|
|
||||||
...filtered,
|
|
||||||
UniqueID.configure({
|
|
||||||
types: ["heading", "paragraph", "transclusion"],
|
|
||||||
updateDocument: false,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Isolate the nested read-only editor's events from the host editor:
|
|
||||||
// - mousedown/click would otherwise make the host node-select the atom
|
|
||||||
// wrapper, blocking native text selection inside.
|
|
||||||
// - dragstart/dragover/drop would otherwise let the host treat events
|
|
||||||
// inside the nested view as drops on the host, duplicating dropped
|
|
||||||
// files at the transclusion's position.
|
|
||||||
const stop = (e: React.SyntheticEvent) => e.stopPropagation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TransclusionLookupProvider hostPageId={hostPageId}>
|
|
||||||
<div
|
|
||||||
onMouseDown={stop}
|
|
||||||
onClick={stop}
|
|
||||||
onDragStart={stop}
|
|
||||||
onDragOver={stop}
|
|
||||||
onDrop={stop}
|
|
||||||
>
|
|
||||||
<EditorProvider
|
|
||||||
editable={false}
|
|
||||||
immediatelyRender={true}
|
|
||||||
extensions={extensions}
|
|
||||||
content={content as any}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TransclusionLookupProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-198
@@ -1,198 +0,0 @@
|
|||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { lookupTransclusion } from "@/features/transclusion/services/transclusion-api";
|
|
||||||
import type { TransclusionLookup } from "@/features/transclusion/types/transclusion.types";
|
|
||||||
|
|
||||||
type LookupKey = string; // `${sourcePageId}::${transclusionId}`
|
|
||||||
|
|
||||||
type Subscriber = {
|
|
||||||
key: LookupKey;
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
setResult: (r: TransclusionLookup) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ContextValue = {
|
|
||||||
/** Register a subscriber. Returns an unsubscribe function. */
|
|
||||||
subscribe: (s: Subscriber) => () => void;
|
|
||||||
/**
|
|
||||||
* Force a re-fetch of `key` and resolve when the response arrives (or the
|
|
||||||
* request fails). Bypasses the cache and any in-flight de-dup so the user
|
|
||||||
* always sees a fresh server read.
|
|
||||||
*/
|
|
||||||
refresh: (key: LookupKey) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TransclusionLookupContext = createContext<ContextValue | null>(null);
|
|
||||||
|
|
||||||
export function TransclusionLookupProvider({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
/**
|
|
||||||
* Retained for API compatibility with previous callers that passed the
|
|
||||||
* host page id; no longer used internally now that cycle prevention lives
|
|
||||||
* on the server side and lookups are stateless.
|
|
||||||
*/
|
|
||||||
hostPageId?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
const subscribersRef = useRef(new Map<LookupKey, Subscriber[]>());
|
|
||||||
const queueRef = useRef(new Set<LookupKey>());
|
|
||||||
const tickRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
// Last looked-up value for each key. Re-subscribers (e.g. when the editor
|
|
||||||
// remounts after switching from static to live) get this immediately
|
|
||||||
// instead of triggering a duplicate fetch.
|
|
||||||
const resultCacheRef = useRef(new Map<LookupKey, TransclusionLookup>());
|
|
||||||
// Keys that are currently in flight in a batch request. A second subscribe
|
|
||||||
// for the same key while the first request is pending is a no-op; the
|
|
||||||
// subscriber is added to subscribersRef and will be notified when the
|
|
||||||
// pending request completes.
|
|
||||||
const inFlightRef = useRef(new Set<LookupKey>());
|
|
||||||
// Resolvers waiting on the next response for a key. Populated by refresh()
|
|
||||||
// so callers can await the fetch round-trip; resolved on success and on
|
|
||||||
// network error so the UI never hangs in a loading state.
|
|
||||||
const pendingRef = useRef(new Map<LookupKey, Array<() => void>>());
|
|
||||||
|
|
||||||
const flush = useCallback(async () => {
|
|
||||||
tickRef.current = null;
|
|
||||||
const keys = Array.from(queueRef.current);
|
|
||||||
queueRef.current.clear();
|
|
||||||
if (keys.length === 0) return;
|
|
||||||
|
|
||||||
for (const k of keys) inFlightRef.current.add(k);
|
|
||||||
|
|
||||||
const references = keys.map((k) => {
|
|
||||||
const [sourcePageId, transclusionId] = k.split("::");
|
|
||||||
return { sourcePageId, transclusionId };
|
|
||||||
});
|
|
||||||
|
|
||||||
const resolveWaiters = (key: LookupKey) => {
|
|
||||||
const waiters = pendingRef.current.get(key);
|
|
||||||
if (!waiters) return;
|
|
||||||
pendingRef.current.delete(key);
|
|
||||||
for (const w of waiters) w();
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { items } = await lookupTransclusion({ references });
|
|
||||||
for (const r of items) {
|
|
||||||
const key = `${r.sourcePageId}::${r.transclusionId}`;
|
|
||||||
resultCacheRef.current.set(key, r);
|
|
||||||
inFlightRef.current.delete(key);
|
|
||||||
const subs = subscribersRef.current.get(key);
|
|
||||||
if (subs) {
|
|
||||||
for (const s of subs) s.setResult(r);
|
|
||||||
}
|
|
||||||
resolveWaiters(key);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Network error — leave subscribers in pending state and clear the
|
|
||||||
// in-flight flag so a future subscribe can retry.
|
|
||||||
for (const k of keys) {
|
|
||||||
inFlightRef.current.delete(k);
|
|
||||||
resolveWaiters(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const enqueue = useCallback(
|
|
||||||
(key: LookupKey) => {
|
|
||||||
queueRef.current.add(key);
|
|
||||||
if (tickRef.current === null) {
|
|
||||||
tickRef.current = setTimeout(flush, 10);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[flush],
|
|
||||||
);
|
|
||||||
|
|
||||||
const subscribe = useCallback<ContextValue["subscribe"]>(
|
|
||||||
(s) => {
|
|
||||||
const list = subscribersRef.current.get(s.key) ?? [];
|
|
||||||
list.push(s);
|
|
||||||
subscribersRef.current.set(s.key, list);
|
|
||||||
|
|
||||||
const cached = resultCacheRef.current.get(s.key);
|
|
||||||
if (cached) {
|
|
||||||
s.setResult(cached);
|
|
||||||
} else if (!inFlightRef.current.has(s.key)) {
|
|
||||||
enqueue(s.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const cur = subscribersRef.current.get(s.key) ?? [];
|
|
||||||
const next = cur.filter((x) => x !== s);
|
|
||||||
if (next.length === 0) subscribersRef.current.delete(s.key);
|
|
||||||
else subscribersRef.current.set(s.key, next);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[enqueue],
|
|
||||||
);
|
|
||||||
|
|
||||||
const refresh = useCallback<ContextValue["refresh"]>(
|
|
||||||
(key) =>
|
|
||||||
new Promise<void>((resolve) => {
|
|
||||||
resultCacheRef.current.delete(key);
|
|
||||||
inFlightRef.current.delete(key);
|
|
||||||
const waiters = pendingRef.current.get(key) ?? [];
|
|
||||||
waiters.push(resolve);
|
|
||||||
pendingRef.current.set(key, waiters);
|
|
||||||
enqueue(key);
|
|
||||||
}),
|
|
||||||
[enqueue],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => () => {
|
|
||||||
if (tickRef.current) clearTimeout(tickRef.current);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = useMemo<ContextValue>(
|
|
||||||
() => ({ subscribe, refresh }),
|
|
||||||
[subscribe, refresh],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TransclusionLookupContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</TransclusionLookupContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTransclusionLookup(
|
|
||||||
sourcePageId: string | null | undefined,
|
|
||||||
transclusionId: string | null | undefined,
|
|
||||||
): {
|
|
||||||
result: TransclusionLookup | null;
|
|
||||||
refresh: () => Promise<void>;
|
|
||||||
} {
|
|
||||||
const ctx = useContext(TransclusionLookupContext);
|
|
||||||
const [result, setResult] = useState<TransclusionLookup | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ctx || !sourcePageId || !transclusionId) return;
|
|
||||||
const key = `${sourcePageId}::${transclusionId}`;
|
|
||||||
const unsubscribe = ctx.subscribe({
|
|
||||||
key,
|
|
||||||
sourcePageId,
|
|
||||||
transclusionId,
|
|
||||||
setResult,
|
|
||||||
});
|
|
||||||
return unsubscribe;
|
|
||||||
}, [ctx, sourcePageId, transclusionId]);
|
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
|
||||||
if (!ctx || !sourcePageId || !transclusionId) return;
|
|
||||||
await ctx.refresh(`${sourcePageId}::${transclusionId}`);
|
|
||||||
}, [ctx, sourcePageId, transclusionId]);
|
|
||||||
|
|
||||||
return { result, refresh };
|
|
||||||
}
|
|
||||||
-204
@@ -1,204 +0,0 @@
|
|||||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
|
||||||
import { ActionIcon, Menu, Tooltip } from "@mantine/core";
|
|
||||||
import {
|
|
||||||
IconDots,
|
|
||||||
IconExternalLink,
|
|
||||||
IconLinkOff,
|
|
||||||
IconRefresh,
|
|
||||||
IconTrash,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
|
||||||
import { useTransclusionLookup } from "./transclusion-lookup-context";
|
|
||||||
import TransclusionContent from "./transclusion-content";
|
|
||||||
import NoAccessPlaceholder from "./no-access-placeholder";
|
|
||||||
import NotFoundPlaceholder from "./not-found-placeholder";
|
|
||||||
import ErrorPlaceholder from "./error-placeholder";
|
|
||||||
import classes from "./transclusion.module.css";
|
|
||||||
import SyncBlockReferencesDropdown from "@/features/transclusion/components/sync-block-references-dropdown";
|
|
||||||
import {
|
|
||||||
useReferencesQuery,
|
|
||||||
useUnsyncReferenceMutation,
|
|
||||||
} from "@/features/transclusion/queries/transclusion-query";
|
|
||||||
import { buildPageUrl } from "@/features/page/page.utils";
|
|
||||||
|
|
||||||
export default function TransclusionReferenceView(props: NodeViewProps) {
|
|
||||||
const isEditable = props.editor.isEditable;
|
|
||||||
const sourcePageId: string | null = props.node.attrs.sourcePageId ?? null;
|
|
||||||
const transclusionId: string | null = props.node.attrs.transclusionId ?? null;
|
|
||||||
const [openMenus, setOpenMenus] = useState(0);
|
|
||||||
const trackOpen = (open: boolean) =>
|
|
||||||
setOpenMenus((n) => Math.max(0, n + (open ? 1 : -1)));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NodeViewWrapper
|
|
||||||
className={classes.includeWrap}
|
|
||||||
data-focused={isEditable && props.selected ? "true" : "false"}
|
|
||||||
data-menu-open={openMenus > 0 ? "true" : "false"}
|
|
||||||
contentEditable={false}
|
|
||||||
>
|
|
||||||
<ErrorBoundary
|
|
||||||
resetKeys={[sourcePageId, transclusionId]}
|
|
||||||
fallback={<ErrorPlaceholder />}
|
|
||||||
>
|
|
||||||
<TransclusionReferenceBody {...props} trackOpen={trackOpen} />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</NodeViewWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TransclusionReferenceBody({
|
|
||||||
editor,
|
|
||||||
node,
|
|
||||||
deleteNode,
|
|
||||||
getPos,
|
|
||||||
trackOpen,
|
|
||||||
}: NodeViewProps & { trackOpen: (open: boolean) => void }) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const sourcePageId: string | null = node.attrs.sourcePageId ?? null;
|
|
||||||
const transclusionId: string | null = node.attrs.transclusionId ?? null;
|
|
||||||
const isEditable = editor.isEditable;
|
|
||||||
|
|
||||||
const { result, refresh } = useTransclusionLookup(
|
|
||||||
sourcePageId,
|
|
||||||
transclusionId,
|
|
||||||
);
|
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
setRefreshing(true);
|
|
||||||
try {
|
|
||||||
await refresh();
|
|
||||||
} finally {
|
|
||||||
setRefreshing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// @ts-ignore - editor.storage.pageId is set by the host editor
|
|
||||||
const hostPageId: string | undefined = editor.storage?.pageId;
|
|
||||||
const unsyncMutation = useUnsyncReferenceMutation();
|
|
||||||
// Cached against the dropdown's identical query so the source link target
|
|
||||||
// is ready as soon as the controls fade in on hover, without a second
|
|
||||||
// fetch.
|
|
||||||
const referencesQuery = useReferencesQuery(
|
|
||||||
sourcePageId,
|
|
||||||
transclusionId,
|
|
||||||
isEditable,
|
|
||||||
);
|
|
||||||
const sourcePageHref = (() => {
|
|
||||||
const source = referencesQuery.data?.source;
|
|
||||||
if (source?.spaceSlug) {
|
|
||||||
return buildPageUrl(source.spaceSlug, source.slugId, source.title);
|
|
||||||
}
|
|
||||||
return sourcePageId ? `/p/${sourcePageId}` : null;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const handleUnsync = async () => {
|
|
||||||
if (!hostPageId || !sourcePageId || !transclusionId) return;
|
|
||||||
try {
|
|
||||||
const { content } = await unsyncMutation.mutateAsync({
|
|
||||||
referencePageId: hostPageId,
|
|
||||||
sourcePageId,
|
|
||||||
transclusionId,
|
|
||||||
});
|
|
||||||
const pos = getPos();
|
|
||||||
if (typeof pos !== "number") return;
|
|
||||||
const from = pos;
|
|
||||||
const to = pos + node.nodeSize;
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.insertContentAt({ from, to }, content as any)
|
|
||||||
.run();
|
|
||||||
} catch {
|
|
||||||
// mutation surfaces errors via React Query; node stays as-is
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isEditable && (
|
|
||||||
<div className={classes.includeControls} contentEditable={false}>
|
|
||||||
{sourcePageId && transclusionId && hostPageId && (
|
|
||||||
<SyncBlockReferencesDropdown
|
|
||||||
sourcePageId={sourcePageId}
|
|
||||||
transclusionId={transclusionId}
|
|
||||||
currentPageId={hostPageId}
|
|
||||||
mode="reference"
|
|
||||||
onOpenChange={trackOpen}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span className={classes.controlsDivider} />
|
|
||||||
<Tooltip label={t("Refresh")}>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="gray"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleRefresh}
|
|
||||||
loading={refreshing}
|
|
||||||
disabled={!sourcePageId || !transclusionId}
|
|
||||||
>
|
|
||||||
<IconRefresh size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
{sourcePageHref && (
|
|
||||||
<Tooltip label={t("Go to source page")}>
|
|
||||||
<ActionIcon
|
|
||||||
component={Link}
|
|
||||||
to={sourcePageHref}
|
|
||||||
variant="subtle"
|
|
||||||
color="gray"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<IconExternalLink size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<Menu position="bottom-end" withinPortal onChange={trackOpen}>
|
|
||||||
<Menu.Target>
|
|
||||||
<ActionIcon variant="subtle" color="gray" size="sm">
|
|
||||||
<IconDots size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
<Menu.Item
|
|
||||||
leftSection={<IconLinkOff size={14} />}
|
|
||||||
onClick={handleUnsync}
|
|
||||||
disabled={
|
|
||||||
unsyncMutation.isPending ||
|
|
||||||
!hostPageId ||
|
|
||||||
!sourcePageId ||
|
|
||||||
!transclusionId
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("Unsync")}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item
|
|
||||||
color="red"
|
|
||||||
leftSection={<IconTrash size={14} />}
|
|
||||||
onClick={() => deleteNode()}
|
|
||||||
>
|
|
||||||
{t("Remove from page")}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!sourcePageId || !transclusionId ? (
|
|
||||||
<NotFoundPlaceholder />
|
|
||||||
) : !result ? (
|
|
||||||
<div style={{ minHeight: 24 }} />
|
|
||||||
) : !("status" in result) ? (
|
|
||||||
<TransclusionContent
|
|
||||||
hostPageId={hostPageId ?? sourcePageId}
|
|
||||||
content={result.content}
|
|
||||||
/>
|
|
||||||
) : result.status === "no_access" ? (
|
|
||||||
<NoAccessPlaceholder />
|
|
||||||
) : (
|
|
||||||
<NotFoundPlaceholder />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import {
|
|
||||||
NodeViewContent,
|
|
||||||
NodeViewProps,
|
|
||||||
NodeViewWrapper,
|
|
||||||
} from "@tiptap/react";
|
|
||||||
import { ActionIcon, Menu, Tooltip } from "@mantine/core";
|
|
||||||
import { notifications } from "@mantine/notifications";
|
|
||||||
import {
|
|
||||||
IconCheck,
|
|
||||||
IconCopy,
|
|
||||||
IconDots,
|
|
||||||
IconLinkOff,
|
|
||||||
IconTrash,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import classes from "./transclusion.module.css";
|
|
||||||
import SyncBlockReferencesDropdown from "@/features/transclusion/components/sync-block-references-dropdown";
|
|
||||||
|
|
||||||
export default function TransclusionView(props: NodeViewProps) {
|
|
||||||
const { editor, node, deleteNode } = props;
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [openMenus, setOpenMenus] = useState(0);
|
|
||||||
const trackOpen = (open: boolean) =>
|
|
||||||
setOpenMenus((n) => Math.max(0, n + (open ? 1 : -1)));
|
|
||||||
|
|
||||||
const isEditable = editor.isEditable;
|
|
||||||
// @ts-ignore - editor.storage.pageId is set by the host editor (page-editor.tsx onCreate)
|
|
||||||
const sourcePageId: string | undefined = editor.storage?.pageId;
|
|
||||||
const transclusionId: string | null = node.attrs.id ?? null;
|
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const handleCopy = async () => {
|
|
||||||
if (!sourcePageId || !transclusionId) return;
|
|
||||||
const html = `<div data-type="transclusionReference" data-source-page-id="${sourcePageId}" data-transclusion-id="${transclusionId}"></div>`;
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.write([
|
|
||||||
new ClipboardItem({
|
|
||||||
"text/html": new Blob([html], { type: "text/html" }),
|
|
||||||
"text/plain": new Blob([html], { type: "text/plain" }),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
} catch {
|
|
||||||
// Fallback for browsers without ClipboardItem write support
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(html);
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setCopied(true);
|
|
||||||
window.setTimeout(() => setCopied(false), 2000);
|
|
||||||
notifications.show({
|
|
||||||
message: t("Copied. Paste on any page to embed this synced block."),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUnsync = () => {
|
|
||||||
editor.chain().focus().unsyncTransclusion().run();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NodeViewWrapper
|
|
||||||
className={classes.transclusionWrap}
|
|
||||||
data-drag-handle
|
|
||||||
data-menu-open={openMenus > 0 ? "true" : "false"}
|
|
||||||
>
|
|
||||||
{isEditable && (
|
|
||||||
<div className={classes.transclusionControls} contentEditable={false}>
|
|
||||||
{sourcePageId && transclusionId && (
|
|
||||||
<SyncBlockReferencesDropdown
|
|
||||||
sourcePageId={sourcePageId}
|
|
||||||
transclusionId={transclusionId}
|
|
||||||
currentPageId={sourcePageId}
|
|
||||||
mode="source"
|
|
||||||
onOpenChange={trackOpen}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className={classes.controlsDivider} />
|
|
||||||
|
|
||||||
<Tooltip label={copied ? t("Copied") : t("Copy synced block")}>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color={copied ? "teal" : "gray"}
|
|
||||||
size="sm"
|
|
||||||
onClick={handleCopy}
|
|
||||||
disabled={!sourcePageId || !transclusionId}
|
|
||||||
>
|
|
||||||
{copied ? <IconCheck size={14} /> : <IconCopy size={14} />}
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Menu position="bottom-end" withinPortal onChange={trackOpen}>
|
|
||||||
<Menu.Target>
|
|
||||||
<ActionIcon variant="subtle" color="gray" size="sm">
|
|
||||||
<IconDots size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
<Menu.Item
|
|
||||||
leftSection={<IconLinkOff size={14} />}
|
|
||||||
onClick={handleUnsync}
|
|
||||||
>
|
|
||||||
{t("Unsync")}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item
|
|
||||||
color="red"
|
|
||||||
leftSection={<IconTrash size={14} />}
|
|
||||||
onClick={() => deleteNode()}
|
|
||||||
>
|
|
||||||
{t("Delete sync block")}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<NodeViewContent />
|
|
||||||
</NodeViewWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
.placeholder {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: var(--mantine-spacing-md);
|
|
||||||
border-radius: var(--mantine-radius-md);
|
|
||||||
background: light-dark(
|
|
||||||
var(--mantine-color-gray-0),
|
|
||||||
var(--mantine-color-dark-6)
|
|
||||||
);
|
|
||||||
border: 1px dashed
|
|
||||||
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
|
||||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholderIcon {
|
|
||||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholderTitle {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: var(--mantine-font-size-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholderSubtext {
|
|
||||||
font-size: var(--mantine-font-size-xs);
|
|
||||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transclusionBadge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 2px 8px;
|
|
||||||
font-size: var(--mantine-font-size-xs);
|
|
||||||
font-weight: 600;
|
|
||||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
|
||||||
background: light-dark(
|
|
||||||
var(--mantine-color-gray-1),
|
|
||||||
var(--mantine-color-dark-5)
|
|
||||||
);
|
|
||||||
border-radius: var(--mantine-radius-sm);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transclusionWrap {
|
|
||||||
position: relative;
|
|
||||||
margin-left: -3rem;
|
|
||||||
margin-right: -3rem;
|
|
||||||
width: calc(100% + 6rem);
|
|
||||||
padding: 0.5em 3rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
transition: border 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transclusionWrap:hover,
|
|
||||||
.transclusionWrap:focus-within {
|
|
||||||
border: 1px solid
|
|
||||||
light-dark(var(--mantine-color-gray-3), var(--mantine-color-gray-7));
|
|
||||||
}
|
|
||||||
|
|
||||||
.transclusionControls {
|
|
||||||
position: absolute;
|
|
||||||
bottom: calc(100% + 8px);
|
|
||||||
right: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
background: var(--mantine-color-body);
|
|
||||||
border: 1px solid var(--mantine-color-default-border);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.15s ease;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover bridge: keeps :hover on the wrap while the cursor crosses the
|
|
||||||
8px gap between wrap and floating chrome, so the menu doesn't fade out
|
|
||||||
on the way to it. */
|
|
||||||
.transclusionControls::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transclusionWrap:hover .transclusionControls,
|
|
||||||
.transclusionWrap:focus-within .transclusionControls,
|
|
||||||
.transclusionWrap[data-menu-open="true"] .transclusionControls {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controlsDivider {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1px;
|
|
||||||
height: 16px;
|
|
||||||
background: var(--mantine-color-default-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.includeWrap {
|
|
||||||
position: relative;
|
|
||||||
margin-left: -3rem;
|
|
||||||
margin-right: -3rem;
|
|
||||||
width: calc(100% + 6rem);
|
|
||||||
padding: 0.5em 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
transition: border 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.includeWrap:hover,
|
|
||||||
.includeWrap[data-focused="true"],
|
|
||||||
.includeWrap[data-menu-open="true"] {
|
|
||||||
border: 1px solid
|
|
||||||
light-dark(var(--mantine-color-gray-3), var(--mantine-color-gray-7));
|
|
||||||
}
|
|
||||||
|
|
||||||
.includeControls {
|
|
||||||
position: absolute;
|
|
||||||
bottom: calc(100% + 8px);
|
|
||||||
right: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
background: var(--mantine-color-body);
|
|
||||||
border: 1px solid var(--mantine-color-default-border);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.15s ease;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover bridge: keeps :hover on the wrap while the cursor crosses the
|
|
||||||
8px gap between wrap and floating chrome, so the menu doesn't fade out
|
|
||||||
on the way to it. */
|
|
||||||
.includeControls::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.includeWrap:hover .includeControls,
|
|
||||||
.includeWrap:focus-within .includeControls,
|
|
||||||
.includeWrap[data-focused="true"] .includeControls,
|
|
||||||
.includeWrap[data-menu-open="true"] .includeControls {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.react-renderer.node-transclusion.ProseMirror-selectednode),
|
|
||||||
:global(.react-renderer.node-transclusionReference.ProseMirror-selectednode) {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 48em) {
|
|
||||||
.transclusionWrap,
|
|
||||||
.includeWrap {
|
|
||||||
margin-left: -1rem;
|
|
||||||
margin-right: -1rem;
|
|
||||||
width: calc(100% + 2rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.transclusionWrap {
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.transclusionControls,
|
|
||||||
.includeControls {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.editingOriginalTag {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0 6px;
|
|
||||||
font-size: var(--mantine-font-size-xs);
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--mantine-color-blue-7);
|
|
||||||
background: light-dark(
|
|
||||||
var(--mantine-color-blue-0),
|
|
||||||
var(--mantine-color-blue-9)
|
|
||||||
);
|
|
||||||
border-radius: var(--mantine-radius-sm);
|
|
||||||
}
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
import { Extension } from "@tiptap/core";
|
|
||||||
import {
|
|
||||||
NodeSelection,
|
|
||||||
Plugin,
|
|
||||||
PluginKey,
|
|
||||||
TextSelection,
|
|
||||||
} from "@tiptap/pm/state";
|
|
||||||
import { Fragment, Slice, Node } from "@tiptap/pm/model";
|
|
||||||
import { EditorView } from "@tiptap/pm/view";
|
|
||||||
|
|
||||||
export interface GlobalDragHandleOptions {
|
|
||||||
/**
|
|
||||||
* The width of the drag handle
|
|
||||||
*/
|
|
||||||
dragHandleWidth: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The treshold for scrolling
|
|
||||||
*/
|
|
||||||
scrollThreshold: number;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The css selector to query for the drag handle. (eg: '.custom-handle').
|
|
||||||
* If handle element is found, that element will be used as drag handle. If not, a default handle will be created
|
|
||||||
*/
|
|
||||||
dragHandleSelector?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tags to be excluded for drag handle
|
|
||||||
*/
|
|
||||||
excludedTags: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom nodes to be included for drag handle
|
|
||||||
*/
|
|
||||||
customNodes: string[];
|
|
||||||
}
|
|
||||||
function absoluteRect(node: Element) {
|
|
||||||
const data = node.getBoundingClientRect();
|
|
||||||
const modal = node.closest('[role="dialog"]');
|
|
||||||
|
|
||||||
if (modal && window.getComputedStyle(modal).transform !== "none") {
|
|
||||||
const modalRect = modal.getBoundingClientRect();
|
|
||||||
|
|
||||||
return {
|
|
||||||
top: data.top - modalRect.top,
|
|
||||||
left: data.left - modalRect.left,
|
|
||||||
width: data.width,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
top: data.top,
|
|
||||||
left: data.left,
|
|
||||||
width: data.width,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodeDOMAtCoords(
|
|
||||||
coords: { x: number; y: number },
|
|
||||||
options: GlobalDragHandleOptions,
|
|
||||||
view: EditorView,
|
|
||||||
) {
|
|
||||||
const selectors = [
|
|
||||||
"li",
|
|
||||||
"p:not(:first-child)",
|
|
||||||
"pre",
|
|
||||||
"blockquote",
|
|
||||||
"h1",
|
|
||||||
"h2",
|
|
||||||
"h3",
|
|
||||||
"h4",
|
|
||||||
"h5",
|
|
||||||
"h6",
|
|
||||||
...options.customNodes.map((node) => `[data-type=${node}]`),
|
|
||||||
].join(", ");
|
|
||||||
return document
|
|
||||||
.elementsFromPoint(coords.x, coords.y)
|
|
||||||
.find((elem: Element) => {
|
|
||||||
// Skip elements that belong to a nested editor (e.g. transclusion
|
|
||||||
// references render their own ProseMirror instance). Only consider
|
|
||||||
// elements whose closest editor is this host view.
|
|
||||||
if (elem.closest(".ProseMirror") !== view.dom) return false;
|
|
||||||
return (
|
|
||||||
elem.parentElement?.matches?.(".ProseMirror") ||
|
|
||||||
elem.matches(selectors)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function nodePosAtDOM(
|
|
||||||
node: Element,
|
|
||||||
view: EditorView,
|
|
||||||
options: GlobalDragHandleOptions,
|
|
||||||
) {
|
|
||||||
const boundingRect = node.getBoundingClientRect();
|
|
||||||
|
|
||||||
return view.posAtCoords({
|
|
||||||
left: boundingRect.left + 50 + options.dragHandleWidth,
|
|
||||||
top: boundingRect.top + 1,
|
|
||||||
})?.inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcNodePos(pos: number, view: EditorView) {
|
|
||||||
const $pos = view.state.doc.resolve(pos);
|
|
||||||
if ($pos.depth > 1) return $pos.before($pos.depth);
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DragHandlePlugin(
|
|
||||||
options: GlobalDragHandleOptions & { pluginKey: string },
|
|
||||||
) {
|
|
||||||
let listType = "";
|
|
||||||
function handleDragStart(event: DragEvent, view: EditorView) {
|
|
||||||
view.focus();
|
|
||||||
|
|
||||||
if (!event.dataTransfer) return;
|
|
||||||
|
|
||||||
const node = nodeDOMAtCoords(
|
|
||||||
{
|
|
||||||
x: event.clientX + 50 + options.dragHandleWidth,
|
|
||||||
y: event.clientY,
|
|
||||||
},
|
|
||||||
options,
|
|
||||||
view,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!(node instanceof Element)) return;
|
|
||||||
|
|
||||||
let draggedNodePos = nodePosAtDOM(node, view, options);
|
|
||||||
if (draggedNodePos == null || draggedNodePos < 0) return;
|
|
||||||
draggedNodePos = calcNodePos(draggedNodePos, view);
|
|
||||||
|
|
||||||
const { from, to } = view.state.selection;
|
|
||||||
const diff = from - to;
|
|
||||||
|
|
||||||
const fromSelectionPos = calcNodePos(from, view);
|
|
||||||
let differentNodeSelected = false;
|
|
||||||
|
|
||||||
const nodePos = view.state.doc.resolve(fromSelectionPos);
|
|
||||||
|
|
||||||
// Check if nodePos points to the top level node
|
|
||||||
if (nodePos.node().type.name === "doc") differentNodeSelected = true;
|
|
||||||
else {
|
|
||||||
const nodeSelection = NodeSelection.create(
|
|
||||||
view.state.doc,
|
|
||||||
nodePos.before(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if the node where the drag event started is part of the current selection
|
|
||||||
differentNodeSelected = !(
|
|
||||||
draggedNodePos + 1 >= nodeSelection.$from.pos &&
|
|
||||||
draggedNodePos <= nodeSelection.$to.pos
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let selection = view.state.selection;
|
|
||||||
if (
|
|
||||||
!differentNodeSelected &&
|
|
||||||
diff !== 0 &&
|
|
||||||
!(view.state.selection instanceof NodeSelection)
|
|
||||||
) {
|
|
||||||
const endSelection = NodeSelection.create(view.state.doc, to - 1);
|
|
||||||
selection = TextSelection.create(
|
|
||||||
view.state.doc,
|
|
||||||
draggedNodePos,
|
|
||||||
endSelection.$to.pos,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
selection = NodeSelection.create(view.state.doc, draggedNodePos);
|
|
||||||
|
|
||||||
// if inline node is selected, e.g mention -> go to the parent node to select the whole node
|
|
||||||
// if table row is selected, go to the parent node to select the whole node
|
|
||||||
if (
|
|
||||||
(selection as NodeSelection).node.type.isInline ||
|
|
||||||
(selection as NodeSelection).node.type.name === "tableRow"
|
|
||||||
) {
|
|
||||||
let $pos = view.state.doc.resolve(selection.from);
|
|
||||||
selection = NodeSelection.create(view.state.doc, $pos.before());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
view.dispatch(view.state.tr.setSelection(selection));
|
|
||||||
|
|
||||||
// If the selected node is a list item, we need to save the type of the wrapping list e.g. OL or UL
|
|
||||||
if (
|
|
||||||
view.state.selection instanceof NodeSelection &&
|
|
||||||
view.state.selection.node.type.name === "listItem"
|
|
||||||
) {
|
|
||||||
listType = node.parentElement!.tagName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const slice = view.state.selection.content();
|
|
||||||
const { dom, text } = view.serializeForClipboard(slice);
|
|
||||||
|
|
||||||
event.dataTransfer.clearData();
|
|
||||||
event.dataTransfer.setData("text/html", dom.innerHTML);
|
|
||||||
event.dataTransfer.setData("text/plain", text);
|
|
||||||
event.dataTransfer.effectAllowed = "move";
|
|
||||||
|
|
||||||
event.dataTransfer.setDragImage(node, 0, 0);
|
|
||||||
|
|
||||||
view.dragging = { slice, move: event.ctrlKey };
|
|
||||||
}
|
|
||||||
|
|
||||||
let dragHandleElement: HTMLElement | null = null;
|
|
||||||
|
|
||||||
function hideDragHandle() {
|
|
||||||
if (dragHandleElement) {
|
|
||||||
dragHandleElement.classList.add("hide");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDragHandle() {
|
|
||||||
if (dragHandleElement) {
|
|
||||||
dragHandleElement.classList.remove("hide");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideHandleOnEditorOut(event: MouseEvent) {
|
|
||||||
if (event.target instanceof Element) {
|
|
||||||
// Check if the relatedTarget class is still inside the editor
|
|
||||||
const relatedTarget = event.relatedTarget as HTMLElement;
|
|
||||||
const isInsideEditor =
|
|
||||||
relatedTarget?.classList.contains("tiptap") ||
|
|
||||||
relatedTarget?.classList.contains("drag-handle");
|
|
||||||
|
|
||||||
if (isInsideEditor) return;
|
|
||||||
}
|
|
||||||
hideDragHandle();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Plugin({
|
|
||||||
key: new PluginKey(options.pluginKey),
|
|
||||||
view: (view) => {
|
|
||||||
const handleBySelector = options.dragHandleSelector
|
|
||||||
? document.querySelector<HTMLElement>(options.dragHandleSelector)
|
|
||||||
: null;
|
|
||||||
dragHandleElement = handleBySelector ?? document.createElement("div");
|
|
||||||
dragHandleElement.draggable = true;
|
|
||||||
dragHandleElement.dataset.dragHandle = "";
|
|
||||||
dragHandleElement.classList.add("drag-handle");
|
|
||||||
|
|
||||||
function onDragHandleDragStart(e: DragEvent) {
|
|
||||||
handleDragStart(e, view);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragHandleElement.addEventListener("dragstart", onDragHandleDragStart);
|
|
||||||
|
|
||||||
function onDragHandleDrag(e: DragEvent) {
|
|
||||||
hideDragHandle();
|
|
||||||
let scrollY = window.scrollY;
|
|
||||||
if (e.clientY < options.scrollThreshold) {
|
|
||||||
window.scrollTo({ top: scrollY - 30, behavior: "smooth" });
|
|
||||||
} else if (window.innerHeight - e.clientY < options.scrollThreshold) {
|
|
||||||
window.scrollTo({ top: scrollY + 30, behavior: "smooth" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dragHandleElement.addEventListener("drag", onDragHandleDrag);
|
|
||||||
|
|
||||||
hideDragHandle();
|
|
||||||
|
|
||||||
if (!handleBySelector) {
|
|
||||||
view?.dom?.parentElement?.appendChild(dragHandleElement);
|
|
||||||
}
|
|
||||||
view?.dom?.parentElement?.addEventListener(
|
|
||||||
"mouseout",
|
|
||||||
hideHandleOnEditorOut,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
destroy: () => {
|
|
||||||
if (!handleBySelector) {
|
|
||||||
dragHandleElement?.remove?.();
|
|
||||||
}
|
|
||||||
dragHandleElement?.removeEventListener("drag", onDragHandleDrag);
|
|
||||||
dragHandleElement?.removeEventListener(
|
|
||||||
"dragstart",
|
|
||||||
onDragHandleDragStart,
|
|
||||||
);
|
|
||||||
dragHandleElement = null;
|
|
||||||
view?.dom?.parentElement?.removeEventListener(
|
|
||||||
"mouseout",
|
|
||||||
hideHandleOnEditorOut,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
handleDOMEvents: {
|
|
||||||
mousemove: (view, event) => {
|
|
||||||
if (!view.editable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = nodeDOMAtCoords(
|
|
||||||
{
|
|
||||||
x: event.clientX + 50 + options.dragHandleWidth,
|
|
||||||
y: event.clientY,
|
|
||||||
},
|
|
||||||
options,
|
|
||||||
view,
|
|
||||||
);
|
|
||||||
|
|
||||||
const notDragging = node?.closest(".not-draggable");
|
|
||||||
const excludedTagList = options.excludedTags
|
|
||||||
.concat(["ol", "ul"])
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(node instanceof Element) ||
|
|
||||||
node.matches(excludedTagList) ||
|
|
||||||
notDragging
|
|
||||||
) {
|
|
||||||
hideDragHandle();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const compStyle = window.getComputedStyle(node);
|
|
||||||
const parsedLineHeight = parseInt(compStyle.lineHeight, 10);
|
|
||||||
const lineHeight = isNaN(parsedLineHeight)
|
|
||||||
? parseInt(compStyle.fontSize) * 1.2
|
|
||||||
: parsedLineHeight;
|
|
||||||
const paddingTop = parseInt(compStyle.paddingTop, 10);
|
|
||||||
|
|
||||||
const rect = absoluteRect(node);
|
|
||||||
|
|
||||||
rect.top += (lineHeight - 24) / 2;
|
|
||||||
rect.top += paddingTop;
|
|
||||||
// Li markers
|
|
||||||
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
|
||||||
rect.left -= options.dragHandleWidth;
|
|
||||||
}
|
|
||||||
rect.width = options.dragHandleWidth;
|
|
||||||
|
|
||||||
if (!dragHandleElement) return;
|
|
||||||
|
|
||||||
dragHandleElement.style.left = `${rect.left - rect.width}px`;
|
|
||||||
dragHandleElement.style.top = `${rect.top}px`;
|
|
||||||
showDragHandle();
|
|
||||||
},
|
|
||||||
keydown: () => {
|
|
||||||
hideDragHandle();
|
|
||||||
},
|
|
||||||
mousewheel: () => {
|
|
||||||
hideDragHandle();
|
|
||||||
},
|
|
||||||
// dragging class is used for CSS
|
|
||||||
dragstart: (view) => {
|
|
||||||
view.dom.classList.add("dragging");
|
|
||||||
},
|
|
||||||
drop: (view, event) => {
|
|
||||||
view.dom.classList.remove("dragging");
|
|
||||||
hideDragHandle();
|
|
||||||
let droppedNode: Node | null = null;
|
|
||||||
const dropPos = view.posAtCoords({
|
|
||||||
left: event.clientX,
|
|
||||||
top: event.clientY,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!dropPos) return;
|
|
||||||
|
|
||||||
if (view.state.selection instanceof NodeSelection) {
|
|
||||||
droppedNode = view.state.selection.node;
|
|
||||||
}
|
|
||||||
if (!droppedNode) return;
|
|
||||||
|
|
||||||
const resolvedPos = view.state.doc.resolve(dropPos.pos);
|
|
||||||
|
|
||||||
const isDroppedInsideList =
|
|
||||||
resolvedPos.parent.type.name === "listItem";
|
|
||||||
|
|
||||||
// If the selected node is a list item and is not dropped inside a list, we need to wrap it inside <ol> tag otherwise ol list items will be transformed into ul list item when dropped
|
|
||||||
if (
|
|
||||||
view.state.selection instanceof NodeSelection &&
|
|
||||||
view.state.selection.node.type.name === "listItem" &&
|
|
||||||
!isDroppedInsideList &&
|
|
||||||
listType == "OL"
|
|
||||||
) {
|
|
||||||
const newList = view.state.schema.nodes.orderedList?.createAndFill(
|
|
||||||
null,
|
|
||||||
droppedNode,
|
|
||||||
);
|
|
||||||
const slice = new Slice(Fragment.from(newList), 0, 0);
|
|
||||||
view.dragging = { slice, move: event.ctrlKey };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dragend: (view) => {
|
|
||||||
view.dom.classList.remove("dragging");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const GlobalDragHandle = Extension.create({
|
|
||||||
name: "globalDragHandle",
|
|
||||||
|
|
||||||
addOptions() {
|
|
||||||
return {
|
|
||||||
dragHandleWidth: 20,
|
|
||||||
scrollTreshold: 100,
|
|
||||||
excludedTags: [],
|
|
||||||
customNodes: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
|
||||||
return [
|
|
||||||
DragHandlePlugin({
|
|
||||||
pluginKey: "globalDragHandle",
|
|
||||||
dragHandleWidth: this.options.dragHandleWidth,
|
|
||||||
scrollThreshold: this.options.scrollThreshold,
|
|
||||||
dragHandleSelector: this.options.dragHandleSelector,
|
|
||||||
excludedTags: this.options.excludedTags,
|
|
||||||
customNodes: this.options.customNodes,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default GlobalDragHandle;
|
|
||||||
@@ -9,6 +9,7 @@ import SubScript from "@tiptap/extension-subscript";
|
|||||||
import { Typography } from "@tiptap/extension-typography";
|
import { Typography } from "@tiptap/extension-typography";
|
||||||
import { TextStyle } from "@tiptap/extension-text-style";
|
import { TextStyle } from "@tiptap/extension-text-style";
|
||||||
import { Color } from "@tiptap/extension-color";
|
import { Color } from "@tiptap/extension-color";
|
||||||
|
import GlobalDragHandle from "tiptap-extension-global-drag-handle";
|
||||||
import { Youtube } from "@tiptap/extension-youtube";
|
import { Youtube } from "@tiptap/extension-youtube";
|
||||||
import SlashCommand, { SlashCommandExtension as Command } from "@/features/editor/extensions/slash-command";
|
import SlashCommand, { SlashCommandExtension as Command } from "@/features/editor/extensions/slash-command";
|
||||||
import renderItems from "@/features/editor/components/slash-menu/render-items";
|
import renderItems from "@/features/editor/components/slash-menu/render-items";
|
||||||
@@ -51,8 +52,6 @@ import {
|
|||||||
Columns,
|
Columns,
|
||||||
Column,
|
Column,
|
||||||
Status,
|
Status,
|
||||||
Transclusion,
|
|
||||||
TransclusionReference,
|
|
||||||
} from "@docmost/editor-ext";
|
} from "@docmost/editor-ext";
|
||||||
import {
|
import {
|
||||||
randomElement,
|
randomElement,
|
||||||
@@ -81,8 +80,6 @@ import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-v
|
|||||||
import EmbedView from "@/features/editor/components/embed/embed-view.tsx";
|
import EmbedView from "@/features/editor/components/embed/embed-view.tsx";
|
||||||
import PdfView from "@/features/editor/components/pdf/pdf-view.tsx";
|
import PdfView from "@/features/editor/components/pdf/pdf-view.tsx";
|
||||||
import SubpagesView from "@/features/editor/components/subpages/subpages-view.tsx";
|
import SubpagesView from "@/features/editor/components/subpages/subpages-view.tsx";
|
||||||
import TransclusionView from "@/features/editor/components/transclusion/transclusion-view.tsx";
|
|
||||||
import TransclusionReferenceView from "@/features/editor/components/transclusion/transclusion-reference-view.tsx";
|
|
||||||
import { common, createLowlight } from "lowlight";
|
import { common, createLowlight } from "lowlight";
|
||||||
import plaintext from "highlight.js/lib/languages/plaintext";
|
import plaintext from "highlight.js/lib/languages/plaintext";
|
||||||
import powershell from "highlight.js/lib/languages/powershell";
|
import powershell from "highlight.js/lib/languages/powershell";
|
||||||
@@ -103,7 +100,6 @@ import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboa
|
|||||||
import EmojiCommand from "./emoji-command";
|
import EmojiCommand from "./emoji-command";
|
||||||
import { countWords } from "alfaaz";
|
import { countWords } from "alfaaz";
|
||||||
import AutoJoiner from "@/features/editor/extensions/autojoiner.ts";
|
import AutoJoiner from "@/features/editor/extensions/autojoiner.ts";
|
||||||
import GlobalDragHandle from "@/features/editor/extensions/drag-handle.ts";
|
|
||||||
|
|
||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
lowlight.register("mermaid", plaintext);
|
lowlight.register("mermaid", plaintext);
|
||||||
@@ -171,7 +167,7 @@ export const mainExtensions = [
|
|||||||
SharedStorage,
|
SharedStorage,
|
||||||
Heading,
|
Heading,
|
||||||
UniqueID.configure({
|
UniqueID.configure({
|
||||||
types: ["heading", "paragraph", "transclusion"],
|
types: ["heading", "paragraph"],
|
||||||
filterTransaction: (transaction) => !isChangeOrigin(transaction),
|
filterTransaction: (transaction) => !isChangeOrigin(transaction),
|
||||||
}),
|
}),
|
||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
@@ -219,9 +215,7 @@ export const mainExtensions = [
|
|||||||
}),
|
}),
|
||||||
Typography,
|
Typography,
|
||||||
TrailingNode,
|
TrailingNode,
|
||||||
GlobalDragHandle.configure({
|
GlobalDragHandle,
|
||||||
customNodes: ["transclusion", "transclusionReference"],
|
|
||||||
}),
|
|
||||||
TextStyle,
|
TextStyle,
|
||||||
Color,
|
Color,
|
||||||
SlashCommand,
|
SlashCommand,
|
||||||
@@ -357,12 +351,6 @@ export const mainExtensions = [
|
|||||||
Status.configure({
|
Status.configure({
|
||||||
view: StatusView,
|
view: StatusView,
|
||||||
}),
|
}),
|
||||||
Transclusion.configure({
|
|
||||||
view: TransclusionView,
|
|
||||||
}),
|
|
||||||
TransclusionReference.configure({
|
|
||||||
view: TransclusionReferenceView,
|
|
||||||
}),
|
|
||||||
MarkdownClipboard.configure({
|
MarkdownClipboard.configure({
|
||||||
transformPastedText: true,
|
transformPastedText: true,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ 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 { 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";
|
||||||
import { TransclusionLookupProvider } from "@/features/editor/components/transclusion/transclusion-lookup-context";
|
|
||||||
|
|
||||||
interface PageEditorProps {
|
interface PageEditorProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@@ -237,14 +236,6 @@ export default function PageEditor({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (event.key === "Tab") {
|
|
||||||
const editor = editorRef.current;
|
|
||||||
if (!editor) return false;
|
|
||||||
event.preventDefault();
|
|
||||||
return editor.view.someProp("handleKeyDown", (f) =>
|
|
||||||
f(editor.view, event)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (platformModifierKey(event) && event.code === "KeyK") {
|
if (platformModifierKey(event) && event.code === "KeyK") {
|
||||||
searchSpotlight.open();
|
searchSpotlight.open();
|
||||||
return true;
|
return true;
|
||||||
@@ -400,60 +391,55 @@ export default function PageEditor({
|
|||||||
}
|
}
|
||||||
}, [yjsConnectionStatus, isSynced]);
|
}, [yjsConnectionStatus, isSynced]);
|
||||||
|
|
||||||
|
if (showStatic) {
|
||||||
|
return (
|
||||||
|
<EditorProvider
|
||||||
|
editable={false}
|
||||||
|
immediatelyRender={true}
|
||||||
|
extensions={mainExtensions}
|
||||||
|
content={content}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransclusionLookupProvider hostPageId={pageId}>
|
<div className="editor-container" style={{ position: "relative" }}>
|
||||||
{showStatic ? (
|
<div ref={menuContainerRef}>
|
||||||
<EditorProvider
|
<EditorContent editor={editor} />
|
||||||
editable={false}
|
|
||||||
immediatelyRender={true}
|
|
||||||
extensions={mainExtensions}
|
|
||||||
content={content}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="editor-container" style={{ position: "relative" }}>
|
|
||||||
<div ref={menuContainerRef}>
|
|
||||||
<EditorContent editor={editor} />
|
|
||||||
|
|
||||||
{editor && (
|
{editor && (
|
||||||
<SearchAndReplaceDialog editor={editor} editable={editable} />
|
<SearchAndReplaceDialog editor={editor} editable={editable} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{editor && editorIsEditable && (
|
{editor && editorIsEditable && (
|
||||||
<div>
|
<div>
|
||||||
<EditorAiMenu editor={editor} />
|
<EditorAiMenu editor={editor} />
|
||||||
<EditorLinkMenu 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} />
|
||||||
<ImageMenu editor={editor} />
|
<ImageMenu editor={editor} />
|
||||||
<VideoMenu editor={editor} />
|
<VideoMenu editor={editor} />
|
||||||
<PdfMenu editor={editor} />
|
<PdfMenu editor={editor} />
|
||||||
<CalloutMenu editor={editor} />
|
<CalloutMenu editor={editor} />
|
||||||
<SubpagesMenu editor={editor} />
|
<SubpagesMenu editor={editor} />
|
||||||
<ExcalidrawMenu editor={editor} />
|
<ExcalidrawMenu editor={editor} />
|
||||||
<DrawioMenu editor={editor} />
|
<DrawioMenu editor={editor} />
|
||||||
<ColumnsMenu editor={editor} />
|
<ColumnsMenu editor={editor} />
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{editor &&
|
|
||||||
!editorIsEditable &&
|
|
||||||
(editable || canComment) &&
|
|
||||||
providersRef.current && (
|
|
||||||
<ReadonlyBubbleMenu editor={editor} />
|
|
||||||
)}
|
|
||||||
{showCommentPopup && (
|
|
||||||
<CommentDialog editor={editor} pageId={pageId} />
|
|
||||||
)}
|
|
||||||
{showReadOnlyCommentPopup && (
|
|
||||||
<CommentDialog editor={editor} pageId={pageId} readOnly />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
)}
|
||||||
onClick={() => editor.commands.focus("end")}
|
{editor && !editorIsEditable && (editable || canComment) && providersRef.current && (
|
||||||
style={{ paddingBottom: "20vh" }}
|
<ReadonlyBubbleMenu editor={editor} />
|
||||||
></div>
|
)}
|
||||||
</div>
|
{showCommentPopup && <CommentDialog editor={editor} pageId={pageId} />}
|
||||||
)}
|
{showReadOnlyCommentPopup && (
|
||||||
</TransclusionLookupProvider>
|
<CommentDialog editor={editor} pageId={pageId} readOnly />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => editor.commands.focus("end")}
|
||||||
|
style={{ paddingBottom: "20vh" }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { Placeholder } from "@tiptap/extension-placeholder";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { readOnlyEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
|
import { readOnlyEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
|
||||||
import { useEditorScroll } from "./hooks/use-editor-scroll";
|
import { useEditorScroll } from "./hooks/use-editor-scroll";
|
||||||
import { TransclusionLookupProvider } from "@/features/editor/components/transclusion/transclusion-lookup-context";
|
|
||||||
|
|
||||||
interface PageEditorProps {
|
interface PageEditorProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -66,7 +65,7 @@ export default function ReadonlyPageEditor({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransclusionLookupProvider hostPageId={pageId ?? "anonymous"}>
|
<>
|
||||||
<div className="page-title">
|
<div className="page-title">
|
||||||
<EditorProvider
|
<EditorProvider
|
||||||
editable={false}
|
editable={false}
|
||||||
@@ -96,6 +95,6 @@ export default function ReadonlyPageEditor({
|
|||||||
}}
|
}}
|
||||||
></EditorProvider>
|
></EditorProvider>
|
||||||
<div style={{ paddingBottom: "20vh" }}></div>
|
<div style={{ paddingBottom: "20vh" }}></div>
|
||||||
</TransclusionLookupProvider>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
IconCheck,
|
IconCheck,
|
||||||
IconFileCode,
|
IconFileCode,
|
||||||
IconFileTypeDocx,
|
IconFileTypeDocx,
|
||||||
IconFileTypePdf,
|
|
||||||
IconFileTypeZip,
|
IconFileTypeZip,
|
||||||
IconMarkdown,
|
IconMarkdown,
|
||||||
IconX,
|
IconX,
|
||||||
@@ -91,14 +90,12 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
const markdownFileRef = useRef<() => void>(null);
|
const markdownFileRef = useRef<() => void>(null);
|
||||||
const htmlFileRef = useRef<() => void>(null);
|
const htmlFileRef = useRef<() => void>(null);
|
||||||
const docxFileRef = useRef<() => void>(null);
|
const docxFileRef = useRef<() => void>(null);
|
||||||
const pdfFileRef = useRef<() => void>(null);
|
|
||||||
const notionFileRef = useRef<() => void>(null);
|
const notionFileRef = useRef<() => void>(null);
|
||||||
const confluenceFileRef = useRef<() => void>(null);
|
const confluenceFileRef = useRef<() => void>(null);
|
||||||
const zipFileRef = useRef<() => void>(null);
|
const zipFileRef = useRef<() => void>(null);
|
||||||
|
|
||||||
const canUseConfluence = useHasFeature(Feature.CONFLUENCE_IMPORT);
|
const canUseConfluence = useHasFeature(Feature.CONFLUENCE_IMPORT);
|
||||||
const canUseDocx = useHasFeature(Feature.DOCX_IMPORT);
|
const canUseDocx = useHasFeature(Feature.DOCX_IMPORT);
|
||||||
const canUsePdf = useHasFeature(Feature.PDF_IMPORT);
|
|
||||||
const upgradeLabel = useUpgradeLabel();
|
const upgradeLabel = useUpgradeLabel();
|
||||||
|
|
||||||
const handleZipUpload = async (selectedFile: File, source: string) => {
|
const handleZipUpload = async (selectedFile: File, source: string) => {
|
||||||
@@ -247,7 +244,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}, [fileTaskId]);
|
}, [fileTaskId]);
|
||||||
|
|
||||||
const maxSingleFileSize = bytes("30mb");
|
const maxSingleFileSize = bytes("20mb");
|
||||||
|
|
||||||
const handleFileUpload = async (selectedFiles: File[]) => {
|
const handleFileUpload = async (selectedFiles: File[]) => {
|
||||||
if (!selectedFiles) {
|
if (!selectedFiles) {
|
||||||
@@ -301,7 +298,6 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
if (markdownFileRef.current) markdownFileRef.current();
|
if (markdownFileRef.current) markdownFileRef.current();
|
||||||
if (htmlFileRef.current) htmlFileRef.current();
|
if (htmlFileRef.current) htmlFileRef.current();
|
||||||
if (docxFileRef.current) docxFileRef.current();
|
if (docxFileRef.current) docxFileRef.current();
|
||||||
if (pdfFileRef.current) pdfFileRef.current();
|
|
||||||
|
|
||||||
const pageCountText =
|
const pageCountText =
|
||||||
pageCount === 1 ? `1 ${t("page")}` : `${pageCount} ${t("pages")}`;
|
pageCount === 1 ? `1 ${t("page")}` : `${pageCount} ${t("pages")}`;
|
||||||
@@ -382,30 +378,6 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
)}
|
)}
|
||||||
</FileButton>
|
</FileButton>
|
||||||
|
|
||||||
<FileButton
|
|
||||||
onChange={handleFileUpload}
|
|
||||||
accept=".pdf"
|
|
||||||
multiple
|
|
||||||
resetRef={pdfFileRef}
|
|
||||||
>
|
|
||||||
{(props) => (
|
|
||||||
<Tooltip
|
|
||||||
label={upgradeLabel}
|
|
||||||
disabled={canUsePdf}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
disabled={!canUsePdf}
|
|
||||||
justify="start"
|
|
||||||
variant="default"
|
|
||||||
leftSection={<IconFileTypePdf size={18} />}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
PDF
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</FileButton>
|
|
||||||
|
|
||||||
<FileButton
|
<FileButton
|
||||||
onChange={(file) => handleZipUpload(file, "notion")}
|
onChange={(file) => handleZipUpload(file, "notion")}
|
||||||
accept="application/zip"
|
accept="application/zip"
|
||||||
|
|||||||
-205
@@ -1,205 +0,0 @@
|
|||||||
.trigger {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 3px 6px 3px 6px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: transparent;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font: inherit;
|
|
||||||
font-size: var(--mantine-font-size-xs);
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-gray-7),
|
|
||||||
var(--mantine-color-dark-1)
|
|
||||||
);
|
|
||||||
transition: background 120ms ease;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger:hover {
|
|
||||||
background: light-dark(
|
|
||||||
var(--mantine-color-gray-1),
|
|
||||||
var(--mantine-color-dark-5)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.triggerIcon {
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-gray-5),
|
|
||||||
var(--mantine-color-dark-2)
|
|
||||||
);
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.triggerChev {
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-gray-5),
|
|
||||||
var(--mantine-color-dark-2)
|
|
||||||
);
|
|
||||||
display: inline-flex;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
background: light-dark(
|
|
||||||
var(--mantine-color-gray-0),
|
|
||||||
var(--mantine-color-dark-6)
|
|
||||||
);
|
|
||||||
border-bottom: 1px solid
|
|
||||||
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-gray-7),
|
|
||||||
var(--mantine-color-dark-1)
|
|
||||||
);
|
|
||||||
font-size: var(--mantine-font-size-xs);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bannerIcon {
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-gray-6),
|
|
||||||
var(--mantine-color-dark-2)
|
|
||||||
);
|
|
||||||
flex: none;
|
|
||||||
display: inline-flex;
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bannerLink {
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-gray-9),
|
|
||||||
var(--mantine-color-dark-0)
|
|
||||||
);
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 2px;
|
|
||||||
text-decoration-thickness: 1px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bannerLink:hover {
|
|
||||||
text-decoration-thickness: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionLabel {
|
|
||||||
font-size: var(--mantine-font-size-xs);
|
|
||||||
font-weight: 500;
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-gray-6),
|
|
||||||
var(--mantine-color-dark-2)
|
|
||||||
);
|
|
||||||
margin: 0 0 6px;
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-gray-9),
|
|
||||||
var(--mantine-color-dark-0)
|
|
||||||
);
|
|
||||||
font-size: var(--mantine-font-size-sm);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background 100ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row:hover {
|
|
||||||
background: light-dark(
|
|
||||||
var(--mantine-color-gray-1),
|
|
||||||
var(--mantine-color-dark-5)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rowIcon {
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-gray-5),
|
|
||||||
var(--mantine-color-dark-2)
|
|
||||||
);
|
|
||||||
flex: none;
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rowEmoji {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1;
|
|
||||||
flex: none;
|
|
||||||
display: inline-flex;
|
|
||||||
width: 18px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rowTitle {
|
|
||||||
font-weight: 500;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
padding: 2px 7px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: light-dark(
|
|
||||||
var(--mantine-color-gray-1),
|
|
||||||
var(--mantine-color-dark-5)
|
|
||||||
);
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-gray-7),
|
|
||||||
var(--mantine-color-dark-1)
|
|
||||||
);
|
|
||||||
text-transform: uppercase;
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badgeAccent {
|
|
||||||
background: light-dark(
|
|
||||||
var(--mantine-color-blue-0),
|
|
||||||
var(--mantine-color-blue-9)
|
|
||||||
);
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-blue-7),
|
|
||||||
var(--mantine-color-blue-2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
padding: 18px 14px;
|
|
||||||
text-align: center;
|
|
||||||
color: light-dark(
|
|
||||||
var(--mantine-color-gray-6),
|
|
||||||
var(--mantine-color-dark-2)
|
|
||||||
);
|
|
||||||
font-size: var(--mantine-font-size-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 18px;
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Loader, Popover } from "@mantine/core";
|
|
||||||
import {
|
|
||||||
IconChevronDown,
|
|
||||||
IconCornerDownLeft,
|
|
||||||
IconFile,
|
|
||||||
IconInfoCircle,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
|
||||||
import { useReferencesQuery } from "@/features/transclusion/queries/transclusion-query";
|
|
||||||
import type { ReferencingPage } from "@/features/transclusion/types/transclusion.types";
|
|
||||||
import { buildPageUrl } from "@/features/page/page.utils";
|
|
||||||
import classes from "./sync-block-references-dropdown.module.css";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
sourcePageId: string | null;
|
|
||||||
transclusionId: string | null;
|
|
||||||
/** The page currently being viewed - used to mark the "THIS PAGE" badge. */
|
|
||||||
currentPageId: string;
|
|
||||||
/**
|
|
||||||
* Source: trigger reads "Editing original".
|
|
||||||
* Reference: trigger reads "Synced to N other pages".
|
|
||||||
*/
|
|
||||||
mode: "source" | "reference";
|
|
||||||
/** Notified whenever the dropdown opens/closes (for keep-chrome-visible). */
|
|
||||||
onOpenChange?: (open: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SyncBlockReferencesDropdown({
|
|
||||||
sourcePageId,
|
|
||||||
transclusionId,
|
|
||||||
currentPageId,
|
|
||||||
mode,
|
|
||||||
onOpenChange,
|
|
||||||
}: Props) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [opened, setOpened] = useState(false);
|
|
||||||
|
|
||||||
const handleOpenChange = (next: boolean) => {
|
|
||||||
setOpened(next);
|
|
||||||
onOpenChange?.(next);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch eagerly so the "Synced to N other pages" count is correct even
|
|
||||||
// before the dropdown is opened. The cache is keyed on (sourcePageId,
|
|
||||||
// transclusionId), so two views (source + reference) share one fetch.
|
|
||||||
const enabled = !!sourcePageId && !!transclusionId;
|
|
||||||
const { data, isLoading } = useReferencesQuery(
|
|
||||||
sourcePageId,
|
|
||||||
transclusionId,
|
|
||||||
enabled,
|
|
||||||
);
|
|
||||||
|
|
||||||
const allPages: Array<{ page: ReferencingPage; isOriginal: boolean }> = [];
|
|
||||||
if (data?.source) {
|
|
||||||
allPages.push({ page: data.source, isOriginal: true });
|
|
||||||
}
|
|
||||||
for (const ref of data?.references ?? []) {
|
|
||||||
allPages.push({ page: ref, isOriginal: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
const otherCount = allPages.filter((p) => p.page.id !== currentPageId).length;
|
|
||||||
const label =
|
|
||||||
mode === "source"
|
|
||||||
? t("Editing original")
|
|
||||||
: t("Synced to {{count}} other page", {
|
|
||||||
count: otherCount,
|
|
||||||
defaultValue_one: "Synced to {{count}} other page",
|
|
||||||
defaultValue_other: "Synced to {{count}} other pages",
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
position="bottom-start"
|
|
||||||
shadow="lg"
|
|
||||||
opened={opened}
|
|
||||||
onChange={handleOpenChange}
|
|
||||||
width={340}
|
|
||||||
withinPortal
|
|
||||||
>
|
|
||||||
<Popover.Target>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={classes.trigger}
|
|
||||||
onClick={() => handleOpenChange(!opened)}
|
|
||||||
aria-expanded={opened}
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
>
|
|
||||||
<span className={classes.triggerIcon}>
|
|
||||||
<IconCornerDownLeft size={14} stroke={1.8} />
|
|
||||||
</span>
|
|
||||||
<span>{label}</span>
|
|
||||||
<span className={classes.triggerChev}>
|
|
||||||
<IconChevronDown size={12} stroke={2} />
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</Popover.Target>
|
|
||||||
|
|
||||||
<Popover.Dropdown className={classes.dropdown}>
|
|
||||||
{mode === "reference" && data?.source && (
|
|
||||||
<div className={classes.banner}>
|
|
||||||
<span className={classes.bannerIcon}>
|
|
||||||
<IconInfoCircle size={16} stroke={1.6} />
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<Trans
|
|
||||||
i18nKey="sourceReadOnlyHint"
|
|
||||||
defaults="This section is read-only here. Edit it on the <link>original source page</link>."
|
|
||||||
components={{
|
|
||||||
link: (
|
|
||||||
<Link
|
|
||||||
to={
|
|
||||||
data.source.spaceSlug
|
|
||||||
? buildPageUrl(
|
|
||||||
data.source.spaceSlug,
|
|
||||||
data.source.slugId,
|
|
||||||
data.source.title,
|
|
||||||
)
|
|
||||||
: `/p/${data.source.id}`
|
|
||||||
}
|
|
||||||
className={classes.bannerLink}
|
|
||||||
onClick={() => handleOpenChange(false)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isLoading ? (
|
|
||||||
<div className={classes.loading}>
|
|
||||||
<Loader size="xs" />
|
|
||||||
</div>
|
|
||||||
) : allPages.length === 0 ? (
|
|
||||||
<div className={classes.empty}>{t("No pages")}</div>
|
|
||||||
) : (
|
|
||||||
<div className={classes.section}>
|
|
||||||
<div className={classes.sectionLabel}>{t("Synced to")}</div>
|
|
||||||
<ul className={classes.list}>
|
|
||||||
{allPages.map(({ page, isOriginal }) => {
|
|
||||||
const isCurrent = page.id === currentPageId;
|
|
||||||
const href = page.spaceSlug
|
|
||||||
? buildPageUrl(page.spaceSlug, page.slugId, page.title)
|
|
||||||
: `/p/${page.id}`;
|
|
||||||
const title = page.title?.length ? page.title : t("Untitled");
|
|
||||||
return (
|
|
||||||
<li key={page.id}>
|
|
||||||
<Link
|
|
||||||
to={href}
|
|
||||||
className={classes.row}
|
|
||||||
onClick={() => handleOpenChange(false)}
|
|
||||||
>
|
|
||||||
{page.icon ? (
|
|
||||||
<span className={classes.rowEmoji}>{page.icon}</span>
|
|
||||||
) : (
|
|
||||||
<span className={classes.rowIcon}>
|
|
||||||
<IconFile size={16} stroke={1.6} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className={classes.rowTitle} title={title}>
|
|
||||||
{title}
|
|
||||||
</span>
|
|
||||||
{isCurrent ? (
|
|
||||||
<span
|
|
||||||
className={`${classes.badge} ${classes.badgeAccent}`}
|
|
||||||
>
|
|
||||||
{t("THIS PAGE")}
|
|
||||||
</span>
|
|
||||||
) : isOriginal ? (
|
|
||||||
<span className={classes.badge}>{t("ORIGINAL")}</span>
|
|
||||||
) : null}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Popover.Dropdown>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
||||||
import {
|
|
||||||
listReferences,
|
|
||||||
unsyncReference,
|
|
||||||
} from "../services/transclusion-api";
|
|
||||||
|
|
||||||
export function useReferencesQuery(
|
|
||||||
sourcePageId: string | null,
|
|
||||||
transclusionId: string | null,
|
|
||||||
enabled: boolean,
|
|
||||||
) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["transclusion-references", sourcePageId, transclusionId],
|
|
||||||
queryFn: () =>
|
|
||||||
listReferences({
|
|
||||||
sourcePageId: sourcePageId!,
|
|
||||||
transclusionId: transclusionId!,
|
|
||||||
}),
|
|
||||||
enabled: enabled && !!sourcePageId && !!transclusionId,
|
|
||||||
staleTime: 10 * 1000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUnsyncReferenceMutation() {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (params: {
|
|
||||||
referencePageId: string;
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
}) => unsyncReference(params),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import api from "@/lib/api-client";
|
|
||||||
import type {
|
|
||||||
ReferencingPagesResponse,
|
|
||||||
TransclusionLookup,
|
|
||||||
} from "../types/transclusion.types";
|
|
||||||
|
|
||||||
export async function lookupTransclusion(params: {
|
|
||||||
references: Array<{ sourcePageId: string; transclusionId: string }>;
|
|
||||||
}): Promise<{ items: TransclusionLookup[] }> {
|
|
||||||
const r = await api.post("/pages/transclusion/lookup", params);
|
|
||||||
return r.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listReferences(params: {
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
}): Promise<ReferencingPagesResponse> {
|
|
||||||
const r = await api.post("/pages/transclusion/references", params);
|
|
||||||
return r.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function unsyncReference(params: {
|
|
||||||
referencePageId: string;
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
}): Promise<{ content: unknown }> {
|
|
||||||
const r = await api.post("/pages/transclusion/unsync-reference", params);
|
|
||||||
return r.data;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
export type TransclusionLookup =
|
|
||||||
| {
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
content: unknown;
|
|
||||||
sourceUpdatedAt: string;
|
|
||||||
}
|
|
||||||
| { sourcePageId: string; transclusionId: string; status: "not_found" }
|
|
||||||
| { sourcePageId: string; transclusionId: string; status: "no_access" };
|
|
||||||
|
|
||||||
export type ReferencingPage = {
|
|
||||||
id: string;
|
|
||||||
slugId: string;
|
|
||||||
title: string | null;
|
|
||||||
icon: string | null;
|
|
||||||
spaceId: string;
|
|
||||||
spaceSlug: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ReferencingPagesResponse = {
|
|
||||||
source: ReferencingPage | null;
|
|
||||||
references: ReferencingPage[];
|
|
||||||
};
|
|
||||||
@@ -33,11 +33,10 @@
|
|||||||
"@ai-sdk/google": "^3.0.52",
|
"@ai-sdk/google": "^3.0.52",
|
||||||
"@ai-sdk/openai": "^3.0.47",
|
"@ai-sdk/openai": "^3.0.47",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.37",
|
"@ai-sdk/openai-compatible": "^2.0.37",
|
||||||
"@aws-sdk/client-s3": "3.1040.0",
|
"@aws-sdk/client-s3": "3.1037.0",
|
||||||
"@aws-sdk/lib-storage": "3.1040.0",
|
"@aws-sdk/lib-storage": "3.1037.0",
|
||||||
"@aws-sdk/s3-request-presigner": "3.1040.0",
|
"@aws-sdk/s3-request-presigner": "3.1037.0",
|
||||||
"@clickhouse/client": "^1.18.2",
|
"@clickhouse/client": "^1.18.2",
|
||||||
"@docmost/pdf-inspector": "1.9.4",
|
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/multipart": "^10.0.0",
|
"@fastify/multipart": "^10.0.0",
|
||||||
"@fastify/static": "^9.1.3",
|
"@fastify/static": "^9.1.3",
|
||||||
@@ -101,6 +100,7 @@
|
|||||||
"p-limit": "^7.3.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.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",
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { LoggerExtension } from './extensions/logger.extension';
|
|||||||
import { CollaborationHandler } from './collaboration.handler';
|
import { CollaborationHandler } from './collaboration.handler';
|
||||||
import { CollabHistoryService } from './services/collab-history.service';
|
import { CollabHistoryService } from './services/collab-history.service';
|
||||||
import { WatcherModule } from '../core/watcher/watcher.module';
|
import { WatcherModule } from '../core/watcher/watcher.module';
|
||||||
import { TransclusionService } from '../core/page/transclusion/transclusion.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -29,7 +28,6 @@ import { TransclusionService } from '../core/page/transclusion/transclusion.serv
|
|||||||
HistoryProcessor,
|
HistoryProcessor,
|
||||||
CollabHistoryService,
|
CollabHistoryService,
|
||||||
CollaborationHandler,
|
CollaborationHandler,
|
||||||
TransclusionService,
|
|
||||||
],
|
],
|
||||||
exports: [CollaborationGateway],
|
exports: [CollaborationGateway],
|
||||||
imports: [TokenModule, WatcherModule],
|
imports: [TokenModule, WatcherModule],
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ import {
|
|||||||
Status,
|
Status,
|
||||||
addUniqueIdsToDoc,
|
addUniqueIdsToDoc,
|
||||||
htmlToMarkdown,
|
htmlToMarkdown,
|
||||||
Transclusion,
|
|
||||||
TransclusionReference,
|
|
||||||
} from '@docmost/editor-ext';
|
} from '@docmost/editor-ext';
|
||||||
import { generateText, getSchema, JSONContent } from '@tiptap/core';
|
import { generateText, getSchema, JSONContent } from '@tiptap/core';
|
||||||
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
|
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
|
||||||
@@ -103,8 +101,6 @@ export const tiptapExtensions = [
|
|||||||
Columns,
|
Columns,
|
||||||
Column,
|
Column,
|
||||||
Status,
|
Status,
|
||||||
Transclusion,
|
|
||||||
TransclusionReference,
|
|
||||||
] as any;
|
] as any;
|
||||||
|
|
||||||
export function jsonToHtml(tiptapJson: any) {
|
export function jsonToHtml(tiptapJson: any) {
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
HISTORY_FAST_THRESHOLD,
|
HISTORY_FAST_THRESHOLD,
|
||||||
HISTORY_INTERVAL,
|
HISTORY_INTERVAL,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { TransclusionService } from '../../core/page/transclusion/transclusion.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PersistenceExtension implements Extension {
|
export class PersistenceExtension implements Extension {
|
||||||
@@ -46,7 +45,6 @@ export class PersistenceExtension implements Extension {
|
|||||||
@InjectQueue(QueueName.HISTORY_QUEUE) private historyQueue: Queue,
|
@InjectQueue(QueueName.HISTORY_QUEUE) private historyQueue: Queue,
|
||||||
@InjectQueue(QueueName.NOTIFICATION_QUEUE) private notificationQueue: Queue,
|
@InjectQueue(QueueName.NOTIFICATION_QUEUE) private notificationQueue: Queue,
|
||||||
private readonly collabHistory: CollabHistoryService,
|
private readonly collabHistory: CollabHistoryService,
|
||||||
private readonly transclusionService: TransclusionService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onLoadDocument(data: onLoadDocumentPayload) {
|
async onLoadDocument(data: onLoadDocumentPayload) {
|
||||||
@@ -136,11 +134,7 @@ export class PersistenceExtension implements Extension {
|
|||||||
try {
|
try {
|
||||||
const existingContributors = page.contributorIds || [];
|
const existingContributors = page.contributorIds || [];
|
||||||
contributorIds = Array.from(
|
contributorIds = Array.from(
|
||||||
new Set([
|
new Set([...existingContributors, ...editingUserIds, page.creatorId]),
|
||||||
...existingContributors,
|
|
||||||
...editingUserIds,
|
|
||||||
page.creatorId,
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//this.logger.debug('Contributors error:' + err?.['message']);
|
//this.logger.debug('Contributors error:' + err?.['message']);
|
||||||
@@ -164,10 +158,6 @@ export class PersistenceExtension implements Extension {
|
|||||||
this.logger.error(`Failed to update page ${pageId}`, err);
|
this.logger.error(`Failed to update page ${pageId}`, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page) {
|
|
||||||
await this.syncTransclusion(pageId, tiptapJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (page) {
|
if (page) {
|
||||||
await this.collabHistory.addContributors(pageId, editingUserIds);
|
await this.collabHistory.addContributors(pageId, editingUserIds);
|
||||||
|
|
||||||
@@ -175,9 +165,7 @@ export class PersistenceExtension implements Extension {
|
|||||||
|
|
||||||
const userMentions = extractUserMentions(mentions);
|
const userMentions = extractUserMentions(mentions);
|
||||||
const oldMentions = page.content ? extractMentions(page.content) : [];
|
const oldMentions = page.content ? extractMentions(page.content) : [];
|
||||||
const oldMentionedUserIds = extractUserMentions(oldMentions).map(
|
const oldMentionedUserIds = extractUserMentions(oldMentions).map((m) => m.entityId);
|
||||||
(m) => m.entityId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userMentions.length > 0) {
|
if (userMentions.length > 0) {
|
||||||
await this.notificationQueue.add(QueueJob.PAGE_MENTION_NOTIFICATION, {
|
await this.notificationQueue.add(QueueJob.PAGE_MENTION_NOTIFICATION, {
|
||||||
@@ -241,29 +229,4 @@ export class PersistenceExtension implements Extension {
|
|||||||
{ jobId: page.id, delay },
|
{ jobId: page.id, delay },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh `page_transclusions` and `page_transclusion_references` to match
|
|
||||||
* the page's current content. Runs outside the page-write transaction and
|
|
||||||
* isolates each call so a failure here cannot affect the page save itself.
|
|
||||||
* The diff is idempotent — the next save converges if a round drops anything.
|
|
||||||
*/
|
|
||||||
private async syncTransclusion(
|
|
||||||
pageId: string,
|
|
||||||
tiptapJson: unknown,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.transclusionService.syncPageTransclusions(pageId, tiptapJson);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(`Failed to sync transclusions for page ${pageId}`, err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this.transclusionService.syncPageReferences(pageId, tiptapJson);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(
|
|
||||||
`Failed to sync transclusion references for page ${pageId}`,
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export const Feature = {
|
|||||||
AI: 'ai',
|
AI: 'ai',
|
||||||
CONFLUENCE_IMPORT: 'import:confluence',
|
CONFLUENCE_IMPORT: 'import:confluence',
|
||||||
DOCX_IMPORT: 'import:docx',
|
DOCX_IMPORT: 'import:docx',
|
||||||
PDF_IMPORT: 'import:pdf',
|
|
||||||
ATTACHMENT_INDEXING: 'attachment:indexing',
|
ATTACHMENT_INDEXING: 'attachment:indexing',
|
||||||
SECURITY_SETTINGS: 'security:settings',
|
SECURITY_SETTINGS: 'security:settings',
|
||||||
MCP: 'mcp',
|
MCP: 'mcp',
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ import { TrashCleanupService } from './services/trash-cleanup.service';
|
|||||||
import { StorageModule } from '../../integrations/storage/storage.module';
|
import { StorageModule } from '../../integrations/storage/storage.module';
|
||||||
import { CollaborationModule } from '../../collaboration/collaboration.module';
|
import { CollaborationModule } from '../../collaboration/collaboration.module';
|
||||||
import { WatcherModule } from '../watcher/watcher.module';
|
import { WatcherModule } from '../watcher/watcher.module';
|
||||||
import { TransclusionModule } from './transclusion/transclusion.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [PageController],
|
controllers: [PageController],
|
||||||
providers: [PageService, PageHistoryService, TrashCleanupService],
|
providers: [PageService, PageHistoryService, TrashCleanupService],
|
||||||
exports: [PageService, PageHistoryService],
|
exports: [PageService, PageHistoryService],
|
||||||
imports: [StorageModule, CollaborationModule, WatcherModule, TransclusionModule],
|
imports: [StorageModule, CollaborationModule, WatcherModule],
|
||||||
})
|
})
|
||||||
export class PageModule {}
|
export class PageModule {}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ import {
|
|||||||
import { markdownToHtml } from '@docmost/editor-ext';
|
import { markdownToHtml } from '@docmost/editor-ext';
|
||||||
import { WatcherService } from '../../watcher/watcher.service';
|
import { WatcherService } from '../../watcher/watcher.service';
|
||||||
import { sql } from 'kysely';
|
import { sql } from 'kysely';
|
||||||
import { TransclusionService } from '../transclusion/transclusion.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PageService {
|
export class PageService {
|
||||||
@@ -72,7 +71,6 @@ export class PageService {
|
|||||||
private eventEmitter: EventEmitter2,
|
private eventEmitter: EventEmitter2,
|
||||||
private collaborationGateway: CollaborationGateway,
|
private collaborationGateway: CollaborationGateway,
|
||||||
private readonly watcherService: WatcherService,
|
private readonly watcherService: WatcherService,
|
||||||
private readonly transclusionService: TransclusionService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findById(
|
async findById(
|
||||||
@@ -602,17 +600,6 @@ export class PageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remap transclusion-reference source pages to their copies when
|
|
||||||
// the source page is also being duplicated in the same operation.
|
|
||||||
if (node.type.name === 'transclusionReference') {
|
|
||||||
const sourcePageId = node.attrs.sourcePageId;
|
|
||||||
if (sourcePageId && pageMap.has(sourcePageId)) {
|
|
||||||
const mappedPage = pageMap.get(sourcePageId);
|
|
||||||
//@ts-ignore
|
|
||||||
node.attrs.sourcePageId = mappedPage.newPageId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update internal page links in link marks
|
// Update internal page links in link marks
|
||||||
for (const mark of node.marks) {
|
for (const mark of node.marks) {
|
||||||
if (
|
if (
|
||||||
@@ -672,31 +659,6 @@ export class PageService {
|
|||||||
|
|
||||||
await this.db.insertInto('pages').values(insertablePages).execute();
|
await this.db.insertInto('pages').values(insertablePages).execute();
|
||||||
|
|
||||||
// Extract transclusions from every duplicated page and persist them in
|
|
||||||
// one statement. Duplication bypasses Yjs onStoreDocument; brand-new
|
|
||||||
// pages never have prior rows so we can skip the diff and just bulk-insert.
|
|
||||||
try {
|
|
||||||
await this.transclusionService.insertTransclusionsForPages(
|
|
||||||
insertablePages.map((p) => ({ id: p.id, content: p.content })),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(
|
|
||||||
'Failed to insert transclusions for duplicated pages',
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.transclusionService.insertReferencesForPages(
|
|
||||||
insertablePages.map((p) => ({ id: p.id, content: p.content })),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(
|
|
||||||
'Failed to insert transclusion references for duplicated pages',
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertedPageIds = insertablePages.map((page) => page.id);
|
const insertedPageIds = insertablePages.map((page) => page.id);
|
||||||
this.eventEmitter.emit(EventName.PAGE_CREATED, {
|
this.eventEmitter.emit(EventName.PAGE_CREATED, {
|
||||||
pageIds: insertedPageIds,
|
pageIds: insertedPageIds,
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { Type } from 'class-transformer';
|
|
||||||
import {
|
|
||||||
ArrayMaxSize,
|
|
||||||
IsArray,
|
|
||||||
IsString,
|
|
||||||
IsUUID,
|
|
||||||
ValidateNested,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
export class LookupReferenceDto {
|
|
||||||
@IsUUID()
|
|
||||||
sourcePageId!: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
transclusionId!: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LookupDto {
|
|
||||||
@IsArray()
|
|
||||||
@ArrayMaxSize(50)
|
|
||||||
@ValidateNested({ each: true })
|
|
||||||
@Type(() => LookupReferenceDto)
|
|
||||||
references!: LookupReferenceDto[];
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { IsString, IsUUID } from 'class-validator';
|
|
||||||
|
|
||||||
export class ReferencesDto {
|
|
||||||
@IsUUID()
|
|
||||||
sourcePageId!: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
transclusionId!: string;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { IsString, IsUUID } from 'class-validator';
|
|
||||||
|
|
||||||
export class UnsyncReferenceDto {
|
|
||||||
@IsUUID()
|
|
||||||
referencePageId!: string;
|
|
||||||
|
|
||||||
@IsUUID()
|
|
||||||
sourcePageId!: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
transclusionId!: string;
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
import {
|
|
||||||
collectReferencesFromPmJson,
|
|
||||||
collectTransclusionsFromPmJson,
|
|
||||||
} from '../utils/transclusion-prosemirror.util';
|
|
||||||
|
|
||||||
describe('collectTransclusionsFromPmJson', () => {
|
|
||||||
it('returns [] for null/undefined doc', () => {
|
|
||||||
expect(collectTransclusionsFromPmJson(null)).toEqual([]);
|
|
||||||
expect(collectTransclusionsFromPmJson(undefined)).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns [] for a doc with no transclusion nodes', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'hi' }] }],
|
|
||||||
};
|
|
||||||
expect(collectTransclusionsFromPmJson(doc)).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('extracts a top-level transclusion with id, name and content', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusion',
|
|
||||||
attrs: { id: 'abc123', name: 'Pricing' },
|
|
||||||
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Body' }] }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const got = collectTransclusionsFromPmJson(doc);
|
|
||||||
expect(got).toHaveLength(1);
|
|
||||||
expect(got[0].transclusionId).toBe('abc123');
|
|
||||||
expect(got[0].name).toBe('Pricing');
|
|
||||||
expect(got[0].content).toEqual({
|
|
||||||
type: 'doc',
|
|
||||||
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Body' }] }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skips transclusion nodes with no id (transient before UniqueID assigns one)', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{ type: 'transclusion', attrs: {}, content: [{ type: 'paragraph' }] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
expect(collectTransclusionsFromPmJson(doc)).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns multiple top-level transclusions', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{ type: 'transclusion', attrs: { id: 'a' }, content: [{ type: 'paragraph' }] },
|
|
||||||
{ type: 'transclusion', attrs: { id: 'b', name: 'Two' }, content: [{ type: 'paragraph' }] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const got = collectTransclusionsFromPmJson(doc);
|
|
||||||
expect(got.map((e) => e.transclusionId)).toEqual(['a', 'b']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not recurse into a nested transclusion (transclusion cannot contain transclusion per schema, but be defensive)', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusion',
|
|
||||||
attrs: { id: 'outer' },
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusion',
|
|
||||||
attrs: { id: 'inner' },
|
|
||||||
content: [{ type: 'paragraph' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const got = collectTransclusionsFromPmJson(doc);
|
|
||||||
expect(got.map((e) => e.transclusionId)).toEqual(['outer']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('finds transclusions nested inside other block containers (e.g. column)', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'column',
|
|
||||||
content: [
|
|
||||||
{ type: 'transclusion', attrs: { id: 'inCol' }, content: [{ type: 'paragraph' }] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
expect(collectTransclusionsFromPmJson(doc).map((e) => e.transclusionId)).toEqual([
|
|
||||||
'inCol',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses the last id when duplicate ids appear (later wins, deterministic)', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{ type: 'transclusion', attrs: { id: 'dup', name: 'first' }, content: [{ type: 'paragraph' }] },
|
|
||||||
{ type: 'transclusion', attrs: { id: 'dup', name: 'second' }, content: [{ type: 'paragraph' }] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const got = collectTransclusionsFromPmJson(doc);
|
|
||||||
expect(got).toHaveLength(1);
|
|
||||||
expect(got[0].name).toBe('second');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('collectReferencesFromPmJson', () => {
|
|
||||||
it('returns [] for null/undefined doc', () => {
|
|
||||||
expect(collectReferencesFromPmJson(null)).toEqual([]);
|
|
||||||
expect(collectReferencesFromPmJson(undefined)).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns [] for a doc with no transclusionReference nodes', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{ type: 'paragraph', content: [{ type: 'text', text: 'hi' }] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
expect(collectReferencesFromPmJson(doc)).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('extracts a top-level reference', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
expect(collectReferencesFromPmJson(doc)).toEqual([
|
|
||||||
{ containingTransclusionId: null, sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skips references missing sourcePageId or transclusionId', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{ type: 'transclusionReference', attrs: { transclusionId: 'e1' } },
|
|
||||||
{ type: 'transclusionReference', attrs: { sourcePageId: 'p1' } },
|
|
||||||
{ type: 'transclusionReference', attrs: {} },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
expect(collectReferencesFromPmJson(doc)).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('finds references nested in other block containers (column, callout, etc.)', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'column',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'callout',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p2', transclusionId: 'e2' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
expect(collectReferencesFromPmJson(doc)).toEqual([
|
|
||||||
{ containingTransclusionId: null, sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
{ containingTransclusionId: null, sourcePageId: 'p2', transclusionId: 'e2' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('also finds references nested inside a transclusion (source) node', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusion',
|
|
||||||
attrs: { id: 'src1' },
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
expect(collectReferencesFromPmJson(doc)).toEqual([
|
|
||||||
{ containingTransclusionId: 'src1', sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dedupes identical (containingTransclusionId, sourcePageId, transclusionId) triples', () => {
|
|
||||||
const doc = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p2', transclusionId: 'e2' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
expect(collectReferencesFromPmJson(doc)).toEqual([
|
|
||||||
{ containingTransclusionId: null, sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
{ containingTransclusionId: null, sourcePageId: 'p2', transclusionId: 'e2' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import {
|
|
||||||
rewriteAttachmentsForUnsync,
|
|
||||||
type AttachmentRewritePlan,
|
|
||||||
} from '../utils/transclusion-unsync.util';
|
|
||||||
|
|
||||||
describe('rewriteAttachmentsForUnsync', () => {
|
|
||||||
const fixedIds = (() => {
|
|
||||||
let i = 0;
|
|
||||||
return () => `new-${++i}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns content unchanged when no attachment nodes are present', () => {
|
|
||||||
const content = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{ type: 'paragraph', content: [{ type: 'text', text: 'hello' }] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const r = rewriteAttachmentsForUnsync(content, fixedIds());
|
|
||||||
expect(r.content).toEqual(content);
|
|
||||||
expect(r.copies).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rewrites attachmentId and src on a single image node', () => {
|
|
||||||
const oldId = '11111111-1111-1111-1111-111111111111';
|
|
||||||
const content = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'image',
|
|
||||||
attrs: {
|
|
||||||
attachmentId: oldId,
|
|
||||||
src: `/api/files/${oldId}/cat.png`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const gen = fixedIds();
|
|
||||||
const r = rewriteAttachmentsForUnsync(content, gen);
|
|
||||||
|
|
||||||
expect(r.copies).toHaveLength(1);
|
|
||||||
const plan: AttachmentRewritePlan = r.copies[0];
|
|
||||||
expect(plan.oldAttachmentId).toBe(oldId);
|
|
||||||
expect(plan.newAttachmentId).toBe('new-1');
|
|
||||||
|
|
||||||
const img = (r.content as any).content[0];
|
|
||||||
expect(img.attrs.attachmentId).toBe('new-1');
|
|
||||||
expect(img.attrs.src).toBe('/api/files/new-1/cat.png');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rewrites every attachment node type (image, video, audio, attachment, drawio, excalidraw, pdf)', () => {
|
|
||||||
const types = [
|
|
||||||
'image',
|
|
||||||
'video',
|
|
||||||
'audio',
|
|
||||||
'attachment',
|
|
||||||
'drawio',
|
|
||||||
'excalidraw',
|
|
||||||
'pdf',
|
|
||||||
] as const;
|
|
||||||
const content = {
|
|
||||||
type: 'doc',
|
|
||||||
content: types.map((t, i) => ({
|
|
||||||
type: t,
|
|
||||||
attrs: {
|
|
||||||
attachmentId: `old-${i}`,
|
|
||||||
src: `/api/files/old-${i}/file`,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
const r = rewriteAttachmentsForUnsync(content, fixedIds());
|
|
||||||
expect(r.copies).toHaveLength(types.length);
|
|
||||||
expect((r.content as any).content.map((n: any) => n.attrs.attachmentId)).toEqual(
|
|
||||||
Array.from({ length: types.length }, (_, i) => `new-${i + 1}`),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reuses one new id per old attachmentId across nodes (dedupe)', () => {
|
|
||||||
const shared = 'shared-old';
|
|
||||||
const content = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'image',
|
|
||||||
attrs: {
|
|
||||||
attachmentId: shared,
|
|
||||||
src: `/api/files/${shared}/a.png`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'image',
|
|
||||||
attrs: {
|
|
||||||
attachmentId: shared,
|
|
||||||
src: `/api/files/${shared}/a.png`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const r = rewriteAttachmentsForUnsync(content, fixedIds());
|
|
||||||
expect(r.copies).toHaveLength(1);
|
|
||||||
expect(r.copies[0].oldAttachmentId).toBe(shared);
|
|
||||||
const newId = r.copies[0].newAttachmentId;
|
|
||||||
expect((r.content as any).content[0].attrs.attachmentId).toBe(newId);
|
|
||||||
expect((r.content as any).content[1].attrs.attachmentId).toBe(newId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not mutate the input content object', () => {
|
|
||||||
const content = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'image',
|
|
||||||
attrs: { attachmentId: 'old-x', src: '/api/files/old-x/x.png' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const snapshot = JSON.parse(JSON.stringify(content));
|
|
||||||
rewriteAttachmentsForUnsync(content, fixedIds());
|
|
||||||
expect(content).toEqual(snapshot);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skips nodes whose attachmentId is missing or not a uuid-shaped string', () => {
|
|
||||||
const content = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{ type: 'image', attrs: {} },
|
|
||||||
{ type: 'image', attrs: { attachmentId: '' } },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const r = rewriteAttachmentsForUnsync(content, fixedIds());
|
|
||||||
expect(r.copies).toEqual([]);
|
|
||||||
expect(r.content).toEqual(content);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('recurses into nested containers (column, callout)', () => {
|
|
||||||
const oldId = 'old-nested';
|
|
||||||
const content = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'callout',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'image',
|
|
||||||
attrs: {
|
|
||||||
attachmentId: oldId,
|
|
||||||
src: `/api/files/${oldId}/x.png`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const r = rewriteAttachmentsForUnsync(content, fixedIds());
|
|
||||||
expect(r.copies).toHaveLength(1);
|
|
||||||
const newId = r.copies[0].newAttachmentId;
|
|
||||||
const inner = (r.content as any).content[0].content[0];
|
|
||||||
expect(inner.attrs.attachmentId).toBe(newId);
|
|
||||||
expect(inner.attrs.src).toBe(`/api/files/${newId}/x.png`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { Test } from '@nestjs/testing';
|
|
||||||
import { TransclusionController } from '../transclusion.controller';
|
|
||||||
import { TransclusionService } from '../transclusion.service';
|
|
||||||
import { JwtAuthGuard } from '../../../../common/guards/jwt-auth.guard';
|
|
||||||
|
|
||||||
describe('TransclusionController.lookup', () => {
|
|
||||||
let controller: TransclusionController;
|
|
||||||
let service: jest.Mocked<TransclusionService>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
service = {
|
|
||||||
lookup: jest.fn(),
|
|
||||||
listReferences: jest.fn(),
|
|
||||||
unsyncReference: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const module = await Test.createTestingModule({
|
|
||||||
controllers: [TransclusionController],
|
|
||||||
providers: [{ provide: TransclusionService, useValue: service }],
|
|
||||||
})
|
|
||||||
.overrideGuard(JwtAuthGuard)
|
|
||||||
.useValue({ canActivate: () => true })
|
|
||||||
.compile();
|
|
||||||
|
|
||||||
controller = module.get(TransclusionController);
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = { id: 'u1' } as any;
|
|
||||||
const ref = { sourcePageId: 'p1', transclusionId: 'e1' };
|
|
||||||
|
|
||||||
it('returns content when lookup succeeds', async () => {
|
|
||||||
service.lookup.mockResolvedValue({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
sourcePageId: 'p1',
|
|
||||||
transclusionId: 'e1',
|
|
||||||
content: { type: 'doc' },
|
|
||||||
sourceUpdatedAt: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const out = await controller.lookup({ references: [ref] } as any, user);
|
|
||||||
expect(out.items[0]).not.toHaveProperty('status');
|
|
||||||
expect((out.items[0] as any).content).toEqual({ type: 'doc' });
|
|
||||||
expect(service.lookup).toHaveBeenCalledWith([ref], 'u1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns no_access when service says no_access', async () => {
|
|
||||||
service.lookup.mockResolvedValue({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
sourcePageId: 'p1',
|
|
||||||
transclusionId: 'e1',
|
|
||||||
status: 'no_access',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const out = await controller.lookup({ references: [ref] } as any, user);
|
|
||||||
expect((out.items[0] as { status?: string }).status).toBe('no_access');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns not_found when service says not_found', async () => {
|
|
||||||
service.lookup.mockResolvedValue({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
sourcePageId: 'p1',
|
|
||||||
transclusionId: 'e1',
|
|
||||||
status: 'not_found',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const out = await controller.lookup({ references: [ref] } as any, user);
|
|
||||||
expect((out.items[0] as { status?: string }).status).toBe('not_found');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,412 +0,0 @@
|
|||||||
import { Test } from '@nestjs/testing';
|
|
||||||
import { TransclusionService } from '../transclusion.service';
|
|
||||||
import { PageTransclusionsRepo } from '@docmost/db/repos/page-transclusions/page-transclusions.repo';
|
|
||||||
import { PageTransclusionReferencesRepo } from '@docmost/db/repos/page-transclusion-references/page-transclusion-references.repo';
|
|
||||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
|
||||||
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
|
||||||
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
|
|
||||||
import { StorageService } from '../../../../integrations/storage/storage.service';
|
|
||||||
|
|
||||||
describe('TransclusionService.syncPageTransclusions', () => {
|
|
||||||
let service: TransclusionService;
|
|
||||||
let repo: jest.Mocked<PageTransclusionsRepo>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const mockRepo: jest.Mocked<Partial<PageTransclusionsRepo>> = {
|
|
||||||
findByPageId: jest.fn(),
|
|
||||||
insert: jest.fn(),
|
|
||||||
update: jest.fn(),
|
|
||||||
deleteByPageAndTransclusionIds: jest.fn(),
|
|
||||||
};
|
|
||||||
const module = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
TransclusionService,
|
|
||||||
{ provide: PageTransclusionsRepo, useValue: mockRepo },
|
|
||||||
{ provide: PageTransclusionReferencesRepo, useValue: {} },
|
|
||||||
{ provide: PageRepo, useValue: {} },
|
|
||||||
{ provide: PagePermissionRepo, useValue: {} },
|
|
||||||
{ provide: AttachmentRepo, useValue: {} },
|
|
||||||
{ provide: StorageService, useValue: {} },
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
service = module.get(TransclusionService);
|
|
||||||
repo = module.get(PageTransclusionsRepo);
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageId = '00000000-0000-0000-0000-000000000001';
|
|
||||||
|
|
||||||
it('inserts new transclusions that did not exist before', async () => {
|
|
||||||
repo.findByPageId.mockResolvedValue([]);
|
|
||||||
const pm = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusion',
|
|
||||||
attrs: { id: 'a', name: 'Hello' },
|
|
||||||
content: [{ type: 'paragraph' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await service.syncPageTransclusions(pageId, pm);
|
|
||||||
|
|
||||||
expect(result).toEqual({ inserted: 1, updated: 0, deleted: 0 });
|
|
||||||
expect(repo.insert).toHaveBeenCalledTimes(1);
|
|
||||||
expect(repo.insert).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
pageId,
|
|
||||||
transclusionId: 'a',
|
|
||||||
name: 'Hello',
|
|
||||||
}),
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
expect(repo.update).not.toHaveBeenCalled();
|
|
||||||
expect(repo.deleteByPageAndTransclusionIds).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates transclusions whose name or content changed', async () => {
|
|
||||||
repo.findByPageId.mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: 'row1',
|
|
||||||
pageId,
|
|
||||||
transclusionId: 'a',
|
|
||||||
name: 'Old',
|
|
||||||
content: { type: 'doc', content: [{ type: 'paragraph' }] },
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
} as any,
|
|
||||||
]);
|
|
||||||
const pm = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusion',
|
|
||||||
attrs: { id: 'a', name: 'New' },
|
|
||||||
content: [
|
|
||||||
{ type: 'paragraph', content: [{ type: 'text', text: 'X' }] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await service.syncPageTransclusions(pageId, pm);
|
|
||||||
|
|
||||||
expect(result).toEqual({ inserted: 0, updated: 1, deleted: 0 });
|
|
||||||
expect(repo.update).toHaveBeenCalledWith(
|
|
||||||
pageId,
|
|
||||||
'a',
|
|
||||||
expect.objectContaining({ name: 'New' }),
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skips update when name and content are unchanged', async () => {
|
|
||||||
const sameContent = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [{ type: 'paragraph' }],
|
|
||||||
};
|
|
||||||
repo.findByPageId.mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: 'row1',
|
|
||||||
pageId,
|
|
||||||
transclusionId: 'a',
|
|
||||||
name: 'Same',
|
|
||||||
content: sameContent,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
} as any,
|
|
||||||
]);
|
|
||||||
const pm = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusion',
|
|
||||||
attrs: { id: 'a', name: 'Same' },
|
|
||||||
content: sameContent.content,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await service.syncPageTransclusions(pageId, pm);
|
|
||||||
|
|
||||||
expect(result).toEqual({ inserted: 0, updated: 0, deleted: 0 });
|
|
||||||
expect(repo.update).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deletes transclusions that no longer appear in the doc', async () => {
|
|
||||||
repo.findByPageId.mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: 'r',
|
|
||||||
pageId,
|
|
||||||
transclusionId: 'gone',
|
|
||||||
name: null,
|
|
||||||
content: { type: 'doc', content: [] },
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
} as any,
|
|
||||||
]);
|
|
||||||
const pm = { type: 'doc', content: [{ type: 'paragraph' }] };
|
|
||||||
|
|
||||||
const result = await service.syncPageTransclusions(pageId, pm);
|
|
||||||
|
|
||||||
expect(result).toEqual({ inserted: 0, updated: 0, deleted: 1 });
|
|
||||||
expect(repo.deleteByPageAndTransclusionIds).toHaveBeenCalledWith(
|
|
||||||
pageId,
|
|
||||||
['gone'],
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles empty doc → noop', async () => {
|
|
||||||
repo.findByPageId.mockResolvedValue([]);
|
|
||||||
const result = await service.syncPageTransclusions(pageId, null);
|
|
||||||
expect(result).toEqual({ inserted: 0, updated: 0, deleted: 0 });
|
|
||||||
expect(repo.insert).not.toHaveBeenCalled();
|
|
||||||
expect(repo.update).not.toHaveBeenCalled();
|
|
||||||
expect(repo.deleteByPageAndTransclusionIds).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes through the trx parameter to repo calls', async () => {
|
|
||||||
repo.findByPageId.mockResolvedValue([]);
|
|
||||||
const trx = { mock: 'trx' } as any;
|
|
||||||
const pm = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{ type: 'transclusion', attrs: { id: 'a' }, content: [{ type: 'paragraph' }] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
await service.syncPageTransclusions(pageId, pm, trx);
|
|
||||||
|
|
||||||
expect(repo.findByPageId).toHaveBeenCalledWith(pageId, trx);
|
|
||||||
expect(repo.insert).toHaveBeenCalledWith(expect.anything(), trx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('TransclusionService.syncPageReferences', () => {
|
|
||||||
let service: TransclusionService;
|
|
||||||
let refRepo: jest.Mocked<PageTransclusionReferencesRepo>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const mockTransclusionsRepo: Partial<PageTransclusionsRepo> = {};
|
|
||||||
const mockRefRepo: jest.Mocked<Partial<PageTransclusionReferencesRepo>> = {
|
|
||||||
findByReferencePageId: jest.fn(),
|
|
||||||
insertMany: jest.fn(),
|
|
||||||
deleteByReferenceAndKeys: jest.fn(),
|
|
||||||
findCyclicEdgesForSource: jest.fn().mockResolvedValue([]),
|
|
||||||
deleteByIds: jest.fn(),
|
|
||||||
};
|
|
||||||
const module = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
TransclusionService,
|
|
||||||
{ provide: PageTransclusionsRepo, useValue: mockTransclusionsRepo },
|
|
||||||
{ provide: PageTransclusionReferencesRepo, useValue: mockRefRepo },
|
|
||||||
{ provide: PageRepo, useValue: {} },
|
|
||||||
{ provide: PagePermissionRepo, useValue: {} },
|
|
||||||
{ provide: AttachmentRepo, useValue: {} },
|
|
||||||
{ provide: StorageService, useValue: {} },
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
service = module.get(TransclusionService);
|
|
||||||
refRepo = module.get(PageTransclusionReferencesRepo);
|
|
||||||
});
|
|
||||||
|
|
||||||
const referencePageId = '00000000-0000-0000-0000-000000000001';
|
|
||||||
|
|
||||||
it('inserts new loose references, no deletes when none existed', async () => {
|
|
||||||
refRepo.findByReferencePageId.mockResolvedValue([]);
|
|
||||||
const pm = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p2', transclusionId: 'e2' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await service.syncPageReferences(referencePageId, pm);
|
|
||||||
|
|
||||||
expect(result).toEqual({ inserted: 2, deleted: 0 });
|
|
||||||
expect(refRepo.insertMany).toHaveBeenCalledWith(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
referencePageId,
|
|
||||||
containingTransclusionId: null,
|
|
||||||
sourcePageId: 'p1',
|
|
||||||
transclusionId: 'e1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
referencePageId,
|
|
||||||
containingTransclusionId: null,
|
|
||||||
sourcePageId: 'p2',
|
|
||||||
transclusionId: 'e2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
expect(refRepo.deleteByReferenceAndKeys).not.toHaveBeenCalled();
|
|
||||||
// Loose references never seed cycle detection.
|
|
||||||
expect(refRepo.findCyclicEdgesForSource).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('records the containing transclusion when references nest in a source', async () => {
|
|
||||||
refRepo.findByReferencePageId.mockResolvedValue([]);
|
|
||||||
const pm = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusion',
|
|
||||||
attrs: { id: 's1' },
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p2', transclusionId: 'e2' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await service.syncPageReferences(referencePageId, pm);
|
|
||||||
|
|
||||||
expect(result).toEqual({ inserted: 1, deleted: 0 });
|
|
||||||
expect(refRepo.insertMany).toHaveBeenCalledWith(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
referencePageId,
|
|
||||||
containingTransclusionId: 's1',
|
|
||||||
sourcePageId: 'p2',
|
|
||||||
transclusionId: 'e2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
expect(refRepo.findCyclicEdgesForSource).toHaveBeenCalledWith(
|
|
||||||
'p2',
|
|
||||||
'e2',
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deletes edges that close a cycle and excludes them from the inserted count', async () => {
|
|
||||||
refRepo.findByReferencePageId.mockResolvedValue([]);
|
|
||||||
refRepo.findCyclicEdgesForSource.mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: 'closing-edge-id',
|
|
||||||
referencePageId,
|
|
||||||
containingTransclusionId: 's1',
|
|
||||||
sourcePageId: 'p2',
|
|
||||||
transclusionId: 'e2',
|
|
||||||
createdAt: new Date(),
|
|
||||||
} as any,
|
|
||||||
]);
|
|
||||||
const pm = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusion',
|
|
||||||
attrs: { id: 's1' },
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p2', transclusionId: 'e2' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await service.syncPageReferences(referencePageId, pm);
|
|
||||||
|
|
||||||
expect(result).toEqual({ inserted: 0, deleted: 0 });
|
|
||||||
expect(refRepo.deleteByIds).toHaveBeenCalledWith(
|
|
||||||
['closing-edge-id'],
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deletes references that no longer appear', async () => {
|
|
||||||
refRepo.findByReferencePageId.mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: 'r1',
|
|
||||||
referencePageId,
|
|
||||||
containingTransclusionId: null,
|
|
||||||
sourcePageId: 'p1',
|
|
||||||
transclusionId: 'e1',
|
|
||||||
createdAt: new Date(),
|
|
||||||
} as any,
|
|
||||||
]);
|
|
||||||
const pm = { type: 'doc', content: [{ type: 'paragraph' }] };
|
|
||||||
|
|
||||||
const result = await service.syncPageReferences(referencePageId, pm);
|
|
||||||
|
|
||||||
expect(result).toEqual({ inserted: 0, deleted: 1 });
|
|
||||||
expect(refRepo.deleteByReferenceAndKeys).toHaveBeenCalledWith(
|
|
||||||
referencePageId,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
containingTransclusionId: null,
|
|
||||||
sourcePageId: 'p1',
|
|
||||||
transclusionId: 'e1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
expect(refRepo.insertMany).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is a no-op when desired matches existing exactly', async () => {
|
|
||||||
refRepo.findByReferencePageId.mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: 'r',
|
|
||||||
referencePageId,
|
|
||||||
containingTransclusionId: null,
|
|
||||||
sourcePageId: 'p1',
|
|
||||||
transclusionId: 'e1',
|
|
||||||
createdAt: new Date(),
|
|
||||||
} as any,
|
|
||||||
]);
|
|
||||||
const pm = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await service.syncPageReferences(referencePageId, pm);
|
|
||||||
|
|
||||||
expect(result).toEqual({ inserted: 0, deleted: 0 });
|
|
||||||
expect(refRepo.insertMany).not.toHaveBeenCalled();
|
|
||||||
expect(refRepo.deleteByReferenceAndKeys).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes through trx parameter to repo calls', async () => {
|
|
||||||
refRepo.findByReferencePageId.mockResolvedValue([]);
|
|
||||||
const trx = { mock: 'trx' } as any;
|
|
||||||
const pm = {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'transclusionReference',
|
|
||||||
attrs: { sourcePageId: 'p1', transclusionId: 'e1' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
await service.syncPageReferences(referencePageId, pm, trx);
|
|
||||||
|
|
||||||
expect(refRepo.findByReferencePageId).toHaveBeenCalledWith(
|
|
||||||
referencePageId,
|
|
||||||
trx,
|
|
||||||
);
|
|
||||||
expect(refRepo.insertMany).toHaveBeenCalledWith(expect.anything(), trx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import {
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
HttpCode,
|
|
||||||
HttpStatus,
|
|
||||||
Post,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { JwtAuthGuard } from '../../../common/guards/jwt-auth.guard';
|
|
||||||
import { AuthUser } from '../../../common/decorators/auth-user.decorator';
|
|
||||||
import { User } from '@docmost/db/types/entity.types';
|
|
||||||
import { TransclusionService } from './transclusion.service';
|
|
||||||
import { LookupDto } from './dto/lookup.dto';
|
|
||||||
import { ReferencesDto } from './dto/references.dto';
|
|
||||||
import { UnsyncReferenceDto } from './dto/unsync-reference.dto';
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Controller('pages/transclusion')
|
|
||||||
export class TransclusionController {
|
|
||||||
constructor(private readonly transclusionService: TransclusionService) {}
|
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@Post('lookup')
|
|
||||||
async lookup(@Body() dto: LookupDto, @AuthUser() user: User) {
|
|
||||||
return this.transclusionService.lookup(
|
|
||||||
dto.references,
|
|
||||||
user?.id ?? null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@Post('references')
|
|
||||||
async references(
|
|
||||||
@Body() dto: ReferencesDto,
|
|
||||||
@AuthUser() user: User,
|
|
||||||
) {
|
|
||||||
return this.transclusionService.listReferences({
|
|
||||||
sourcePageId: dto.sourcePageId,
|
|
||||||
transclusionId: dto.transclusionId,
|
|
||||||
viewerUserId: user.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@Post('unsync-reference')
|
|
||||||
async unsyncReference(
|
|
||||||
@Body() dto: UnsyncReferenceDto,
|
|
||||||
@AuthUser() user: User,
|
|
||||||
) {
|
|
||||||
return this.transclusionService.unsyncReference(
|
|
||||||
dto.referencePageId,
|
|
||||||
dto.sourcePageId,
|
|
||||||
dto.transclusionId,
|
|
||||||
user.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TransclusionController } from './transclusion.controller';
|
|
||||||
import { TransclusionService } from './transclusion.service';
|
|
||||||
import { StorageModule } from '../../../integrations/storage/storage.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [StorageModule],
|
|
||||||
controllers: [TransclusionController],
|
|
||||||
providers: [TransclusionService],
|
|
||||||
exports: [TransclusionService],
|
|
||||||
})
|
|
||||||
export class TransclusionModule {}
|
|
||||||
@@ -1,526 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
Logger,
|
|
||||||
ForbiddenException,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { isDeepStrictEqual } from 'node:util';
|
|
||||||
import { v7 as uuid7 } from 'uuid';
|
|
||||||
import { KyselyTransaction } from '@docmost/db/types/kysely.types';
|
|
||||||
import { PageTransclusionsRepo } from '@docmost/db/repos/page-transclusions/page-transclusions.repo';
|
|
||||||
import { PageTransclusionReferencesRepo } from '@docmost/db/repos/page-transclusion-references/page-transclusion-references.repo';
|
|
||||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
|
||||||
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
|
||||||
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
|
|
||||||
import { StorageService } from '../../../integrations/storage/storage.service';
|
|
||||||
import {
|
|
||||||
collectReferencesFromPmJson,
|
|
||||||
collectTransclusionsFromPmJson,
|
|
||||||
} from './utils/transclusion-prosemirror.util';
|
|
||||||
import { rewriteAttachmentsForUnsync } from './utils/transclusion-unsync.util';
|
|
||||||
import { TransclusionLookup } from './transclusion.types';
|
|
||||||
import { Page } from '@docmost/db/types/entity.types';
|
|
||||||
|
|
||||||
type ReferencingPageInfo = {
|
|
||||||
id: string;
|
|
||||||
slugId: string;
|
|
||||||
title: string | null;
|
|
||||||
icon: string | null;
|
|
||||||
spaceId: string;
|
|
||||||
spaceSlug: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TransclusionService {
|
|
||||||
private readonly logger = new Logger(TransclusionService.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly pageTransclusionsRepo: PageTransclusionsRepo,
|
|
||||||
private readonly pageTransclusionReferencesRepo: PageTransclusionReferencesRepo,
|
|
||||||
private readonly pageRepo: PageRepo,
|
|
||||||
private readonly pagePermissionRepo: PagePermissionRepo,
|
|
||||||
private readonly attachmentRepo: AttachmentRepo,
|
|
||||||
private readonly storageService: StorageService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async syncPageTransclusions(
|
|
||||||
pageId: string,
|
|
||||||
pmJson: unknown,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<{ inserted: number; updated: number; deleted: number }> {
|
|
||||||
const desired = collectTransclusionsFromPmJson(pmJson);
|
|
||||||
const desiredById = new Map(desired.map((d) => [d.transclusionId, d]));
|
|
||||||
|
|
||||||
const existing = await this.pageTransclusionsRepo.findByPageId(pageId, trx);
|
|
||||||
const existingById = new Map(existing.map((e) => [e.transclusionId, e]));
|
|
||||||
|
|
||||||
let inserted = 0;
|
|
||||||
let updated = 0;
|
|
||||||
let deleted = 0;
|
|
||||||
|
|
||||||
for (const d of desired) {
|
|
||||||
const prev = existingById.get(d.transclusionId);
|
|
||||||
if (!prev) {
|
|
||||||
await this.pageTransclusionsRepo.insert(
|
|
||||||
{
|
|
||||||
pageId,
|
|
||||||
transclusionId: d.transclusionId,
|
|
||||||
name: d.name,
|
|
||||||
content: d.content as any,
|
|
||||||
},
|
|
||||||
trx,
|
|
||||||
);
|
|
||||||
inserted += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameChanged = prev.name !== d.name;
|
|
||||||
const contentChanged = !isDeepStrictEqual(prev.content, d.content);
|
|
||||||
if (nameChanged || contentChanged) {
|
|
||||||
await this.pageTransclusionsRepo.update(
|
|
||||||
pageId,
|
|
||||||
d.transclusionId,
|
|
||||||
{ name: d.name, content: d.content as any },
|
|
||||||
trx,
|
|
||||||
);
|
|
||||||
updated += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removedIds = existing
|
|
||||||
.filter((e) => !desiredById.has(e.transclusionId))
|
|
||||||
.map((e) => e.transclusionId);
|
|
||||||
if (removedIds.length > 0) {
|
|
||||||
await this.pageTransclusionsRepo.deleteByPageAndTransclusionIds(
|
|
||||||
pageId,
|
|
||||||
removedIds,
|
|
||||||
trx,
|
|
||||||
);
|
|
||||||
deleted = removedIds.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { inserted, updated, deleted };
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncPageReferences(
|
|
||||||
referencePageId: string,
|
|
||||||
pmJson: unknown,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<{ inserted: number; deleted: number }> {
|
|
||||||
const desired = collectReferencesFromPmJson(pmJson);
|
|
||||||
const keyOf = (s: {
|
|
||||||
containingTransclusionId: string | null;
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
}) =>
|
|
||||||
`${s.containingTransclusionId ?? ''}::${s.sourcePageId}::${s.transclusionId}`;
|
|
||||||
const desiredKeys = new Set(desired.map(keyOf));
|
|
||||||
|
|
||||||
const existing = await this.pageTransclusionReferencesRepo.findByReferencePageId(
|
|
||||||
referencePageId,
|
|
||||||
trx,
|
|
||||||
);
|
|
||||||
const existingKeys = new Set(existing.map(keyOf));
|
|
||||||
|
|
||||||
const toInsert = desired
|
|
||||||
.filter((d) => !existingKeys.has(keyOf(d)))
|
|
||||||
.map((d) => ({
|
|
||||||
referencePageId,
|
|
||||||
containingTransclusionId: d.containingTransclusionId,
|
|
||||||
sourcePageId: d.sourcePageId,
|
|
||||||
transclusionId: d.transclusionId,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const toDelete = existing
|
|
||||||
.filter((e) => !desiredKeys.has(keyOf(e)))
|
|
||||||
.map((e) => ({
|
|
||||||
containingTransclusionId: e.containingTransclusionId,
|
|
||||||
sourcePageId: e.sourcePageId,
|
|
||||||
transclusionId: e.transclusionId,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (toInsert.length > 0) {
|
|
||||||
await this.pageTransclusionReferencesRepo.insertMany(toInsert, trx);
|
|
||||||
}
|
|
||||||
if (toDelete.length > 0) {
|
|
||||||
await this.pageTransclusionReferencesRepo.deleteByReferenceAndKeys(
|
|
||||||
referencePageId,
|
|
||||||
toDelete,
|
|
||||||
trx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const removedCount = await this.removeCyclicEdgesIntroducedBy(
|
|
||||||
toInsert,
|
|
||||||
trx,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
inserted: toInsert.length - removedCount,
|
|
||||||
deleted: toDelete.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run cycle detection rooted at each newly-introduced edge's target and
|
|
||||||
* delete any closing edge that belongs to a cycle. Lookups for those rows
|
|
||||||
* then return `not_found`, which the editor renders as the cycle-aware
|
|
||||||
* placeholder. Returns the count of rows removed.
|
|
||||||
*/
|
|
||||||
private async removeCyclicEdgesIntroducedBy(
|
|
||||||
candidates: ReadonlyArray<{
|
|
||||||
referencePageId: string;
|
|
||||||
containingTransclusionId: string | null;
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
}>,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<number> {
|
|
||||||
const seedKeys = new Set<string>();
|
|
||||||
const seeds: Array<{ sourcePageId: string; transclusionId: string }> = [];
|
|
||||||
for (const c of candidates) {
|
|
||||||
if (c.containingTransclusionId === null) continue;
|
|
||||||
const key = `${c.sourcePageId}::${c.transclusionId}`;
|
|
||||||
if (seedKeys.has(key)) continue;
|
|
||||||
seedKeys.add(key);
|
|
||||||
seeds.push({
|
|
||||||
sourcePageId: c.sourcePageId,
|
|
||||||
transclusionId: c.transclusionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (seeds.length === 0) return 0;
|
|
||||||
|
|
||||||
const offendingIds = new Set<string>();
|
|
||||||
for (const seed of seeds) {
|
|
||||||
const cyclicEdges =
|
|
||||||
await this.pageTransclusionReferencesRepo.findCyclicEdgesForSource(
|
|
||||||
seed.sourcePageId,
|
|
||||||
seed.transclusionId,
|
|
||||||
trx,
|
|
||||||
);
|
|
||||||
for (const edge of cyclicEdges) offendingIds.add(edge.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offendingIds.size === 0) return 0;
|
|
||||||
|
|
||||||
await this.pageTransclusionReferencesRepo.deleteByIds(
|
|
||||||
Array.from(offendingIds),
|
|
||||||
trx,
|
|
||||||
);
|
|
||||||
return offendingIds.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract transclusions from each page's PM JSON and bulk-insert into
|
|
||||||
* `page_transclusions` in a single statement. Intended for brand-new pages
|
|
||||||
* (e.g. duplication, import) where there is nothing to diff against.
|
|
||||||
*/
|
|
||||||
async insertTransclusionsForPages(
|
|
||||||
pages: Array<{ id: string; content: unknown }>,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<{ inserted: number }> {
|
|
||||||
const rows: Parameters<PageTransclusionsRepo['insertMany']>[0] = [];
|
|
||||||
for (const page of pages) {
|
|
||||||
const snapshots = collectTransclusionsFromPmJson(page.content);
|
|
||||||
for (const s of snapshots) {
|
|
||||||
rows.push({
|
|
||||||
pageId: page.id,
|
|
||||||
transclusionId: s.transclusionId,
|
|
||||||
name: s.name,
|
|
||||||
content: s.content as any,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rows.length === 0) return { inserted: 0 };
|
|
||||||
await this.pageTransclusionsRepo.insertMany(rows, trx);
|
|
||||||
return { inserted: rows.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Walk each page's PM JSON for `transclusionReference` nodes and bulk-insert
|
|
||||||
* one row per `(containing, source, target)` triple. For brand-new pages
|
|
||||||
* (duplication, import) where there is nothing to diff against.
|
|
||||||
*
|
|
||||||
* Cycle detection runs once per distinct seed source after the bulk insert;
|
|
||||||
* any closing edges are removed so lookups return `not_found` and the
|
|
||||||
* editor renders the cycle-aware placeholder.
|
|
||||||
*/
|
|
||||||
async insertReferencesForPages(
|
|
||||||
pages: Array<{ id: string; content: unknown }>,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<{ inserted: number }> {
|
|
||||||
const rows: Array<{
|
|
||||||
referencePageId: string;
|
|
||||||
containingTransclusionId: string | null;
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
}> = [];
|
|
||||||
for (const page of pages) {
|
|
||||||
const refs = collectReferencesFromPmJson(page.content);
|
|
||||||
for (const r of refs) {
|
|
||||||
rows.push({
|
|
||||||
referencePageId: page.id,
|
|
||||||
containingTransclusionId: r.containingTransclusionId,
|
|
||||||
sourcePageId: r.sourcePageId,
|
|
||||||
transclusionId: r.transclusionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rows.length === 0) return { inserted: 0 };
|
|
||||||
await this.pageTransclusionReferencesRepo.insertMany(rows, trx);
|
|
||||||
|
|
||||||
const removedCount = await this.removeCyclicEdgesIntroducedBy(rows, trx);
|
|
||||||
return { inserted: rows.length - removedCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
async lookup(
|
|
||||||
references: Array<{ sourcePageId: string; transclusionId: string }>,
|
|
||||||
viewerUserId: string | null,
|
|
||||||
): Promise<{ items: TransclusionLookup[] }> {
|
|
||||||
if (references.length === 0) return { items: [] };
|
|
||||||
|
|
||||||
const items: TransclusionLookup[] = new Array(references.length).fill(null);
|
|
||||||
const pendingIdx = references.map((_, i) => i);
|
|
||||||
|
|
||||||
// 1) permission filter on the candidate pageIds (auth users only;
|
|
||||||
// unauthenticated share viewers get no_access for any private page).
|
|
||||||
const candidatePageIds = Array.from(
|
|
||||||
new Set(pendingIdx.map((i) => references[i].sourcePageId)),
|
|
||||||
);
|
|
||||||
const accessibleSet = viewerUserId
|
|
||||||
? new Set(
|
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
|
||||||
pageIds: candidatePageIds,
|
|
||||||
userId: viewerUserId,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
: new Set<string>();
|
|
||||||
|
|
||||||
// 2) one DB hit for all (page_id, transclusion_id) keys still pending and accessible
|
|
||||||
const accessiblePending = pendingIdx.filter((i) =>
|
|
||||||
accessibleSet.has(references[i].sourcePageId),
|
|
||||||
);
|
|
||||||
const rows = await this.pageTransclusionsRepo.findManyByPageAndTransclusion(
|
|
||||||
accessiblePending.map((i) => ({
|
|
||||||
pageId: references[i].sourcePageId,
|
|
||||||
transclusionId: references[i].transclusionId,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
const rowKey = (r: { pageId: string; transclusionId: string }) =>
|
|
||||||
`${r.pageId}::${r.transclusionId}`;
|
|
||||||
const rowMap = new Map(rows.map((r) => [rowKey(r), r]));
|
|
||||||
|
|
||||||
// 3) pull updatedAt from each accessible page so we can return
|
|
||||||
// sourceUpdatedAt on each successful result.
|
|
||||||
const accessiblePageIds = Array.from(
|
|
||||||
new Set(accessiblePending.map((i) => references[i].sourcePageId)),
|
|
||||||
);
|
|
||||||
const pageMeta = new Map<string, Date>();
|
|
||||||
for (const pid of accessiblePageIds) {
|
|
||||||
const p = await this.pageRepo.findById(pid);
|
|
||||||
if (p && !p.deletedAt) pageMeta.set(p.id, p.updatedAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) stitch the results
|
|
||||||
for (const i of pendingIdx) {
|
|
||||||
const ref = references[i];
|
|
||||||
if (!accessibleSet.has(ref.sourcePageId)) {
|
|
||||||
items[i] = {
|
|
||||||
sourcePageId: ref.sourcePageId,
|
|
||||||
transclusionId: ref.transclusionId,
|
|
||||||
status: 'no_access',
|
|
||||||
};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const updatedAt = pageMeta.get(ref.sourcePageId);
|
|
||||||
if (!updatedAt) {
|
|
||||||
items[i] = {
|
|
||||||
sourcePageId: ref.sourcePageId,
|
|
||||||
transclusionId: ref.transclusionId,
|
|
||||||
status: 'not_found',
|
|
||||||
};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = rowMap.get(`${ref.sourcePageId}::${ref.transclusionId}`);
|
|
||||||
if (!row) {
|
|
||||||
items[i] = {
|
|
||||||
sourcePageId: ref.sourcePageId,
|
|
||||||
transclusionId: ref.transclusionId,
|
|
||||||
status: 'not_found',
|
|
||||||
};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
items[i] = {
|
|
||||||
sourcePageId: ref.sourcePageId,
|
|
||||||
transclusionId: ref.transclusionId,
|
|
||||||
content: row.content,
|
|
||||||
sourceUpdatedAt: updatedAt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { items };
|
|
||||||
}
|
|
||||||
|
|
||||||
async listReferences(opts: {
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
viewerUserId: string;
|
|
||||||
}): Promise<{
|
|
||||||
source: ReferencingPageInfo | null;
|
|
||||||
references: ReferencingPageInfo[];
|
|
||||||
}> {
|
|
||||||
const { sourcePageId, transclusionId, viewerUserId } = opts;
|
|
||||||
|
|
||||||
const referencePageIds =
|
|
||||||
await this.pageTransclusionReferencesRepo.findReferencePageIdsByTransclusion(
|
|
||||||
sourcePageId,
|
|
||||||
transclusionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const candidatePageIds = Array.from(
|
|
||||||
new Set([sourcePageId, ...referencePageIds]),
|
|
||||||
);
|
|
||||||
const accessibleSet = new Set(
|
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
|
||||||
pageIds: candidatePageIds,
|
|
||||||
userId: viewerUserId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const accessibleIds = candidatePageIds.filter((id) =>
|
|
||||||
accessibleSet.has(id),
|
|
||||||
);
|
|
||||||
if (accessibleIds.length === 0) {
|
|
||||||
return { source: null, references: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await Promise.all(
|
|
||||||
accessibleIds.map((id) =>
|
|
||||||
this.pageRepo.findById(id, { includeSpace: true }),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const byId = new Map<string, ReferencingPageInfo>();
|
|
||||||
for (const p of rows) {
|
|
||||||
if (!p || p.deletedAt) continue;
|
|
||||||
const space = (p as Page & { space?: { slug?: string } }).space;
|
|
||||||
byId.set(p.id, {
|
|
||||||
id: p.id,
|
|
||||||
slugId: p.slugId,
|
|
||||||
title: p.title ?? null,
|
|
||||||
icon: p.icon ?? null,
|
|
||||||
spaceId: p.spaceId,
|
|
||||||
spaceSlug: space?.slug ?? null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = byId.get(sourcePageId) ?? null;
|
|
||||||
const references = referencePageIds
|
|
||||||
.map((id) => byId.get(id))
|
|
||||||
.filter((p): p is ReferencingPageInfo => Boolean(p));
|
|
||||||
|
|
||||||
return { source, references };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a `transclusionReference` into a self-contained copy on the
|
|
||||||
* reference page: load source content, generate fresh attachment ids, copy storage
|
|
||||||
* files, insert new attachment rows, return rewritten content. The caller
|
|
||||||
* (controller) returns the content blob to the client which then performs
|
|
||||||
* `editor.commands.insertContentAt(range, content)` to replace the
|
|
||||||
* reference node. The next Yjs save naturally cleans up the
|
|
||||||
* page_transclusion_references row, but we also delete it eagerly here so a
|
|
||||||
* crash between server response and client save doesn't leave a stale row.
|
|
||||||
*/
|
|
||||||
async unsyncReference(
|
|
||||||
referencePageId: string,
|
|
||||||
sourcePageId: string,
|
|
||||||
transclusionId: string,
|
|
||||||
viewerUserId: string,
|
|
||||||
): Promise<{ content: unknown }> {
|
|
||||||
const referencePage = await this.pageRepo.findById(referencePageId);
|
|
||||||
if (!referencePage || referencePage.deletedAt) {
|
|
||||||
throw new NotFoundException('Reference page not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourcePage = await this.pageRepo.findById(sourcePageId);
|
|
||||||
if (!sourcePage || sourcePage.deletedAt) {
|
|
||||||
throw new NotFoundException('Source page not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessible = new Set(
|
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
|
||||||
pageIds: [referencePageId, sourcePageId],
|
|
||||||
userId: viewerUserId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (!accessible.has(referencePageId) || !accessible.has(sourcePageId)) {
|
|
||||||
throw new ForbiddenException();
|
|
||||||
}
|
|
||||||
|
|
||||||
const transclusion =
|
|
||||||
await this.pageTransclusionsRepo.findByPageAndTransclusion(
|
|
||||||
sourcePageId,
|
|
||||||
transclusionId,
|
|
||||||
);
|
|
||||||
if (!transclusion) {
|
|
||||||
throw new NotFoundException('Sync block not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { content, copies } = rewriteAttachmentsForUnsync(
|
|
||||||
transclusion.content,
|
|
||||||
() => uuid7(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (copies.length > 0) {
|
|
||||||
const oldIds = copies.map((c) => c.oldAttachmentId);
|
|
||||||
const oldRows = await this.attachmentRepo.findBySpaceId(sourcePage.spaceId);
|
|
||||||
const byOldId = new Map(
|
|
||||||
oldRows
|
|
||||||
.filter(
|
|
||||||
(a) => oldIds.includes(a.id) && a.pageId === sourcePageId,
|
|
||||||
)
|
|
||||||
.map((a) => [a.id, a]),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const plan of copies) {
|
|
||||||
const old = byOldId.get(plan.oldAttachmentId);
|
|
||||||
if (!old) continue;
|
|
||||||
|
|
||||||
const newFilePath = old.filePath
|
|
||||||
.split(plan.oldAttachmentId)
|
|
||||||
.join(plan.newAttachmentId);
|
|
||||||
try {
|
|
||||||
await this.storageService.copy(old.filePath, newFilePath);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(
|
|
||||||
`unsync: failed to copy attachment ${old.id}`,
|
|
||||||
err as Error,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await this.attachmentRepo.insertAttachment({
|
|
||||||
id: plan.newAttachmentId,
|
|
||||||
type: old.type,
|
|
||||||
filePath: newFilePath,
|
|
||||||
fileName: old.fileName,
|
|
||||||
fileSize: old.fileSize,
|
|
||||||
mimeType: old.mimeType,
|
|
||||||
fileExt: old.fileExt,
|
|
||||||
creatorId: viewerUserId,
|
|
||||||
workspaceId: referencePage.workspaceId,
|
|
||||||
pageId: referencePageId,
|
|
||||||
spaceId: referencePage.spaceId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.pageTransclusionReferencesRepo.deleteOne(
|
|
||||||
referencePageId,
|
|
||||||
sourcePageId,
|
|
||||||
transclusionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { content };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
export type TransclusionLookup =
|
|
||||||
| {
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
content: unknown;
|
|
||||||
sourceUpdatedAt: Date;
|
|
||||||
}
|
|
||||||
| { sourcePageId: string; transclusionId: string; status: 'not_found' }
|
|
||||||
| { sourcePageId: string; transclusionId: string; status: 'no_access' };
|
|
||||||
|
|
||||||
export type TransclusionNodeSnapshot = {
|
|
||||||
transclusionId: string;
|
|
||||||
name: string | null;
|
|
||||||
content: unknown;
|
|
||||||
};
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import { TransclusionNodeSnapshot } from '../transclusion.types';
|
|
||||||
|
|
||||||
const TRANSCLUSION_TYPE = 'transclusion';
|
|
||||||
const REFERENCE_TYPE = 'transclusionReference';
|
|
||||||
|
|
||||||
export type TransclusionReferenceSnapshot = {
|
|
||||||
/**
|
|
||||||
* Id of the `transclusion` (source) node whose content holds this reference,
|
|
||||||
* or `null` if the reference is loose on the page (not nested inside a source).
|
|
||||||
* Used by the cycle-detection CTE to walk source-to-source edges.
|
|
||||||
*/
|
|
||||||
containingTransclusionId: string | null;
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Walks a ProseMirror JSON document and returns one snapshot per top-level
|
|
||||||
* `transclusion` node. Does not recurse into transclusions (schema disallows
|
|
||||||
* nesting). Skips transclusion nodes without an id (transient state). When
|
|
||||||
* duplicate ids are encountered, the later occurrence wins so the result is
|
|
||||||
* deterministic.
|
|
||||||
*/
|
|
||||||
export function collectTransclusionsFromPmJson(
|
|
||||||
doc: unknown,
|
|
||||||
): TransclusionNodeSnapshot[] {
|
|
||||||
if (!doc || typeof doc !== 'object') return [];
|
|
||||||
|
|
||||||
const byId = new Map<string, TransclusionNodeSnapshot>();
|
|
||||||
|
|
||||||
const visit = (node: any): void => {
|
|
||||||
if (!node || typeof node !== 'object') return;
|
|
||||||
|
|
||||||
if (node.type === TRANSCLUSION_TYPE) {
|
|
||||||
const id = node.attrs?.id;
|
|
||||||
if (typeof id === 'string' && id.length > 0) {
|
|
||||||
const name =
|
|
||||||
typeof node.attrs?.name === 'string' && node.attrs.name.length > 0
|
|
||||||
? node.attrs.name
|
|
||||||
: null;
|
|
||||||
byId.set(id, {
|
|
||||||
transclusionId: id,
|
|
||||||
name,
|
|
||||||
content: { type: 'doc', content: node.content ?? [] },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return; // do not recurse into transclusion children
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(node.content)) {
|
|
||||||
for (const child of node.content) visit(child);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
visit(doc);
|
|
||||||
return Array.from(byId.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Walks a ProseMirror JSON document and returns one snapshot per unique
|
|
||||||
* `(containingTransclusionId, sourcePageId, transclusionId)` triple found on
|
|
||||||
* `transclusionReference` nodes. Recurses into every container, including
|
|
||||||
* `transclusion` (a source node may contain a reference to another source).
|
|
||||||
* Order preserved by first-seen.
|
|
||||||
*/
|
|
||||||
export function collectReferencesFromPmJson(
|
|
||||||
doc: unknown,
|
|
||||||
): TransclusionReferenceSnapshot[] {
|
|
||||||
if (!doc || typeof doc !== 'object') return [];
|
|
||||||
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const out: TransclusionReferenceSnapshot[] = [];
|
|
||||||
|
|
||||||
const visit = (node: any, containingTransclusionId: string | null): void => {
|
|
||||||
if (!node || typeof node !== 'object') return;
|
|
||||||
|
|
||||||
if (node.type === REFERENCE_TYPE) {
|
|
||||||
const sourcePageId = node.attrs?.sourcePageId;
|
|
||||||
const transclusionId = node.attrs?.transclusionId;
|
|
||||||
if (
|
|
||||||
typeof sourcePageId === 'string' &&
|
|
||||||
sourcePageId.length > 0 &&
|
|
||||||
typeof transclusionId === 'string' &&
|
|
||||||
transclusionId.length > 0
|
|
||||||
) {
|
|
||||||
const key = `${containingTransclusionId ?? ''}::${sourcePageId}::${transclusionId}`;
|
|
||||||
if (!seen.has(key)) {
|
|
||||||
seen.add(key);
|
|
||||||
out.push({
|
|
||||||
containingTransclusionId,
|
|
||||||
sourcePageId,
|
|
||||||
transclusionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return; // atom node - no children
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextContainer =
|
|
||||||
node.type === TRANSCLUSION_TYPE && typeof node.attrs?.id === 'string'
|
|
||||||
? node.attrs.id
|
|
||||||
: containingTransclusionId;
|
|
||||||
|
|
||||||
if (Array.isArray(node.content)) {
|
|
||||||
for (const child of node.content) visit(child, nextContainer);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
visit(doc, null);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { isAttachmentNode } from '../../../../common/helpers/prosemirror/utils';
|
|
||||||
|
|
||||||
export type AttachmentRewritePlan = {
|
|
||||||
oldAttachmentId: string;
|
|
||||||
newAttachmentId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RewriteResult = {
|
|
||||||
content: unknown;
|
|
||||||
copies: AttachmentRewritePlan[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Walk a ProseMirror JSON tree, rewrite every attachment-like node so its
|
|
||||||
* `attachmentId` (and any `src` substring matching that id) point at a fresh
|
|
||||||
* id. Each unique old id maps to exactly one new id; the caller is responsible
|
|
||||||
* for actually copying the underlying storage file.
|
|
||||||
*
|
|
||||||
* Pure: does not mutate the input. Returns a deep clone.
|
|
||||||
*/
|
|
||||||
export function rewriteAttachmentsForUnsync(
|
|
||||||
content: unknown,
|
|
||||||
generateId: () => string,
|
|
||||||
): RewriteResult {
|
|
||||||
const cloned = content ? JSON.parse(JSON.stringify(content)) : content;
|
|
||||||
const idMap = new Map<string, string>();
|
|
||||||
|
|
||||||
const visit = (node: any): void => {
|
|
||||||
if (!node || typeof node !== 'object') return;
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof node.type === 'string' &&
|
|
||||||
isAttachmentNode(node.type) &&
|
|
||||||
node.attrs
|
|
||||||
) {
|
|
||||||
const oldId = node.attrs.attachmentId;
|
|
||||||
if (typeof oldId === 'string' && oldId.length > 0) {
|
|
||||||
let newId = idMap.get(oldId);
|
|
||||||
if (!newId) {
|
|
||||||
newId = generateId();
|
|
||||||
idMap.set(oldId, newId);
|
|
||||||
}
|
|
||||||
node.attrs.attachmentId = newId;
|
|
||||||
if (typeof node.attrs.src === 'string' && node.attrs.src.includes(oldId)) {
|
|
||||||
node.attrs.src = node.attrs.src.split(oldId).join(newId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(node.content)) {
|
|
||||||
for (const child of node.content) visit(child);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
visit(cloned);
|
|
||||||
|
|
||||||
const copies: AttachmentRewritePlan[] = Array.from(idMap.entries()).map(
|
|
||||||
([oldAttachmentId, newAttachmentId]) => ({
|
|
||||||
oldAttachmentId,
|
|
||||||
newAttachmentId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { content: cloned, copies };
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,6 @@ import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
|||||||
import { PageRepo } from './repos/page/page.repo';
|
import { PageRepo } from './repos/page/page.repo';
|
||||||
import { PagePermissionRepo } from './repos/page/page-permission.repo';
|
import { PagePermissionRepo } from './repos/page/page-permission.repo';
|
||||||
import { CommentRepo } from './repos/comment/comment.repo';
|
import { CommentRepo } from './repos/comment/comment.repo';
|
||||||
import { PageTransclusionsRepo } from './repos/page-transclusions/page-transclusions.repo';
|
|
||||||
import { PageTransclusionReferencesRepo } from './repos/page-transclusion-references/page-transclusion-references.repo';
|
|
||||||
import { PageHistoryRepo } from './repos/page/page-history.repo';
|
import { PageHistoryRepo } from './repos/page/page-history.repo';
|
||||||
import { AttachmentRepo } from './repos/attachment/attachment.repo';
|
import { AttachmentRepo } from './repos/attachment/attachment.repo';
|
||||||
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||||
@@ -77,8 +75,6 @@ import { normalizePostgresUrl } from '../common/helpers';
|
|||||||
SpaceMemberRepo,
|
SpaceMemberRepo,
|
||||||
PageRepo,
|
PageRepo,
|
||||||
PagePermissionRepo,
|
PagePermissionRepo,
|
||||||
PageTransclusionsRepo,
|
|
||||||
PageTransclusionReferencesRepo,
|
|
||||||
PageHistoryRepo,
|
PageHistoryRepo,
|
||||||
CommentRepo,
|
CommentRepo,
|
||||||
FavoriteRepo,
|
FavoriteRepo,
|
||||||
@@ -101,8 +97,6 @@ import { normalizePostgresUrl } from '../common/helpers';
|
|||||||
SpaceMemberRepo,
|
SpaceMemberRepo,
|
||||||
PageRepo,
|
PageRepo,
|
||||||
PagePermissionRepo,
|
PagePermissionRepo,
|
||||||
PageTransclusionsRepo,
|
|
||||||
PageTransclusionReferencesRepo,
|
|
||||||
PageHistoryRepo,
|
PageHistoryRepo,
|
||||||
CommentRepo,
|
CommentRepo,
|
||||||
FavoriteRepo,
|
FavoriteRepo,
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import { type Kysely, sql } from 'kysely';
|
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
|
||||||
await db.schema
|
|
||||||
.createTable('page_transclusions')
|
|
||||||
.addColumn('id', 'uuid', (col) =>
|
|
||||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
|
||||||
)
|
|
||||||
.addColumn('page_id', 'uuid', (col) =>
|
|
||||||
col.notNull().references('pages.id').onDelete('cascade'),
|
|
||||||
)
|
|
||||||
.addColumn('transclusion_id', 'varchar', (col) => col.notNull())
|
|
||||||
.addColumn('name', 'text')
|
|
||||||
.addColumn('content', 'jsonb', (col) => col.notNull())
|
|
||||||
.addColumn('created_at', 'timestamptz', (col) =>
|
|
||||||
col.notNull().defaultTo(sql`now()`),
|
|
||||||
)
|
|
||||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
|
||||||
col.notNull().defaultTo(sql`now()`),
|
|
||||||
)
|
|
||||||
.addUniqueConstraint('page_transclusions_page_transclusion_unique', [
|
|
||||||
'page_id',
|
|
||||||
'transclusion_id',
|
|
||||||
])
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createIndex('idx_page_transclusions_page_id')
|
|
||||||
.on('page_transclusions')
|
|
||||||
.column('page_id')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createTable('page_transclusion_references')
|
|
||||||
.addColumn('id', 'uuid', (col) =>
|
|
||||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
|
||||||
)
|
|
||||||
.addColumn('reference_page_id', 'uuid', (col) =>
|
|
||||||
col.notNull().references('pages.id').onDelete('cascade'),
|
|
||||||
)
|
|
||||||
.addColumn('containing_transclusion_id', 'varchar')
|
|
||||||
.addColumn('source_page_id', 'uuid', (col) =>
|
|
||||||
col.notNull().references('pages.id').onDelete('cascade'),
|
|
||||||
)
|
|
||||||
.addColumn('transclusion_id', 'varchar', (col) => col.notNull())
|
|
||||||
.addColumn('created_at', 'timestamptz', (col) =>
|
|
||||||
col.notNull().defaultTo(sql`now()`),
|
|
||||||
)
|
|
||||||
.addUniqueConstraint('page_transclusion_references_unique', [
|
|
||||||
'reference_page_id',
|
|
||||||
'containing_transclusion_id',
|
|
||||||
'source_page_id',
|
|
||||||
'transclusion_id',
|
|
||||||
])
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createIndex('idx_page_transclusion_references_reference_page_id')
|
|
||||||
.on('page_transclusion_references')
|
|
||||||
.column('reference_page_id')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createIndex('idx_page_transclusion_references_source')
|
|
||||||
.on('page_transclusion_references')
|
|
||||||
.columns(['source_page_id', 'transclusion_id'])
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createIndex('idx_page_transclusion_references_container')
|
|
||||||
.on('page_transclusion_references')
|
|
||||||
.columns(['reference_page_id', 'containing_transclusion_id'])
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
|
||||||
await db.schema.dropTable('page_transclusion_references').execute();
|
|
||||||
await db.schema.dropTable('page_transclusions').execute();
|
|
||||||
}
|
|
||||||
-181
@@ -1,181 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
|
||||||
import { sql } from 'kysely';
|
|
||||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
|
||||||
import { dbOrTx } from '@docmost/db/utils';
|
|
||||||
import {
|
|
||||||
InsertablePageTransclusionReference,
|
|
||||||
PageTransclusionReference,
|
|
||||||
} from '@docmost/db/types/entity.types';
|
|
||||||
|
|
||||||
export type TransclusionReferenceKey = {
|
|
||||||
containingTransclusionId: string | null;
|
|
||||||
sourcePageId: string;
|
|
||||||
transclusionId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PageTransclusionReferencesRepo {
|
|
||||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
|
||||||
|
|
||||||
async findByReferencePageId(
|
|
||||||
referencePageId: string,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<PageTransclusionReference[]> {
|
|
||||||
return dbOrTx(this.db, trx)
|
|
||||||
.selectFrom('pageTransclusionReferences')
|
|
||||||
.selectAll()
|
|
||||||
.where('referencePageId', '=', referencePageId)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async findReferencePageIdsByTransclusion(
|
|
||||||
sourcePageId: string,
|
|
||||||
transclusionId: string,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<string[]> {
|
|
||||||
const rows = await dbOrTx(this.db, trx)
|
|
||||||
.selectFrom('pageTransclusionReferences')
|
|
||||||
.select('referencePageId')
|
|
||||||
.distinct()
|
|
||||||
.where('sourcePageId', '=', sourcePageId)
|
|
||||||
.where('transclusionId', '=', transclusionId)
|
|
||||||
.execute();
|
|
||||||
return rows.map((r) => r.referencePageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async insertMany(
|
|
||||||
rows: InsertablePageTransclusionReference[],
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<void> {
|
|
||||||
if (rows.length === 0) return;
|
|
||||||
await dbOrTx(this.db, trx)
|
|
||||||
.insertInto('pageTransclusionReferences')
|
|
||||||
.values(rows)
|
|
||||||
.onConflict((oc) =>
|
|
||||||
oc
|
|
||||||
.columns([
|
|
||||||
'referencePageId',
|
|
||||||
'containingTransclusionId',
|
|
||||||
'sourcePageId',
|
|
||||||
'transclusionId',
|
|
||||||
])
|
|
||||||
.doNothing(),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteByReferenceAndKeys(
|
|
||||||
referencePageId: string,
|
|
||||||
keys: TransclusionReferenceKey[],
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<void> {
|
|
||||||
if (keys.length === 0) return;
|
|
||||||
await dbOrTx(this.db, trx)
|
|
||||||
.deleteFrom('pageTransclusionReferences')
|
|
||||||
.where('referencePageId', '=', referencePageId)
|
|
||||||
.where((eb) =>
|
|
||||||
eb.or(
|
|
||||||
keys.map((k) =>
|
|
||||||
eb.and([
|
|
||||||
k.containingTransclusionId === null
|
|
||||||
? eb('containingTransclusionId', 'is', null)
|
|
||||||
: eb(
|
|
||||||
'containingTransclusionId',
|
|
||||||
'=',
|
|
||||||
k.containingTransclusionId,
|
|
||||||
),
|
|
||||||
eb('sourcePageId', '=', k.sourcePageId),
|
|
||||||
eb('transclusionId', '=', k.transclusionId),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteOne(
|
|
||||||
referencePageId: string,
|
|
||||||
sourcePageId: string,
|
|
||||||
transclusionId: string,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<void> {
|
|
||||||
await dbOrTx(this.db, trx)
|
|
||||||
.deleteFrom('pageTransclusionReferences')
|
|
||||||
.where('referencePageId', '=', referencePageId)
|
|
||||||
.where('sourcePageId', '=', sourcePageId)
|
|
||||||
.where('transclusionId', '=', transclusionId)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteByIds(ids: string[], trx?: KyselyTransaction): Promise<void> {
|
|
||||||
if (ids.length === 0) return;
|
|
||||||
await dbOrTx(this.db, trx)
|
|
||||||
.deleteFrom('pageTransclusionReferences')
|
|
||||||
.where('id', 'in', ids)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds reference rows that participate in a cycle reachable from a given
|
|
||||||
* source `(pageId, transclusionId)`. The walk follows source-to-source edges
|
|
||||||
* (rows where `containing_transclusion_id IS NOT NULL`); loose page-level
|
|
||||||
* references are not graph edges and are ignored.
|
|
||||||
*
|
|
||||||
* Returned rows are the *closing edges* — those whose insertion completed a
|
|
||||||
* cycle. They are the safe set to remove to break the cycle while preserving
|
|
||||||
* unrelated structure.
|
|
||||||
*/
|
|
||||||
async findCyclicEdgesForSource(
|
|
||||||
sourcePageId: string,
|
|
||||||
transclusionId: string,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<PageTransclusionReference[]> {
|
|
||||||
const rows = await sql<PageTransclusionReference>`
|
|
||||||
WITH RECURSIVE walk(
|
|
||||||
start_page,
|
|
||||||
start_id,
|
|
||||||
page_id,
|
|
||||||
transclusion_id,
|
|
||||||
edge_id,
|
|
||||||
is_cycle,
|
|
||||||
path
|
|
||||||
) AS (
|
|
||||||
SELECT
|
|
||||||
${sourcePageId}::uuid,
|
|
||||||
${transclusionId}::varchar,
|
|
||||||
${sourcePageId}::uuid,
|
|
||||||
${transclusionId}::varchar,
|
|
||||||
NULL::uuid,
|
|
||||||
false,
|
|
||||||
ARRAY[(${sourcePageId}::uuid, ${transclusionId}::varchar)]
|
|
||||||
UNION ALL
|
|
||||||
SELECT
|
|
||||||
w.start_page,
|
|
||||||
w.start_id,
|
|
||||||
r.source_page_id,
|
|
||||||
r.transclusion_id,
|
|
||||||
r.id,
|
|
||||||
(r.source_page_id, r.transclusion_id) = ANY(w.path),
|
|
||||||
w.path || ARRAY[(r.source_page_id, r.transclusion_id)]
|
|
||||||
FROM page_transclusion_references r
|
|
||||||
JOIN walk w
|
|
||||||
ON r.reference_page_id = w.page_id
|
|
||||||
AND r.containing_transclusion_id = w.transclusion_id
|
|
||||||
WHERE r.containing_transclusion_id IS NOT NULL
|
|
||||||
AND NOT w.is_cycle
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
r.id,
|
|
||||||
r.created_at AS "createdAt",
|
|
||||||
r.reference_page_id AS "referencePageId",
|
|
||||||
r.containing_transclusion_id AS "containingTransclusionId",
|
|
||||||
r.source_page_id AS "sourcePageId",
|
|
||||||
r.transclusion_id AS "transclusionId"
|
|
||||||
FROM walk w
|
|
||||||
JOIN page_transclusion_references r ON r.id = w.edge_id
|
|
||||||
WHERE w.is_cycle
|
|
||||||
`.execute(dbOrTx(this.db, trx));
|
|
||||||
return rows.rows;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
|
||||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
|
||||||
import { dbOrTx } from '@docmost/db/utils';
|
|
||||||
import {
|
|
||||||
InsertablePageTransclusion,
|
|
||||||
PageTransclusion,
|
|
||||||
UpdatablePageTransclusion,
|
|
||||||
} from '@docmost/db/types/entity.types';
|
|
||||||
import { sql } from 'kysely';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PageTransclusionsRepo {
|
|
||||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
|
||||||
|
|
||||||
async findByPageId(
|
|
||||||
pageId: string,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<PageTransclusion[]> {
|
|
||||||
return dbOrTx(this.db, trx)
|
|
||||||
.selectFrom('pageTransclusions')
|
|
||||||
.selectAll()
|
|
||||||
.where('pageId', '=', pageId)
|
|
||||||
.orderBy(sql`name asc nulls last`)
|
|
||||||
.orderBy('createdAt', 'asc')
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async findByPageAndTransclusion(
|
|
||||||
pageId: string,
|
|
||||||
transclusionId: string,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<PageTransclusion | undefined> {
|
|
||||||
return dbOrTx(this.db, trx)
|
|
||||||
.selectFrom('pageTransclusions')
|
|
||||||
.selectAll()
|
|
||||||
.where('pageId', '=', pageId)
|
|
||||||
.where('transclusionId', '=', transclusionId)
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
async findManyByPageAndTransclusion(
|
|
||||||
keys: Array<{ pageId: string; transclusionId: string }>,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<PageTransclusion[]> {
|
|
||||||
if (keys.length === 0) return [];
|
|
||||||
return dbOrTx(this.db, trx)
|
|
||||||
.selectFrom('pageTransclusions')
|
|
||||||
.selectAll()
|
|
||||||
.where((eb) =>
|
|
||||||
eb.or(
|
|
||||||
keys.map((k) =>
|
|
||||||
eb.and([
|
|
||||||
eb('pageId', '=', k.pageId),
|
|
||||||
eb('transclusionId', '=', k.transclusionId),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async insert(
|
|
||||||
data: InsertablePageTransclusion,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<PageTransclusion> {
|
|
||||||
return dbOrTx(this.db, trx)
|
|
||||||
.insertInto('pageTransclusions')
|
|
||||||
.values(data)
|
|
||||||
.returningAll()
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
}
|
|
||||||
|
|
||||||
async insertMany(
|
|
||||||
data: InsertablePageTransclusion[],
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<void> {
|
|
||||||
if (data.length === 0) return;
|
|
||||||
await dbOrTx(this.db, trx)
|
|
||||||
.insertInto('pageTransclusions')
|
|
||||||
.values(data)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(
|
|
||||||
pageId: string,
|
|
||||||
transclusionId: string,
|
|
||||||
data: UpdatablePageTransclusion,
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<void> {
|
|
||||||
await dbOrTx(this.db, trx)
|
|
||||||
.updateTable('pageTransclusions')
|
|
||||||
.set({ ...data, updatedAt: new Date() })
|
|
||||||
.where('pageId', '=', pageId)
|
|
||||||
.where('transclusionId', '=', transclusionId)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteByPageAndTransclusionIds(
|
|
||||||
pageId: string,
|
|
||||||
transclusionIds: string[],
|
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<void> {
|
|
||||||
if (transclusionIds.length === 0) return;
|
|
||||||
await dbOrTx(this.db, trx)
|
|
||||||
.deleteFrom('pageTransclusions')
|
|
||||||
.where('pageId', '=', pageId)
|
|
||||||
.where('transclusionId', 'in', transclusionIds)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-21
@@ -228,25 +228,6 @@ export interface GroupUsers {
|
|||||||
userId: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageTransclusionReferences {
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
transclusionId: string;
|
|
||||||
referencePageId: string;
|
|
||||||
containingTransclusionId: string | null;
|
|
||||||
id: Generated<string>;
|
|
||||||
sourcePageId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageTransclusions {
|
|
||||||
content: Json;
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
transclusionId: string;
|
|
||||||
id: Generated<string>;
|
|
||||||
name: string | null;
|
|
||||||
pageId: string;
|
|
||||||
updatedAt: Generated<Timestamp>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageHistory {
|
export interface PageHistory {
|
||||||
content: Json | null;
|
content: Json | null;
|
||||||
contributorIds: Generated<string[] | null>;
|
contributorIds: Generated<string[] | null>;
|
||||||
@@ -590,8 +571,6 @@ export interface DB {
|
|||||||
groupUsers: GroupUsers;
|
groupUsers: GroupUsers;
|
||||||
notifications: Notifications;
|
notifications: Notifications;
|
||||||
pageAccess: PageAccess;
|
pageAccess: PageAccess;
|
||||||
pageTransclusionReferences: PageTransclusionReferences;
|
|
||||||
pageTransclusions: PageTransclusions;
|
|
||||||
pagePermissions: PagePermissions;
|
pagePermissions: PagePermissions;
|
||||||
pageHistory: PageHistory;
|
pageHistory: PageHistory;
|
||||||
pageVerifications: PageVerifications;
|
pageVerifications: PageVerifications;
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
Groups,
|
Groups,
|
||||||
Notifications,
|
Notifications,
|
||||||
PageAccess as _PageAccess,
|
PageAccess as _PageAccess,
|
||||||
PageTransclusions,
|
|
||||||
PageTransclusionReferences,
|
|
||||||
PagePermissions as _PagePermissions,
|
PagePermissions as _PagePermissions,
|
||||||
PageVerifications as _PageVerifications,
|
PageVerifications as _PageVerifications,
|
||||||
PageVerifiers as _PageVerifiers,
|
PageVerifiers as _PageVerifiers,
|
||||||
@@ -147,18 +145,6 @@ export type Favorite = Selectable<Favorites>;
|
|||||||
export type InsertableFavorite = Insertable<Favorites>;
|
export type InsertableFavorite = Insertable<Favorites>;
|
||||||
export type UpdatableFavorite = Updateable<Omit<Favorites, 'id'>>;
|
export type UpdatableFavorite = Updateable<Omit<Favorites, 'id'>>;
|
||||||
|
|
||||||
// Page Transclusion
|
|
||||||
export type PageTransclusion = Selectable<PageTransclusions>;
|
|
||||||
export type InsertablePageTransclusion = Insertable<PageTransclusions>;
|
|
||||||
export type UpdatablePageTransclusion = Updateable<Omit<PageTransclusions, 'id'>>;
|
|
||||||
|
|
||||||
// Page Transclusion Reference
|
|
||||||
export type PageTransclusionReference = Selectable<PageTransclusionReferences>;
|
|
||||||
export type InsertablePageTransclusionReference = Insertable<PageTransclusionReferences>;
|
|
||||||
export type UpdatablePageTransclusionReference = Updateable<
|
|
||||||
Omit<PageTransclusionReferences, 'id'>
|
|
||||||
>;
|
|
||||||
|
|
||||||
// File Task
|
// File Task
|
||||||
export type FileTask = Selectable<FileTasks>;
|
export type FileTask = Selectable<FileTasks>;
|
||||||
export type InsertableFileTask = Insertable<FileTasks>;
|
export type InsertableFileTask = Insertable<FileTasks>;
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 211783940c...109829076c
@@ -51,9 +51,9 @@ export class ImportController {
|
|||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
const validFileExtensions = ['.md', '.html', '.docx', '.pdf'];
|
const validFileExtensions = ['.md', '.html', '.docx'];
|
||||||
|
|
||||||
const maxFileSize = bytes('30mb');
|
const maxFileSize = bytes('20mb');
|
||||||
|
|
||||||
let file = null;
|
let file = null;
|
||||||
try {
|
try {
|
||||||
@@ -102,7 +102,6 @@ export class ImportController {
|
|||||||
'.md': 'markdown',
|
'.md': 'markdown',
|
||||||
'.html': 'html',
|
'.html': 'html',
|
||||||
'.docx': 'docx',
|
'.docx': 'docx',
|
||||||
'.pdf': 'pdf',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (createdPage) {
|
if (createdPage) {
|
||||||
|
|||||||
@@ -63,10 +63,7 @@ export class ImportService {
|
|||||||
let createdPage = null;
|
let createdPage = null;
|
||||||
|
|
||||||
// For DOCX, we need the page ID upfront so images can reference it
|
// For DOCX, we need the page ID upfront so images can reference it
|
||||||
const pageId =
|
const pageId = fileExtension === '.docx' ? uuid7() : undefined;
|
||||||
fileExtension === '.docx' || fileExtension === '.pdf'
|
|
||||||
? uuid7()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (fileExtension.endsWith('.md')) {
|
if (fileExtension.endsWith('.md')) {
|
||||||
@@ -81,14 +78,6 @@ export class ImportService {
|
|||||||
pageId,
|
pageId,
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
} else if (fileExtension.endsWith('.pdf')) {
|
|
||||||
prosemirrorState = await this.processPdf(
|
|
||||||
fileBuffer,
|
|
||||||
workspaceId,
|
|
||||||
spaceId,
|
|
||||||
pageId,
|
|
||||||
userId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = 'Error processing file content';
|
const message = 'Error processing file content';
|
||||||
@@ -167,7 +156,7 @@ export class ImportService {
|
|||||||
let DocxImportModule: any;
|
let DocxImportModule: any;
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
DocxImportModule = require('./../../../ee/document-import/docx-import.service');
|
DocxImportModule = require('./../../../ee/docx-import/docx-import.service');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
'DOCX import requested but EE module not bundled in this build',
|
'DOCX import requested but EE module not bundled in this build',
|
||||||
@@ -193,42 +182,6 @@ export class ImportService {
|
|||||||
return this.processHTML(html);
|
return this.processHTML(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
async processPdf(
|
|
||||||
fileBuffer: Buffer,
|
|
||||||
workspaceId: string,
|
|
||||||
spaceId: string,
|
|
||||||
pageId: string,
|
|
||||||
userId: string,
|
|
||||||
): Promise<any> {
|
|
||||||
let PdfImportModule: any;
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
||||||
PdfImportModule = require('./../../../ee/document-import/pdf-import.service');
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(
|
|
||||||
'PDF import requested but EE module not bundled in this build',
|
|
||||||
);
|
|
||||||
throw new BadRequestException(
|
|
||||||
'This feature requires a valid enterprise license.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pdfImportService = this.moduleRef.get(
|
|
||||||
PdfImportModule.PdfImportService,
|
|
||||||
{ strict: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const html = await pdfImportService.convertPdfToHtml(
|
|
||||||
fileBuffer,
|
|
||||||
workspaceId,
|
|
||||||
spaceId,
|
|
||||||
pageId,
|
|
||||||
userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.processHTML(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createYdoc(prosemirrorJson: any): Promise<Buffer | null> {
|
async createYdoc(prosemirrorJson: any): Promise<Buffer | null> {
|
||||||
if (prosemirrorJson) {
|
if (prosemirrorJson) {
|
||||||
// this.logger.debug(`Converting prosemirror json state to ydoc`);
|
// this.logger.debug(`Converting prosemirror json state to ydoc`);
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export * from "./lib/markdown";
|
|||||||
export * from "./lib/search-and-replace";
|
export * from "./lib/search-and-replace";
|
||||||
export * from "./lib/embed-provider";
|
export * from "./lib/embed-provider";
|
||||||
export * from "./lib/subpages";
|
export * from "./lib/subpages";
|
||||||
export * from "./lib/transclusion";
|
|
||||||
export * from "./lib/highlight";
|
export * from "./lib/highlight";
|
||||||
export * from "./lib/heading/heading";
|
export * from "./lib/heading/heading";
|
||||||
export * from "./lib/unique-id";
|
export * from "./lib/unique-id";
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./transclusion";
|
|
||||||
export * from "./transclusion-reference";
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { mergeAttributes, Node } from "@tiptap/core";
|
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
|
||||||
|
|
||||||
export interface TransclusionReferenceOptions {
|
|
||||||
HTMLAttributes: Record<string, any>;
|
|
||||||
view: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransclusionReferenceAttributes {
|
|
||||||
sourcePageId?: string | null;
|
|
||||||
transclusionId?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@tiptap/core" {
|
|
||||||
interface Commands<ReturnType> {
|
|
||||||
transclusionReference: {
|
|
||||||
insertTransclusionReference: (
|
|
||||||
attributes: TransclusionReferenceAttributes,
|
|
||||||
) => ReturnType;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TransclusionReference = Node.create<TransclusionReferenceOptions>({
|
|
||||||
name: "transclusionReference",
|
|
||||||
|
|
||||||
addOptions() {
|
|
||||||
return {
|
|
||||||
HTMLAttributes: {},
|
|
||||||
view: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
group: "block",
|
|
||||||
atom: true,
|
|
||||||
selectable: true,
|
|
||||||
draggable: false,
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
sourcePageId: {
|
|
||||||
default: null,
|
|
||||||
parseHTML: (el) => el.getAttribute("data-source-page-id"),
|
|
||||||
renderHTML: (attrs) =>
|
|
||||||
attrs.sourcePageId
|
|
||||||
? { "data-source-page-id": attrs.sourcePageId }
|
|
||||||
: {},
|
|
||||||
},
|
|
||||||
transclusionId: {
|
|
||||||
default: null,
|
|
||||||
parseHTML: (el) => el.getAttribute("data-transclusion-id"),
|
|
||||||
renderHTML: (attrs) =>
|
|
||||||
attrs.transclusionId
|
|
||||||
? { "data-transclusion-id": attrs.transclusionId }
|
|
||||||
: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return [
|
|
||||||
"div",
|
|
||||||
mergeAttributes(
|
|
||||||
{ "data-type": this.name },
|
|
||||||
this.options.HTMLAttributes,
|
|
||||||
HTMLAttributes,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
insertTransclusionReference:
|
|
||||||
(attributes) =>
|
|
||||||
({ commands }) =>
|
|
||||||
commands.insertContent({
|
|
||||||
type: this.name,
|
|
||||||
attrs: attributes,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addNodeView() {
|
|
||||||
if (!this.options.view) return null;
|
|
||||||
this.editor.isInitialized = true;
|
|
||||||
return ReactNodeViewRenderer(this.options.view);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import { mergeAttributes, Node } from "@tiptap/core";
|
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
|
||||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
||||||
import { ReplaceStep, ReplaceAroundStep } from "@tiptap/pm/transform";
|
|
||||||
|
|
||||||
export interface TransclusionOptions {
|
|
||||||
HTMLAttributes: Record<string, any>;
|
|
||||||
view: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransclusionAttributes {
|
|
||||||
id?: string | null;
|
|
||||||
name?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@tiptap/core" {
|
|
||||||
interface Commands<ReturnType> {
|
|
||||||
transclusion: {
|
|
||||||
insertTransclusion: (attributes?: TransclusionAttributes) => ReturnType;
|
|
||||||
setTransclusionName: (name: string | null) => ReturnType;
|
|
||||||
toggleTransclusion: () => ReturnType;
|
|
||||||
unsyncTransclusion: () => ReturnType;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Transclusion = Node.create<TransclusionOptions>({
|
|
||||||
name: "transclusion",
|
|
||||||
|
|
||||||
addOptions() {
|
|
||||||
return {
|
|
||||||
HTMLAttributes: {},
|
|
||||||
view: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
group: "block",
|
|
||||||
content: "block+",
|
|
||||||
defining: true,
|
|
||||||
isolating: true,
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
id: {
|
|
||||||
default: null,
|
|
||||||
parseHTML: (el) => el.getAttribute("data-id"),
|
|
||||||
renderHTML: (attrs) =>
|
|
||||||
attrs.id ? { "data-id": attrs.id } : {},
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
default: null,
|
|
||||||
parseHTML: (el) => el.getAttribute("data-name"),
|
|
||||||
renderHTML: (attrs) =>
|
|
||||||
attrs.name ? { "data-name": attrs.name } : {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return [
|
|
||||||
"div",
|
|
||||||
mergeAttributes(
|
|
||||||
{ "data-type": this.name },
|
|
||||||
this.options.HTMLAttributes,
|
|
||||||
HTMLAttributes,
|
|
||||||
),
|
|
||||||
0,
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
insertTransclusion:
|
|
||||||
(attributes) =>
|
|
||||||
({ commands, state, chain }) => {
|
|
||||||
const { $from } = state.selection;
|
|
||||||
for (let depth = $from.depth; depth > 0; depth -= 1) {
|
|
||||||
if ($from.node(depth).type.name === this.name) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = {
|
|
||||||
type: this.name,
|
|
||||||
attrs: attributes ?? {},
|
|
||||||
content: [{ type: "paragraph" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const parent = $from.parent;
|
|
||||||
const isEmptyParagraph =
|
|
||||||
parent.type.name === "paragraph" && parent.content.size === 0;
|
|
||||||
|
|
||||||
if (isEmptyParagraph) {
|
|
||||||
return chain()
|
|
||||||
.insertContentAt(
|
|
||||||
{ from: $from.before(), to: $from.after() },
|
|
||||||
node,
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
return commands.insertContent(node);
|
|
||||||
},
|
|
||||||
setTransclusionName:
|
|
||||||
(name) =>
|
|
||||||
({ commands }) =>
|
|
||||||
commands.updateAttributes(this.name, { name }),
|
|
||||||
toggleTransclusion:
|
|
||||||
() =>
|
|
||||||
({ commands }) =>
|
|
||||||
commands.toggleWrap(this.name),
|
|
||||||
unsyncTransclusion:
|
|
||||||
() =>
|
|
||||||
({ state, tr, dispatch }) => {
|
|
||||||
const { $from } = state.selection;
|
|
||||||
// Walk up to the nearest transclusion wrapper.
|
|
||||||
let depth = $from.depth;
|
|
||||||
while (depth > 0 && $from.node(depth).type.name !== this.name) {
|
|
||||||
depth -= 1;
|
|
||||||
}
|
|
||||||
if (depth === 0) return false;
|
|
||||||
|
|
||||||
const node = $from.node(depth);
|
|
||||||
const start = $from.before(depth);
|
|
||||||
const end = start + node.nodeSize;
|
|
||||||
|
|
||||||
if (dispatch) {
|
|
||||||
tr.replaceWith(start, end, node.content);
|
|
||||||
dispatch(tr);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addNodeView() {
|
|
||||||
if (!this.options.view) return null;
|
|
||||||
// Force the react node view to render immediately using flush sync
|
|
||||||
this.editor.isInitialized = true;
|
|
||||||
return ReactNodeViewRenderer(this.options.view);
|
|
||||||
},
|
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
|
||||||
const typeName = this.name;
|
|
||||||
return [
|
|
||||||
new Plugin({
|
|
||||||
key: new PluginKey(`${typeName}-noNesting`),
|
|
||||||
filterTransaction: (tr, state) => {
|
|
||||||
if (!tr.docChanged) return true;
|
|
||||||
for (const step of tr.steps) {
|
|
||||||
if (
|
|
||||||
!(step instanceof ReplaceStep) &&
|
|
||||||
!(step instanceof ReplaceAroundStep)
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let sliceContainsTransclusion = false;
|
|
||||||
step.slice.content.descendants((node) => {
|
|
||||||
if (node.type.name === typeName) {
|
|
||||||
sliceContainsTransclusion = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
if (!sliceContainsTransclusion) continue;
|
|
||||||
|
|
||||||
const $insert = state.doc.resolve(step.from);
|
|
||||||
for (let depth = $insert.depth; depth > 0; depth -= 1) {
|
|
||||||
if ($insert.node(depth).type.name === typeName) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Generated
+282
-149
@@ -382,6 +382,9 @@ importers:
|
|||||||
socket.io-client:
|
socket.io-client:
|
||||||
specifier: ^4.8.3
|
specifier: ^4.8.3
|
||||||
version: 4.8.3
|
version: 4.8.3
|
||||||
|
tiptap-extension-global-drag-handle:
|
||||||
|
specifier: ^0.1.18
|
||||||
|
version: 0.1.18
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.3.6
|
specifier: ^4.3.6
|
||||||
version: 4.3.6
|
version: 4.3.6
|
||||||
@@ -468,20 +471,17 @@ importers:
|
|||||||
specifier: ^2.0.37
|
specifier: ^2.0.37
|
||||||
version: 2.0.37(zod@4.3.6)
|
version: 2.0.37(zod@4.3.6)
|
||||||
'@aws-sdk/client-s3':
|
'@aws-sdk/client-s3':
|
||||||
specifier: 3.1040.0
|
specifier: 3.1037.0
|
||||||
version: 3.1040.0
|
version: 3.1037.0
|
||||||
'@aws-sdk/lib-storage':
|
'@aws-sdk/lib-storage':
|
||||||
specifier: 3.1040.0
|
specifier: 3.1037.0
|
||||||
version: 3.1040.0(@aws-sdk/client-s3@3.1040.0)
|
version: 3.1037.0(@aws-sdk/client-s3@3.1037.0)
|
||||||
'@aws-sdk/s3-request-presigner':
|
'@aws-sdk/s3-request-presigner':
|
||||||
specifier: 3.1040.0
|
specifier: 3.1037.0
|
||||||
version: 3.1040.0
|
version: 3.1037.0
|
||||||
'@clickhouse/client':
|
'@clickhouse/client':
|
||||||
specifier: ^1.18.2
|
specifier: ^1.18.2
|
||||||
version: 1.18.2
|
version: 1.18.2
|
||||||
'@docmost/pdf-inspector':
|
|
||||||
specifier: 1.9.4
|
|
||||||
version: 1.9.4
|
|
||||||
'@fastify/cookie':
|
'@fastify/cookie':
|
||||||
specifier: ^11.0.2
|
specifier: ^11.0.2
|
||||||
version: 11.0.2
|
version: 11.0.2
|
||||||
@@ -671,6 +671,9 @@ importers:
|
|||||||
passport-jwt:
|
passport-jwt:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
|
pdfjs-dist:
|
||||||
|
specifier: ^5.5.207
|
||||||
|
version: 5.5.207
|
||||||
pg-tsquery:
|
pg-tsquery:
|
||||||
specifier: ^8.4.2
|
specifier: ^8.4.2
|
||||||
version: 8.4.2
|
version: 8.4.2
|
||||||
@@ -938,55 +941,55 @@ packages:
|
|||||||
'@aws-crypto/util@5.2.0':
|
'@aws-crypto/util@5.2.0':
|
||||||
resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
|
resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
|
||||||
|
|
||||||
'@aws-sdk/client-s3@3.1040.0':
|
'@aws-sdk/client-s3@3.1037.0':
|
||||||
resolution: {integrity: sha512-Ldfby1xDrlZwNY2NxP9pwdVrf8sqHbGBKP1UkoG/oWcePGlGhjY8iVwy8hRy9f1EQfHVFWIFunwHaPQxhYTnWQ==}
|
resolution: {integrity: sha512-DBmA1jAW8ST6C4srBxeL1/RLIir/d8WOm4s4mi59mGp6mBktHM59Kwb7GuURaCO60cotuce5zr0sKpMLPcBQyA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/core@3.974.7':
|
'@aws-sdk/core@3.974.5':
|
||||||
resolution: {integrity: sha512-YhRC90ofz5oolTJZlA8voU/oUrCB2azi8Usx51k8hhB5LpWbYQMMXKUqSqkoL0Cru+RQJgWTHpAfEDDIwfUhJw==}
|
resolution: {integrity: sha512-lMPlYlYfQdNZhlkJgnkmESwrY+hNh3PljmZ+37oAqLNdJ6rnILAwFSyc6B3bJeDOtMORNnMQIej0aTRuOlDyhQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/crc64-nvme@3.972.7':
|
'@aws-sdk/crc64-nvme@3.972.7':
|
||||||
resolution: {integrity: sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg==}
|
resolution: {integrity: sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-env@3.972.33':
|
'@aws-sdk/credential-provider-env@3.972.31':
|
||||||
resolution: {integrity: sha512-bJV7eViSJV6GSuuN+VIdNVPdwPsNSf75BiC2v5alPrjR/OCcqgKwSZInKbDFz9mNeizldsyf67jt6YSIiv53Cw==}
|
resolution: {integrity: sha512-X/yGB73LmDW/6MdDJGCDzZBUXnM3ys4vs9l+5ZTJmiEswDdP1OjeoAFlFjVGS9o4KB2wZWQ9KOfdVNSSK6Ep3w==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-http@3.972.35':
|
'@aws-sdk/credential-provider-http@3.972.33':
|
||||||
resolution: {integrity: sha512-x/BQGEIdq0oI+4WxLjKmnQvT7CnF9r8ezdGt7wXwxb7ckHXQz0Zmgxt8v3Ne0JaT3R5YefmuybHX6E8EnsDXyA==}
|
resolution: {integrity: sha512-c0ZF+lwoWVvX5iCaGKL5T/4DnIw88CGqxA0BcBs3U86mIp5EZYPVg+KSPkMXOyokmADvNewiMUfSG2uFwjRp0g==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.972.37':
|
'@aws-sdk/credential-provider-ini@3.972.35':
|
||||||
resolution: {integrity: sha512-eUTpmWfd/BKsq9medhCRcu+GRAhFP2Zrn7/2jKDHHOOjCkhrMoTp/t4cEthqFoG7gE0VGp5wUxrXTdvBCmSmJg==}
|
resolution: {integrity: sha512-jsU4u/cRkKFLKQS0k918FQ27fzXLG5ENiLWQMYE6581zLeI2hWh04ptlrvZMB3wJT/5d+vSzJk74X1CMFr4y8Q==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-login@3.972.37':
|
'@aws-sdk/credential-provider-login@3.972.35':
|
||||||
resolution: {integrity: sha512-Ty68y8ISSC+g5Q3D0K8uAaoINwvfaOslnNpsF/LgVUxyosYXHawcK2yV4HLXDVugiTTYLQfJfcw0ce5meAGkKw==}
|
resolution: {integrity: sha512-5oa3j0cA50jPqgNhZ9XdJVopuzUf1klRb28/2MfLYWWiPi9DRVvbrBWT+DidbHTT36520VuXZJahQwR+YgSjrg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.38':
|
'@aws-sdk/credential-provider-node@3.972.36':
|
||||||
resolution: {integrity: sha512-BQ9XYnBDVxR2HuV5huXYQYF/PZMTsY+EnwfGnCU2cA8Zw63XpkOtPY8WqiMIZMQCrKPQQEiFURS/o9CIolRLqg==}
|
resolution: {integrity: sha512-4nT2T8Z7vH8KE9EdjEsuIlHpZSlcaK2PrKbQBjuUGU46BCCzF3WvP0u0Uiosni3Ykmmn4rWLVawoOCLotUtCbg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-process@3.972.33':
|
'@aws-sdk/credential-provider-process@3.972.31':
|
||||||
resolution: {integrity: sha512-yfjGksI9WQbdMObb0VeLXqzTLI+a0qXLJT9gCDiv0+X/xjPpI3mTz6a5FibrhpuEKIe0gSgvs3MaoFZy5cx4WA==}
|
resolution: {integrity: sha512-eKeT4MXumpBJsrDLCYcSzIkFPVTFn/es7It2oogp2OhU/ic7P/+xzFpQx9ZhwtXS57Mc5S42BPWi7lHmvs/nYg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.972.37':
|
'@aws-sdk/credential-provider-sso@3.972.35':
|
||||||
resolution: {integrity: sha512-fpwE+20ntpp3i9Xb9vUuQfXLDKYHH+5I2V+ZG96SX1nBzrruhy10RXDgmN7t1etOz3c55stlA3TeQASUA451NQ==}
|
resolution: {integrity: sha512-bCuBdfnj0KGDMdLp6utMTLiJcFN2ek9EgZinxQZZSc3FxjJ/HSqeqab2cjbnoNfy8RM6suDCsRkmVY1izp9I+A==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-web-identity@3.972.37':
|
'@aws-sdk/credential-provider-web-identity@3.972.35':
|
||||||
resolution: {integrity: sha512-aryawqyebf+3WhAFNHfF62rekFpYtVcVN7dQ89qnAWsa4n5hJst8qBG6gXC24WHtW7Nnhkf9ScYnjwo0Brn3bw==}
|
resolution: {integrity: sha512-swW6Bwvl8lanyEMtZOWE/oR6yqcRQH4HTQZUVsnDVgoXvRjRywpYpLv2BWwjUFyjPrqsdX6FeTkf4tMSe/qFTQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/lib-storage@3.1040.0':
|
'@aws-sdk/lib-storage@3.1037.0':
|
||||||
resolution: {integrity: sha512-4cPpxs/2Gnzm7dKn2EBTPf0/rPNM5BoOleWSNn03QPGkELPDg9VmVwUhXZsDcb8k8F9wm5ft9n7BSNXKAjoIWw==}
|
resolution: {integrity: sha512-ZFg5Vf4RKS48xTm7DfXTeR0Rvn/Fcu6YFdRygGnvhA+gW3W0WtsRqM1CzkWevYBztdUUAsZqtGbMj9Eu0OaeEg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@aws-sdk/client-s3': ^3.1040.0
|
'@aws-sdk/client-s3': ^3.1037.0
|
||||||
|
|
||||||
'@aws-sdk/middleware-bucket-endpoint@3.972.10':
|
'@aws-sdk/middleware-bucket-endpoint@3.972.10':
|
||||||
resolution: {integrity: sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA==}
|
resolution: {integrity: sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA==}
|
||||||
@@ -996,8 +999,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ==}
|
resolution: {integrity: sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/middleware-flexible-checksums@3.974.15':
|
'@aws-sdk/middleware-flexible-checksums@3.974.13':
|
||||||
resolution: {integrity: sha512-j4Zp7rA1HfhDTteICnx/tPax4N/v5wmytgguXExUGyEwQ8Ug4EBA4kjp9puFAN1UZoBVpxoiXMiuTFvjaHjeEw==}
|
resolution: {integrity: sha512-b6QUe2hQX9XsnCzp6mtzVaERhganDKeb8lmGL6pVhr7rRVH9S9keDFW7uKytuuqmcY5943FixoGqn/QL+sbUBA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/middleware-host-header@3.972.10':
|
'@aws-sdk/middleware-host-header@3.972.10':
|
||||||
@@ -1016,36 +1019,36 @@ packages:
|
|||||||
resolution: {integrity: sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==}
|
resolution: {integrity: sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/middleware-sdk-s3@3.972.36':
|
'@aws-sdk/middleware-sdk-s3@3.972.34':
|
||||||
resolution: {integrity: sha512-YhPix+0x/MdQrb1Ug1GDKeS5fqylIy+naz800asX8II4jqfTk2KY2KhmmYCwZcky8YWtRQQwWCGdoqeAnip8Uw==}
|
resolution: {integrity: sha512-/UL96JKjsjdodcRRMKl99tLQvK6Oi9ptLC9iU1yiTF/ruaDX0mtBBtnLNZDxIZRJOCVOtB49ed1YaTadqygk8Q==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/middleware-ssec@3.972.10':
|
'@aws-sdk/middleware-ssec@3.972.10':
|
||||||
resolution: {integrity: sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==}
|
resolution: {integrity: sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/middleware-user-agent@3.972.37':
|
'@aws-sdk/middleware-user-agent@3.972.35':
|
||||||
resolution: {integrity: sha512-N1oNpdiLoVAWYD3WFBnUi3LlfoDA06ZHo4ozyjbsJNLvILzvt//0CnR8N+CZ0NWeYgVB/5V59ivixHCWCx2ALw==}
|
resolution: {integrity: sha512-hOFWNOjVmOocpRlrU04nYxjMOeoe0Obu5AXEuhB8zblMCPl3cG1hdluQCZERRKFyhMQjwZnDbhSHjoMUjetFGw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/nested-clients@3.997.5':
|
'@aws-sdk/nested-clients@3.997.3':
|
||||||
resolution: {integrity: sha512-jGFr6DxtcMTmzOkG/a0jCZYv4BBDmeNYVeO+/memSoDkYCJu4Y58xviYmzwJfYyIVSts+X/BVjJm1uGBnwHEMg==}
|
resolution: {integrity: sha512-SivE6GP228IVgfsrr2c/vqTg95X0Qj39Yw4uIrcddpkUzIltNMoNOR62leHOLhODfjv9K8X2mPTwS69A5kT0nQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/region-config-resolver@3.972.13':
|
'@aws-sdk/region-config-resolver@3.972.13':
|
||||||
resolution: {integrity: sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==}
|
resolution: {integrity: sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/s3-request-presigner@3.1040.0':
|
'@aws-sdk/s3-request-presigner@3.1037.0':
|
||||||
resolution: {integrity: sha512-AmesZGG/B5sDIiWahyY11fOkXSsuHc7LciE88YFURehMVSdEORo2Vzz1d2kBgmJG9oar5Vmmwf9X/w7mqb7ytg==}
|
resolution: {integrity: sha512-rZQS8DxrqPYXzOvaoysf6L4fHmgFbndZz3GfUMhlHG1iWmcQqH7v0AGhpjyNBY3cYAX8+CAkOkD4VUrntnHNbQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/signature-v4-multi-region@3.996.24':
|
'@aws-sdk/signature-v4-multi-region@3.996.22':
|
||||||
resolution: {integrity: sha512-amP7tLikppN940wbBFISYqiuzVmpzMS9U3mcgtmVLjX4fdWI/SNCvrXv6ZxfVzTT4cT0rPKOLhFah2xLwzREWw==}
|
resolution: {integrity: sha512-/rXhMXteD+BqhFd0nYprAgcZ/KtU+963uftPqd3tiFcFfooHZINXUGtOmo2SQjRVauCTNqIEzkwuSETdZFqTTA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1039.0':
|
'@aws-sdk/token-providers@3.1036.0':
|
||||||
resolution: {integrity: sha512-NMSFL2HwkAOoCeLCQiqoOq5pT3vVbSjww2QZTuYgYknVwhhv125PSDzZIcL5EYnlxuPWjEOdauZK+FspkZDVdw==}
|
resolution: {integrity: sha512-aNSJ6jjDYayxN9ZA1JpycVScX93Lx03kKZ1EXt3DGOTahcWVLJj3oLAlop0xKP+vP2Ga2t49p1tEaMkTbCCaZA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/types@3.973.8':
|
'@aws-sdk/types@3.973.8':
|
||||||
@@ -1071,8 +1074,8 @@ packages:
|
|||||||
'@aws-sdk/util-user-agent-browser@3.972.10':
|
'@aws-sdk/util-user-agent-browser@3.972.10':
|
||||||
resolution: {integrity: sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==}
|
resolution: {integrity: sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==}
|
||||||
|
|
||||||
'@aws-sdk/util-user-agent-node@3.973.23':
|
'@aws-sdk/util-user-agent-node@3.973.21':
|
||||||
resolution: {integrity: sha512-gGwq8L2Euw0aNG6Ey4EktiAo3fSCVoDy1CaBIthd+oeaKHPXUrNaApMewQ6La5Hv0lcznOtECZaNvYyc5LXXfA==}
|
resolution: {integrity: sha512-Av4UHTcAWgdvbN0IP9pbtf4Qa1+6LtJqQdZWj5pLn5J67w0pnJJAZZ+7JPPcj2KN3378zD2JDM9DwJKEyvyMTQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
aws-crt: '>=1.0.0'
|
aws-crt: '>=1.0.0'
|
||||||
@@ -1080,8 +1083,8 @@ packages:
|
|||||||
aws-crt:
|
aws-crt:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@aws-sdk/xml-builder@3.972.22':
|
'@aws-sdk/xml-builder@3.972.19':
|
||||||
resolution: {integrity: sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==}
|
resolution: {integrity: sha512-Cw8IOMdBUEIl8ZlhRC3Dc/E64D5B5/8JhV6vhPLiPfJwcRC84S6F8aBOIi/N4vR9ZyA4I5Cc0Ateb/9EHaJXeQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws/lambda-invoke-store@0.2.3':
|
'@aws/lambda-invoke-store@0.2.3':
|
||||||
@@ -1823,9 +1826,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==}
|
resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@docmost/pdf-inspector@1.9.4':
|
|
||||||
resolution: {integrity: sha512-G5DNyDtLNxybTXWakqi7PuOEuSb/A2ZjDlv2WCkOkiHszPeILdrC+G0a4e4UP10yxvzuLfb23pJ5jy8fUSYZPw==}
|
|
||||||
|
|
||||||
'@emnapi/core@1.8.1':
|
'@emnapi/core@1.8.1':
|
||||||
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
|
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
|
||||||
|
|
||||||
@@ -2762,6 +2762,76 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-android-arm64@0.1.97':
|
||||||
|
resolution: {integrity: sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-arm64@0.1.97':
|
||||||
|
resolution: {integrity: sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-x64@0.1.97':
|
||||||
|
resolution: {integrity: sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.97':
|
||||||
|
resolution: {integrity: sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.97':
|
||||||
|
resolution: {integrity: sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl@0.1.97':
|
||||||
|
resolution: {integrity: sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
|
||||||
|
resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu@0.1.97':
|
||||||
|
resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-musl@0.1.97':
|
||||||
|
resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.97':
|
||||||
|
resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc@0.1.97':
|
||||||
|
resolution: {integrity: sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas@0.1.97':
|
||||||
|
resolution: {integrity: sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.12':
|
'@napi-rs/wasm-runtime@0.2.12':
|
||||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||||
|
|
||||||
@@ -4278,8 +4348,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==}
|
resolution: {integrity: sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/middleware-retry@4.5.7':
|
'@smithy/middleware-retry@4.5.5':
|
||||||
resolution: {integrity: sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==}
|
resolution: {integrity: sha512-wnYOpB5vATFKWrY2Z9Alb0KhjZI6AbzU6Fbz3Hq2GnURdRYWB4q+qWivQtSTwXcmWUA3MZ6krfwL6Cq5MAbxsA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/middleware-serde@4.2.20':
|
'@smithy/middleware-serde@4.2.20':
|
||||||
@@ -4314,8 +4384,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==}
|
resolution: {integrity: sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/service-error-classification@4.3.1':
|
'@smithy/service-error-classification@4.3.0':
|
||||||
resolution: {integrity: sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==}
|
resolution: {integrity: sha512-9jKsBYQRPR0xBLgc2415RsA5PIcP2sis4oBdN9s0D13cg1B1284mNTjx9Yc+BEERXzuPm5ObktI96OxsKh8E9A==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/shared-ini-file-loader@4.4.9':
|
'@smithy/shared-ini-file-loader@4.4.9':
|
||||||
@@ -4382,10 +4452,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==}
|
resolution: {integrity: sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/util-retry@4.3.6':
|
'@smithy/util-retry@4.3.4':
|
||||||
resolution: {integrity: sha512-p6/FO1n2KxMeQyna067i0uJ6TSbb165ZhnRtCpWh4Foxqbfc6oW+XITaL8QkFJj3KFnDe2URt4gOhgU06EP9ew==}
|
resolution: {integrity: sha512-FY1UQQ1VFmMwiYp1GVS4MeaGD5O0blLNYK0xCRHU+mJgeoH/hSY8Ld8sJWKQ6uznkh14HveRGQJncgPyNl9J+A==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
deprecated: '@smithy/util-retry v4.3.6 contains a bug in Adaptive Retry, see https://github.com/smithy-lang/smithy-typescript/issues/1993. Upgrade to 4.3.7+'
|
|
||||||
|
|
||||||
'@smithy/util-stream@4.5.25':
|
'@smithy/util-stream@4.5.25':
|
||||||
resolution: {integrity: sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==}
|
resolution: {integrity: sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==}
|
||||||
@@ -4403,8 +4472,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==}
|
resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/util-waiter@4.3.0':
|
'@smithy/util-waiter@4.2.16':
|
||||||
resolution: {integrity: sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==}
|
resolution: {integrity: sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/uuid@1.1.2':
|
'@smithy/uuid@1.1.2':
|
||||||
@@ -6860,8 +6929,8 @@ packages:
|
|||||||
fast-xml-builder@1.1.5:
|
fast-xml-builder@1.1.5:
|
||||||
resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==}
|
resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==}
|
||||||
|
|
||||||
fast-xml-parser@5.7.2:
|
fast-xml-parser@5.7.1:
|
||||||
resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==}
|
resolution: {integrity: sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
fastify-ip@2.0.0:
|
fastify-ip@2.0.0:
|
||||||
@@ -8482,6 +8551,9 @@ packages:
|
|||||||
node-int64@0.4.0:
|
node-int64@0.4.0:
|
||||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||||
|
|
||||||
|
node-readable-to-web-readable-stream@0.4.2:
|
||||||
|
resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
|
||||||
|
|
||||||
node-releases@2.0.27:
|
node-releases@2.0.27:
|
||||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||||
|
|
||||||
@@ -8773,6 +8845,10 @@ packages:
|
|||||||
pause@0.0.1:
|
pause@0.0.1:
|
||||||
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
|
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
|
||||||
|
|
||||||
|
pdfjs-dist@5.5.207:
|
||||||
|
resolution: {integrity: sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==}
|
||||||
|
engines: {node: '>=20.19.0 || >=22.13.0 || >=24'}
|
||||||
|
|
||||||
peberminta@0.9.0:
|
peberminta@0.9.0:
|
||||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||||
|
|
||||||
@@ -9893,6 +9969,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tiptap-extension-global-drag-handle@0.1.18:
|
||||||
|
resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==}
|
||||||
|
|
||||||
tlds@1.261.0:
|
tlds@1.261.0:
|
||||||
resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==}
|
resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -10249,7 +10328,6 @@ packages:
|
|||||||
|
|
||||||
uuid@10.0.0:
|
uuid@10.0.0:
|
||||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
uuid@11.1.0:
|
uuid@11.1.0:
|
||||||
@@ -10809,29 +10887,29 @@ snapshots:
|
|||||||
'@smithy/util-utf8': 2.3.0
|
'@smithy/util-utf8': 2.3.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/client-s3@3.1040.0':
|
'@aws-sdk/client-s3@3.1037.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-crypto/sha1-browser': 5.2.0
|
'@aws-crypto/sha1-browser': 5.2.0
|
||||||
'@aws-crypto/sha256-browser': 5.2.0
|
'@aws-crypto/sha256-browser': 5.2.0
|
||||||
'@aws-crypto/sha256-js': 5.2.0
|
'@aws-crypto/sha256-js': 5.2.0
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/credential-provider-node': 3.972.38
|
'@aws-sdk/credential-provider-node': 3.972.36
|
||||||
'@aws-sdk/middleware-bucket-endpoint': 3.972.10
|
'@aws-sdk/middleware-bucket-endpoint': 3.972.10
|
||||||
'@aws-sdk/middleware-expect-continue': 3.972.10
|
'@aws-sdk/middleware-expect-continue': 3.972.10
|
||||||
'@aws-sdk/middleware-flexible-checksums': 3.974.15
|
'@aws-sdk/middleware-flexible-checksums': 3.974.13
|
||||||
'@aws-sdk/middleware-host-header': 3.972.10
|
'@aws-sdk/middleware-host-header': 3.972.10
|
||||||
'@aws-sdk/middleware-location-constraint': 3.972.10
|
'@aws-sdk/middleware-location-constraint': 3.972.10
|
||||||
'@aws-sdk/middleware-logger': 3.972.10
|
'@aws-sdk/middleware-logger': 3.972.10
|
||||||
'@aws-sdk/middleware-recursion-detection': 3.972.11
|
'@aws-sdk/middleware-recursion-detection': 3.972.11
|
||||||
'@aws-sdk/middleware-sdk-s3': 3.972.36
|
'@aws-sdk/middleware-sdk-s3': 3.972.34
|
||||||
'@aws-sdk/middleware-ssec': 3.972.10
|
'@aws-sdk/middleware-ssec': 3.972.10
|
||||||
'@aws-sdk/middleware-user-agent': 3.972.37
|
'@aws-sdk/middleware-user-agent': 3.972.35
|
||||||
'@aws-sdk/region-config-resolver': 3.972.13
|
'@aws-sdk/region-config-resolver': 3.972.13
|
||||||
'@aws-sdk/signature-v4-multi-region': 3.996.24
|
'@aws-sdk/signature-v4-multi-region': 3.996.22
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@aws-sdk/util-endpoints': 3.996.8
|
'@aws-sdk/util-endpoints': 3.996.8
|
||||||
'@aws-sdk/util-user-agent-browser': 3.972.10
|
'@aws-sdk/util-user-agent-browser': 3.972.10
|
||||||
'@aws-sdk/util-user-agent-node': 3.973.23
|
'@aws-sdk/util-user-agent-node': 3.973.21
|
||||||
'@smithy/config-resolver': 4.4.17
|
'@smithy/config-resolver': 4.4.17
|
||||||
'@smithy/core': 3.23.17
|
'@smithy/core': 3.23.17
|
||||||
'@smithy/eventstream-serde-browser': 4.2.14
|
'@smithy/eventstream-serde-browser': 4.2.14
|
||||||
@@ -10845,7 +10923,7 @@ snapshots:
|
|||||||
'@smithy/md5-js': 4.2.14
|
'@smithy/md5-js': 4.2.14
|
||||||
'@smithy/middleware-content-length': 4.2.14
|
'@smithy/middleware-content-length': 4.2.14
|
||||||
'@smithy/middleware-endpoint': 4.4.32
|
'@smithy/middleware-endpoint': 4.4.32
|
||||||
'@smithy/middleware-retry': 4.5.7
|
'@smithy/middleware-retry': 4.5.5
|
||||||
'@smithy/middleware-serde': 4.2.20
|
'@smithy/middleware-serde': 4.2.20
|
||||||
'@smithy/middleware-stack': 4.2.14
|
'@smithy/middleware-stack': 4.2.14
|
||||||
'@smithy/node-config-provider': 4.3.14
|
'@smithy/node-config-provider': 4.3.14
|
||||||
@@ -10861,18 +10939,18 @@ snapshots:
|
|||||||
'@smithy/util-defaults-mode-node': 4.2.54
|
'@smithy/util-defaults-mode-node': 4.2.54
|
||||||
'@smithy/util-endpoints': 3.4.2
|
'@smithy/util-endpoints': 3.4.2
|
||||||
'@smithy/util-middleware': 4.2.14
|
'@smithy/util-middleware': 4.2.14
|
||||||
'@smithy/util-retry': 4.3.6
|
'@smithy/util-retry': 4.3.4
|
||||||
'@smithy/util-stream': 4.5.25
|
'@smithy/util-stream': 4.5.25
|
||||||
'@smithy/util-utf8': 4.2.2
|
'@smithy/util-utf8': 4.2.2
|
||||||
'@smithy/util-waiter': 4.3.0
|
'@smithy/util-waiter': 4.2.16
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/core@3.974.7':
|
'@aws-sdk/core@3.974.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@aws-sdk/xml-builder': 3.972.22
|
'@aws-sdk/xml-builder': 3.972.19
|
||||||
'@smithy/core': 3.23.17
|
'@smithy/core': 3.23.17
|
||||||
'@smithy/node-config-provider': 4.3.14
|
'@smithy/node-config-provider': 4.3.14
|
||||||
'@smithy/property-provider': 4.2.14
|
'@smithy/property-provider': 4.2.14
|
||||||
@@ -10882,7 +10960,7 @@ snapshots:
|
|||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
'@smithy/util-base64': 4.3.2
|
'@smithy/util-base64': 4.3.2
|
||||||
'@smithy/util-middleware': 4.2.14
|
'@smithy/util-middleware': 4.2.14
|
||||||
'@smithy/util-retry': 4.3.6
|
'@smithy/util-retry': 4.3.4
|
||||||
'@smithy/util-utf8': 4.2.2
|
'@smithy/util-utf8': 4.2.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
@@ -10891,17 +10969,17 @@ snapshots:
|
|||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-env@3.972.33':
|
'@aws-sdk/credential-provider-env@3.972.31':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/property-provider': 4.2.14
|
'@smithy/property-provider': 4.2.14
|
||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-http@3.972.35':
|
'@aws-sdk/credential-provider-http@3.972.33':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/fetch-http-handler': 5.3.17
|
'@smithy/fetch-http-handler': 5.3.17
|
||||||
'@smithy/node-http-handler': 4.6.1
|
'@smithy/node-http-handler': 4.6.1
|
||||||
@@ -10912,16 +10990,16 @@ snapshots:
|
|||||||
'@smithy/util-stream': 4.5.25
|
'@smithy/util-stream': 4.5.25
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.972.37':
|
'@aws-sdk/credential-provider-ini@3.972.35':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/credential-provider-env': 3.972.33
|
'@aws-sdk/credential-provider-env': 3.972.31
|
||||||
'@aws-sdk/credential-provider-http': 3.972.35
|
'@aws-sdk/credential-provider-http': 3.972.33
|
||||||
'@aws-sdk/credential-provider-login': 3.972.37
|
'@aws-sdk/credential-provider-login': 3.972.35
|
||||||
'@aws-sdk/credential-provider-process': 3.972.33
|
'@aws-sdk/credential-provider-process': 3.972.31
|
||||||
'@aws-sdk/credential-provider-sso': 3.972.37
|
'@aws-sdk/credential-provider-sso': 3.972.35
|
||||||
'@aws-sdk/credential-provider-web-identity': 3.972.37
|
'@aws-sdk/credential-provider-web-identity': 3.972.35
|
||||||
'@aws-sdk/nested-clients': 3.997.5
|
'@aws-sdk/nested-clients': 3.997.3
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/credential-provider-imds': 4.2.14
|
'@smithy/credential-provider-imds': 4.2.14
|
||||||
'@smithy/property-provider': 4.2.14
|
'@smithy/property-provider': 4.2.14
|
||||||
@@ -10931,10 +11009,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-login@3.972.37':
|
'@aws-sdk/credential-provider-login@3.972.35':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/nested-clients': 3.997.5
|
'@aws-sdk/nested-clients': 3.997.3
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/property-provider': 4.2.14
|
'@smithy/property-provider': 4.2.14
|
||||||
'@smithy/protocol-http': 5.3.14
|
'@smithy/protocol-http': 5.3.14
|
||||||
@@ -10944,14 +11022,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.38':
|
'@aws-sdk/credential-provider-node@3.972.36':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/credential-provider-env': 3.972.33
|
'@aws-sdk/credential-provider-env': 3.972.31
|
||||||
'@aws-sdk/credential-provider-http': 3.972.35
|
'@aws-sdk/credential-provider-http': 3.972.33
|
||||||
'@aws-sdk/credential-provider-ini': 3.972.37
|
'@aws-sdk/credential-provider-ini': 3.972.35
|
||||||
'@aws-sdk/credential-provider-process': 3.972.33
|
'@aws-sdk/credential-provider-process': 3.972.31
|
||||||
'@aws-sdk/credential-provider-sso': 3.972.37
|
'@aws-sdk/credential-provider-sso': 3.972.35
|
||||||
'@aws-sdk/credential-provider-web-identity': 3.972.37
|
'@aws-sdk/credential-provider-web-identity': 3.972.35
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/credential-provider-imds': 4.2.14
|
'@smithy/credential-provider-imds': 4.2.14
|
||||||
'@smithy/property-provider': 4.2.14
|
'@smithy/property-provider': 4.2.14
|
||||||
@@ -10961,20 +11039,20 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-process@3.972.33':
|
'@aws-sdk/credential-provider-process@3.972.31':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/property-provider': 4.2.14
|
'@smithy/property-provider': 4.2.14
|
||||||
'@smithy/shared-ini-file-loader': 4.4.9
|
'@smithy/shared-ini-file-loader': 4.4.9
|
||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.972.37':
|
'@aws-sdk/credential-provider-sso@3.972.35':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/nested-clients': 3.997.5
|
'@aws-sdk/nested-clients': 3.997.3
|
||||||
'@aws-sdk/token-providers': 3.1039.0
|
'@aws-sdk/token-providers': 3.1036.0
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/property-provider': 4.2.14
|
'@smithy/property-provider': 4.2.14
|
||||||
'@smithy/shared-ini-file-loader': 4.4.9
|
'@smithy/shared-ini-file-loader': 4.4.9
|
||||||
@@ -10983,10 +11061,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-web-identity@3.972.37':
|
'@aws-sdk/credential-provider-web-identity@3.972.35':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/nested-clients': 3.997.5
|
'@aws-sdk/nested-clients': 3.997.3
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/property-provider': 4.2.14
|
'@smithy/property-provider': 4.2.14
|
||||||
'@smithy/shared-ini-file-loader': 4.4.9
|
'@smithy/shared-ini-file-loader': 4.4.9
|
||||||
@@ -10995,9 +11073,9 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/lib-storage@3.1040.0(@aws-sdk/client-s3@3.1040.0)':
|
'@aws-sdk/lib-storage@3.1037.0(@aws-sdk/client-s3@3.1037.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/client-s3': 3.1040.0
|
'@aws-sdk/client-s3': 3.1037.0
|
||||||
'@smithy/middleware-endpoint': 4.4.32
|
'@smithy/middleware-endpoint': 4.4.32
|
||||||
'@smithy/protocol-http': 5.3.14
|
'@smithy/protocol-http': 5.3.14
|
||||||
'@smithy/smithy-client': 4.12.13
|
'@smithy/smithy-client': 4.12.13
|
||||||
@@ -11024,12 +11102,12 @@ snapshots:
|
|||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/middleware-flexible-checksums@3.974.15':
|
'@aws-sdk/middleware-flexible-checksums@3.974.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-crypto/crc32': 5.2.0
|
'@aws-crypto/crc32': 5.2.0
|
||||||
'@aws-crypto/crc32c': 5.2.0
|
'@aws-crypto/crc32c': 5.2.0
|
||||||
'@aws-crypto/util': 5.2.0
|
'@aws-crypto/util': 5.2.0
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/crc64-nvme': 3.972.7
|
'@aws-sdk/crc64-nvme': 3.972.7
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/is-array-buffer': 4.2.2
|
'@smithy/is-array-buffer': 4.2.2
|
||||||
@@ -11068,9 +11146,9 @@ snapshots:
|
|||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/middleware-sdk-s3@3.972.36':
|
'@aws-sdk/middleware-sdk-s3@3.972.34':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@aws-sdk/util-arn-parser': 3.972.3
|
'@aws-sdk/util-arn-parser': 3.972.3
|
||||||
'@smithy/core': 3.23.17
|
'@smithy/core': 3.23.17
|
||||||
@@ -11091,32 +11169,32 @@ snapshots:
|
|||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/middleware-user-agent@3.972.37':
|
'@aws-sdk/middleware-user-agent@3.972.35':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@aws-sdk/util-endpoints': 3.996.8
|
'@aws-sdk/util-endpoints': 3.996.8
|
||||||
'@smithy/core': 3.23.17
|
'@smithy/core': 3.23.17
|
||||||
'@smithy/protocol-http': 5.3.14
|
'@smithy/protocol-http': 5.3.14
|
||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
'@smithy/util-retry': 4.3.6
|
'@smithy/util-retry': 4.3.4
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/nested-clients@3.997.5':
|
'@aws-sdk/nested-clients@3.997.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-crypto/sha256-browser': 5.2.0
|
'@aws-crypto/sha256-browser': 5.2.0
|
||||||
'@aws-crypto/sha256-js': 5.2.0
|
'@aws-crypto/sha256-js': 5.2.0
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/middleware-host-header': 3.972.10
|
'@aws-sdk/middleware-host-header': 3.972.10
|
||||||
'@aws-sdk/middleware-logger': 3.972.10
|
'@aws-sdk/middleware-logger': 3.972.10
|
||||||
'@aws-sdk/middleware-recursion-detection': 3.972.11
|
'@aws-sdk/middleware-recursion-detection': 3.972.11
|
||||||
'@aws-sdk/middleware-user-agent': 3.972.37
|
'@aws-sdk/middleware-user-agent': 3.972.35
|
||||||
'@aws-sdk/region-config-resolver': 3.972.13
|
'@aws-sdk/region-config-resolver': 3.972.13
|
||||||
'@aws-sdk/signature-v4-multi-region': 3.996.24
|
'@aws-sdk/signature-v4-multi-region': 3.996.22
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@aws-sdk/util-endpoints': 3.996.8
|
'@aws-sdk/util-endpoints': 3.996.8
|
||||||
'@aws-sdk/util-user-agent-browser': 3.972.10
|
'@aws-sdk/util-user-agent-browser': 3.972.10
|
||||||
'@aws-sdk/util-user-agent-node': 3.973.23
|
'@aws-sdk/util-user-agent-node': 3.973.21
|
||||||
'@smithy/config-resolver': 4.4.17
|
'@smithy/config-resolver': 4.4.17
|
||||||
'@smithy/core': 3.23.17
|
'@smithy/core': 3.23.17
|
||||||
'@smithy/fetch-http-handler': 5.3.17
|
'@smithy/fetch-http-handler': 5.3.17
|
||||||
@@ -11124,7 +11202,7 @@ snapshots:
|
|||||||
'@smithy/invalid-dependency': 4.2.14
|
'@smithy/invalid-dependency': 4.2.14
|
||||||
'@smithy/middleware-content-length': 4.2.14
|
'@smithy/middleware-content-length': 4.2.14
|
||||||
'@smithy/middleware-endpoint': 4.4.32
|
'@smithy/middleware-endpoint': 4.4.32
|
||||||
'@smithy/middleware-retry': 4.5.7
|
'@smithy/middleware-retry': 4.5.5
|
||||||
'@smithy/middleware-serde': 4.2.20
|
'@smithy/middleware-serde': 4.2.20
|
||||||
'@smithy/middleware-stack': 4.2.14
|
'@smithy/middleware-stack': 4.2.14
|
||||||
'@smithy/node-config-provider': 4.3.14
|
'@smithy/node-config-provider': 4.3.14
|
||||||
@@ -11140,7 +11218,7 @@ snapshots:
|
|||||||
'@smithy/util-defaults-mode-node': 4.2.54
|
'@smithy/util-defaults-mode-node': 4.2.54
|
||||||
'@smithy/util-endpoints': 3.4.2
|
'@smithy/util-endpoints': 3.4.2
|
||||||
'@smithy/util-middleware': 4.2.14
|
'@smithy/util-middleware': 4.2.14
|
||||||
'@smithy/util-retry': 4.3.6
|
'@smithy/util-retry': 4.3.4
|
||||||
'@smithy/util-utf8': 4.2.2
|
'@smithy/util-utf8': 4.2.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -11154,9 +11232,9 @@ snapshots:
|
|||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/s3-request-presigner@3.1040.0':
|
'@aws-sdk/s3-request-presigner@3.1037.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/signature-v4-multi-region': 3.996.24
|
'@aws-sdk/signature-v4-multi-region': 3.996.22
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@aws-sdk/util-format-url': 3.972.10
|
'@aws-sdk/util-format-url': 3.972.10
|
||||||
'@smithy/middleware-endpoint': 4.4.32
|
'@smithy/middleware-endpoint': 4.4.32
|
||||||
@@ -11165,19 +11243,19 @@ snapshots:
|
|||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/signature-v4-multi-region@3.996.24':
|
'@aws-sdk/signature-v4-multi-region@3.996.22':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/middleware-sdk-s3': 3.972.36
|
'@aws-sdk/middleware-sdk-s3': 3.972.34
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/protocol-http': 5.3.14
|
'@smithy/protocol-http': 5.3.14
|
||||||
'@smithy/signature-v4': 5.3.14
|
'@smithy/signature-v4': 5.3.14
|
||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1039.0':
|
'@aws-sdk/token-providers@3.1036.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.974.7
|
'@aws-sdk/core': 3.974.5
|
||||||
'@aws-sdk/nested-clients': 3.997.5
|
'@aws-sdk/nested-clients': 3.997.3
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/property-provider': 4.2.14
|
'@smithy/property-provider': 4.2.14
|
||||||
'@smithy/shared-ini-file-loader': 4.4.9
|
'@smithy/shared-ini-file-loader': 4.4.9
|
||||||
@@ -11221,20 +11299,19 @@ snapshots:
|
|||||||
bowser: 2.14.1
|
bowser: 2.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/util-user-agent-node@3.973.23':
|
'@aws-sdk/util-user-agent-node@3.973.21':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/middleware-user-agent': 3.972.37
|
'@aws-sdk/middleware-user-agent': 3.972.35
|
||||||
'@aws-sdk/types': 3.973.8
|
'@aws-sdk/types': 3.973.8
|
||||||
'@smithy/node-config-provider': 4.3.14
|
'@smithy/node-config-provider': 4.3.14
|
||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
'@smithy/util-config-provider': 4.2.2
|
'@smithy/util-config-provider': 4.2.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/xml-builder@3.972.22':
|
'@aws-sdk/xml-builder@3.972.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodable/entities': 2.1.0
|
|
||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
fast-xml-parser: 5.7.2
|
fast-xml-parser: 5.7.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws/lambda-invoke-store@0.2.3': {}
|
'@aws/lambda-invoke-store@0.2.3': {}
|
||||||
@@ -12116,8 +12193,6 @@ snapshots:
|
|||||||
|
|
||||||
'@csstools/css-tokenizer@3.0.3': {}
|
'@csstools/css-tokenizer@3.0.3': {}
|
||||||
|
|
||||||
'@docmost/pdf-inspector@1.9.4': {}
|
|
||||||
|
|
||||||
'@emnapi/core@1.8.1':
|
'@emnapi/core@1.8.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/wasi-threads': 1.1.0
|
'@emnapi/wasi-threads': 1.1.0
|
||||||
@@ -13118,6 +13193,54 @@ snapshots:
|
|||||||
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2':
|
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-android-arm64@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-arm64@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-x64@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-musl@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas@0.1.97':
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/canvas-android-arm64': 0.1.97
|
||||||
|
'@napi-rs/canvas-darwin-arm64': 0.1.97
|
||||||
|
'@napi-rs/canvas-darwin-x64': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-x64-musl': 0.1.97
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc': 0.1.97
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc': 0.1.97
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.12':
|
'@napi-rs/wasm-runtime@0.2.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.8.1
|
'@emnapi/core': 1.8.1
|
||||||
@@ -14680,16 +14803,16 @@ snapshots:
|
|||||||
'@smithy/util-middleware': 4.2.14
|
'@smithy/util-middleware': 4.2.14
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@smithy/middleware-retry@4.5.7':
|
'@smithy/middleware-retry@4.5.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/core': 3.23.17
|
'@smithy/core': 3.23.17
|
||||||
'@smithy/node-config-provider': 4.3.14
|
'@smithy/node-config-provider': 4.3.14
|
||||||
'@smithy/protocol-http': 5.3.14
|
'@smithy/protocol-http': 5.3.14
|
||||||
'@smithy/service-error-classification': 4.3.1
|
'@smithy/service-error-classification': 4.3.0
|
||||||
'@smithy/smithy-client': 4.12.13
|
'@smithy/smithy-client': 4.12.13
|
||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
'@smithy/util-middleware': 4.2.14
|
'@smithy/util-middleware': 4.2.14
|
||||||
'@smithy/util-retry': 4.3.6
|
'@smithy/util-retry': 4.3.4
|
||||||
'@smithy/uuid': 1.1.2
|
'@smithy/uuid': 1.1.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
@@ -14740,7 +14863,7 @@ snapshots:
|
|||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@smithy/service-error-classification@4.3.1':
|
'@smithy/service-error-classification@4.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
|
|
||||||
@@ -14840,9 +14963,9 @@ snapshots:
|
|||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@smithy/util-retry@4.3.6':
|
'@smithy/util-retry@4.3.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/service-error-classification': 4.3.1
|
'@smithy/service-error-classification': 4.3.0
|
||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
@@ -14871,7 +14994,7 @@ snapshots:
|
|||||||
'@smithy/util-buffer-from': 4.2.2
|
'@smithy/util-buffer-from': 4.2.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@smithy/util-waiter@4.3.0':
|
'@smithy/util-waiter@4.2.16':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/types': 4.14.1
|
'@smithy/types': 4.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -17731,7 +17854,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
path-expression-matcher: 1.5.0
|
path-expression-matcher: 1.5.0
|
||||||
|
|
||||||
fast-xml-parser@5.7.2:
|
fast-xml-parser@5.7.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodable/entities': 2.1.0
|
'@nodable/entities': 2.1.0
|
||||||
fast-xml-builder: 1.1.5
|
fast-xml-builder: 1.1.5
|
||||||
@@ -19504,6 +19627,9 @@ snapshots:
|
|||||||
|
|
||||||
node-int64@0.4.0: {}
|
node-int64@0.4.0: {}
|
||||||
|
|
||||||
|
node-readable-to-web-readable-stream@0.4.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-releases@2.0.27: {}
|
node-releases@2.0.27: {}
|
||||||
|
|
||||||
nodemailer@8.0.5: {}
|
nodemailer@8.0.5: {}
|
||||||
@@ -19855,6 +19981,11 @@ snapshots:
|
|||||||
|
|
||||||
pause@0.0.1: {}
|
pause@0.0.1: {}
|
||||||
|
|
||||||
|
pdfjs-dist@5.5.207:
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/canvas': 0.1.97
|
||||||
|
node-readable-to-web-readable-stream: 0.4.2
|
||||||
|
|
||||||
peberminta@0.9.0: {}
|
peberminta@0.9.0: {}
|
||||||
|
|
||||||
pend@1.2.0: {}
|
pend@1.2.0: {}
|
||||||
@@ -21248,6 +21379,8 @@ snapshots:
|
|||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
tiptap-extension-global-drag-handle@0.1.18: {}
|
||||||
|
|
||||||
tlds@1.261.0: {}
|
tlds@1.261.0: {}
|
||||||
|
|
||||||
tldts-core@6.1.72: {}
|
tldts-core@6.1.72: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user