mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: implement switchable task list node
This commit is contained in:
@ -95,6 +95,73 @@ func convertFromASTNode(rawNode ast.Node) *apiv2pb.Node {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertToASTNodes(nodes []*apiv2pb.Node) []ast.Node {
|
||||||
|
rawNodes := []ast.Node{}
|
||||||
|
for _, node := range nodes {
|
||||||
|
rawNode := convertToASTNode(node)
|
||||||
|
rawNodes = append(rawNodes, rawNode)
|
||||||
|
}
|
||||||
|
return rawNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToASTNode(node *apiv2pb.Node) ast.Node {
|
||||||
|
switch n := node.Node.(type) {
|
||||||
|
case *apiv2pb.Node_LineBreakNode:
|
||||||
|
return &ast.LineBreak{}
|
||||||
|
case *apiv2pb.Node_ParagraphNode:
|
||||||
|
children := convertToASTNodes(n.ParagraphNode.Children)
|
||||||
|
return &ast.Paragraph{Children: children}
|
||||||
|
case *apiv2pb.Node_CodeBlockNode:
|
||||||
|
return &ast.CodeBlock{Language: n.CodeBlockNode.Language, Content: n.CodeBlockNode.Content}
|
||||||
|
case *apiv2pb.Node_HeadingNode:
|
||||||
|
children := convertToASTNodes(n.HeadingNode.Children)
|
||||||
|
return &ast.Heading{Level: int(n.HeadingNode.Level), Children: children}
|
||||||
|
case *apiv2pb.Node_HorizontalRuleNode:
|
||||||
|
return &ast.HorizontalRule{Symbol: n.HorizontalRuleNode.Symbol}
|
||||||
|
case *apiv2pb.Node_BlockquoteNode:
|
||||||
|
children := convertToASTNodes(n.BlockquoteNode.Children)
|
||||||
|
return &ast.Blockquote{Children: children}
|
||||||
|
case *apiv2pb.Node_OrderedListNode:
|
||||||
|
children := convertToASTNodes(n.OrderedListNode.Children)
|
||||||
|
return &ast.OrderedList{Number: n.OrderedListNode.Number, Children: children}
|
||||||
|
case *apiv2pb.Node_UnorderedListNode:
|
||||||
|
children := convertToASTNodes(n.UnorderedListNode.Children)
|
||||||
|
return &ast.UnorderedList{Symbol: n.UnorderedListNode.Symbol, Children: children}
|
||||||
|
case *apiv2pb.Node_TaskListNode:
|
||||||
|
children := convertToASTNodes(n.TaskListNode.Children)
|
||||||
|
return &ast.TaskList{Symbol: n.TaskListNode.Symbol, Complete: n.TaskListNode.Complete, Children: children}
|
||||||
|
case *apiv2pb.Node_MathBlockNode:
|
||||||
|
return &ast.MathBlock{Content: n.MathBlockNode.Content}
|
||||||
|
case *apiv2pb.Node_TextNode:
|
||||||
|
return &ast.Text{Content: n.TextNode.Content}
|
||||||
|
case *apiv2pb.Node_BoldNode:
|
||||||
|
children := convertToASTNodes(n.BoldNode.Children)
|
||||||
|
return &ast.Bold{Symbol: n.BoldNode.Symbol, Children: children}
|
||||||
|
case *apiv2pb.Node_ItalicNode:
|
||||||
|
return &ast.Italic{Symbol: n.ItalicNode.Symbol, Content: n.ItalicNode.Content}
|
||||||
|
case *apiv2pb.Node_BoldItalicNode:
|
||||||
|
return &ast.BoldItalic{Symbol: n.BoldItalicNode.Symbol, Content: n.BoldItalicNode.Content}
|
||||||
|
case *apiv2pb.Node_CodeNode:
|
||||||
|
return &ast.Code{Content: n.CodeNode.Content}
|
||||||
|
case *apiv2pb.Node_ImageNode:
|
||||||
|
return &ast.Image{AltText: n.ImageNode.AltText, URL: n.ImageNode.Url}
|
||||||
|
case *apiv2pb.Node_LinkNode:
|
||||||
|
return &ast.Link{Text: n.LinkNode.Text, URL: n.LinkNode.Url}
|
||||||
|
case *apiv2pb.Node_AutoLinkNode:
|
||||||
|
return &ast.AutoLink{URL: n.AutoLinkNode.Url}
|
||||||
|
case *apiv2pb.Node_TagNode:
|
||||||
|
return &ast.Tag{Content: n.TagNode.Content}
|
||||||
|
case *apiv2pb.Node_StrikethroughNode:
|
||||||
|
return &ast.Strikethrough{Content: n.StrikethroughNode.Content}
|
||||||
|
case *apiv2pb.Node_EscapingCharacterNode:
|
||||||
|
return &ast.EscapingCharacter{Symbol: n.EscapingCharacterNode.Symbol}
|
||||||
|
case *apiv2pb.Node_MathNode:
|
||||||
|
return &ast.Math{Content: n.MathNode.Content}
|
||||||
|
default:
|
||||||
|
return &ast.Text{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func traverseASTNodes(nodes []ast.Node, fn func(ast.Node)) {
|
func traverseASTNodes(nodes []ast.Node, fn func(ast.Node)) {
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
fn(node)
|
fn(node)
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/usememos/memos/plugin/gomark/ast"
|
"github.com/usememos/memos/plugin/gomark/ast"
|
||||||
"github.com/usememos/memos/plugin/gomark/parser"
|
"github.com/usememos/memos/plugin/gomark/parser"
|
||||||
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||||
|
"github.com/usememos/memos/plugin/gomark/restore"
|
||||||
"github.com/usememos/memos/plugin/webhook"
|
"github.com/usememos/memos/plugin/webhook"
|
||||||
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
|
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
|
||||||
storepb "github.com/usememos/memos/proto/gen/store"
|
storepb "github.com/usememos/memos/proto/gen/store"
|
||||||
@ -232,6 +233,10 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else if path == "nodes" {
|
||||||
|
nodes := convertToASTNodes(request.Memo.Nodes)
|
||||||
|
content := restore.Restore(nodes)
|
||||||
|
update.Content = &content
|
||||||
} else if path == "visibility" {
|
} else if path == "visibility" {
|
||||||
visibility := convertVisibilityToStore(request.Memo.Visibility)
|
visibility := convertVisibilityToStore(request.Memo.Visibility)
|
||||||
update.Visibility = &visibility
|
update.Visibility = &visibility
|
||||||
|
23
plugin/gomark/ast/utils.go
Normal file
23
plugin/gomark/ast/utils.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package ast
|
||||||
|
|
||||||
|
func FindPrevSiblingExceptLineBreak(node Node) Node {
|
||||||
|
if node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
prev := node.PrevSibling()
|
||||||
|
if prev != nil && prev.Type() == LineBreakNode {
|
||||||
|
return FindPrevSiblingExceptLineBreak(prev)
|
||||||
|
}
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindNextSiblingExceptLineBreak(node Node) Node {
|
||||||
|
if node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
next := node.NextSibling()
|
||||||
|
if next != nil && next.Type() == LineBreakNode {
|
||||||
|
return FindNextSiblingExceptLineBreak(next)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
@ -49,7 +49,6 @@ func ParseBlock(tokens []*tokenizer.Token) ([]ast.Node, error) {
|
|||||||
func ParseBlockWithParsers(tokens []*tokenizer.Token, blockParsers []BlockParser) ([]ast.Node, error) {
|
func ParseBlockWithParsers(tokens []*tokenizer.Token, blockParsers []BlockParser) ([]ast.Node, error) {
|
||||||
nodes := []ast.Node{}
|
nodes := []ast.Node{}
|
||||||
var prevNode ast.Node
|
var prevNode ast.Node
|
||||||
var skipNextLineBreakFlag bool
|
|
||||||
for len(tokens) > 0 {
|
for len(tokens) > 0 {
|
||||||
for _, blockParser := range blockParsers {
|
for _, blockParser := range blockParsers {
|
||||||
size, matched := blockParser.Match(tokens)
|
size, matched := blockParser.Match(tokens)
|
||||||
@ -59,21 +58,12 @@ func ParseBlockWithParsers(tokens []*tokenizer.Token, blockParsers []BlockParser
|
|||||||
return nil, errors.New("parse error")
|
return nil, errors.New("parse error")
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type() == ast.LineBreakNode && skipNextLineBreakFlag {
|
|
||||||
if prevNode != nil && ast.IsBlockNode(prevNode) {
|
|
||||||
tokens = tokens[size:]
|
|
||||||
skipNextLineBreakFlag = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens = tokens[size:]
|
tokens = tokens[size:]
|
||||||
if prevNode != nil {
|
if prevNode != nil {
|
||||||
prevNode.SetNextSibling(node)
|
prevNode.SetNextSibling(node)
|
||||||
node.SetPrevSibling(prevNode)
|
node.SetPrevSibling(prevNode)
|
||||||
}
|
}
|
||||||
prevNode = node
|
prevNode = node
|
||||||
skipNextLineBreakFlag = true
|
|
||||||
nodes = append(nodes, node)
|
nodes = append(nodes, node)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -96,6 +96,7 @@ func TestParser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
&ast.LineBreak{},
|
||||||
&ast.Paragraph{
|
&ast.Paragraph{
|
||||||
Children: []ast.Node{
|
Children: []ast.Node{
|
||||||
&ast.Text{
|
&ast.Text{
|
||||||
@ -126,6 +127,7 @@ func TestParser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
&ast.LineBreak{},
|
||||||
&ast.CodeBlock{
|
&ast.CodeBlock{
|
||||||
Language: "javascript",
|
Language: "javascript",
|
||||||
Content: "console.log(\"Hello world!\");",
|
Content: "console.log(\"Hello world!\");",
|
||||||
@ -143,6 +145,7 @@ func TestParser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
&ast.LineBreak{},
|
&ast.LineBreak{},
|
||||||
|
&ast.LineBreak{},
|
||||||
&ast.Paragraph{
|
&ast.Paragraph{
|
||||||
Children: []ast.Node{
|
Children: []ast.Node{
|
||||||
&ast.Text{
|
&ast.Text{
|
||||||
@ -163,6 +166,7 @@ func TestParser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
&ast.LineBreak{},
|
||||||
&ast.TaskList{
|
&ast.TaskList{
|
||||||
Symbol: tokenizer.Hyphen,
|
Symbol: tokenizer.Hyphen,
|
||||||
Complete: false,
|
Complete: false,
|
||||||
@ -186,6 +190,7 @@ func TestParser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
&ast.LineBreak{},
|
||||||
&ast.TaskList{
|
&ast.TaskList{
|
||||||
Symbol: tokenizer.Hyphen,
|
Symbol: tokenizer.Hyphen,
|
||||||
Complete: true,
|
Complete: true,
|
||||||
|
@ -72,8 +72,19 @@ func (r *HTMLRenderer) RenderNode(node ast.Node) {
|
|||||||
|
|
||||||
// RenderNodes renders a slice of AST nodes to HTML.
|
// RenderNodes renders a slice of AST nodes to HTML.
|
||||||
func (r *HTMLRenderer) RenderNodes(nodes []ast.Node) {
|
func (r *HTMLRenderer) RenderNodes(nodes []ast.Node) {
|
||||||
|
var prevNode ast.Node
|
||||||
|
var skipNextLineBreakFlag bool
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
|
if node.Type() == ast.LineBreakNode && skipNextLineBreakFlag {
|
||||||
|
if prevNode != nil && ast.IsBlockNode(prevNode) {
|
||||||
|
skipNextLineBreakFlag = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r.RenderNode(node)
|
r.RenderNode(node)
|
||||||
|
prevNode = node
|
||||||
|
skipNextLineBreakFlag = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +122,7 @@ func (r *HTMLRenderer) renderHorizontalRule(_ *ast.HorizontalRule) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *HTMLRenderer) renderBlockquote(node *ast.Blockquote) {
|
func (r *HTMLRenderer) renderBlockquote(node *ast.Blockquote) {
|
||||||
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
|
prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node)
|
||||||
if prevSibling == nil || prevSibling.Type() != ast.BlockquoteNode {
|
if prevSibling == nil || prevSibling.Type() != ast.BlockquoteNode {
|
||||||
r.output.WriteString("<blockquote>")
|
r.output.WriteString("<blockquote>")
|
||||||
}
|
}
|
||||||
@ -122,7 +133,7 @@ func (r *HTMLRenderer) renderBlockquote(node *ast.Blockquote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *HTMLRenderer) renderTaskList(node *ast.TaskList) {
|
func (r *HTMLRenderer) renderTaskList(node *ast.TaskList) {
|
||||||
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
|
prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node)
|
||||||
if prevSibling == nil || prevSibling.Type() != ast.TaskListNode {
|
if prevSibling == nil || prevSibling.Type() != ast.TaskListNode {
|
||||||
r.output.WriteString("<ul>")
|
r.output.WriteString("<ul>")
|
||||||
}
|
}
|
||||||
@ -140,7 +151,7 @@ func (r *HTMLRenderer) renderTaskList(node *ast.TaskList) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *HTMLRenderer) renderUnorderedList(node *ast.UnorderedList) {
|
func (r *HTMLRenderer) renderUnorderedList(node *ast.UnorderedList) {
|
||||||
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
|
prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node)
|
||||||
if prevSibling == nil || prevSibling.Type() != ast.UnorderedListNode {
|
if prevSibling == nil || prevSibling.Type() != ast.UnorderedListNode {
|
||||||
r.output.WriteString("<ul>")
|
r.output.WriteString("<ul>")
|
||||||
}
|
}
|
||||||
@ -153,7 +164,7 @@ func (r *HTMLRenderer) renderUnorderedList(node *ast.UnorderedList) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *HTMLRenderer) renderOrderedList(node *ast.OrderedList) {
|
func (r *HTMLRenderer) renderOrderedList(node *ast.OrderedList) {
|
||||||
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
|
prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node)
|
||||||
if prevSibling == nil || prevSibling.Type() != ast.OrderedListNode {
|
if prevSibling == nil || prevSibling.Type() != ast.OrderedListNode {
|
||||||
r.output.WriteString("<ol>")
|
r.output.WriteString("<ol>")
|
||||||
}
|
}
|
||||||
|
@ -72,8 +72,19 @@ func (r *StringRenderer) RenderNode(node ast.Node) {
|
|||||||
|
|
||||||
// RenderNodes renders a slice of AST nodes to raw string.
|
// RenderNodes renders a slice of AST nodes to raw string.
|
||||||
func (r *StringRenderer) RenderNodes(nodes []ast.Node) {
|
func (r *StringRenderer) RenderNodes(nodes []ast.Node) {
|
||||||
|
var prevNode ast.Node
|
||||||
|
var skipNextLineBreakFlag bool
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
|
if node.Type() == ast.LineBreakNode && skipNextLineBreakFlag {
|
||||||
|
if prevNode != nil && ast.IsBlockNode(prevNode) {
|
||||||
|
skipNextLineBreakFlag = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r.RenderNode(node)
|
r.RenderNode(node)
|
||||||
|
prevNode = node
|
||||||
|
skipNextLineBreakFlag = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,10 +44,11 @@ import Text from "./Text";
|
|||||||
import UnorderedList from "./UnorderedList";
|
import UnorderedList from "./UnorderedList";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
index: string;
|
||||||
node: Node;
|
node: Node;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Renderer: React.FC<Props> = ({ node }: Props) => {
|
const Renderer: React.FC<Props> = ({ index, node }: Props) => {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case NodeType.LINE_BREAK:
|
case NodeType.LINE_BREAK:
|
||||||
return <LineBreak />;
|
return <LineBreak />;
|
||||||
@ -66,7 +67,7 @@ const Renderer: React.FC<Props> = ({ node }: Props) => {
|
|||||||
case NodeType.UNORDERED_LIST:
|
case NodeType.UNORDERED_LIST:
|
||||||
return <UnorderedList {...(node.unorderedListNode as UnorderedListNode)} />;
|
return <UnorderedList {...(node.unorderedListNode as UnorderedListNode)} />;
|
||||||
case NodeType.TASK_LIST:
|
case NodeType.TASK_LIST:
|
||||||
return <TaskList {...(node.taskListNode as TaskListNode)} />;
|
return <TaskList index={index} {...(node.taskListNode as TaskListNode)} />;
|
||||||
case NodeType.MATH_BLOCK:
|
case NodeType.MATH_BLOCK:
|
||||||
return <Math {...(node.mathBlockNode as MathNode)} block={true} />;
|
return <Math {...(node.mathBlockNode as MathNode)} block={true} />;
|
||||||
case NodeType.TEXT:
|
case NodeType.TEXT:
|
||||||
|
@ -1,23 +1,51 @@
|
|||||||
import { Checkbox } from "@mui/joy";
|
import { Checkbox } from "@mui/joy";
|
||||||
import { Node } from "@/types/proto/api/v2/markdown_service";
|
import { useContext } from "react";
|
||||||
|
import { useMemoStore } from "@/store/v1";
|
||||||
|
import { Node, NodeType } from "@/types/proto/api/v2/markdown_service";
|
||||||
import Renderer from "./Renderer";
|
import Renderer from "./Renderer";
|
||||||
|
import { RendererContext } from "./types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
index: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
complete: boolean;
|
complete: boolean;
|
||||||
children: Node[];
|
children: Node[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskList: React.FC<Props> = ({ complete, children }: Props) => {
|
const TaskList: React.FC<Props> = ({ index, complete, children }: Props) => {
|
||||||
|
const context = useContext(RendererContext);
|
||||||
|
const memoStore = useMemoStore();
|
||||||
|
|
||||||
|
const handleCheckboxChange = async (on: boolean) => {
|
||||||
|
const nodeIndex = Number(index);
|
||||||
|
if (isNaN(nodeIndex)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = context.nodes[nodeIndex];
|
||||||
|
if (node.type !== NodeType.TASK_LIST || !node.taskListNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.taskListNode!.complete = on;
|
||||||
|
await memoStore.updateMemo(
|
||||||
|
{
|
||||||
|
id: context.memoId,
|
||||||
|
nodes: context.nodes,
|
||||||
|
},
|
||||||
|
["nodes"]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul>
|
<ul>
|
||||||
<li className="grid grid-cols-[24px_1fr] gap-1">
|
<li className="grid grid-cols-[24px_1fr] gap-1">
|
||||||
<div className="w-7 h-6 flex justify-center items-center">
|
<div className="w-7 h-6 flex justify-center items-center">
|
||||||
<Checkbox size="sm" checked={complete} readOnly />
|
<Checkbox size="sm" checked={complete} disabled={context.readonly} onChange={(e) => handleCheckboxChange(e.target.checked)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{children.map((child, index) => (
|
{children.map((child, subIndex) => (
|
||||||
<Renderer key={`${child.type}-${index}`} node={child} />
|
<Renderer key={`${child.type}-${subIndex}`} index={`${index}-${subIndex}`} node={child} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,16 +1,24 @@
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
|
import { useMemoStore } from "@/store/v1";
|
||||||
import { Node } from "@/types/proto/api/v2/markdown_service";
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
import Renderer from "./Renderer";
|
import Renderer from "./Renderer";
|
||||||
|
import { RendererContext } from "./types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
memoId: number;
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
|
readonly?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onMemoContentClick?: (e: React.MouseEvent) => void;
|
onMemoContentClick?: (e: React.MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemoContent: React.FC<Props> = (props: Props) => {
|
const MemoContent: React.FC<Props> = (props: Props) => {
|
||||||
const { className, onMemoContentClick } = props;
|
const { className, memoId, nodes, onMemoContentClick } = props;
|
||||||
|
const currentUser = useCurrentUser();
|
||||||
|
const memoStore = useMemoStore();
|
||||||
const memoContentContainerRef = useRef<HTMLDivElement>(null);
|
const memoContentContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const allowEdit = currentUser?.id === memoStore.getMemoById(memoId)?.creatorId && !props.readonly;
|
||||||
|
|
||||||
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
||||||
if (onMemoContentClick) {
|
if (onMemoContentClick) {
|
||||||
@ -19,17 +27,25 @@ const MemoContent: React.FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RendererContext.Provider
|
||||||
|
value={{
|
||||||
|
memoId,
|
||||||
|
nodes,
|
||||||
|
readonly: !allowEdit,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className={`w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-300 ${className || ""}`}>
|
<div className={`w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-300 ${className || ""}`}>
|
||||||
<div
|
<div
|
||||||
ref={memoContentContainerRef}
|
ref={memoContentContainerRef}
|
||||||
className="w-full max-w-full word-break text-base leading-6 space-y-1"
|
className="w-full max-w-full word-break text-base leading-6 space-y-1"
|
||||||
onClick={handleMemoContentClick}
|
onClick={handleMemoContentClick}
|
||||||
>
|
>
|
||||||
{props.nodes.map((node, index) => (
|
{nodes.map((node, index) => (
|
||||||
<Renderer key={`${node.type}-${index}`} node={node} />
|
<Renderer key={`${node.type}-${index}`} index={String(index)} node={node} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</RendererContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1 +1,14 @@
|
|||||||
export interface RendererContext {}
|
import { createContext } from "react";
|
||||||
|
import { UNKNOWN_ID } from "@/helpers/consts";
|
||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
memoId: number;
|
||||||
|
nodes: Node[];
|
||||||
|
readonly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RendererContext = createContext<Context>({
|
||||||
|
memoId: UNKNOWN_ID,
|
||||||
|
nodes: [],
|
||||||
|
});
|
||||||
|
@ -251,7 +251,7 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MemoContent nodes={memo.nodes} onMemoContentClick={handleMemoContentClick} />
|
<MemoContent memoId={memo.id} nodes={memo.nodes} onMemoContentClick={handleMemoContentClick} />
|
||||||
<MemoResourceListView resourceList={memo.resources} />
|
<MemoResourceListView resourceList={memo.resources} />
|
||||||
<MemoRelationListView memo={memo} relationList={referenceRelations} />
|
<MemoRelationListView memo={memo} relationList={referenceRelations} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -100,7 +100,7 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<span className="w-full px-6 pt-5 pb-2 text-sm text-gray-500">{getDateTimeString(memo.displayTime)}</span>
|
<span className="w-full px-6 pt-5 pb-2 text-sm text-gray-500">{getDateTimeString(memo.displayTime)}</span>
|
||||||
<div className="w-full px-6 text-base pb-4">
|
<div className="w-full px-6 text-base pb-4">
|
||||||
<MemoContent nodes={memo.nodes} />
|
<MemoContent memoId={memo.id} nodes={memo.nodes} readonly={true} />
|
||||||
<MemoResourceListView resourceList={memo.resources} />
|
<MemoResourceListView resourceList={memo.resources} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-between items-center w-full bg-gray-100 dark:bg-zinc-900 py-4 px-6">
|
<div className="flex flex-row justify-between items-center w-full bg-gray-100 dark:bg-zinc-900 py-4 px-6">
|
||||||
|
@ -18,7 +18,7 @@ const TimelineMemo = (props: Props) => {
|
|||||||
<div className="w-full flex flex-row justify-start items-center mt-0.5 mb-1 text-sm font-mono text-gray-500 dark:text-gray-400">
|
<div className="w-full flex flex-row justify-start items-center mt-0.5 mb-1 text-sm font-mono text-gray-500 dark:text-gray-400">
|
||||||
<span className="opacity-80">{getTimeString(memo.displayTime)}</span>
|
<span className="opacity-80">{getTimeString(memo.displayTime)}</span>
|
||||||
</div>
|
</div>
|
||||||
<MemoContent nodes={memo.nodes} />
|
<MemoContent memoId={memo.id} nodes={memo.nodes} />
|
||||||
<MemoResourceListView resourceList={memo.resources} />
|
<MemoResourceListView resourceList={memo.resources} />
|
||||||
<MemoRelationListView memo={memo} relationList={relations} />
|
<MemoRelationListView memo={memo} relationList={relations} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -105,7 +105,7 @@ const Archived = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MemoContent nodes={memo.nodes} />
|
<MemoContent memoId={memo.id} nodes={memo.nodes} readonly={true} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -139,7 +139,7 @@ const MemoDetail = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<MemoContent nodes={memo.nodes} />
|
<MemoContent memoId={memo.id} nodes={memo.nodes} readonly={true} />
|
||||||
<MemoResourceListView resourceList={memo.resources} />
|
<MemoResourceListView resourceList={memo.resources} />
|
||||||
<MemoRelationListView memo={memo} relationList={referenceRelations} />
|
<MemoRelationListView memo={memo} relationList={referenceRelations} />
|
||||||
<div className="w-full mt-3 flex flex-row justify-between items-center gap-2">
|
<div className="w-full mt-3 flex flex-row justify-between items-center gap-2">
|
||||||
|
Reference in New Issue
Block a user