mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat: integrations
This commit is contained in:
@@ -25,3 +25,4 @@ export * from "./lib/heading/heading";
|
||||
export * from "./lib/unique-id";
|
||||
export * from "./lib/shared-storage";
|
||||
export * from "./lib/recreate-transform";
|
||||
export * from "./lib/integration-link";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export { IntegrationLink } from "./integration-link";
|
||||
export type {
|
||||
IntegrationLinkOptions,
|
||||
IntegrationLinkAttributes,
|
||||
} from "./integration-link";
|
||||
export {
|
||||
integrationLinkPatterns,
|
||||
matchIntegrationLink,
|
||||
} from "./integration-link-patterns";
|
||||
export type { IntegrationLinkPattern } from "./integration-link-patterns";
|
||||
@@ -0,0 +1,41 @@
|
||||
export type IntegrationLinkPattern = {
|
||||
provider: string;
|
||||
regex: RegExp;
|
||||
};
|
||||
|
||||
export const integrationLinkPatterns: IntegrationLinkPattern[] = [
|
||||
// GitHub (cloud + GHE): /:owner/:repo/pull/:num or /issues/:num
|
||||
{
|
||||
provider: "github",
|
||||
regex:
|
||||
/^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/(pull|issues)\/(\d+)/,
|
||||
},
|
||||
// GitLab (cloud + self-hosted): /-/issues/:num or /-/merge_requests/:num
|
||||
{
|
||||
provider: "gitlab",
|
||||
regex:
|
||||
/^https?:\/\/[^\/]+\/(.+)\/-\/(issues|merge_requests)\/(\d+)/,
|
||||
},
|
||||
// Jira (cloud + server): /browse/KEY-123
|
||||
{
|
||||
provider: "jira",
|
||||
regex: /^https?:\/\/[^\/]+\/browse\/([A-Z][A-Z0-9]+-\d+)/,
|
||||
},
|
||||
// Linear (cloud only): /team/issue/KEY-123
|
||||
{
|
||||
provider: "linear",
|
||||
regex: /^https?:\/\/linear\.app\/([^\/]+)\/issue\/([A-Z]+-\d+)/,
|
||||
},
|
||||
];
|
||||
|
||||
export function matchIntegrationLink(
|
||||
url: string,
|
||||
): { provider: string; match: RegExpMatchArray } | null {
|
||||
for (const pattern of integrationLinkPatterns) {
|
||||
const match = url.match(pattern.regex);
|
||||
if (match) {
|
||||
return { provider: pattern.provider, match };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { sanitizeUrl } from "../utils";
|
||||
|
||||
export interface IntegrationLinkOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
view: any;
|
||||
}
|
||||
|
||||
export interface IntegrationLinkAttributes {
|
||||
url: string;
|
||||
provider: string;
|
||||
unfurlData: Record<string, any> | null;
|
||||
status: "pending" | "loaded" | "error";
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
integrationLink: {
|
||||
setIntegrationLink: (
|
||||
attributes: Partial<IntegrationLinkAttributes>,
|
||||
) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const IntegrationLink = Node.create<IntegrationLinkOptions>({
|
||||
name: "integrationLink",
|
||||
inline: false,
|
||||
group: "block",
|
||||
isolating: true,
|
||||
atom: true,
|
||||
defining: true,
|
||||
draggable: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
view: null,
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
url: {
|
||||
default: "",
|
||||
parseHTML: (element) => {
|
||||
const url = element.getAttribute("data-url");
|
||||
return sanitizeUrl(url);
|
||||
},
|
||||
renderHTML: (attributes: IntegrationLinkAttributes) => ({
|
||||
"data-url": sanitizeUrl(attributes.url),
|
||||
}),
|
||||
},
|
||||
provider: {
|
||||
default: "",
|
||||
parseHTML: (element) => element.getAttribute("data-provider"),
|
||||
renderHTML: (attributes: IntegrationLinkAttributes) => ({
|
||||
"data-provider": attributes.provider,
|
||||
}),
|
||||
},
|
||||
unfurlData: {
|
||||
default: null,
|
||||
parseHTML: (element) => {
|
||||
const data = element.getAttribute("data-unfurl");
|
||||
if (!data) return null;
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
renderHTML: (attributes: IntegrationLinkAttributes) => ({
|
||||
"data-unfurl": attributes.unfurlData
|
||||
? JSON.stringify(attributes.unfurlData)
|
||||
: null,
|
||||
}),
|
||||
},
|
||||
status: {
|
||||
default: "pending",
|
||||
parseHTML: (element) => element.getAttribute("data-status") ?? "pending",
|
||||
renderHTML: (attributes: IntegrationLinkAttributes) => ({
|
||||
"data-status": attributes.status,
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: `div[data-type="${this.name}"]`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
const url = HTMLAttributes["data-url"];
|
||||
const safeUrl = sanitizeUrl(url);
|
||||
|
||||
return [
|
||||
"div",
|
||||
mergeAttributes(
|
||||
{ "data-type": this.name },
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes,
|
||||
),
|
||||
["a", { href: safeUrl, target: "_blank", rel: "noopener" }, safeUrl],
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setIntegrationLink:
|
||||
(attrs) =>
|
||||
({ commands }) => {
|
||||
return commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: {
|
||||
...attrs,
|
||||
url: sanitizeUrl(attrs.url),
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
this.editor.isInitialized = true;
|
||||
return ReactNodeViewRenderer(this.options.view);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user