diff --git a/apps/web/package.json b/apps/web/package.json index 14889ab27..8f9a30483 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -37,6 +37,7 @@ "@t3-oss/env-nextjs": "^0.11.1", "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "catalog:", + "@tiptap/extension-image": "^2.14.0", "@tiptap/extension-link": "^2.22.2", "@tiptap/extension-mention": "^3.0.9", "@tiptap/extension-placeholder": "^2.14.0", diff --git a/apps/web/src/components/Editor.tsx b/apps/web/src/components/Editor.tsx index 0f576446f..87a172ab4 100644 --- a/apps/web/src/components/Editor.tsx +++ b/apps/web/src/components/Editor.tsx @@ -7,6 +7,7 @@ import type { import type { Instance as TippyInstance } from "tippy.js"; import { Button } from "@headlessui/react"; import { t } from "@lingui/core/macro"; +import Image from "@tiptap/extension-image"; import Link from "@tiptap/extension-link"; import Mention from "@tiptap/extension-mention"; import Placeholder from "@tiptap/extension-placeholder"; @@ -44,10 +45,42 @@ import { twMerge } from "tailwind-merge"; import tippy from "tippy.js"; import { Markdown } from "tiptap-markdown"; +import { env } from "next-runtime-env"; + import { getAvatarUrl } from "~/utils/helpers"; import Avatar from "./Avatar"; +import { FileAttachmentNode } from "./FileAttachment/FileAttachmentNode"; import { YouTubeNode } from "./YouTubeEmbed/YouTubeNode"; +async function uploadFile( + file: File, + cardPublicId: string, +): Promise<{ publicId: string } | null> { + const baseUrl = env("NEXT_PUBLIC_BASE_URL") ?? ""; + try { + const response = await fetch( + `${baseUrl}/api/upload/attachment?cardPublicId=${encodeURIComponent(cardPublicId)}`, + { + method: "POST", + headers: { + "Content-Type": file.type, + "x-original-filename": file.name, + }, + body: file, + }, + ); + if (!response.ok) return null; + const data = (await response.json()) as { + attachment?: { publicId?: string }; + }; + const publicId = data.attachment?.publicId; + if (!publicId) return null; + return { publicId }; + } catch { + return null; + } +} + declare module "@tiptap/core" { interface Commands { slashSuggestion: { @@ -445,6 +478,8 @@ export default function Editor({ enableYouTubeEmbed = true, placeholder, disableHeadings = false, + cardPublicId, + onFileUpload, }: { content: string | null; onChange?: (value: string) => void; @@ -454,6 +489,8 @@ export default function Editor({ enableYouTubeEmbed?: boolean; placeholder?: string; disableHeadings?: boolean; + cardPublicId?: string; + onFileUpload?: () => void; }) { const containerRef = useRef(null); @@ -550,6 +587,8 @@ export default function Editor({ superscriptTwo: false, superscriptThree: false, }), + Image.configure({ inline: false, allowBase64: false }), + FileAttachmentNode, ...(enableYouTubeEmbed ? [YouTubeNode] : []), ], content, @@ -561,7 +600,6 @@ export default function Editor({ ?.contains(event.relatedTarget as Node) ) return; - // Only trigger onBlur if the click was outside both the editor and menu if (!containerRef.current?.contains(event.relatedTarget as Node)) { onBlur?.(); } @@ -570,6 +608,73 @@ export default function Editor({ attributes: { class: "outline-none focus:outline-none focus-visible:ring-0", }, + handlePaste: (view, event) => { + if (!cardPublicId || readOnly) return false; + const items = Array.from(event.clipboardData?.items ?? []); + const fileItem = items.find((item) => item.kind === "file"); + if (!fileItem) return false; + const file = fileItem.getAsFile(); + if (!file) return false; + event.preventDefault(); + void uploadFile(file, cardPublicId).then((result) => { + if (!result) return; + const mediaUrl = `/api/media/${result.publicId}`; + if (file.type.startsWith("image/")) { + view.dispatch( + view.state.tr.replaceSelectionWith( + view.state.schema.nodes.image!.create({ src: mediaUrl }), + ), + ); + } else { + view.dispatch( + view.state.tr.replaceSelectionWith( + view.state.schema.nodes.fileAttachment!.create({ + href: mediaUrl, + filename: file.name, + }), + ), + ); + } + onFileUpload?.(); + }); + return true; + }, + handleDrop: (view, event) => { + if (!cardPublicId || readOnly) return false; + const files = Array.from(event.dataTransfer?.files ?? []); + const file = files[0]; + if (!file) return false; + event.preventDefault(); + const pos = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + void uploadFile(file, cardPublicId).then((result) => { + if (!result) return; + const mediaUrl = `/api/media/${result.publicId}`; + const insertPos = pos?.pos ?? view.state.tr.doc.content.size; + if (file.type.startsWith("image/")) { + view.dispatch( + view.state.tr.insert( + insertPos, + view.state.schema.nodes.image!.create({ src: mediaUrl }), + ), + ); + } else { + view.dispatch( + view.state.tr.insert( + insertPos, + view.state.schema.nodes.fileAttachment!.create({ + href: mediaUrl, + filename: file.name, + }), + ), + ); + } + onFileUpload?.(); + }); + return true; + }, }, editable: !readOnly, injectCSS: false, @@ -610,6 +715,12 @@ export default function Editor({ .tiptap [data-youtube] { margin: 1rem 0; } + .tiptap img { + max-width: 100%; + height: auto; + border-radius: 0.5rem; + margin: 0.5rem 0; + } `} {!readOnly && editor && } ; +} + +export const FileAttachmentNode = Node.create({ + name: "fileAttachment", + group: "block", + atom: true, + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + addAttributes() { + return { + href: { + default: null, + parseHTML: (element) => element.getAttribute("data-href"), + renderHTML: (attributes) => { + if (!attributes.href) return {}; + return { "data-href": attributes.href as string }; + }, + }, + filename: { + default: "File", + parseHTML: (element) => element.getAttribute("data-filename"), + renderHTML: (attributes) => { + return { "data-filename": attributes.filename as string }; + }, + }, + }; + }, + + parseHTML() { + return [{ tag: "div[data-file-attachment]" }]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes( + { "data-file-attachment": "" }, + this.options.HTMLAttributes, + HTMLAttributes, + ), + ]; + }, + + addNodeView() { + return ReactNodeViewRenderer(FileAttachmentNodeView); + }, +}); diff --git a/apps/web/src/components/FileAttachment/FileAttachmentNodeView.tsx b/apps/web/src/components/FileAttachment/FileAttachmentNodeView.tsx new file mode 100644 index 000000000..973766efc --- /dev/null +++ b/apps/web/src/components/FileAttachment/FileAttachmentNodeView.tsx @@ -0,0 +1,29 @@ +import type { NodeViewProps } from "@tiptap/react"; +import { NodeViewWrapper } from "@tiptap/react"; +import { HiOutlineDocumentText, HiOutlineArrowDownTray } from "react-icons/hi2"; + +export default function FileAttachmentNodeView({ node }: NodeViewProps) { + const { href, filename } = node.attrs as { + href: string; + filename: string; + }; + + return ( + + + + + {filename} + + + + + ); +} diff --git a/apps/web/src/pages/api/media/[attachmentPublicId].ts b/apps/web/src/pages/api/media/[attachmentPublicId].ts new file mode 100644 index 000000000..c47150d31 --- /dev/null +++ b/apps/web/src/pages/api/media/[attachmentPublicId].ts @@ -0,0 +1,66 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { createNextApiContext } from "@kan/api/trpc"; +import { withApiLogging } from "@kan/api/utils/apiLogging"; +import { assertPermission } from "@kan/api/utils/permissions"; +import { withRateLimit } from "@kan/api/utils/rateLimit"; +import * as cardAttachmentRepo from "@kan/db/repository/cardAttachment.repo"; +import { generateAttachmentUrl } from "@kan/shared/utils"; + +export default withRateLimit( + { points: 200, duration: 60 }, + withApiLogging(async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { attachmentPublicId } = req.query; + if ( + typeof attachmentPublicId !== "string" || + attachmentPublicId.length < 12 + ) { + return res.status(400).json({ error: "Invalid attachment ID" }); + } + + try { + const { user, db } = await createNextApiContext(req); + + if (!user) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const attachment = await cardAttachmentRepo.getByPublicId( + db, + attachmentPublicId, + ); + + if (!attachment || attachment.deletedAt) { + return res.status(404).json({ error: "Attachment not found" }); + } + + const workspaceId = attachment.card?.list?.board?.workspaceId; + if (!workspaceId) { + return res.status(404).json({ error: "Attachment not found" }); + } + + try { + await assertPermission(db, user.id, workspaceId, "card:view"); + } catch { + return res.status(403).json({ error: "Permission denied" }); + } + + const url = await generateAttachmentUrl(attachment.s3Key); + if (!url) { + return res + .status(500) + .json({ error: "Unable to generate download URL" }); + } + + res.setHeader("Cache-Control", "private, max-age=3600, immutable"); + res.setHeader("Location", url); + return res.status(302).end(); + } catch { + return res.status(500).json({ error: "Internal server error" }); + } + }), +); diff --git a/apps/web/src/views/card/components/Comment.tsx b/apps/web/src/views/card/components/Comment.tsx index 6653c563e..827b94c57 100644 --- a/apps/web/src/views/card/components/Comment.tsx +++ b/apps/web/src/views/card/components/Comment.tsx @@ -184,6 +184,8 @@ const Comment = ({ enableYouTubeEmbed={false} placeholder={t`Add comment... (type '/' to open commands or '@' to mention)`} disableHeadings={true} + cardPublicId={cardPublicId} + onFileUpload={() => void invalidateCard(utils, cardPublicId)} />
diff --git a/apps/web/src/views/card/components/NewCommentForm.tsx b/apps/web/src/views/card/components/NewCommentForm.tsx index c1a9aaded..51f279e85 100644 --- a/apps/web/src/views/card/components/NewCommentForm.tsx +++ b/apps/web/src/views/card/components/NewCommentForm.tsx @@ -67,6 +67,8 @@ const NewCommentForm = ({ enableYouTubeEmbed={false} placeholder={t`Add comment... (type '/' to open commands or '@' to mention)`} disableHeadings={true} + cardPublicId={cardPublicId} + onFileUpload={() => void invalidateCard(utils, cardPublicId)} />
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a5dc9e56..14fb21d98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,6 +133,9 @@ importers: '@tanstack/react-query': specifier: 'catalog:' version: 5.85.6(react@18.3.1) + '@tiptap/extension-image': + specifier: ^2.14.0 + version: 2.27.2(@tiptap/core@2.26.1(@tiptap/pm@2.26.1)) '@tiptap/extension-link': specifier: ^2.22.2 version: 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1) @@ -3065,95 +3068,111 @@ packages: '@react-email/body@0.2.0': resolution: {integrity: sha512-9GCWmVmKUAoRfloboCd+RKm6X17xn7eGL7HnpAZUnjBXBilWCxsKnLMTC/ixSHDKS/A/057M1Tx6ZUXd89sVBw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/button@0.2.0': resolution: {integrity: sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/code-block@0.2.0': resolution: {integrity: sha512-eIrPW9PIFgDopQU0e/OPpwCW2QWQDtNZDSsiN4sJO8KdMnWWnXJicnRfzrit5rHwFo+Y98i+w/Y5ScnBAFr1dQ==} engines: {node: '>=22.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/code-inline@0.0.5': resolution: {integrity: sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/column@0.0.13': resolution: {integrity: sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/components@1.0.1': resolution: {integrity: sha512-HnL0Y/up61sOBQT2cQg9N/kCoW0bP727gDs2MkFWQYELg6+iIHidMDvENXFC0f1ZE6hTB+4t7sszptvTcJWsDA==} engines: {node: '>=22.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/container@0.0.15': resolution: {integrity: sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/font@0.0.9': resolution: {integrity: sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/head@0.0.12': resolution: {integrity: sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/heading@0.0.15': resolution: {integrity: sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/hr@0.0.11': resolution: {integrity: sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/html@0.0.11': resolution: {integrity: sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/img@0.0.11': resolution: {integrity: sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/link@0.0.12': resolution: {integrity: sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/markdown@0.0.17': resolution: {integrity: sha512-6op3AfsBC9BJKkhG+eoMFRFWlr0/f3FYbtQrK+VhGzJocEAY0WINIFN+W8xzXr//3IL0K/aKtnH3FtpIuescQQ==} engines: {node: '>=22.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/preview@0.0.13': resolution: {integrity: sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc @@ -3167,18 +3186,21 @@ packages: '@react-email/row@0.0.12': resolution: {integrity: sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/section@0.0.16': resolution: {integrity: sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc '@react-email/tailwind@2.0.1': resolution: {integrity: sha512-/xq0IDYVY7863xPY7cdI45Xoz7M6CnIQBJcQvbqN7MNVpopfH9f+mhjayV1JGfKaxlGWuxfLKhgi9T2shsnEFg==} engines: {node: '>=22.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: '@react-email/body': 0.2.0 '@react-email/button': 0.2.0 @@ -3217,6 +3239,7 @@ packages: '@react-email/text@0.1.5': resolution: {integrity: sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc @@ -3908,6 +3931,11 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 + '@tiptap/extension-image@2.27.2': + resolution: {integrity: sha512-5zL/BY41FIt72azVrCrv3n+2YJ/JyO8wxCcA4Dk1eXIobcgVyIdo4rG39gCqIOiqziAsqnqoj12QHTBtHsJ6mQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-italic@2.26.1': resolution: {integrity: sha512-pOs6oU4LyGO89IrYE4jbE8ZYsPwMMIiKkYfXcfeD9NtpGNBnjeVXXF5I9ndY2ANrCAgC8k58C3/powDRf0T2yA==} peerDependencies: @@ -4582,6 +4610,7 @@ packages: basic-ftp@5.0.5: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.1, please upgrade before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} @@ -5786,25 +5815,29 @@ packages: glob@10.3.10: resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} @@ -12333,6 +12366,10 @@ snapshots: '@tiptap/core': 2.26.1(@tiptap/pm@2.26.1) '@tiptap/pm': 2.26.1 + '@tiptap/extension-image@2.27.2(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))': + dependencies: + '@tiptap/core': 2.26.1(@tiptap/pm@2.26.1) + '@tiptap/extension-italic@2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))': dependencies: '@tiptap/core': 2.26.1(@tiptap/pm@2.26.1)