diff --git a/apps/server/src/collaboration/collaboration.util.ts b/apps/server/src/collaboration/collaboration.util.ts index d8802b34..d50001b8 100644 --- a/apps/server/src/collaboration/collaboration.util.ts +++ b/apps/server/src/collaboration/collaboration.util.ts @@ -38,6 +38,8 @@ import { Columns, Column, Status, + WikilinkNode, + DatabaseView, addUniqueIdsToDoc, htmlToMarkdown, } from '@docmost/editor-ext'; @@ -101,6 +103,8 @@ export const tiptapExtensions = [ Columns, Column, Status, + WikilinkNode, + DatabaseView, ] as any; export function jsonToHtml(tiptapJson: any) { diff --git a/packages/editor-ext/src/index.ts b/packages/editor-ext/src/index.ts index f338bcfa..59cbd9ab 100644 --- a/packages/editor-ext/src/index.ts +++ b/packages/editor-ext/src/index.ts @@ -30,4 +30,6 @@ export * from "./lib/columns"; export * from "./lib/status"; export * from "./lib/pdf"; export * from "./lib/resizable-nodeview"; +export * from "./lib/wikilink"; +export * from "./lib/database-view"; diff --git a/packages/editor-ext/src/lib/database-view.ts b/packages/editor-ext/src/lib/database-view.ts new file mode 100644 index 00000000..c09f5588 --- /dev/null +++ b/packages/editor-ext/src/lib/database-view.ts @@ -0,0 +1,88 @@ +import { Node, mergeAttributes } from "@tiptap/core"; + +export interface DatabaseViewAttrs { + tableId: string; + viewId: string; + viewType: string; + bridgeUrl?: string | null; +} + +declare module "@tiptap/core" { + interface Commands { + databaseView: { + insertDatabaseView: (attrs: DatabaseViewAttrs) => ReturnType; + }; + } +} + +/** + * Shared DatabaseView node (schema only, no NodeView). + * + * Registered on the Hocuspocus server so embedded Baserow views survive + * collab saves. The client extends this node to attach the React renderer. + */ +export const DatabaseView = Node.create({ + name: "database-view", + group: "block", + atom: true, + selectable: true, + draggable: true, + + addAttributes() { + return { + tableId: { + default: "", + parseHTML: (el: HTMLElement) => el.getAttribute("data-table-id") ?? "", + renderHTML: (attrs) => ({ "data-table-id": attrs.tableId }), + }, + viewId: { + default: "", + parseHTML: (el: HTMLElement) => el.getAttribute("data-view-id") ?? "", + renderHTML: (attrs) => ({ "data-view-id": attrs.viewId }), + }, + viewType: { + default: "grid", + parseHTML: (el: HTMLElement) => + el.getAttribute("data-view-type") ?? "grid", + renderHTML: (attrs) => ({ "data-view-type": attrs.viewType }), + }, + bridgeUrl: { + default: null, + parseHTML: (el: HTMLElement) => + el.getAttribute("data-bridge-url") ?? null, + renderHTML: (attrs) => + attrs.bridgeUrl ? { "data-bridge-url": attrs.bridgeUrl } : {}, + }, + }; + }, + + parseHTML() { + return [{ tag: "div[data-node-type=database-view]" }]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes(HTMLAttributes, { "data-node-type": "database-view" }), + ]; + }, + + addCommands() { + return { + insertDatabaseView: + (attrs: DatabaseViewAttrs) => + ({ commands }) => + commands.insertContent({ + type: this.name, + attrs: { + tableId: attrs.tableId, + viewId: attrs.viewId, + viewType: attrs.viewType, + bridgeUrl: attrs.bridgeUrl ?? null, + }, + }), + }; + }, +}); + +export default DatabaseView; diff --git a/packages/editor-ext/src/lib/wikilink.ts b/packages/editor-ext/src/lib/wikilink.ts new file mode 100644 index 00000000..f23ff871 --- /dev/null +++ b/packages/editor-ext/src/lib/wikilink.ts @@ -0,0 +1,99 @@ +import { Node } from "@tiptap/core"; + +export interface WikilinkAttrs { + pageId: string | null; + slugId?: string | null; + spaceSlug?: string | null; + title: string; + alias: string | null; +} + +declare module "@tiptap/core" { + interface Commands { + wikilink: { + insertWikilink: (attrs: WikilinkAttrs) => ReturnType; + }; + } +} + +/** + * Shared Wikilink node (schema only, no NodeView). + * + * Lives in @docmost/editor-ext so both the Hocuspocus server and the React + * client share the exact same schema. The client extends this node to add + * the React NodeView, input rule, and suggestion plugin. + * + * Without registering this node on the server, Hocuspocus' jsonToNode strips + * wikilink nodes on save (unknown node type). + */ +export const WikilinkNode = Node.create({ + name: "wikilink", + group: "inline", + inline: true, + atom: true, + selectable: true, + + addAttributes() { + return { + pageId: { + default: null, + parseHTML: (el: HTMLElement) => el.getAttribute("data-page-id"), + renderHTML: (attrs) => + attrs.pageId ? { "data-page-id": attrs.pageId } : {}, + }, + slugId: { + default: null, + parseHTML: (el: HTMLElement) => el.getAttribute("data-slug-id"), + renderHTML: (attrs) => + attrs.slugId ? { "data-slug-id": attrs.slugId } : {}, + }, + spaceSlug: { + default: null, + parseHTML: (el: HTMLElement) => el.getAttribute("data-space-slug"), + renderHTML: (attrs) => + attrs.spaceSlug ? { "data-space-slug": attrs.spaceSlug } : {}, + }, + title: { + default: "", + parseHTML: (el: HTMLElement) => + el.getAttribute("data-title") ?? el.textContent ?? "", + renderHTML: (attrs) => ({ "data-title": attrs.title }), + }, + alias: { + default: null, + parseHTML: (el: HTMLElement) => el.getAttribute("data-alias"), + renderHTML: (attrs) => + attrs.alias ? { "data-alias": attrs.alias } : {}, + }, + }; + }, + + parseHTML() { + return [{ tag: "span[data-wikilink]" }]; + }, + + renderHTML({ HTMLAttributes, node }) { + const display = node.attrs.alias ?? node.attrs.title ?? "?"; + const isBroken = !node.attrs.pageId; + return [ + "span", + { + "data-wikilink": "true", + ...HTMLAttributes, + class: isBroken ? "wikilink wikilink--broken" : "wikilink", + }, + `[[${display}]]`, + ]; + }, + + addCommands() { + return { + insertWikilink: + (attrs: WikilinkAttrs) => + ({ commands }) => + commands.insertContent({ type: this.name, attrs }), + }; + }, +}); + +export default WikilinkNode;