mirror of
https://github.com/docmost/docmost.git
synced 2026-08-30 02:25:01 +08:00
feat: integrations
This commit is contained in:
@@ -34,7 +34,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
withArrow
|
||||
>
|
||||
<Popover.Target>
|
||||
<Tooltip label={t("Add link")} withArrow>
|
||||
<Tooltip label={t("Add link")} withArrow withinPortal={false}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { uploadAttachmentAction } from "../attachment/upload-attachment-action";
|
||||
import { createMentionAction } from "@/features/editor/components/link/internal-link-paste.ts";
|
||||
import { INTERNAL_LINK_REGEX } from "@/lib/constants.ts";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { matchIntegrationLink } from "@docmost/editor-ext";
|
||||
|
||||
export const handlePaste = (
|
||||
editor: Editor,
|
||||
@@ -13,6 +14,21 @@ export const handlePaste = (
|
||||
) => {
|
||||
const clipboardData = event.clipboardData.getData("text/plain");
|
||||
|
||||
const integrationMatch = matchIntegrationLink(clipboardData.trim());
|
||||
if (integrationMatch && editor.state.selection.empty) {
|
||||
event.preventDefault();
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.setIntegrationLink({
|
||||
url: clipboardData.trim(),
|
||||
provider: integrationMatch.provider,
|
||||
status: "pending",
|
||||
})
|
||||
.run();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (INTERNAL_LINK_REGEX.test(clipboardData)) {
|
||||
// we have to do this validation here to allow the default link extension to takeover if needs be
|
||||
event.preventDefault();
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
.card {
|
||||
max-width: 100%;
|
||||
cursor: pointer;
|
||||
transition: border-color 150ms ease;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--mantine-color-blue-4);
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import {
|
||||
Card,
|
||||
Group,
|
||||
Text,
|
||||
Badge,
|
||||
Avatar,
|
||||
Skeleton,
|
||||
Anchor,
|
||||
Stack,
|
||||
} from "@mantine/core";
|
||||
import { useEffect, useCallback, memo } from "react";
|
||||
import { unfurlUrl } from "@/features/integration/services/integration-service";
|
||||
import classes from "./integration-link-view.module.css";
|
||||
|
||||
const providerIcons: Record<string, string> = {
|
||||
github: "https://github.githubassets.com/favicons/favicon-dark.svg",
|
||||
gitlab: "https://gitlab.com/assets/favicon-72a2cad5025aa931d6ea56c3201d1f18e68a8571da3c2571592f63571e0c5571.png",
|
||||
jira: "https://wac-cdn.atlassian.com/assets/img/favicons/atlassian/favicon.png",
|
||||
linear: "https://linear.app/favicon.ico",
|
||||
};
|
||||
|
||||
function IntegrationLinkView(props: any) {
|
||||
const { node, updateAttributes, editor } = props;
|
||||
const { url, provider, unfurlData, status } = node.attrs;
|
||||
|
||||
const doUnfurl = useCallback(async () => {
|
||||
if (status !== "pending" || !url) return;
|
||||
|
||||
try {
|
||||
const result = await unfurlUrl({ url });
|
||||
if (result) {
|
||||
updateAttributes({
|
||||
unfurlData: result,
|
||||
status: "loaded",
|
||||
});
|
||||
} else {
|
||||
updateAttributes({ status: "error" });
|
||||
}
|
||||
} catch {
|
||||
updateAttributes({ status: "error" });
|
||||
}
|
||||
}, [url, status, updateAttributes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "pending") {
|
||||
doUnfurl();
|
||||
}
|
||||
}, [status, doUnfurl]);
|
||||
|
||||
if (status === "pending") {
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle="">
|
||||
<Card className={classes.card} withBorder padding="sm" radius="sm">
|
||||
<Group gap="sm">
|
||||
<Skeleton circle height={24} />
|
||||
<Stack gap={4} style={{ flex: 1 }}>
|
||||
<Skeleton height={14} width="60%" />
|
||||
<Skeleton height={10} width="80%" />
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "error" || !unfurlData) {
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle="">
|
||||
<Card className={classes.card} withBorder padding="sm" radius="sm">
|
||||
<Anchor href={url} target="_blank" rel="noopener" size="sm">
|
||||
{url}
|
||||
</Anchor>
|
||||
</Card>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const iconUrl = providerIcons[provider] ?? undefined;
|
||||
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle="">
|
||||
<Card
|
||||
className={classes.card}
|
||||
withBorder
|
||||
padding="sm"
|
||||
radius="sm"
|
||||
component="a"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style={{ textDecoration: "none", color: "inherit" }}
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
{unfurlData.authorAvatarUrl ? (
|
||||
<Avatar src={unfurlData.authorAvatarUrl} size={28} radius="xl" />
|
||||
) : iconUrl ? (
|
||||
<Avatar src={iconUrl} size={28} radius="sm" />
|
||||
) : null}
|
||||
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Text size="sm" fw={600} truncate>
|
||||
{unfurlData.title}
|
||||
</Text>
|
||||
{unfurlData.status && (
|
||||
<Badge
|
||||
size="xs"
|
||||
variant="light"
|
||||
color={unfurlData.statusColor ?? "gray"}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{unfurlData.status}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{unfurlData.description && (
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>
|
||||
{unfurlData.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Group gap="xs">
|
||||
{iconUrl && (
|
||||
<Avatar src={iconUrl} size={14} radius="sm" />
|
||||
)}
|
||||
<Text size="xs" c="dimmed">
|
||||
{unfurlData.provider}
|
||||
</Text>
|
||||
{unfurlData.author && (
|
||||
<Text size="xs" c="dimmed">
|
||||
· {unfurlData.author}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(IntegrationLinkView);
|
||||
@@ -27,7 +27,7 @@ export const LinkPreviewPanel = ({
|
||||
<>
|
||||
<Card withBorder radius="md" padding="xs" bg="var(--mantine-color-body)">
|
||||
<Flex align="center">
|
||||
<Tooltip label={url}>
|
||||
<Tooltip label={url} withArrow withinPortal={false}>
|
||||
<Anchor
|
||||
href={url}
|
||||
target="_blank"
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
Highlight,
|
||||
UniqueID,
|
||||
SharedStorage,
|
||||
IntegrationLink,
|
||||
} from "@docmost/editor-ext";
|
||||
import {
|
||||
randomElement,
|
||||
@@ -60,6 +61,7 @@ import DrawioView from "../components/drawio/drawio-view";
|
||||
import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx";
|
||||
import EmbedView from "@/features/editor/components/embed/embed-view.tsx";
|
||||
import SubpagesView from "@/features/editor/components/subpages/subpages-view.tsx";
|
||||
import IntegrationLinkView from "@/features/editor/components/integration-link/integration-link-view.tsx";
|
||||
import { common, createLowlight } from "lowlight";
|
||||
import plaintext from "highlight.js/lib/languages/plaintext";
|
||||
import powershell from "highlight.js/lib/languages/powershell";
|
||||
@@ -231,6 +233,9 @@ export const mainExtensions = [
|
||||
Subpages.configure({
|
||||
view: SubpagesView,
|
||||
}),
|
||||
IntegrationLink.configure({
|
||||
view: IntegrationLinkView,
|
||||
}),
|
||||
MarkdownClipboard.configure({
|
||||
transformPastedText: true,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user