Compare commits

...

9 Commits

Author SHA1 Message Date
Kyle Doherty
c076d8e2b4 text fix 2025-07-22 14:46:12 -04:00
Kyle Doherty
d486011399 layout and styling tweaks 2025-07-22 14:23:24 -04:00
Aleksandr Lesnenko
4e88f438e9 reorganize question embed node files 2025-07-22 13:01:35 -04:00
Ryan Laurie
5b6d472c96 cleanup some llm garbage 2025-07-22 10:38:00 -06:00
Ryan Laurie
60373ead57 add markdown exports
# Conflicts:
#	enterprise/frontend/src/metabase-enterprise/reports/components/ReportPage.tsx
2025-07-22 10:01:30 -06:00
Ryan Laurie
a4a927799d Enable CI skipping on PRs (#61307)
* dont run tests on pr with ci skip

* static viz too

* label instead of title
2025-07-22 09:59:24 -06:00
Aleksandr Lesnenko
8e75cd74f4 Report editing (#61290)
* basic tiptap

* new menu item

* vibecoded embedding

* lint, prettier

* layout

* refactor vibecoded layout

* ui

* useless placeholder

* fix selection

* lint

* ui

* ui

* ux

* ui

* experimental, possibly revert: notebook editor
2025-07-22 11:52:14 -04:00
Aleksandr Lesnenko
8252df95f2 actually tsx 2025-07-21 12:47:54 -04:00
Aleksandr Lesnenko
1cd6a43417 report route 2025-07-21 12:18:23 -04:00
23 changed files with 2109 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ on:
jobs:
files-changed:
if: ${{ github.event.pull_request.labels.*.name != 'ci skip' }}
name: Check which files changed
runs-on: ubuntu-22.04
timeout-minutes: 5
@@ -35,6 +36,7 @@ jobs:
static-viz-files-changed:
name: Check whether static-viz files changed
if: ${{ github.event.pull_request.labels.*.name != 'ci skip' }}
runs-on: ubuntu-22.04
timeout-minutes: 10
outputs:

View File

@@ -39,3 +39,4 @@ import "./user_provisioning";
import "./clean_up";
import "./metabot";
import "./database_replication";
import "./reports";

View File

@@ -0,0 +1,113 @@
.editor {
height: 100%;
display: flex;
flex-direction: column;
}
.editorContent {
flex: 1;
cursor: text;
position: relative;
}
.editorContent :global(.ProseMirror) {
padding: 2rem 0 4rem 0;
min-height: 100%;
outline: none;
max-width: 900px;
margin: 0 auto;
}
.editorContent :global(.ProseMirror h1) {
font-size: 2.5rem;
font-weight: 600;
margin: 1.5rem 0 1rem;
line-height: 1.2;
}
.editorContent :global(.ProseMirror h2) {
font-size: 2rem;
font-weight: 600;
margin: 1.5rem 0 1rem;
line-height: 1.3;
}
.editorContent :global(.ProseMirror h3) {
font-size: 1.5rem;
font-weight: 600;
margin: 1.2rem 0 0.8rem;
line-height: 1.4;
}
.editorContent :global(.ProseMirror p) {
margin: 0.75rem 0;
line-height: 1.75;
font-size: 1rem;
}
.editorContent :global(.ProseMirror ul) {
margin: 0.75rem 0;
padding-left: 2rem;
list-style-type: disc;
}
.editorContent :global(.ProseMirror ol) {
margin: 0.75rem 0;
padding-left: 2rem;
list-style-type: decimal;
}
.editorContent :global(.ProseMirror li) {
margin: 0.25rem 0;
line-height: 1.75;
display: list-item;
}
.editorContent :global(.ProseMirror ul ul) {
list-style-type: circle;
}
.editorContent :global(.ProseMirror ul ul ul) {
list-style-type: square;
}
.editorContent :global(.ProseMirror code) {
background-color: #f3f4f6;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.875em;
}
.editorContent :global(.ProseMirror pre) {
background-color: #1e1e1e;
color: #d4d4d4;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.editorContent :global(.ProseMirror pre code) {
background: none;
padding: 0;
color: inherit;
}
.editorContent :global(.ProseMirror blockquote) {
border-left: 3px solid #4a5568;
padding-left: 1rem;
margin: 1rem 0;
color: #718096;
font-style: italic;
}
.editorContent :global(.ProseMirror hr) {
border: none;
border-top: 1px solid #e2e8f0;
margin: 2rem 0;
}
.editorContent :global(.ProseMirror:focus) {
outline: none;
}

View File

@@ -0,0 +1,104 @@
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import type React from "react";
import { useEffect } from "react";
import { Box } from "metabase/ui";
import styles from "./Editor.module.css";
import { QuestionMentionPlugin } from "./QuestionMentionPlugin";
import {
MarkdownSerializer,
serializeToMarkdown,
} from "./extensions/MarkdownExtensions";
import { QuestionEmbed } from "./extensions/QuestionEmbed";
interface EditorProps {
onEditorReady?: (editor: any) => void;
onQuestionRefsChange?: (refs: Array<{ id: number; name: string }>) => void;
}
export const Editor: React.FC<EditorProps> = ({
onEditorReady,
onQuestionRefsChange,
}) => {
const editor = useEditor({
extensions: [
StarterKit,
QuestionEmbed.configure({
HTMLAttributes: {
class: "question-embed",
},
}),
MarkdownSerializer,
],
content: "",
autofocus: true,
});
// Track question references
useEffect(() => {
if (!editor || !onQuestionRefsChange) {
return;
}
const updateQuestionRefs = () => {
const refs: Array<{ id: number; name: string }> = [];
editor.state.doc.descendants((node) => {
if (node.type.name === "questionEmbed") {
refs.push({
id: node.attrs.questionId,
name: node.attrs.customName || node.attrs.questionName,
});
}
});
onQuestionRefsChange(refs);
};
updateQuestionRefs();
editor.on("update", updateQuestionRefs);
return () => {
editor.off("update", updateQuestionRefs);
};
}, [editor, onQuestionRefsChange]);
// Notify parent when editor is ready
useEffect(() => {
if (editor && onEditorReady) {
// Add getMarkdown method to storage for easy access
editor.storage.markdown = {
getMarkdown: () => {
const markdown = serializeToMarkdown(editor.state.doc);
return markdown;
},
};
onEditorReady(editor);
}
}, [editor, onEditorReady]);
if (!editor) {
return null;
}
return (
<Box className={styles.editor}>
<Box
className={styles.editorContent}
onClick={(e) => {
// Focus editor when clicking on empty space
const target = e.target as HTMLElement;
if (
target.classList.contains(styles.editorContent) ||
target.classList.contains("ProseMirror")
) {
editor.commands.focus("end");
}
}}
>
<EditorContent editor={editor} />
<QuestionMentionPlugin editor={editor} />
</Box>
</Box>
);
};

View File

@@ -0,0 +1,19 @@
.toolbar {
position: sticky;
top: 0;
z-index: 10;
border-radius: 0;
border-bottom: 1px solid var(--mb-color-border);
background-color: var(--mb-color-bg-white);
width: 100%;
}
.toolbarContent {
max-width: 900px;
margin: 0 auto;
}
.headingButton {
font-weight: bold;
font-size: 12px;
}

View File

@@ -0,0 +1,148 @@
import type { Editor } from "@tiptap/react";
import {
ActionIcon,
Box,
Divider,
Group,
Icon,
Stack,
Tooltip,
} from "metabase/ui";
import styles from "./EditorToolbar.module.css";
interface EditorToolbarProps {
editor: Editor;
}
export const EditorToolbar = ({ editor }: EditorToolbarProps) => {
const toggleBold = () => editor.chain().focus().toggleBold().run();
const toggleItalic = () => editor.chain().focus().toggleItalic().run();
const toggleCode = () => editor.chain().focus().toggleCode().run();
const toggleHeading = (level: 1 | 2 | 3 | 4 | 5 | 6) =>
editor.chain().focus().toggleHeading({ level }).run();
const toggleBulletList = () =>
editor.chain().focus().toggleBulletList().run();
const toggleOrderedList = () =>
editor.chain().focus().toggleOrderedList().run();
const toggleBlockquote = () =>
editor.chain().focus().toggleBlockquote().run();
const insertHorizontalRule = () =>
editor.chain().focus().setHorizontalRule().run();
return (
<Box p="sm" className={styles.toolbar}>
<div className={styles.toolbarContent}>
<Stack gap="xs">
<Group gap="sm">
<Group gap="sm">
<Tooltip label="Bold (Ctrl+B)">
<ActionIcon
onClick={toggleBold}
variant={editor.isActive("bold") ? "filled" : "subtle"}
>
<Icon name="bolt" />
</ActionIcon>
</Tooltip>
<Tooltip label="Italic (Ctrl+I)">
<ActionIcon
onClick={toggleItalic}
variant={editor.isActive("italic") ? "filled" : "subtle"}
>
<Icon name="pencil" />
</ActionIcon>
</Tooltip>
<Tooltip label="Code">
<ActionIcon
onClick={toggleCode}
variant={editor.isActive("code") ? "filled" : "subtle"}
>
<Icon name="format_code" />
</ActionIcon>
</Tooltip>
</Group>
<Divider orientation="vertical" />
<Group gap="sm">
<Tooltip label="Heading 1">
<ActionIcon
onClick={() => toggleHeading(1)}
variant={
editor.isActive("heading", { level: 1 })
? "filled"
: "subtle"
}
>
{/* eslint-disable-next-line i18next/no-literal-string */}
<span className={styles.headingButton}>H1</span>
</ActionIcon>
</Tooltip>
<Tooltip label="Heading 2">
<ActionIcon
onClick={() => toggleHeading(2)}
variant={
editor.isActive("heading", { level: 2 })
? "filled"
: "subtle"
}
>
{/* eslint-disable-next-line i18next/no-literal-string */}
<span className={styles.headingButton}>H2</span>
</ActionIcon>
</Tooltip>
<Tooltip label="Heading 3">
<ActionIcon
onClick={() => toggleHeading(3)}
variant={
editor.isActive("heading", { level: 3 })
? "filled"
: "subtle"
}
>
{/* eslint-disable-next-line i18next/no-literal-string */}
<span className={styles.headingButton}>H3</span>
</ActionIcon>
</Tooltip>
</Group>
<Divider orientation="vertical" />
<Group gap="sm">
<Tooltip label="Bullet List">
<ActionIcon
onClick={toggleBulletList}
variant={editor.isActive("bulletList") ? "filled" : "subtle"}
>
<Icon name="list" />
</ActionIcon>
</Tooltip>
<Tooltip label="Numbered List">
<ActionIcon
onClick={toggleOrderedList}
variant={editor.isActive("orderedList") ? "filled" : "subtle"}
>
<Icon name="number" />
</ActionIcon>
</Tooltip>
<Tooltip label="Blockquote">
<ActionIcon
onClick={toggleBlockquote}
variant={editor.isActive("blockquote") ? "filled" : "subtle"}
>
<Icon name="curved" />
</ActionIcon>
</Tooltip>
<Tooltip label="Horizontal Rule">
<ActionIcon onClick={insertHorizontalRule} variant="subtle">
<Icon name="dash" />
</ActionIcon>
</Tooltip>
</Group>
</Group>
</Stack>
</div>
</Box>
);
};

View File

@@ -0,0 +1,210 @@
import type { Editor } from "@tiptap/react";
import { useEffect, useRef, useState } from "react";
import { useListRecentsQuery } from "metabase/api";
import Search from "metabase/entities/search";
import { getName } from "metabase/lib/name";
import { useDispatch } from "metabase/lib/redux";
import { RecentsListContent } from "metabase/nav/components/search/RecentsList/RecentsListContent";
import { SearchResults } from "metabase/nav/components/search/SearchResults";
import { Popover } from "metabase/ui";
import type {
RecentItem,
SearchModel,
UnrestrictedLinkEntity,
} from "metabase-types/api";
const MODELS_TO_SEARCH: SearchModel[] = ["card", "dataset"];
interface QuestionMentionPluginProps {
editor: Editor;
}
export const QuestionMentionPlugin = ({
editor,
}: QuestionMentionPluginProps) => {
const dispatch = useDispatch();
const [showPopover, setShowPopover] = useState(false);
const [query, setQuery] = useState("");
const [mentionRange, setMentionRange] = useState<{
from: number;
to: number;
} | null>(null);
const [anchorPos, setAnchorPos] = useState<{ x: number; y: number } | null>(
null,
);
const virtualRef = useRef<HTMLDivElement>(null);
const { data: recents = [], isLoading: isRecentsLoading } =
useListRecentsQuery(undefined, { refetchOnMountOrArgChange: true });
const filteredRecents = recents
.filter(
(item: RecentItem) => item.model === "card" || item.model === "dataset",
)
.slice(0, 5);
useEffect(() => {
if (!editor) {
return;
}
const updateHandler = () => {
const { $from } = editor.state.selection;
const text = $from.nodeBefore?.text || "";
// Check if we're typing after @
if (text && text.endsWith("@")) {
const from = $from.pos - 1;
setMentionRange({ from, to: $from.pos });
// Get cursor position
const coords = editor.view.coordsAtPos(from);
setAnchorPos({ x: coords.left, y: coords.bottom });
setShowPopover(true);
setQuery("");
} else if (mentionRange && showPopover) {
// Check if we're still in mention mode
const currentText = editor.state.doc.textBetween(
mentionRange.from,
Math.min(editor.state.doc.content.size, $from.pos),
"",
);
if (currentText.startsWith("@")) {
setQuery(currentText.slice(1));
setMentionRange({ from: mentionRange.from, to: $from.pos });
} else {
setShowPopover(false);
setMentionRange(null);
}
}
};
const keydownHandler = (event: KeyboardEvent) => {
if (!showPopover) {
return;
}
// Only prevent default for arrow keys to stop cursor movement in editor
// Let the popover dropdown handle the actual navigation
if (["ArrowUp", "ArrowDown"].includes(event.key)) {
event.preventDefault();
return;
}
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
setShowPopover(false);
setMentionRange(null);
editor.commands.focus();
}
};
editor.on("update", updateHandler);
editor.on("selectionUpdate", updateHandler);
// Add keydown listener to the editor's DOM element
const editorElement = editor.view.dom;
editorElement.addEventListener("keydown", keydownHandler, true);
return () => {
editor.off("update", updateHandler);
editor.off("selectionUpdate", updateHandler);
editorElement.removeEventListener("keydown", keydownHandler, true);
};
}, [editor, mentionRange, showPopover]);
const handleSelect = (item: UnrestrictedLinkEntity) => {
if (!mentionRange) {
return;
}
const wrappedItem = Search.wrapEntity(item, dispatch);
editor
.chain()
.focus()
.deleteRange(mentionRange)
.insertContent({
type: "questionEmbed",
attrs: {
questionId: wrappedItem.id,
questionName: wrappedItem.name,
model: wrappedItem.model,
},
})
.run();
setShowPopover(false);
setMentionRange(null);
};
const handleRecentSelect = (item: RecentItem) => {
handleSelect({
...item,
description: item.description ?? undefined,
name: getName(item),
});
};
// Position the virtual reference at cursor position
useEffect(() => {
if (virtualRef.current && anchorPos) {
virtualRef.current.style.position = "fixed";
virtualRef.current.style.left = `${anchorPos.x}px`;
virtualRef.current.style.top = `${anchorPos.y}px`;
virtualRef.current.style.width = "1px";
virtualRef.current.style.height = "1px";
}
}, [anchorPos]);
return (
<>
<Popover
opened={showPopover}
position="bottom-start"
width={320}
shadow="md"
withinPortal
closeOnClickOutside={false}
middlewares={{ flip: true, shift: true }}
onClose={() => {
setShowPopover(false);
setMentionRange(null);
}}
>
<Popover.Target>
<div
ref={virtualRef}
style={{ position: "fixed", pointerEvents: "none" }}
/>
</Popover.Target>
<Popover.Dropdown
style={{
maxHeight: 400,
overflow: "auto",
}}
>
{query.length > 0 ? (
<SearchResults
searchText={query}
forceEntitySelect
onEntitySelect={handleSelect}
models={MODELS_TO_SEARCH}
/>
) : (
<RecentsListContent
isLoading={isRecentsLoading}
results={filteredRecents}
onClick={handleRecentSelect}
/>
)}
</Popover.Dropdown>
</Popover>
</>
);
};

View File

@@ -0,0 +1,122 @@
import { Extension } from "@tiptap/core";
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
export const MarkdownSerializer = Extension.create({
name: "markdownSerializer",
addCommands() {
return {
getMarkdown:
() =>
({ editor }) => {
const { doc } = editor.state;
return serializeToMarkdown(doc);
},
setMarkdown:
(markdown: string) =>
({ commands }) => {
const html = parseMarkdownToHTML(markdown);
return commands.setContent(html);
},
};
},
addStorage() {
return {
markdown: {
getMarkdown: () => "",
},
};
},
});
export function serializeToMarkdown(doc: ProseMirrorNode): string {
let markdown = "";
doc.forEach((node) => {
if (node.type.name === "paragraph") {
let paragraphContent = "";
node.forEach((child) => {
if (child.type.name === "text") {
paragraphContent += child.text;
} else if (child.type.name === "questionEmbed") {
if (child.attrs.customName) {
paragraphContent += `{{card:${child.attrs.questionId}:${child.attrs.customName}}}`;
} else {
paragraphContent += `{{card:${child.attrs.questionId}}}`;
}
}
});
markdown += paragraphContent + "\n\n";
} else if (node.type.name === "heading") {
const level = node.attrs.level || 1;
const prefix = "#".repeat(level);
markdown += `${prefix} ${node.textContent}\n\n`;
} else if (node.type.name === "bulletList") {
node.forEach((listItem) => {
markdown += `- ${listItem.textContent}\n`;
});
markdown += "\n";
} else if (node.type.name === "orderedList") {
let index = 1;
node.forEach((listItem) => {
markdown += `${index}. ${listItem.textContent}\n`;
index++;
});
markdown += "\n";
} else if (node.type.name === "questionEmbed") {
if (node.attrs.customName) {
markdown += `{{card:${node.attrs.questionId}:${node.attrs.customName}}}\n\n`;
} else {
markdown += `{{card:${node.attrs.questionId}}}\n\n`;
}
} else if (node.type.name === "codeBlock") {
markdown += `\`\`\`${node.attrs.language || ""}\n${node.textContent}\n\`\`\`\n\n`;
} else {
markdown += node.textContent + "\n\n";
}
});
return markdown.trim();
}
function parseMarkdownToHTML(markdown: string): string {
let html = markdown
// Match both {{card:id}} and {{card:id:custom name}}
.replace(/{{card:(\d+)(?::([^}]+))?}}/g, (match, id, customName) => {
if (customName) {
return `<div data-type="question-embed" data-question-id="${id}" data-custom-name="${customName}" data-model="card"></div>`;
}
return `<div data-type="question-embed" data-question-id="${id}" data-model="card"></div>`;
})
.replace(/^### (.*$)/gim, "<h3>$1</h3>")
.replace(/^## (.*$)/gim, "<h2>$1</h2>")
.replace(/^# (.*$)/gim, "<h1>$1</h1>")
.replace(/^\* (.+)/gim, "<li>$1</li>")
.replace(/^- (.+)/gim, "<li>$1</li>")
.replace(/^\d+\. (.+)/gim, "<li>$1</li>")
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.+?)\*/g, "<em>$1</em>")
.replace(
/```(\w*)\n([\s\S]*?)```/g,
'<pre><code class="language-$1">$2</code></pre>',
)
.replace(/`(.+?)`/g, "<code>$1</code>")
.replace(/\n\n/g, "</p><p>")
.replace(/^(?!<)/, "<p>")
.replace(/(?!>)$/, "</p>");
html = html.replace(/<li>(.*?)<\/li>/g, (match) => {
if (
html.indexOf(match) > 0 &&
!html.substring(0, html.indexOf(match)).endsWith("</ul>")
) {
return "<ul>" + match;
}
return match;
});
html = html.replace(/(<\/li>)(?![\s\S]*<li>)/g, "$1</ul>");
return html;
}

View File

@@ -0,0 +1,158 @@
import { useEffect, useState } from "react";
import { t } from "ttag";
import { useCreateCardMutation } from "metabase/api";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { Notebook } from "metabase/querying/notebook/components/Notebook";
import { loadMetadataForCard } from "metabase/questions/actions";
import { getMetadata } from "metabase/selectors/metadata";
import { Box, Button, Loader, Modal, Text } from "metabase/ui";
import Question from "metabase-lib/v1/Question";
import type { Card } from "metabase-types/api";
interface ModifyQuestionModalProps {
card: Card;
isOpen: boolean;
onClose: () => void;
onSave: (newCard: Card) => void;
}
export const ModifyQuestionModal = ({
card,
isOpen,
onClose,
onSave,
}: ModifyQuestionModalProps) => {
const dispatch = useDispatch();
const metadata = useSelector(getMetadata);
const [modifiedQuestion, setModifiedQuestion] = useState<Question | null>(
null,
);
const [isLoadingMetadata, setIsLoadingMetadata] = useState(true);
const [metadataLoaded, setMetadataLoaded] = useState(false);
const [createCard] = useCreateCardMutation();
// Load metadata when modal opens
useEffect(() => {
if (isOpen && card) {
setIsLoadingMetadata(true);
setMetadataLoaded(false);
// Load metadata for the card
dispatch(loadMetadataForCard(card) as any)
.then(() => {
setMetadataLoaded(true);
})
.catch((error: any) => {
console.error("Failed to load metadata:", error);
setIsLoadingMetadata(false);
});
}
}, [isOpen, dispatch, card]);
// Create question when metadata is available
useEffect(() => {
if (metadataLoaded && metadata && card) {
// Check if we have the necessary metadata
const database = metadata.database(card.dataset_query?.database);
const table = card.dataset_query?.query?.["source-table"]
? metadata.table(card.dataset_query.query["source-table"])
: null;
if (database && (card.dataset_query?.type === "native" || table)) {
// Metadata is ready, create the question
const question = new Question(card, metadata);
setModifiedQuestion(question);
setIsLoadingMetadata(false);
}
}
}, [metadataLoaded, metadata, card]);
const handleSave = async () => {
if (!modifiedQuestion) {
return;
}
try {
const newCard = await createCard({
dataset_query: modifiedQuestion.datasetQuery(),
display: modifiedQuestion.display(),
name: t`Copy of ${card.name}`,
visualization_settings:
modifiedQuestion.card().visualization_settings || {},
collection_id: card.collection_id,
}).unwrap();
onSave(newCard);
onClose();
} catch (error) {
console.error("Failed to save modified question:", error);
}
};
return (
<Modal
opened={isOpen}
onClose={onClose}
size="80%"
title={t`Modify question`}
padding="lg"
>
{isLoadingMetadata ? (
<Box
style={{
height: "70vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Loader size="lg" />
</Box>
) : modifiedQuestion ? (
<>
<Box style={{ height: "70vh", overflow: "auto" }}>
<Notebook
question={modifiedQuestion}
isDirty={true}
isRunnable={true}
isResultDirty={true}
reportTimezone="UTC"
hasVisualizeButton={false}
updateQuestion={async (question: Question) => {
setModifiedQuestion(question);
}}
runQuestionQuery={async () => {}}
/>
</Box>
<Box
mt="lg"
style={{
display: "flex",
justifyContent: "flex-end",
gap: "0.5rem",
}}
>
<Button variant="subtle" onClick={onClose}>
{t`Cancel`}
</Button>
<Button variant="filled" onClick={handleSave}>
{t`Save and use`}
</Button>
</Box>
</>
) : (
<Box
style={{
height: "70vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Text>{t`Failed to load question data`}</Text>
</Box>
)}
</Modal>
);
};

View File

@@ -0,0 +1,53 @@
.embedContainer {
margin: 1rem 0;
}
.embedWrapper {
display: block;
width: 100%;
}
.questionEmbed {
border: 1px solid var(--mb-color-border);
border-radius: var(--mantine-radius-md);
margin: 1rem 0;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
cursor: pointer;
transition: border-color 0.2s ease;
}
.questionEmbed.selected {
border-color: var(--mb-color-brand);
outline: none;
}
.questionEmbed * {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.questionHeader {
padding: 0.5rem 1rem;
}
.questionResults {
height: 400px;
position: relative;
}
.loadingContainer {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem;
}
.errorContainer {
padding: 1rem;
color: var(--mb-color-error);
}

View File

@@ -0,0 +1,309 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { type NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import { useEffect, useRef, useState } from "react";
import { t } from "ttag";
import { useGetCardQuery, useGetCardQueryQuery } from "metabase/api";
import { Box, Icon, Loader, Menu, Text, TextInput } from "metabase/ui";
import Visualization from "metabase/visualizations/components/Visualization";
import { ModifyQuestionModal } from "./ModifyQuestionModal";
import styles from "./QuestionEmbedNode.module.css";
export interface QuestionEmbedAttributes {
questionId: number;
questionName: string;
customName?: string;
model: string;
}
export const QuestionEmbedNode = Node.create<{
HTMLAttributes: Record<string, any>;
}>({
name: "questionEmbed",
group: "block",
atom: true,
draggable: true,
selectable: true,
addAttributes() {
return {
questionId: {
default: null,
parseHTML: (element) =>
parseInt(element.getAttribute("data-question-id") || "0"),
},
questionName: {
default: "",
},
customName: {
default: null,
parseHTML: (element) => element.getAttribute("data-custom-name"),
},
model: {
default: "card",
},
};
},
parseHTML() {
return [
{
tag: 'div[data-type="question-embed"]',
},
];
},
renderHTML({ node, HTMLAttributes }) {
return [
"div",
mergeAttributes(
HTMLAttributes,
{
"data-type": "question-embed",
"data-question-id": node.attrs.questionId,
"data-question-name": node.attrs.questionName,
"data-model": node.attrs.model,
},
this.options.HTMLAttributes,
),
node.attrs.customName
? `{{card:${node.attrs.questionId}:${node.attrs.customName}}}`
: `{{card:${node.attrs.questionId}}}`,
];
},
renderText({ node }) {
if (node.attrs.customName) {
return `{{card:${node.attrs.questionId}:${node.attrs.customName}}}`;
}
return `{{card:${node.attrs.questionId}}}`;
},
addNodeView() {
return () => {
const dom = document.createElement("div");
dom.setAttribute("data-type", "question-embed");
dom.className = styles.embedContainer;
const content = document.createElement("div");
dom.appendChild(content);
return {
dom,
contentDOM: content,
};
};
},
});
export const QuestionEmbedComponent = ({
node,
updateAttributes,
selected,
editor,
getPos,
}: NodeViewProps) => {
const { questionId, questionName, customName } = node.attrs;
const { data: card, isLoading, error } = useGetCardQuery({ id: questionId });
const { data: results } = useGetCardQueryQuery({ cardId: questionId });
const [isEditingTitle, setIsEditingTitle] = useState(false);
const [editedTitle, setEditedTitle] = useState(customName || "");
const titleInputRef = useRef<HTMLInputElement>(null);
const [isModifyModalOpen, setIsModifyModalOpen] = useState(false);
const displayName = customName || card?.name || questionName;
const isGuiQuestion = card?.dataset_query?.type !== "native";
useEffect(() => {
if (isEditingTitle && titleInputRef.current) {
titleInputRef.current.focus();
titleInputRef.current.select();
}
}, [isEditingTitle]);
const handleTitleSave = () => {
const trimmedTitle = editedTitle.trim();
if (trimmedTitle && trimmedTitle !== card?.name) {
updateAttributes({ customName: trimmedTitle });
} else {
updateAttributes({ customName: null });
setEditedTitle("");
}
setIsEditingTitle(false);
};
const handleTitleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
handleTitleSave();
} else if (e.key === "Escape") {
setEditedTitle(customName || "");
setIsEditingTitle(false);
}
};
const handleReplaceQuestion = () => {
// Get the position of this node in the editor
const pos = editor.state.doc.nodeAt(0) ? getPos() : 0;
if (typeof pos === "number") {
editor
.chain()
.focus()
.setTextSelection({ from: pos, to: pos + node.nodeSize })
.deleteSelection()
.insertContent("@")
.run();
}
};
if (isLoading) {
return (
<NodeViewWrapper className={styles.embedWrapper}>
<Box className={styles.loadingContainer}>
<Loader size="sm" />
<Text>Loading question...</Text>
</Box>
</NodeViewWrapper>
);
}
if (error) {
return (
<NodeViewWrapper className={styles.embedWrapper}>
<Box className={styles.errorContainer}>
<Text color="error">{t`Failed to load question: {questionName}`}</Text>
</Box>
</NodeViewWrapper>
);
}
return (
<NodeViewWrapper className={styles.embedWrapper}>
<Box
className={`${styles.questionEmbed} ${selected ? styles.selected : ""}`}
>
{card && (
<Box className={styles.questionHeader}>
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "0.5rem",
}}
>
{isEditingTitle ? (
<TextInput
ref={titleInputRef}
value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)}
onBlur={handleTitleSave}
onKeyDown={handleTitleKeyDown}
size="md"
style={{ flex: 1 }}
styles={{
input: {
fontWeight: 700,
fontSize: "1rem",
border: "1px solid transparent",
padding: 0,
height: "auto",
minHeight: "auto",
lineHeight: 1.55,
backgroundColor: "transparent",
"&:focus": {
border: "1px solid var(--mb-color-border)",
backgroundColor: "var(--mb-color-bg-white)",
padding: "0 0.25rem",
},
},
}}
/>
) : (
<Text
size="md"
color="text-dark"
fw={700}
onClick={() => {
setEditedTitle(displayName);
setIsEditingTitle(true);
}}
style={{ cursor: "pointer", flex: 1 }}
>
{displayName}
</Text>
)}
{!isEditingTitle && (
<Menu withinPortal position="bottom-end">
<Menu.Target>
<Box
component="button"
style={{
background: "none",
border: "none",
cursor: "pointer",
padding: "0.25rem",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "4px",
}}
onClick={(e: React.MouseEvent) => e.stopPropagation()}
>
<Icon
name="ellipsis"
size={16}
color="var(--mb-color-text-medium)"
/>
</Box>
</Menu.Target>
<Menu.Dropdown>
{isGuiQuestion && (
<Menu.Item onClick={() => setIsModifyModalOpen(true)}>
{t`Modify question`}
</Menu.Item>
)}
<Menu.Item onClick={handleReplaceQuestion}>
{t`Replace question`}
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Box>
</Box>
)}
{results && card ? (
<Box className={styles.questionResults}>
<Visualization
rawSeries={[{ card, data: results.data }]}
isEditing={false}
isDashboard={false}
/>
</Box>
) : (
<Box className={styles.loadingContainer}>
<Loader size="sm" />
<Text>Loading results...</Text>
</Box>
)}
</Box>
{isModifyModalOpen && card && (
<ModifyQuestionModal
card={card}
isOpen={isModifyModalOpen}
onClose={() => setIsModifyModalOpen(false)}
onSave={(newCard) => {
updateAttributes({
questionId: newCard.id,
questionName: newCard.name,
customName: null,
});
setIsModifyModalOpen(false);
}}
/>
)}
</NodeViewWrapper>
);
};

View File

@@ -0,0 +1,11 @@
import { ReactNodeViewRenderer } from "@tiptap/react";
import { QuestionEmbedComponent, QuestionEmbedNode } from "./QuestionEmbedNode";
export * from "./QuestionEmbedNode";
export const QuestionEmbed = QuestionEmbedNode.extend({
addNodeView() {
return ReactNodeViewRenderer(QuestionEmbedComponent);
},
});

View File

@@ -0,0 +1 @@
export * from "./Editor";

View File

@@ -0,0 +1,96 @@
.reportPage {
display: flex;
flex-direction: column;
height: calc(100vh - var(--nav-sidebar-height, 60px));
background-color: var(--mb-color-bg-white);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 0;
background-color: var(--mb-color-bg-white);
gap: 1rem;
max-width: 900px;
margin: 0 auto;
width: 100%;
}
.titleInput {
flex: 1;
max-width: 600px;
border: none;
outline: none;
background: transparent;
font-size: 2rem;
font-weight: 700;
line-height: 1.2;
color: var(--mb-color-text-dark);
font-family: inherit;
padding: 0;
margin: 0;
}
.titleInput::placeholder {
color: var(--mb-color-text-light);
opacity: 0.7;
}
.titleInput:focus {
outline: none;
}
.contentArea {
display: flex;
flex: 1;
overflow: hidden;
}
.mainContent {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.documentContainer {
width: 100%;
padding: 0 0 4rem 0;
min-height: 100%;
}
.sidebar {
width: 300px;
flex-shrink: 0;
background-color: var(--mb-color-bg-light);
border-left: 1px solid var(--mb-color-border);
overflow-y: auto;
}
.questionRef {
padding: 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.questionRef:hover {
background-color: var(--mb-color-bg-medium);
}
.highlighted {
animation: highlight 2s ease-out;
}
@keyframes highlight {
0% {
background-color: var(--mb-color-brand-light);
box-shadow: 0 0 0 4px var(--mb-color-brand-light);
}
100% {
background-color: transparent;
box-shadow: none;
}
}

View File

@@ -0,0 +1,217 @@
import { useCallback, useState } from "react";
import { t } from "ttag";
import {
ActionIcon,
Box,
Button,
Flex,
Icon,
Loader,
Menu,
Paper,
Stack,
Text,
} from "metabase/ui";
import { Editor } from "./Editor";
import styles from "./ReportPage.module.css";
import { downloadFile, getDownloadableMarkdown } from "./exports";
export const ReportPage = () => {
const [editorInstance, setEditorInstance] = useState<any>(null);
const [questionRefs, setQuestionRefs] = useState<
Array<{ id: number; name: string }>
>([]);
const [isDownloading, setIsDownloading] = useState(false);
const [reportTitle, setReportTitle] = useState("");
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const handleSave = useCallback(() => {
if (!editorInstance) {
return;
}
const markdown = editorInstance.storage.markdown?.getMarkdown();
const reportData = {
title: reportTitle,
content: markdown,
questionReferences: questionRefs,
createdAt: new Date().toISOString(),
};
// eslint-disable-next-line no-console
console.log("Report data to save:", reportData);
}, [editorInstance, questionRefs, reportTitle]);
const handleSaveAndRun = useCallback(() => {
handleSave();
// TODO: Add run logic here
}, [handleSave]);
const toggleSidebar = useCallback(() => {
setIsSidebarOpen((prev) => !prev);
}, []);
const handleQuestionClick = useCallback(
(questionId: number) => {
if (!editorInstance) {
return;
}
// Find the question embed node in the document
const { doc } = editorInstance.state;
let targetPos = null;
doc.descendants((node: any, pos: number) => {
if (
node.type.name === "questionEmbed" &&
node.attrs.questionId === questionId
) {
targetPos = pos;
return false;
}
});
if (targetPos !== null) {
// Scroll to the node and highlight it
editorInstance
.chain()
.focus()
.setTextSelection(targetPos)
.scrollIntoView()
.run();
// Add highlight effect
const domNode = editorInstance.view.nodeDOM(targetPos);
if (domNode) {
domNode.classList.add(styles.highlighted);
setTimeout(() => {
domNode.classList.remove(styles.highlighted);
}, 2000);
}
}
},
[editorInstance],
);
const handleDownloadMarkdown = useCallback(() => {
if (!editorInstance) {
return;
}
(async () => {
try {
setIsDownloading(true);
const rawMarkdown = editorInstance.storage.markdown?.getMarkdown();
const processedMarkdown = await getDownloadableMarkdown(
rawMarkdown,
questionRefs,
);
downloadFile(processedMarkdown);
} catch (error) {
console.error("Failed to download markdown:", error);
} finally {
setIsDownloading(false);
}
})();
}, [questionRefs, editorInstance]);
return (
<Box className={styles.reportPage}>
<Box className={styles.contentArea}>
<Box className={styles.mainContent}>
<Flex p="sm">
<ActionIcon
variant="subtle"
size="md"
ml="auto"
onClick={toggleSidebar}
aria-label={isSidebarOpen ? t`Hide sidebar` : t`Show sidebar`}
>
<Icon name={isSidebarOpen ? "sidebar_open" : "sidebar_closed"} />
</ActionIcon>
</Flex>
<Box className={styles.documentContainer}>
<Box className={styles.header} mt="xl" pt="xl">
<input
value={reportTitle}
onChange={(event) => setReportTitle(event.currentTarget.value)}
placeholder={t`New report`}
className={styles.titleInput}
/>
<Box
style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}
>
<Button variant="filled" onClick={handleSaveAndRun} size="md">
{t`Save and run`}
</Button>
<Menu position="bottom-end">
<Menu.Target>
<ActionIcon
variant="subtle"
size="md"
aria-label={t`More options`}
>
<Icon name="ellipsis" />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={
isDownloading ? (
<Loader size="xs" />
) : (
<Icon name="download" />
)
}
onClick={handleDownloadMarkdown}
disabled={isDownloading}
>
{isDownloading ? t`Downloading...` : t`Download`}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Box>
</Box>
<Editor
onEditorReady={setEditorInstance}
onQuestionRefsChange={setQuestionRefs}
/>
</Box>
</Box>
{isSidebarOpen && (
<Box className={styles.sidebar}>
<Stack gap="lg" p="lg">
<Paper>
<Text size="sm" fw="bold" mb="sm">
{t`Question References`}
</Text>
{questionRefs.length === 0 ? (
<Text size="sm" color="text-light">
{t`No questions embedded yet`}
</Text>
) : (
<Stack gap="xs">
{questionRefs.map((ref) => (
<Box
key={ref.id}
className={styles.questionRef}
onClick={() => handleQuestionClick(ref.id)}
>
<Text size="sm">{ref.name}</Text>
</Box>
))}
</Stack>
)}
</Paper>
</Stack>
</Box>
)}
</Box>
</Box>
);
};

View File

@@ -0,0 +1,93 @@
import {
getChartImagePngDataUri,
getChartSelector,
} from "metabase/visualizations/lib/image-exports";
// Helper function to convert a question to PNG data URL using existing Metabase utilities
export const convertQuestionToPng = async (
questionId: number,
): Promise<string | null> => {
try {
// Use the existing chart selection and PNG generation system
const cardSelector = getChartSelector({ cardId: questionId });
const pngDataUrl = await getChartImagePngDataUri(cardSelector);
return pngDataUrl || null;
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Failed to convert question ${questionId} to PNG:`, error);
return null;
}
};
export const getDownloadableMarkdown = async (
markdown: string,
questionRefs: any,
): Promise<string> => {
let processedMarkdown = markdown;
// Extract card references from markdown using regex
const cardPattern = /{{card:(\d+)(?::([^}]+))?}}/g;
const cardMatches = Array.from(markdown.matchAll(cardPattern));
// eslint-disable-next-line no-console
console.log("Processing cards found in markdown:", cardMatches.length);
if (cardMatches.length === 0) {
return processedMarkdown;
}
for (let i = 0; i < cardMatches.length; i++) {
const match = cardMatches[i];
const [fullMatch, questionIdStr, customName] = match;
const questionId = parseInt(questionIdStr, 10);
try {
// Convert question to PNG using existing Metabase utilities
const pngDataUrl = await convertQuestionToPng(questionId);
if (!pngDataUrl) {
// eslint-disable-next-line no-console
console.warn(`Failed to generate PNG for question ${questionId}`);
continue;
}
// Get question name from questionRefs for fallback
const questionRef = questionRefs.find(
(ref: any) => ref.id === questionId,
);
const displayName =
customName || questionRef?.name || `Question ${questionId}`;
// Replace the specific card reference in markdown with embedded image
const specificPattern = new RegExp(
fullMatch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
"g",
);
processedMarkdown = processedMarkdown.replace(
specificPattern,
`![${displayName}](${pngDataUrl})`,
);
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Failed to process card ${questionId}:`, error);
// Keep the original card reference if conversion fails
}
}
return processedMarkdown;
};
export const downloadFile = (markdown: string) => {
// Create and download the file
const blob = new Blob([markdown], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `report-${new Date().toISOString().split("T")[0]}.md`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};

View File

@@ -0,0 +1,6 @@
import { PLUGIN_REPORTS } from "metabase/plugins";
import { getRoutes } from "./routes";
PLUGIN_REPORTS.getRoutes = getRoutes;
PLUGIN_REPORTS.shouldShowReportInNewItemMenu = () => true;

View File

@@ -0,0 +1,5 @@
import { Route } from "metabase/hoc/Title";
import { ReportPage } from "./components/ReportPage";
export const getRoutes = () => <Route path="report" component={ReportPage} />;

View File

@@ -5,7 +5,10 @@ import { t } from "ttag";
import { ForwardRefLink } from "metabase/common/components/Link";
import { useDispatch, useSelector } from "metabase/lib/redux";
import * as Urls from "metabase/lib/urls";
import { PLUGIN_EMBEDDING_IFRAME_SDK_SETUP } from "metabase/plugins";
import {
PLUGIN_EMBEDDING_IFRAME_SDK_SETUP,
PLUGIN_REPORTS,
} from "metabase/plugins";
import { setOpenModal } from "metabase/redux/ui";
import { getSetting } from "metabase/selectors/settings";
import { Box, Icon, Menu } from "metabase/ui";
@@ -90,6 +93,19 @@ const NewItemMenuView = ({
</Menu.Item>,
);
if (PLUGIN_REPORTS.shouldShowReportInNewItemMenu()) {
items.push(
<Menu.Item
key="report"
component={ForwardRefLink}
to="/report"
leftSection={<Icon name="document" />}
>
{t`Report`}
</Menu.Item>,
);
}
// This is a non-standard way of feature gating, akin to using hasPremiumFeature. Do not do this for more complex setups.
if (PLUGIN_EMBEDDING_IFRAME_SDK_SETUP.shouldShowEmbedInNewItemMenu()) {
items.push(

View File

@@ -786,3 +786,8 @@ export const PLUGIN_SMTP_OVERRIDE = {
CloudSMTPConnectionCard: PluginPlaceholder,
SMTPOverrideConnectionForm: PluginPlaceholder,
};
export const PLUGIN_REPORTS = {
getRoutes: () => null as React.ReactElement | null,
shouldShowReportInNewItemMenu: () => false,
};

View File

@@ -42,6 +42,7 @@ import {
PLUGIN_EMBEDDING_IFRAME_SDK_SETUP,
PLUGIN_LANDING_PAGE,
PLUGIN_METABOT,
PLUGIN_REPORTS,
} from "metabase/plugins";
import { QueryBuilder } from "metabase/query_builder/containers/QueryBuilder";
import { loadCurrentUser } from "metabase/redux/user";
@@ -178,6 +179,8 @@ export const getRoutes = (store) => {
component={TrashCollectionLanding}
/>
{PLUGIN_REPORTS.getRoutes()}
<Route
path="embed-iframe"
component={PLUGIN_EMBEDDING_IFRAME_SDK_SETUP.SdkIframeEmbedSetup}

View File

@@ -36,6 +36,12 @@
"@tanstack/react-table": "^8.21.2",
"@tanstack/react-virtual": "^3.13.2",
"@tippyjs/react": "^4.2.6",
"@tiptap/core": "^3.0.7",
"@tiptap/extension-mention": "^3.0.7",
"@tiptap/pm": "^3.0.7",
"@tiptap/react": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@tiptap/suggestion": "^3.0.7",
"@types/react-router": "3.0.27",
"@types/redux-auth-wrapper": "^2.0.16",
"@uiw/react-codemirror": "^4.23.6",

413
yarn.lock
View File

@@ -3151,6 +3151,11 @@
redux-thunk "^3.1.0"
reselect "^5.1.0"
"@remirror/core-constants@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f"
integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==
"@rspack/binding-darwin-arm64@1.3.11":
version "1.3.11"
resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.3.11.tgz#cf5e845545087b097f3da0b5a31a35f0ec7f1881"
@@ -4035,6 +4040,211 @@
dependencies:
tippy.js "^6.3.1"
"@tiptap/core@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.0.7.tgz#2ac4915525dbdbff259357b0ac4ef3f27f9908fe"
integrity sha512-/NC0BbekWzi5sC+s7gRrGIv33cUfuiZUG5DWx8TNedA6b6aTFPHUe+2wKRPaPQ0pfGdOWU0nsOkboUJ9dAjl4g==
"@tiptap/extension-blockquote@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-3.0.7.tgz#f38b4c13d24c44ff32e4f0865c007d0f80f382aa"
integrity sha512-bYJ7r4hYcBZ7GI0LSV0Oxb9rmy/qb0idAf/osvflG2r1tf5CsiW5NYAqlOYAsIVA2OCwXELDlRGCgeKBQ26Kyw==
"@tiptap/extension-bold@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.0.7.tgz#b371f71e66b59a1b408f7325715c6ccf45f687c6"
integrity sha512-CQG07yvrIsScLe5NplAuCkVh0sd97Udv1clAGbqfzeV8YfzpV3M7J/Vb09pWyovx3SjDqfsZpkr3RemeKEPY9Q==
"@tiptap/extension-bubble-menu@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.0.7.tgz#687b6327802ab2a11b4cc5a5705413d581258e35"
integrity sha512-/oL5kgOHm1AJtyLC6v1+txk/RI9WvI4/gDQ6oWukmT7aQHIfqvCW0DN/ahmX9nxGFAIRlbrooVxLn5Y6/P0adQ==
dependencies:
"@floating-ui/dom" "^1.0.0"
"@tiptap/extension-bullet-list@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.0.7.tgz#b4bb4afc7e85cd6d45090e1b519ba751820d49ca"
integrity sha512-9gPc3Tw2Bw7qKLbyW0s05YntE77127pOXQXcclB4I3MXAuz/K03f+DGuSRhOq9K2Oo86BPHdL5I9Ap9cmuS0Tg==
"@tiptap/extension-code-block@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-3.0.7.tgz#e42c66bcf0d3e72de3698bef08790d4fbcf30371"
integrity sha512-WifMv7N1G1Fnd2oZ+g80FjBpV/eI/fxHKCK3hw03l8LoWgeFaU/6LC93qTV6idkfia3YwiA6WnuyOqlI0FSZ9A==
"@tiptap/extension-code@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-3.0.7.tgz#680ea5097e143a50e280334312131537f7c1819f"
integrity sha512-6wdUqtXbnIuyKR7xteF2UCnsW2dLNtBKxWvAiOweA7L41HYvburh/tjbkffkNc5KP2XsKzdGbygpunwJMPj6+A==
"@tiptap/extension-document@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-3.0.7.tgz#ade2cadc3143a034460b6073a260a8192c8fc269"
integrity sha512-HJg1nPPZ9fv5oEMwpONeIfT0FjTrgNGuGAat/hgcBi/R2GUNir2/PM/3d6y8QtkR/EgkgcFakCc9azySXLmyUQ==
"@tiptap/extension-dropcursor@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.0.7.tgz#14e91fa16094948336ee62cf94eb335473e2edad"
integrity sha512-0i2XWdRgYbj6PEPC+pMcGiF/hwg0jl+MavPt1733qWzoDqMEls9cEBTQ9S4HS0TI/jbN/kNavTQ5LlI33kWrww==
"@tiptap/extension-floating-menu@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.0.7.tgz#7fea74301913412352d9edcd10760c2cd2de65fa"
integrity sha512-JJv9pV8EwTcGe2w/1hMhjAhfmvoCh8ha3Rh/9soWfe8FfwRnQQC6ykqmYWuAx1HDoS+sNYPNUbyDxIwgnbIc+w==
"@tiptap/extension-gapcursor@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.0.7.tgz#ef8d23e3cb4d7d3399ed1747fef68f5c9eb72b53"
integrity sha512-F4ERd5r59WHbY0ALBbrJ/2z9dl+7VSmsMV/ZkzTgq0TZV9KKz3SsCFcCdIZEYzRCEp69/yYtkTofN10xIa+J6A==
"@tiptap/extension-hard-break@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.0.7.tgz#16dfe01614ab7b37c64e837ef57f21f93171e038"
integrity sha512-OWrFrKp9PDs9nKJRmyPX22YoscqmoW25VZYeUfvNcAYtI84xYz871s1JmLZkpxqOyI9TafUADFiaRISDnX5EcA==
"@tiptap/extension-heading@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-3.0.7.tgz#0cbc29243cd2e6819417fe567638bced58ea3c7a"
integrity sha512-uS7fFcilFuzKEvhUgndELqlGweD+nZeLOb6oqUE5hM49vECjM7qVjVQnlhV+MH2W1w8eD08cn1lu6lDxaMOe5w==
"@tiptap/extension-horizontal-rule@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.0.7.tgz#a2e522c919ca03e21b22a0f9f8cb098958cfb74b"
integrity sha512-m0r4tzfVX3r0ZD7uvDf/GAiVr7lJjYwhZHC+M+JMhYXVI6eB9OXXzhdOIsw9W5QcmhCBaqU+VuPKUusTn4TKLg==
"@tiptap/extension-italic@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.0.7.tgz#37899906aa5b36ea992e3f1fc268542228007562"
integrity sha512-L05cehSOd7iZWI/igPb90TgQ6RKk2UuuYdatmXff3QUJpYPYct6abcrMb+CeFKJqE9vaXy46dCQkOuPW+bFwkA==
"@tiptap/extension-link@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-3.0.7.tgz#b0c77d8790786b136a0b7e5ae2e9a222780f96cf"
integrity sha512-e53MddBSVKpxxQ2JmHfyZQ2VBLwqlZxqwn0DQHFMXyCKTzpdUC0DOtkvrY7OVz6HA3yz29qR+qquQxIxcDPrfg==
dependencies:
linkifyjs "^4.2.0"
"@tiptap/extension-list-item@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-3.0.7.tgz#04f9747fda565ffa7493cc9bc27143e931960099"
integrity sha512-QfW+dtukl5v6oOA1n4wtAYev5yY78nqc2O8jHGZD18xhqNVerh2xBVIH9wOGHPz4q5Em2Ju7xbqXYl0vg2De+w==
"@tiptap/extension-list-keymap@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.0.7.tgz#34c83041143dc6bcaa444110bca80d550ef0e3bd"
integrity sha512-KJWXsyHU8E6SGmlZMHNjSg+XrkmCncJT2l5QGEjTUjlhqwulu+4psTDRio9tCdtepiasTL7qEekGWAhz9wEgzQ==
"@tiptap/extension-list@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-list/-/extension-list-3.0.7.tgz#f3402814fa97fac6a862fe87e690fdff7435c2a1"
integrity sha512-rwu5dXRO0YLyxndMHI17PoxK0x0ZaMZKRZflqOy8fSnXNwd3Tdy8/6a9tsmpgO38kOZEYuvMVaeB7J/+UeBVLg==
"@tiptap/extension-mention@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-3.0.7.tgz#f504718eaef62922d6be85ba32f93fdf771fe4a4"
integrity sha512-PHEx6NdmarjvPPvTd8D9AqK1JIaVYTsnQLxJUERakOLzujgUCToZ7FpMQDhPj97YLvF0t3jeyjZOPmFuj5kw4w==
"@tiptap/extension-ordered-list@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.0.7.tgz#8cd570dd7e42953cf0d6f31b166616ae103eb621"
integrity sha512-F/cbG0vt1cjkoJ4A65E6vpZQizZwnE4gJHKAw3ymDdCoZKYaO4OV1UTo98W/jgryORy/HLO12+hogsRvgRvK9Q==
"@tiptap/extension-paragraph@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-3.0.7.tgz#ddf56f59be5181310c2dafe50e1d746703ffca22"
integrity sha512-1lp+/CbYmm1ZnR6CNlreUIWCNQk0cBzLVgS5R8SKfVyYaXo11qQq6Yq8URLhpuge4yXkPGMhClwCLzJ9D9R+eg==
"@tiptap/extension-strike@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.0.7.tgz#dd710ddd1ffd1c1a26a8bc96a2fb6fa8b362196e"
integrity sha512-WUCd5CMgS6pg0ZGKXsaxVrnEvO/h6XUehebL0yggAsRKSoGERInR2iLfhU4p1f4zk0cD3ydNLJdqZu0H/MIABw==
"@tiptap/extension-text@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-3.0.7.tgz#aa9a833bf8508bd859ac731d1daf47be296fcb9f"
integrity sha512-yf5dNcPLB5SbQ0cQq8qyjiMj9khx4Y4EJoyrDSAok/9zYM3ULqwTPkTSZ2eW6VX/grJeyBVleeBHk1PjJ7NiVw==
"@tiptap/extension-underline@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-3.0.7.tgz#4a46cca26d78bcb2d551a1a880e13087c3d20b77"
integrity sha512-pw2v5kbkovaWaC1G2IxP7g94vmUMlRBzZlCnLEyfFxtGa9LVAsUFlFFWaYJEmq7ZPG/tblWCnFfEZuQqFVd8Sg==
"@tiptap/extensions@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.0.7.tgz#510daa0c683e82631a0b3c2989c42ba9c4a99c5d"
integrity sha512-GkXX5l7Q/543BKsC14j8M3qT+75ILb7138zy7cZoHm/s1ztV1XTknpEswBZIRZA9n6qq+Wd9g5qkbR879s6xhA==
"@tiptap/pm@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.0.7.tgz#131836ced943762237a93c3f8ecae4830e4c77f6"
integrity sha512-f8PnWjYqbMCxny8cyjbFNeIyeOYLECTa/7gj8DJr53Ns+P94b4kYIt/GkveR5KoOxsbmXi8Uc4mjcR1giQPaIQ==
dependencies:
prosemirror-changeset "^2.3.0"
prosemirror-collab "^1.3.1"
prosemirror-commands "^1.6.2"
prosemirror-dropcursor "^1.8.1"
prosemirror-gapcursor "^1.3.2"
prosemirror-history "^1.4.1"
prosemirror-inputrules "^1.4.0"
prosemirror-keymap "^1.2.2"
prosemirror-markdown "^1.13.1"
prosemirror-menu "^1.2.4"
prosemirror-model "^1.24.1"
prosemirror-schema-basic "^1.2.3"
prosemirror-schema-list "^1.5.0"
prosemirror-state "^1.4.3"
prosemirror-tables "^1.6.4"
prosemirror-trailing-node "^3.0.0"
prosemirror-transform "^1.10.2"
prosemirror-view "^1.38.1"
"@tiptap/react@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-3.0.7.tgz#274a819df05fda296c53f6c2d1224de8906e7b46"
integrity sha512-d7uq4KHi52DsN4dRYY5p1ei+uaAq0h+xUUOW9JxPspe2/xM88ZmvKBZIYlqyWix0CGx7BRNmCDLcP6toOmW/MQ==
dependencies:
"@types/use-sync-external-store" "^0.0.6"
fast-deep-equal "^3.1.3"
use-sync-external-store "^1.4.0"
optionalDependencies:
"@tiptap/extension-bubble-menu" "^3.0.7"
"@tiptap/extension-floating-menu" "^3.0.7"
"@tiptap/starter-kit@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-3.0.7.tgz#a7e20e01fb0a0dc110334b548319d235ae0ffef3"
integrity sha512-oTHZp6GXQQaZfZi8Fh7klH2YUeGq73XPF35CFw41mwdWdUUUms3ipaCKFqUyEYO21JMf3pZylJLxUucx5U7isg==
dependencies:
"@tiptap/core" "^3.0.7"
"@tiptap/extension-blockquote" "^3.0.7"
"@tiptap/extension-bold" "^3.0.7"
"@tiptap/extension-bullet-list" "^3.0.7"
"@tiptap/extension-code" "^3.0.7"
"@tiptap/extension-code-block" "^3.0.7"
"@tiptap/extension-document" "^3.0.7"
"@tiptap/extension-dropcursor" "^3.0.7"
"@tiptap/extension-gapcursor" "^3.0.7"
"@tiptap/extension-hard-break" "^3.0.7"
"@tiptap/extension-heading" "^3.0.7"
"@tiptap/extension-horizontal-rule" "^3.0.7"
"@tiptap/extension-italic" "^3.0.7"
"@tiptap/extension-link" "^3.0.7"
"@tiptap/extension-list" "^3.0.7"
"@tiptap/extension-list-item" "^3.0.7"
"@tiptap/extension-list-keymap" "^3.0.7"
"@tiptap/extension-ordered-list" "^3.0.7"
"@tiptap/extension-paragraph" "^3.0.7"
"@tiptap/extension-strike" "^3.0.7"
"@tiptap/extension-text" "^3.0.7"
"@tiptap/extension-underline" "^3.0.7"
"@tiptap/extensions" "^3.0.7"
"@tiptap/pm" "^3.0.7"
"@tiptap/suggestion@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-3.0.7.tgz#a513bd94d7fba90fddc83cabb8a7a4c35501616d"
integrity sha512-HSMvzAejdvcnVaRZOhXJWAvQqaQs3UYDZaA0ZnzgiJ/sNSbtTyn9XVbX6MfVNYrbtBua4iKaXuJwp6CP0KdHQg==
"@tokenizer/token@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
@@ -4756,6 +4966,11 @@
dependencies:
"@types/geojson" "*"
"@types/linkify-it@^5":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
"@types/lodash.clonedeep@^4.5.9":
version "4.5.9"
resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz#ea48276c7cc18d080e00bb56cf965bcceb3f0fc1"
@@ -4792,6 +5007,14 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.186.tgz#862e5514dd7bd66ada6c70ee5fce844b06c8ee97"
integrity sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==
"@types/markdown-it@^14.0.0":
version "14.1.2"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
dependencies:
"@types/linkify-it" "^5"
"@types/mdurl" "^2"
"@types/mdast@^3.0.0":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.4.tgz#8ee6b5200751b6cadb9a043ca39612693ad6cb9e"
@@ -4806,6 +5029,11 @@
dependencies:
"@types/unist" "*"
"@types/mdurl@^2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
"@types/mdx@^2.0.0":
version "2.0.13"
resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd"
@@ -5225,6 +5453,11 @@
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
"@types/use-sync-external-store@^0.0.6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc"
integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==
"@types/uuid@^9.0.1":
version "9.0.8"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
@@ -8269,7 +8502,7 @@ create-react-class@^15.5.1:
loose-envify "^1.3.1"
object-assign "^4.1.1"
crelt@^1.0.5:
crelt@^1.0.0, crelt@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
@@ -14094,6 +14327,11 @@ linkify-it@^5.0.0:
dependencies:
uc.micro "^2.0.0"
linkifyjs@^4.2.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.1.tgz#1f246ebf4be040002accd1f4535b6af7c7e37898"
integrity sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==
lint-staged@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.1.2.tgz#443636a0cfd834d5518d57d228130dc04c83d6fb"
@@ -14462,7 +14700,7 @@ map-or-similar@^1.5.0:
resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08"
integrity sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==
markdown-it@^14.1.0:
markdown-it@^14.0.0, markdown-it@^14.1.0:
version "14.1.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
@@ -15997,6 +16235,11 @@ ora@^8.0.1:
string-width "^7.0.0"
strip-ansi "^7.1.0"
orderedmap@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
os-browserify@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
@@ -17298,6 +17541,160 @@ property-information@^6.0.0:
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d"
integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==
prosemirror-changeset@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz#eee3299cfabc7a027694e9abdc4e85505e9dd5e7"
integrity sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==
dependencies:
prosemirror-transform "^1.0.0"
prosemirror-collab@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33"
integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-commands@^1.0.0, prosemirror-commands@^1.6.2:
version "1.7.1"
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz#d101fef85618b1be53d5b99ea17bee5600781b38"
integrity sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.10.2"
prosemirror-dropcursor@^1.8.1:
version "1.8.2"
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz#2ed30c4796109ddeb1cf7282372b3850528b7228"
integrity sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
prosemirror-view "^1.1.0"
prosemirror-gapcursor@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4"
integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==
dependencies:
prosemirror-keymap "^1.0.0"
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-view "^1.0.0"
prosemirror-history@^1.0.0, prosemirror-history@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.4.1.tgz#cc370a46fb629e83a33946a0e12612e934ab8b98"
integrity sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==
dependencies:
prosemirror-state "^1.2.2"
prosemirror-transform "^1.0.0"
prosemirror-view "^1.31.0"
rope-sequence "^1.3.0"
prosemirror-inputrules@^1.4.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz#e22bfaf1d6ea4fe240ad447c184af3d520d43c37"
integrity sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz#c0f6ab95f75c0b82c97e44eb6aaf29cbfc150472"
integrity sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==
dependencies:
prosemirror-state "^1.0.0"
w3c-keyname "^2.2.0"
prosemirror-markdown@^1.13.1:
version "1.13.2"
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz#863eb3fd5f57a444e4378174622b562735b1c503"
integrity sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==
dependencies:
"@types/markdown-it" "^14.0.0"
markdown-it "^14.0.0"
prosemirror-model "^1.25.0"
prosemirror-menu@^1.2.4:
version "1.2.5"
resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz#dea00e7b623cea89f4d76963bee22d2ac2343250"
integrity sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==
dependencies:
crelt "^1.0.0"
prosemirror-commands "^1.0.0"
prosemirror-history "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-model@^1.0.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.24.1, prosemirror-model@^1.25.0:
version "1.25.2"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.25.2.tgz#39ca862f76f354237efb94441dbc9f79e81cb247"
integrity sha512-BVypCAJ4SL6jOiTsDffP3Wp6wD69lRhI4zg/iT8JXjp3ccZFiq5WyguxvMKmdKFC3prhaig7wSr8dneDToHE1Q==
dependencies:
orderedmap "^2.0.0"
prosemirror-schema-basic@^1.2.3:
version "1.2.4"
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz#389ce1ec09b8a30ea9bbb92c58569cb690c2d695"
integrity sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==
dependencies:
prosemirror-model "^1.25.0"
prosemirror-schema-list@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz#5869c8f749e8745c394548bb11820b0feb1e32f5"
integrity sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.7.3"
prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080"
integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-view "^1.27.0"
prosemirror-tables@^1.6.4:
version "1.7.1"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.7.1.tgz#df2507f285c6c7563097b4904cb7c4b9e0cd724b"
integrity sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==
dependencies:
prosemirror-keymap "^1.2.2"
prosemirror-model "^1.25.0"
prosemirror-state "^1.4.3"
prosemirror-transform "^1.10.3"
prosemirror-view "^1.39.1"
prosemirror-trailing-node@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz#5bc223d4fc1e8d9145e4079ec77a932b54e19e04"
integrity sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==
dependencies:
"@remirror/core-constants" "3.0.0"
escape-string-regexp "^4.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.3, prosemirror-transform@^1.7.3:
version "1.10.4"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz#56419eac14f9f56612c806ae46f9238648f3f02e"
integrity sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==
dependencies:
prosemirror-model "^1.21.0"
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.38.1, prosemirror-view@^1.39.1:
version "1.40.1"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.40.1.tgz#4a12711b45a707b240a1789d45b99df6f13e7c16"
integrity sha512-pbwUjt3G7TlsQQHDiYSupWBhJswpLVB09xXm1YiJPdkjkh9Pe7Y51XdLh5VWIZmROLY8UpUpG03lkdhm9lzIBA==
dependencies:
prosemirror-model "^1.20.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@@ -18657,6 +19054,11 @@ rollup-pluginutils@^2.8.1:
dependencies:
estree-walker "^0.6.1"
rope-sequence@^1.3.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425"
integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
rrweb-cssom@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1"
@@ -21265,6 +21667,11 @@ use-sync-external-store@^1.0.0:
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
use-sync-external-store@^1.4.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -21450,7 +21857,7 @@ vm-browserify@^1.0.1, vm-browserify@^1.1.2:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
w3c-keyname@^2.2.4:
w3c-keyname@^2.2.0, w3c-keyname@^2.2.4:
version "2.2.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==