mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
chore: implement node renderer components
This commit is contained in:
@ -46,7 +46,6 @@ enum NodeType {
|
|||||||
ESCAPING_CHARACTER = 19;
|
ESCAPING_CHARACTER = 19;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the Node message.
|
|
||||||
message Node {
|
message Node {
|
||||||
NodeType type = 1;
|
NodeType type = 1;
|
||||||
oneof node {
|
oneof node {
|
||||||
|
@ -1092,7 +1092,7 @@
|
|||||||
<a name="memos-api-v2-Node"></a>
|
<a name="memos-api-v2-Node"></a>
|
||||||
|
|
||||||
### Node
|
### Node
|
||||||
Define the Node message.
|
|
||||||
|
|
||||||
|
|
||||||
| Field | Type | Label | Description |
|
| Field | Type | Label | Description |
|
||||||
|
@ -215,7 +215,6 @@ func (x *ParseMarkdownResponse) GetNodes() []*Node {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the Node message.
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
|
18
web/src/components/MemoContentV1/Blockquote.tsx
Normal file
18
web/src/components/MemoContentV1/Blockquote.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Blockquote: React.FC<Props> = ({ children }: Props) => {
|
||||||
|
return (
|
||||||
|
<blockquote>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</blockquote>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Blockquote;
|
19
web/src/components/MemoContentV1/Bold.tsx
Normal file
19
web/src/components/MemoContentV1/Bold.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Bold: React.FC<Props> = ({ children }: Props) => {
|
||||||
|
return (
|
||||||
|
<strong>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</strong>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Bold;
|
14
web/src/components/MemoContentV1/BoldItalic.tsx
Normal file
14
web/src/components/MemoContentV1/BoldItalic.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BoldItalic: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return (
|
||||||
|
<strong>
|
||||||
|
<em>{content}</em>
|
||||||
|
</strong>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BoldItalic;
|
9
web/src/components/MemoContentV1/Code.tsx
Normal file
9
web/src/components/MemoContentV1/Code.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Code: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return <code>{content}</code>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Code;
|
42
web/src/components/MemoContentV1/CodeBlock.tsx
Normal file
42
web/src/components/MemoContentV1/CodeBlock.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import hljs from "highlight.js";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
language: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeBlock: React.FC<Props> = ({ language, content }: Props) => {
|
||||||
|
const formatedLanguage = language.toLowerCase() || "plaintext";
|
||||||
|
let highlightedCode = hljs.highlightAuto(content).value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const temp = hljs.highlight(content, {
|
||||||
|
language: formatedLanguage,
|
||||||
|
}).value;
|
||||||
|
highlightedCode = temp;
|
||||||
|
} catch (error) {
|
||||||
|
// Skip error and use default highlighted code.
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopyButtonClick = () => {
|
||||||
|
copy(content);
|
||||||
|
toast.success("Copied to clipboard!");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre className="group w-full my-1 p-3 rounded bg-gray-100 dark:bg-zinc-600 whitespace-pre-wrap relative">
|
||||||
|
<button
|
||||||
|
className="text-xs font-mono italic absolute top-0 right-0 px-2 leading-6 border btn-text rounded opacity-0 group-hover:opacity-60"
|
||||||
|
onClick={handleCopyButtonClick}
|
||||||
|
>
|
||||||
|
copy
|
||||||
|
</button>
|
||||||
|
<code className={classNames(`language-${formatedLanguage}`, "block")} dangerouslySetInnerHTML={{ __html: highlightedCode }}></code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeBlock;
|
9
web/src/components/MemoContentV1/EscapingCharacter.tsx
Normal file
9
web/src/components/MemoContentV1/EscapingCharacter.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EscapingCharacter: React.FC<Props> = ({ symbol }: Props) => {
|
||||||
|
return <span>{symbol}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EscapingCharacter;
|
20
web/src/components/MemoContentV1/Heading.tsx
Normal file
20
web/src/components/MemoContentV1/Heading.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
level: number;
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Heading: React.FC<Props> = ({ level, children }: Props) => {
|
||||||
|
const Head = `h${level}` as keyof JSX.IntrinsicElements;
|
||||||
|
return (
|
||||||
|
<Head>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</Head>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Heading;
|
9
web/src/components/MemoContentV1/HorizontalRule.tsx
Normal file
9
web/src/components/MemoContentV1/HorizontalRule.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HorizontalRule: React.FC<Props> = () => {
|
||||||
|
return <hr />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HorizontalRule;
|
10
web/src/components/MemoContentV1/Image.tsx
Normal file
10
web/src/components/MemoContentV1/Image.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
interface Props {
|
||||||
|
altText: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image: React.FC<Props> = ({ altText, url }: Props) => {
|
||||||
|
return <img alt={altText} src={url} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Image;
|
10
web/src/components/MemoContentV1/Italic.tsx
Normal file
10
web/src/components/MemoContentV1/Italic.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Italic: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return <em>{content}</em>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Italic;
|
7
web/src/components/MemoContentV1/LineBreak.tsx
Normal file
7
web/src/components/MemoContentV1/LineBreak.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
interface Props {}
|
||||||
|
|
||||||
|
const LineBreak: React.FC<Props> = () => {
|
||||||
|
return <br />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LineBreak;
|
14
web/src/components/MemoContentV1/Link.tsx
Normal file
14
web/src/components/MemoContentV1/Link.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Link: React.FC<Props> = ({ text, url }: Props) => {
|
||||||
|
return (
|
||||||
|
<a className="text-blue-600 dark:text-blue-400 cursor-pointer underline break-all hover:opacity-80 decoration-1" href={url}>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Link;
|
26
web/src/components/MemoContentV1/OrderedList.tsx
Normal file
26
web/src/components/MemoContentV1/OrderedList.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
number: string;
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderedList: React.FC<Props> = ({ number, children }: Props) => {
|
||||||
|
return (
|
||||||
|
<ol>
|
||||||
|
<li className="grid grid-cols-[24px_1fr] gap-1">
|
||||||
|
<div className="w-7 h-6 flex justify-center items-center">
|
||||||
|
<span className="opacity-80">{number}.</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderedList;
|
18
web/src/components/MemoContentV1/Paragraph.tsx
Normal file
18
web/src/components/MemoContentV1/Paragraph.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Paragraph: React.FC<Props> = ({ children }: Props) => {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Paragraph;
|
92
web/src/components/MemoContentV1/Renderer.tsx
Normal file
92
web/src/components/MemoContentV1/Renderer.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
BlockquoteNode,
|
||||||
|
BoldItalicNode,
|
||||||
|
BoldNode,
|
||||||
|
CodeBlockNode,
|
||||||
|
CodeNode,
|
||||||
|
EscapingCharacterNode,
|
||||||
|
HeadingNode,
|
||||||
|
HorizontalRuleNode,
|
||||||
|
ImageNode,
|
||||||
|
ItalicNode,
|
||||||
|
LinkNode,
|
||||||
|
Node,
|
||||||
|
NodeType,
|
||||||
|
OrderedListNode,
|
||||||
|
ParagraphNode,
|
||||||
|
StrikethroughNode,
|
||||||
|
TagNode,
|
||||||
|
TaskListNode,
|
||||||
|
TextNode,
|
||||||
|
UnorderedListNode,
|
||||||
|
} from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Blockquote from "./Blockquote";
|
||||||
|
import Bold from "./Bold";
|
||||||
|
import BoldItalic from "./BoldItalic";
|
||||||
|
import Code from "./Code";
|
||||||
|
import CodeBlock from "./CodeBlock";
|
||||||
|
import EscapingCharacter from "./EscapingCharacter";
|
||||||
|
import Heading from "./Heading";
|
||||||
|
import HorizontalRule from "./HorizontalRule";
|
||||||
|
import Image from "./Image";
|
||||||
|
import Italic from "./Italic";
|
||||||
|
import LineBreak from "./LineBreak";
|
||||||
|
import Link from "./Link";
|
||||||
|
import OrderedList from "./OrderedList";
|
||||||
|
import Paragraph from "./Paragraph";
|
||||||
|
import Strikethrough from "./Strikethrough";
|
||||||
|
import Tag from "./Tag";
|
||||||
|
import TaskList from "./TaskList";
|
||||||
|
import Text from "./Text";
|
||||||
|
import UnorderedList from "./UnorderedList";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
node: Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Renderer: React.FC<Props> = ({ node }: Props) => {
|
||||||
|
switch (node.type) {
|
||||||
|
case NodeType.LINE_BREAK:
|
||||||
|
return <LineBreak />;
|
||||||
|
case NodeType.PARAGRAPH:
|
||||||
|
return <Paragraph {...(node.paragraphNode as ParagraphNode)} />;
|
||||||
|
case NodeType.CODE_BLOCK:
|
||||||
|
return <CodeBlock {...(node.codeBlockNode as CodeBlockNode)} />;
|
||||||
|
case NodeType.HEADING:
|
||||||
|
return <Heading {...(node.headingNode as HeadingNode)} />;
|
||||||
|
case NodeType.HORIZONTAL_RULE:
|
||||||
|
return <HorizontalRule {...(node.horizontalRuleNode as HorizontalRuleNode)} />;
|
||||||
|
case NodeType.BLOCKQUOTE:
|
||||||
|
return <Blockquote {...(node.blockquoteNode as BlockquoteNode)} />;
|
||||||
|
case NodeType.ORDERED_LIST:
|
||||||
|
return <OrderedList {...(node.orderedListNode as OrderedListNode)} />;
|
||||||
|
case NodeType.UNORDERED_LIST:
|
||||||
|
return <UnorderedList {...(node.unorderedListNode as UnorderedListNode)} />;
|
||||||
|
case NodeType.TASK_LIST:
|
||||||
|
return <TaskList {...(node.taskListNode as TaskListNode)} />;
|
||||||
|
case NodeType.TEXT:
|
||||||
|
return <Text {...(node.textNode as TextNode)} />;
|
||||||
|
case NodeType.BOLD:
|
||||||
|
return <Bold {...(node.boldNode as BoldNode)} />;
|
||||||
|
case NodeType.ITALIC:
|
||||||
|
return <Italic {...(node.italicNode as ItalicNode)} />;
|
||||||
|
case NodeType.BOLD_ITALIC:
|
||||||
|
return <BoldItalic {...(node.boldItalicNode as BoldItalicNode)} />;
|
||||||
|
case NodeType.CODE:
|
||||||
|
return <Code {...(node.codeNode as CodeNode)} />;
|
||||||
|
case NodeType.IMAGE:
|
||||||
|
return <Image {...(node.imageNode as ImageNode)} />;
|
||||||
|
case NodeType.LINK:
|
||||||
|
return <Link {...(node.linkNode as LinkNode)} />;
|
||||||
|
case NodeType.TAG:
|
||||||
|
return <Tag {...(node.tagNode as TagNode)} />;
|
||||||
|
case NodeType.STRIKETHROUGH:
|
||||||
|
return <Strikethrough {...(node.strikethroughNode as StrikethroughNode)} />;
|
||||||
|
case NodeType.ESCAPING_CHARACTER:
|
||||||
|
return <EscapingCharacter {...(node.escapingCharacterNode as EscapingCharacterNode)} />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Renderer;
|
9
web/src/components/MemoContentV1/Strikethrough.tsx
Normal file
9
web/src/components/MemoContentV1/Strikethrough.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Strikethrough: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return <del>{content}</del>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Strikethrough;
|
9
web/src/components/MemoContentV1/Tag.tsx
Normal file
9
web/src/components/MemoContentV1/Tag.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tag: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return <span className="inline-block w-auto text-blue-600 dark:text-blue-400">#{content}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tag;
|
28
web/src/components/MemoContentV1/TaskList.tsx
Normal file
28
web/src/components/MemoContentV1/TaskList.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Checkbox } from "@mui/joy";
|
||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
complete: boolean;
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskList: React.FC<Props> = ({ complete, children }: Props) => {
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
<li className="grid grid-cols-[24px_1fr] gap-1">
|
||||||
|
<div className="w-7 h-6 flex justify-center items-center">
|
||||||
|
<Checkbox size="sm" checked={complete} readOnly />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskList;
|
9
web/src/components/MemoContentV1/Text.tsx
Normal file
9
web/src/components/MemoContentV1/Text.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Text: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return <span>{content}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Text;
|
26
web/src/components/MemoContentV1/UnorderedList.tsx
Normal file
26
web/src/components/MemoContentV1/UnorderedList.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const UnorderedList: React.FC<Props> = ({ children }: Props) => {
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
<li className="grid grid-cols-[24px_1fr] gap-1">
|
||||||
|
<div className="w-7 h-6 flex justify-center items-center">
|
||||||
|
<span className="opacity-80">•</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnorderedList;
|
48
web/src/components/MemoContentV1/index.tsx
Normal file
48
web/src/components/MemoContentV1/index.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { markdownServiceClient } from "@/grpcweb";
|
||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
className?: string;
|
||||||
|
onMemoContentClick?: (e: React.MouseEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoContentV1: React.FC<Props> = (props: Props) => {
|
||||||
|
const { className, content, onMemoContentClick } = props;
|
||||||
|
const [nodes, setNodes] = useState<Node[]>([]);
|
||||||
|
const memoContentContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
markdownServiceClient
|
||||||
|
.parseMarkdown({
|
||||||
|
markdown: content,
|
||||||
|
})
|
||||||
|
.then(({ nodes }) => {
|
||||||
|
setNodes(nodes);
|
||||||
|
});
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
||||||
|
if (onMemoContentClick) {
|
||||||
|
onMemoContentClick(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-300 ${className || ""}`}>
|
||||||
|
<div
|
||||||
|
ref={memoContentContainerRef}
|
||||||
|
className="w-full max-w-full word-break text-base leading-6 space-y-1"
|
||||||
|
onClick={handleMemoContentClick}
|
||||||
|
>
|
||||||
|
{nodes.map((node, index) => (
|
||||||
|
<Renderer key={`${node.type}-${index}`} node={node} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemoContentV1;
|
1
web/src/components/MemoContentV1/types/context.ts
Normal file
1
web/src/components/MemoContentV1/types/context.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export interface RendererContext {}
|
1
web/src/components/MemoContentV1/types/index.ts
Normal file
1
web/src/components/MemoContentV1/types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./context";
|
@ -1,13 +1,12 @@
|
|||||||
import { Select, Option, Button, IconButton, Divider } from "@mui/joy";
|
import { Select, Option, Button, IconButton, Divider } from "@mui/joy";
|
||||||
import { isNumber, last, uniq, uniqBy } from "lodash-es";
|
import { isNumber, last, uniqBy } from "lodash-es";
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useLocalStorage from "react-use/lib/useLocalStorage";
|
import useLocalStorage from "react-use/lib/useLocalStorage";
|
||||||
import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts";
|
import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import { getMatchedNodes } from "@/labs/marked";
|
import { useGlobalStore, useMemoStore, useResourceStore } from "@/store/module";
|
||||||
import { useGlobalStore, useMemoStore, useResourceStore, useTagStore } from "@/store/module";
|
|
||||||
import { useUserV1Store } from "@/store/v1";
|
import { useUserV1Store } from "@/store/v1";
|
||||||
import { Resource } from "@/types/proto/api/v2/resource_service";
|
import { Resource } from "@/types/proto/api/v2/resource_service";
|
||||||
import { UserSetting, User_Role } from "@/types/proto/api/v2/user_service";
|
import { UserSetting, User_Role } from "@/types/proto/api/v2/user_service";
|
||||||
@ -52,7 +51,6 @@ const MemoEditor = (props: Props) => {
|
|||||||
} = useGlobalStore();
|
} = useGlobalStore();
|
||||||
const userV1Store = useUserV1Store();
|
const userV1Store = useUserV1Store();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const tagStore = useTagStore();
|
|
||||||
const resourceStore = useResourceStore();
|
const resourceStore = useResourceStore();
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
@ -335,13 +333,6 @@ const MemoEditor = (props: Props) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Upsert tag with the content.
|
|
||||||
const matchedNodes = getMatchedNodes(content);
|
|
||||||
const tagNameList = uniq(matchedNodes.filter((node) => node.parserName === "tag").map((node) => node.matchedContent.slice(1)));
|
|
||||||
for (const tagName of tagNameList) {
|
|
||||||
await tagStore.upsertTag(tagName);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState((prevState) => ({
|
setState((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
resourceList: [],
|
resourceList: [],
|
||||||
|
@ -2,6 +2,7 @@ import { createChannel, createClientFactory, FetchTransport } from "nice-grpc-we
|
|||||||
import { ActivityServiceDefinition } from "./types/proto/api/v2/activity_service";
|
import { ActivityServiceDefinition } from "./types/proto/api/v2/activity_service";
|
||||||
import { AuthServiceDefinition } from "./types/proto/api/v2/auth_service";
|
import { AuthServiceDefinition } from "./types/proto/api/v2/auth_service";
|
||||||
import { InboxServiceDefinition } from "./types/proto/api/v2/inbox_service";
|
import { InboxServiceDefinition } from "./types/proto/api/v2/inbox_service";
|
||||||
|
import { MarkdownServiceDefinition } from "./types/proto/api/v2/markdown_service";
|
||||||
import { MemoServiceDefinition } from "./types/proto/api/v2/memo_service";
|
import { MemoServiceDefinition } from "./types/proto/api/v2/memo_service";
|
||||||
import { ResourceServiceDefinition } from "./types/proto/api/v2/resource_service";
|
import { ResourceServiceDefinition } from "./types/proto/api/v2/resource_service";
|
||||||
import { SystemServiceDefinition } from "./types/proto/api/v2/system_service";
|
import { SystemServiceDefinition } from "./types/proto/api/v2/system_service";
|
||||||
@ -35,3 +36,5 @@ export const inboxServiceClient = clientFactory.create(InboxServiceDefinition, c
|
|||||||
export const activityServiceClient = clientFactory.create(ActivityServiceDefinition, channel);
|
export const activityServiceClient = clientFactory.create(ActivityServiceDefinition, channel);
|
||||||
|
|
||||||
export const webhookServiceClient = clientFactory.create(WebhookServiceDefinition, channel);
|
export const webhookServiceClient = clientFactory.create(WebhookServiceDefinition, channel);
|
||||||
|
|
||||||
|
export const markdownServiceClient = clientFactory.create(MarkdownServiceDefinition, channel);
|
||||||
|
@ -69,16 +69,6 @@
|
|||||||
code {
|
code {
|
||||||
@apply block;
|
@apply block;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.codeblock-copy-btn {
|
|
||||||
@apply flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.codeblock-copy-btn {
|
|
||||||
@apply btn-normal absolute hidden top-2 right-2 border-solid border-2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
@ -6,7 +6,7 @@ import { Link, useParams } from "react-router-dom";
|
|||||||
import FloatingNavButton from "@/components/FloatingNavButton";
|
import FloatingNavButton from "@/components/FloatingNavButton";
|
||||||
import Icon from "@/components/Icon";
|
import Icon from "@/components/Icon";
|
||||||
import Memo from "@/components/Memo";
|
import Memo from "@/components/Memo";
|
||||||
import MemoContent from "@/components/MemoContent";
|
import MemoContentV1 from "@/components/MemoContentV1";
|
||||||
import MemoEditor from "@/components/MemoEditor";
|
import MemoEditor from "@/components/MemoEditor";
|
||||||
import showMemoEditorDialog from "@/components/MemoEditor/MemoEditorDialog";
|
import showMemoEditorDialog from "@/components/MemoEditor/MemoEditorDialog";
|
||||||
import MemoRelationListView from "@/components/MemoRelationListView";
|
import MemoRelationListView from "@/components/MemoRelationListView";
|
||||||
@ -133,7 +133,7 @@ const MemoDetail = () => {
|
|||||||
<div className="w-full mb-4 flex flex-row justify-start items-center mr-1">
|
<div className="w-full mb-4 flex flex-row justify-start items-center mr-1">
|
||||||
<span className="text-gray-400 select-none">{getDateTimeString(memo.displayTs)}</span>
|
<span className="text-gray-400 select-none">{getDateTimeString(memo.displayTs)}</span>
|
||||||
</div>
|
</div>
|
||||||
<MemoContent content={memo.content} />
|
<MemoContentV1 content={memo.content} />
|
||||||
<MemoResourceListView resourceList={memo.resourceList} />
|
<MemoResourceListView resourceList={memo.resourceList} />
|
||||||
<MemoRelationListView memo={memo} relationList={referenceRelations} />
|
<MemoRelationListView memo={memo} relationList={referenceRelations} />
|
||||||
<div className="w-full mt-4 flex flex-col sm:flex-row justify-start sm:justify-between sm:items-center gap-2">
|
<div className="w-full mt-4 flex flex-col sm:flex-row justify-start sm:justify-between sm:items-center gap-2">
|
||||||
|
Reference in New Issue
Block a user