Compare commits

..

7 Commits

Author SHA1 Message Date
22641e1a30 fix type issue 2025-06-09 16:13:42 -07:00
e19343afba upgrade packages 2025-06-09 15:59:45 -07:00
d62bcbe9fa upgrade tiptap editor extensions 2025-06-09 15:53:15 -07:00
d8da307a61 feat: enhance excalidraw (#1240)
* WIP

* use next excalidraw version

* support local persistence for excalidraw library.

Co-authored-by: Drauggy <n.fomenko@safe-tech.ru>

---------

Co-authored-by: Drauggy <n.fomenko@safe-tech.ru>
2025-06-09 23:25:36 +01:00
50b3f9ddd9 generic iframe embed (#1234) 2025-06-09 22:32:23 +01:00
0029f84d50 feat: toggle table header row and column (#1203)
* feat: toggle table header row and column
* switch position
2025-06-09 05:39:43 +01:00
6d024fc3de feat: bulk page imports (#1219)
* refactor imports - WIP

* Add readstream

* WIP

* fix attachmentId render

* fix attachmentId render

* turndown video tag

* feat: add stream upload support and improve file handling

- Add stream upload functionality to storage drivers\n- Improve ZIP file extraction with better encoding handling\n- Fix attachment ID rendering issues\n- Add AWS S3 upload stream support\n- Update dependencies for better compatibility

* WIP

* notion formatter

* move embed parser to editor-ext package

* import embeds

* utility files

* cleanup

* Switch from happy-dom to cheerio
* Refine code

* WIP

* bug fixes and UI

* sync

* WIP

* sync

* keep import modal mounted

* Show modal during upload

* WIP

* WIP
2025-06-09 04:29:27 +01:00
12 changed files with 2124 additions and 798 deletions

View File

@ -15,45 +15,45 @@
"@docmost/editor-ext": "workspace:*", "@docmost/editor-ext": "workspace:*",
"@emoji-mart/data": "^1.2.1", "@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
"@excalidraw/excalidraw": "^0.17.6", "@excalidraw/excalidraw": "0.18.0-864353b",
"@mantine/core": "^7.17.0", "@mantine/core": "^7.17.0",
"@mantine/form": "^7.17.0", "@mantine/form": "^7.17.0",
"@mantine/hooks": "^7.17.0", "@mantine/hooks": "^7.17.0",
"@mantine/modals": "^7.17.0", "@mantine/modals": "^7.17.0",
"@mantine/notifications": "^7.17.0", "@mantine/notifications": "^7.17.0",
"@mantine/spotlight": "^7.17.0", "@mantine/spotlight": "^7.17.0",
"@tabler/icons-react": "^3.22.0", "@tabler/icons-react": "^3.34.0",
"@tanstack/react-query": "^5.61.4", "@tanstack/react-query": "^5.80.6",
"@tiptap/extension-character-count": "^2.11.5", "@tiptap/extension-character-count": "^2.14.0",
"axios": "^1.8.4", "axios": "^1.9.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"emoji-mart": "^5.6.0", "emoji-mart": "^5.6.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"highlightjs-sap-abap": "^0.3.0", "highlightjs-sap-abap": "^0.3.0",
"i18next": "^23.14.0", "i18next": "^23.14.0",
"i18next-http-backend": "^2.6.1", "i18next-http-backend": "^2.6.1",
"jotai": "^2.12.1", "jotai": "^2.12.5",
"jotai-optics": "^0.4.0", "jotai-optics": "^0.4.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"katex": "0.16.21", "katex": "0.16.22",
"lowlight": "^3.2.0", "lowlight": "^3.3.0",
"mermaid": "^11.4.1", "mermaid": "^11.6.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-arborist": "3.4.0", "react-arborist": "3.4.0",
"react-clear-modal": "^2.0.11", "react-clear-modal": "^2.0.15",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-drawio": "^1.0.1", "react-drawio": "^1.0.1",
"react-error-boundary": "^4.1.2", "react-error-boundary": "^4.1.2",
"react-helmet-async": "^2.0.5", "react-helmet-async": "^2.0.5",
"react-i18next": "^15.0.1", "react-i18next": "^15.0.1",
"react-router-dom": "^7.0.1", "react-router-dom": "^7.0.1",
"semver": "^7.7.1", "semver": "^7.7.2",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tiptap-extension-global-drag-handle": "^0.1.18", "tiptap-extension-global-drag-handle": "^0.1.18",
"zod": "^3.23.8" "zod": "^3.25.56"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.16.0", "@eslint/js": "^9.16.0",
@ -77,6 +77,6 @@
"prettier": "^3.4.1", "prettier": "^3.4.1",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"typescript-eslint": "^8.17.0", "typescript-eslint": "^8.17.0",
"vite": "^6.3.2" "vite": "^6.3.5"
} }
} }

View File

@ -36,5 +36,5 @@ export interface IVerifyUserToken {
} }
export interface ICollabToken { export interface ICollabToken {
token: string; token?: string;
} }

View File

@ -18,7 +18,10 @@ import { useForm, zodResolver } from "@mantine/form";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import i18n from "i18next"; import i18n from "i18next";
import { getEmbedProviderById, getEmbedUrlAndProvider } from '@docmost/editor-ext'; import {
getEmbedProviderById,
getEmbedUrlAndProvider,
} from "@docmost/editor-ext";
const schema = z.object({ const schema = z.object({
url: z url: z
@ -49,6 +52,10 @@ export default function EmbedView(props: NodeViewProps) {
async function onSubmit(data: { url: string }) { async function onSubmit(data: { url: string }) {
if (provider) { if (provider) {
const embedProvider = getEmbedProviderById(provider); const embedProvider = getEmbedProviderById(provider);
if (embedProvider.id === "iframe") {
updateAttributes({ src: data.url });
return;
}
if (embedProvider.regex.test(data.url)) { if (embedProvider.regex.test(data.url)) {
updateAttributes({ src: data.url }); updateAttributes({ src: data.url });
} else { } else {

View File

@ -0,0 +1,42 @@
type LibraryItems = any;
type LibraryPersistedData = {
libraryItems: LibraryItems;
};
export interface LibraryPersistenceAdapter {
load(metadata: { source: "load" | "save" }):
| Promise<{ libraryItems: LibraryItems } | null>
| {
libraryItems: LibraryItems;
}
| null;
save(libraryData: LibraryPersistedData): Promise<void> | void;
}
const LOCAL_STORAGE_KEY = "excalidrawLibrary";
export const localStorageLibraryAdapter: LibraryPersistenceAdapter = {
async load() {
try {
const data = localStorage.getItem(LOCAL_STORAGE_KEY);
if (data) {
return JSON.parse(data);
}
} catch (e) {
console.error("Error downloading Excalidraw library from localStorage", e);
}
return null;
},
async save(libraryData) {
try {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(libraryData));
} catch (e) {
console.error(
"Error while saving library from Excalidraw to localStorage",
e,
);
}
},
};

View File

@ -13,7 +13,8 @@ import { uploadFile } from "@/features/page/services/page-service.ts";
import { svgStringToFile } from "@/lib"; import { svgStringToFile } from "@/lib";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { getFileUrl } from "@/lib/config.ts"; import { getFileUrl } from "@/lib/config.ts";
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types/types"; import "@excalidraw/excalidraw/index.css";
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
import { IAttachment } from "@/lib/types"; import { IAttachment } from "@/lib/types";
import ReactClearModal from "react-clear-modal"; import ReactClearModal from "react-clear-modal";
import clsx from "clsx"; import clsx from "clsx";
@ -21,6 +22,8 @@ import { IconEdit } from "@tabler/icons-react";
import { lazy } from "react"; import { lazy } from "react";
import { Suspense } from "react"; import { Suspense } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHandleLibrary } from "@excalidraw/excalidraw";
import { localStorageLibraryAdapter } from "@/features/editor/components/excalidraw/excalidraw-utils.ts";
const Excalidraw = lazy(() => const Excalidraw = lazy(() =>
import("@excalidraw/excalidraw").then((module) => ({ import("@excalidraw/excalidraw").then((module) => ({
@ -35,6 +38,10 @@ export default function ExcalidrawView(props: NodeViewProps) {
const [excalidrawAPI, setExcalidrawAPI] = const [excalidrawAPI, setExcalidrawAPI] =
useState<ExcalidrawImperativeAPI>(null); useState<ExcalidrawImperativeAPI>(null);
useHandleLibrary({
excalidrawAPI,
adapter: localStorageLibraryAdapter,
});
const [excalidrawData, setExcalidrawData] = useState<any>(null); const [excalidrawData, setExcalidrawData] = useState<any>(null);
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const computedColorScheme = useComputedColorScheme(); const computedColorScheme = useComputedColorScheme();

View File

@ -17,8 +17,8 @@ import {
IconTable, IconTable,
IconTypography, IconTypography,
IconMenu4, IconMenu4,
IconCalendar, IconCalendar, IconAppWindow,
} from "@tabler/icons-react"; } from '@tabler/icons-react';
import { import {
CommandProps, CommandProps,
SlashMenuGroupedItemsType, SlashMenuGroupedItemsType,
@ -357,6 +357,20 @@ const CommandGroups: SlashMenuGroupedItemsType = {
.run(); .run();
}, },
}, },
{
title: "Iframe embed",
description: "Embed any Iframe",
searchTerms: ["iframe"],
icon: IconAppWindow,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setEmbed({ provider: "iframe" })
.run();
},
},
{ {
title: "Airtable", title: "Airtable",
description: "Embed Airtable", description: "Embed Airtable",

View File

@ -17,9 +17,9 @@ import {
IconColumnRemove, IconColumnRemove,
IconRowInsertBottom, IconRowInsertBottom,
IconRowInsertTop, IconRowInsertTop,
IconRowRemove, IconRowRemove, IconTableColumn, IconTableRow,
IconTrashX, IconTrashX,
} from "@tabler/icons-react"; } from '@tabler/icons-react';
import { isCellSelection } from "@docmost/editor-ext"; import { isCellSelection } from "@docmost/editor-ext";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -50,6 +50,14 @@ export const TableMenu = React.memo(
return posToDOMRect(editor.view, selection.from, selection.to); return posToDOMRect(editor.view, selection.from, selection.to);
}, [editor]); }, [editor]);
const toggleHeaderColumn = useCallback(() => {
editor.chain().focus().toggleHeaderColumn().run();
}, [editor]);
const toggleHeaderRow = useCallback(() => {
editor.chain().focus().toggleHeaderRow().run();
}, [editor]);
const addColumnLeft = useCallback(() => { const addColumnLeft = useCallback(() => {
editor.chain().focus().addColumnBefore().run(); editor.chain().focus().addColumnBefore().run();
}, [editor]); }, [editor]);
@ -180,6 +188,30 @@ export const TableMenu = React.memo(
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip position="top" label={t("Toggle header row")}
>
<ActionIcon
onClick={toggleHeaderRow}
variant="default"
size="lg"
aria-label={t("Toggle header row")}
>
<IconTableRow size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Toggle header column")}
>
<ActionIcon
onClick={toggleHeaderColumn}
variant="default"
size="lg"
aria-label={t("Toggle header column")}
>
<IconTableColumn size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Delete table")}> <Tooltip position="top" label={t("Delete table")}>
<ActionIcon <ActionIcon
onClick={deleteTable} onClick={deleteTable}

View File

@ -1,9 +1,10 @@
import api from "@/lib/api-client"; import api from "@/lib/api-client";
import { IPageHistory } from "@/features/page-history/types/page.types"; import { IPageHistory } from "@/features/page-history/types/page.types";
import { IPagination } from "@/lib/types.ts";
export async function getPageHistoryList( export async function getPageHistoryList(
pageId: string, pageId: string,
): Promise<IPageHistory[]> { ): Promise<IPagination<IPageHistory>> {
const req = await api.post("/pages/history", { const req = await api.post("/pages/history", {
pageId, pageId,
}); });

View File

@ -36,54 +36,54 @@
"@casl/ability": "^6.7.3", "@casl/ability": "^6.7.3",
"@fastify/cookie": "^11.0.2", "@fastify/cookie": "^11.0.2",
"@fastify/multipart": "^9.0.3", "@fastify/multipart": "^9.0.3",
"@fastify/static": "^8.1.1", "@fastify/static": "^8.2.0",
"@nestjs/bullmq": "^11.0.2", "@nestjs/bullmq": "^11.0.2",
"@nestjs/common": "^11.0.20", "@nestjs/common": "^11.1.3",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.20", "@nestjs/core": "^11.1.3",
"@nestjs/event-emitter": "^3.0.0", "@nestjs/event-emitter": "^3.0.1",
"@nestjs/jwt": "^11.0.0", "@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "^2.1.0", "@nestjs/mapped-types": "^2.1.0",
"@nestjs/passport": "^11.0.5", "@nestjs/passport": "^11.0.5",
"@nestjs/platform-fastify": "^11.0.20", "@nestjs/platform-fastify": "^11.1.3",
"@nestjs/platform-socket.io": "^11.0.20", "@nestjs/platform-socket.io": "^11.1.3",
"@nestjs/schedule": "^5.0.1", "@nestjs/schedule": "^6.0.0",
"@nestjs/terminus": "^11.0.0", "@nestjs/terminus": "^11.0.0",
"@nestjs/websockets": "^11.0.20", "@nestjs/websockets": "^11.1.3",
"@node-saml/passport-saml": "^5.0.1", "@node-saml/passport-saml": "^5.0.1",
"@react-email/components": "0.0.28", "@react-email/components": "0.0.28",
"@react-email/render": "1.0.2", "@react-email/render": "1.0.2",
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bullmq": "^5.41.3", "bullmq": "^5.53.2",
"cache-manager": "^6.4.0", "cache-manager": "^6.4.3",
"cheerio": "^1.0.0", "cheerio": "^1.1.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"cookie": "^1.0.2", "cookie": "^1.0.2",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"happy-dom": "^15.11.6", "happy-dom": "^15.11.6",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"kysely": "^0.27.5", "kysely": "^0.28.2",
"kysely-migration-cli": "^0.4.2", "kysely-migration-cli": "^0.4.2",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"nanoid": "3.3.11", "nanoid": "3.3.11",
"nestjs-kysely": "^1.1.0", "nestjs-kysely": "^1.2.0",
"nodemailer": "^6.10.0", "nodemailer": "^7.0.3",
"openid-client": "^5.7.1", "openid-client": "^5.7.1",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"pg": "^8.13.3", "pg": "^8.16.0",
"pg-tsquery": "^8.4.2", "pg-tsquery": "^8.4.2",
"postmark": "^4.0.5", "postmark": "^4.0.5",
"react": "^18.3.1", "react": "^18.3.1",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.2",
"sanitize-filename-ts": "^1.0.2", "sanitize-filename-ts": "^1.0.2",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"stripe": "^17.5.0", "stripe": "^17.5.0",
"tmp-promise": "^3.0.3", "tmp-promise": "^3.0.3",
"ws": "^8.18.0", "ws": "^8.18.2",
"yauzl": "^3.2.0" "yauzl": "^3.2.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -26,52 +26,52 @@
"@joplin/turndown": "^4.0.74", "@joplin/turndown": "^4.0.74",
"@joplin/turndown-plugin-gfm": "^1.0.56", "@joplin/turndown-plugin-gfm": "^1.0.56",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@tiptap/core": "^2.10.3", "@tiptap/core": "^2.14.0",
"@tiptap/extension-code-block": "^2.10.3", "@tiptap/extension-code-block": "^2.14.0",
"@tiptap/extension-code-block-lowlight": "^2.10.3", "@tiptap/extension-code-block-lowlight": "^2.14.0",
"@tiptap/extension-collaboration": "^2.10.3", "@tiptap/extension-collaboration": "^2.14.0",
"@tiptap/extension-collaboration-cursor": "^2.10.3", "@tiptap/extension-collaboration-cursor": "^2.14.0",
"@tiptap/extension-color": "^2.10.3", "@tiptap/extension-color": "^2.14.0",
"@tiptap/extension-document": "^2.10.3", "@tiptap/extension-document": "^2.14.0",
"@tiptap/extension-heading": "^2.10.3", "@tiptap/extension-heading": "^2.14.0",
"@tiptap/extension-highlight": "^2.10.3", "@tiptap/extension-highlight": "^2.14.0",
"@tiptap/extension-history": "^2.10.3", "@tiptap/extension-history": "^2.14.0",
"@tiptap/extension-image": "^2.10.3", "@tiptap/extension-image": "^2.14.0",
"@tiptap/extension-link": "^2.10.3", "@tiptap/extension-link": "^2.14.0",
"@tiptap/extension-list-item": "^2.10.3", "@tiptap/extension-list-item": "^2.14.0",
"@tiptap/extension-list-keymap": "^2.10.3", "@tiptap/extension-list-keymap": "^2.14.0",
"@tiptap/extension-placeholder": "^2.10.3", "@tiptap/extension-placeholder": "^2.14.0",
"@tiptap/extension-subscript": "^2.10.3", "@tiptap/extension-subscript": "^2.14.0",
"@tiptap/extension-superscript": "^2.10.3", "@tiptap/extension-superscript": "^2.14.0",
"@tiptap/extension-table": "^2.10.3", "@tiptap/extension-table": "^2.14.0",
"@tiptap/extension-table-cell": "^2.10.3", "@tiptap/extension-table-cell": "^2.14.0",
"@tiptap/extension-table-header": "^2.10.3", "@tiptap/extension-table-header": "^2.14.0",
"@tiptap/extension-table-row": "^2.10.3", "@tiptap/extension-table-row": "^2.14.0",
"@tiptap/extension-task-item": "^2.10.3", "@tiptap/extension-task-item": "^2.14.0",
"@tiptap/extension-task-list": "^2.10.3", "@tiptap/extension-task-list": "^2.14.0",
"@tiptap/extension-text": "^2.10.3", "@tiptap/extension-text": "^2.14.0",
"@tiptap/extension-text-align": "^2.10.3", "@tiptap/extension-text-align": "^2.14.0",
"@tiptap/extension-text-style": "^2.10.3", "@tiptap/extension-text-style": "^2.14.0",
"@tiptap/extension-typography": "^2.10.3", "@tiptap/extension-typography": "^2.14.0",
"@tiptap/extension-underline": "^2.10.3", "@tiptap/extension-underline": "^2.14.0",
"@tiptap/extension-youtube": "^2.10.3", "@tiptap/extension-youtube": "^2.14.0",
"@tiptap/html": "^2.10.3", "@tiptap/html": "^2.14.0",
"@tiptap/pm": "^2.10.3", "@tiptap/pm": "^2.14.0",
"@tiptap/react": "^2.10.3", "@tiptap/react": "^2.14.0",
"@tiptap/starter-kit": "^2.10.3", "@tiptap/starter-kit": "^2.14.0",
"@tiptap/suggestion": "^2.10.3", "@tiptap/suggestion": "^2.14.0",
"bytes": "^3.1.2", "bytes": "^3.1.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dompurify": "^3.2.4", "dompurify": "^3.2.6",
"fractional-indexing-jittered": "^1.0.0", "fractional-indexing-jittered": "^1.0.0",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"linkifyjs": "^4.2.0", "linkifyjs": "^4.2.0",
"marked": "^13.0.3", "marked": "13.0.3",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"y-indexeddb": "^9.0.12", "y-indexeddb": "^9.0.12",
"yjs": "^13.6.20" "yjs": "^13.6.27"
}, },
"devDependencies": { "devDependencies": {
"@nx/js": "20.4.5", "@nx/js": "20.4.5",

View File

@ -104,6 +104,14 @@ export const embedProviders: IEmbedProvider[] = [
return url; return url;
}, },
}, },
{
id: "iframe",
name: "Iframe",
regex: /any-iframe/,
getEmbedUrl: (match, url) => {
return url;
},
},
]; ];
export function getEmbedProviderById(id: string) { export function getEmbedProviderById(id: string) {

2661
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff