mirror of
https://github.com/docmost/docmost.git
synced 2026-08-23 04:32:11 +10:00
wip
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
export { IntegrationLink } from "./integration-link";
|
||||
export { IntegrationLink, createIntegrationAttributes } from "./integration-link";
|
||||
export type {
|
||||
IntegrationLinkOptions,
|
||||
IntegrationLinkAttributes,
|
||||
} from "./integration-link";
|
||||
export { IntegrationMention } from "./integration-mention";
|
||||
export {
|
||||
integrationLinkPatterns,
|
||||
matchIntegrationLink,
|
||||
|
||||
@@ -4,6 +4,19 @@ export type IntegrationLinkPattern = {
|
||||
};
|
||||
|
||||
export const integrationLinkPatterns: IntegrationLinkPattern[] = [
|
||||
// Slack message permalink (host-specific; must precede the host-agnostic
|
||||
// GitHub patterns, whose repo form would swallow /archives/<channel>)
|
||||
{
|
||||
provider: "slack",
|
||||
regex:
|
||||
/^https?:\/\/[a-z0-9-]+\.slack\.com\/archives\/([a-zA-Z0-9-]+)\/p(\d+)(?:\?thread_ts=[\d.]+&cid=[A-Za-z\d]+)?$/,
|
||||
},
|
||||
// Slack channel
|
||||
{
|
||||
provider: "slack",
|
||||
regex:
|
||||
/^https?:\/\/[a-z0-9-]+\.slack\.com\/archives\/([a-zA-Z0-9-]+)\/?$/,
|
||||
},
|
||||
// GitHub PR commit (must be before generic PR pattern)
|
||||
{
|
||||
provider: "github",
|
||||
@@ -76,6 +89,18 @@ export const integrationLinkPatterns: IntegrationLinkPattern[] = [
|
||||
regex:
|
||||
/^https?:\/\/[^\/]+\/(.+)\/-\/issues\/(\d+)/,
|
||||
},
|
||||
// GitLab work item (new URL format for issues)
|
||||
{
|
||||
provider: "gitlab",
|
||||
regex:
|
||||
/^https?:\/\/[^\/]+\/(.+)\/-\/work_items\/(\d+)/,
|
||||
},
|
||||
// GitLab work item opened as a drawer over the list (?show=base64 payload)
|
||||
{
|
||||
provider: "gitlab",
|
||||
regex:
|
||||
/^https?:\/\/[^\/]+\/(.+)\/-\/work_items\/?\?(?:.*&)?show=/,
|
||||
},
|
||||
// GitLab commit
|
||||
{
|
||||
provider: "gitlab",
|
||||
|
||||
@@ -14,6 +14,55 @@ export interface IntegrationLinkAttributes {
|
||||
status: "pending" | "loaded" | "error";
|
||||
}
|
||||
|
||||
// Shared by IntegrationLink (block) and IntegrationMention (inline) so both
|
||||
// serialize the same attrs and stay convertible into each other.
|
||||
export function createIntegrationAttributes() {
|
||||
return {
|
||||
url: {
|
||||
default: "",
|
||||
parseHTML: (element: HTMLElement) => {
|
||||
const url = element.getAttribute("data-url");
|
||||
return sanitizeUrl(url);
|
||||
},
|
||||
renderHTML: (attributes: IntegrationLinkAttributes) => ({
|
||||
"data-url": sanitizeUrl(attributes.url),
|
||||
}),
|
||||
},
|
||||
provider: {
|
||||
default: "",
|
||||
parseHTML: (element: HTMLElement) => element.getAttribute("data-provider"),
|
||||
renderHTML: (attributes: IntegrationLinkAttributes) => ({
|
||||
"data-provider": attributes.provider,
|
||||
}),
|
||||
},
|
||||
unfurlData: {
|
||||
default: null,
|
||||
parseHTML: (element: HTMLElement) => {
|
||||
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: HTMLElement) =>
|
||||
element.getAttribute("data-status") ?? "pending",
|
||||
renderHTML: (attributes: IntegrationLinkAttributes) => ({
|
||||
"data-status": attributes.status,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
integrationLink: {
|
||||
@@ -41,49 +90,7 @@ export const IntegrationLink = Node.create<IntegrationLinkOptions>({
|
||||
},
|
||||
|
||||
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,
|
||||
}),
|
||||
},
|
||||
};
|
||||
return createIntegrationAttributes();
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { sanitizeUrl } from "../utils";
|
||||
import {
|
||||
createIntegrationAttributes,
|
||||
IntegrationLinkAttributes,
|
||||
IntegrationLinkOptions,
|
||||
} from "./integration-link";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
integrationMention: {
|
||||
setIntegrationMention: (
|
||||
attributes: Partial<IntegrationLinkAttributes>,
|
||||
) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Inline counterpart of IntegrationLink: same attrs, flows with text.
|
||||
export const IntegrationMention = Node.create<IntegrationLinkOptions>({
|
||||
name: "integrationMention",
|
||||
inline: true,
|
||||
group: "inline",
|
||||
atom: true,
|
||||
selectable: true,
|
||||
draggable: false,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
view: null,
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return createIntegrationAttributes();
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: `span[data-type="${this.name}"]`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
const url = HTMLAttributes["data-url"];
|
||||
const safeUrl = sanitizeUrl(url);
|
||||
|
||||
return [
|
||||
"span",
|
||||
mergeAttributes(
|
||||
{ "data-type": this.name },
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes,
|
||||
),
|
||||
["a", { href: safeUrl, target: "_blank", rel: "noopener" }, safeUrl],
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setIntegrationMention:
|
||||
(attrs) =>
|
||||
({ commands }) => {
|
||||
return commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: {
|
||||
...attrs,
|
||||
url: sanitizeUrl(attrs.url),
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(this.options.view);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user