Compare commits
9 Commits
python-run
...
report-lif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c076d8e2b4 | ||
|
|
d486011399 | ||
|
|
4e88f438e9 | ||
|
|
5b6d472c96 | ||
|
|
60373ead57 | ||
|
|
a4a927799d | ||
|
|
8e75cd74f4 | ||
|
|
8252df95f2 | ||
|
|
1cd6a43417 |
2
.github/workflows/run-tests.yml
vendored
2
.github/workflows/run-tests.yml
vendored
@@ -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:
|
||||
|
||||
@@ -39,3 +39,4 @@ import "./user_provisioning";
|
||||
import "./clean_up";
|
||||
import "./metabot";
|
||||
import "./database_replication";
|
||||
import "./reports";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./Editor";
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
``,
|
||||
);
|
||||
} 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);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { PLUGIN_REPORTS } from "metabase/plugins";
|
||||
|
||||
import { getRoutes } from "./routes";
|
||||
|
||||
PLUGIN_REPORTS.getRoutes = getRoutes;
|
||||
PLUGIN_REPORTS.shouldShowReportInNewItemMenu = () => true;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Route } from "metabase/hoc/Title";
|
||||
|
||||
import { ReportPage } from "./components/ReportPage";
|
||||
|
||||
export const getRoutes = () => <Route path="report" component={ReportPage} />;
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
413
yarn.lock
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user