diff --git a/api/v2/markdown_service.go b/api/v2/markdown_service.go index d61ea15e..0b720cbf 100644 --- a/api/v2/markdown_service.go +++ b/api/v2/markdown_service.go @@ -90,3 +90,25 @@ func convertFromASTNode(rawNode ast.Node) *apiv2pb.Node { return node } + +func traverseASTNodes(nodes []ast.Node, fn func(ast.Node)) { + for _, node := range nodes { + fn(node) + switch n := node.(type) { + case *ast.Paragraph: + traverseASTNodes(n.Children, fn) + case *ast.Heading: + traverseASTNodes(n.Children, fn) + case *ast.Blockquote: + traverseASTNodes(n.Children, fn) + case *ast.OrderedList: + traverseASTNodes(n.Children, fn) + case *ast.UnorderedList: + traverseASTNodes(n.Children, fn) + case *ast.TaskList: + traverseASTNodes(n.Children, fn) + case *ast.Bold: + traverseASTNodes(n.Children, fn) + } + } +} diff --git a/api/v2/memo_service.go b/api/v2/memo_service.go index f515cf78..2045ebd1 100644 --- a/api/v2/memo_service.go +++ b/api/v2/memo_service.go @@ -16,6 +16,7 @@ import ( apiv1 "github.com/usememos/memos/api/v1" "github.com/usememos/memos/internal/log" + "github.com/usememos/memos/plugin/gomark/ast" "github.com/usememos/memos/plugin/gomark/parser" "github.com/usememos/memos/plugin/gomark/parser/tokenizer" "github.com/usememos/memos/plugin/webhook" @@ -34,6 +35,11 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe return nil, status.Errorf(codes.PermissionDenied, "permission denied") } + nodes, err := parser.Parse(tokenizer.Tokenize(request.Content)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse memo content") + } + create := &store.Memo{ CreatorID: user.ID, Content: request.Content, @@ -45,6 +51,18 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe } metric.Enqueue("memo create") + // Dynamically upsert tags from memo content. + traverseASTNodes(nodes, func(node ast.Node) { + if tag, ok := node.(*ast.Tag); ok { + if _, err := s.Store.UpsertTag(ctx, &store.Tag{ + Name: tag.Content, + CreatorID: user.ID, + }); err != nil { + log.Warn("Failed to create tag", zap.Error(err)) + } + } + }) + memoMessage, err := s.convertMemoFromStore(ctx, memo) if err != nil { return nil, errors.Wrap(err, "failed to convert memo") @@ -198,6 +216,22 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe for _, path := range request.UpdateMask.Paths { if path == "content" { update.Content = &request.Memo.Content + nodes, err := parser.Parse(tokenizer.Tokenize(*update.Content)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse memo content") + } + + // Dynamically upsert tags from memo content. + traverseASTNodes(nodes, func(node ast.Node) { + if tag, ok := node.(*ast.Tag); ok { + if _, err := s.Store.UpsertTag(ctx, &store.Tag{ + Name: tag.Content, + CreatorID: user.ID, + }); err != nil { + log.Warn("Failed to create tag", zap.Error(err)) + } + } + }) } else if path == "visibility" { visibility := convertVisibilityToStore(request.Memo.Visibility) update.Visibility = &visibility diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index b26c61b3..65457cb1 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -281,6 +281,7 @@ const MemoEditor = (props: Props) => { id: memo.id, relations: state.relationList, }); + await memoStore.getOrFetchMemoById(memo.id, { skipCache: true }); if (onConfirm) { onConfirm(memo.id); } @@ -310,6 +311,7 @@ const MemoEditor = (props: Props) => { id: memo.id, relations: state.relationList, }); + await memoStore.getOrFetchMemoById(memo.id, { skipCache: true }); if (onConfirm) { onConfirm(memo.id); } @@ -319,57 +321,15 @@ const MemoEditor = (props: Props) => { console.error(error); toast.error(error.response.data.message); } + setState((state) => { return { ...state, isRequesting: false, + resourceList: [], + relationList: [], }; }); - - setState((prevState) => ({ - ...prevState, - resourceList: [], - })); - }; - - const handleCheckBoxBtnClick = () => { - if (!editorRef.current) { - return; - } - const currentPosition = editorRef.current?.getCursorPosition(); - const currentLineNumber = editorRef.current?.getCursorLineNumber(); - const currentLine = editorRef.current?.getLine(currentLineNumber); - let newLine = ""; - let cursorChange = 0; - if (/^- \[( |x|X)\] /.test(currentLine)) { - newLine = currentLine.replace(/^- \[( |x|X)\] /, ""); - cursorChange = -6; - } else if (/^\d+\. |- /.test(currentLine)) { - const match = currentLine.match(/^\d+\. |- /) ?? [""]; - newLine = currentLine.replace(/^\d+\. |- /, "- [ ] "); - cursorChange = -match[0].length + 6; - } else { - newLine = "- [ ] " + currentLine; - cursorChange = 6; - } - editorRef.current?.setLine(currentLineNumber, newLine); - editorRef.current.setCursorPosition(currentPosition + cursorChange); - editorRef.current?.scrollToCursor(); - }; - - const handleCodeBlockBtnClick = () => { - if (!editorRef.current) { - return; - } - - const cursorPosition = editorRef.current.getCursorPosition(); - const prevValue = editorRef.current.getContent().slice(0, cursorPosition); - if (prevValue === "" || prevValue.endsWith("\n")) { - editorRef.current?.insertText("", "```\n", "\n```"); - } else { - editorRef.current?.insertText("", "\n```\n", "\n```"); - } - editorRef.current?.scrollToCursor(); }; const handleTagSelectorClick = useCallback((tag: string) => { @@ -419,18 +379,6 @@ const MemoEditor = (props: Props) => { > - - - - - - @@ -456,7 +404,7 @@ const MemoEditor = (props: Props) => {
-
diff --git a/web/src/components/TagList.tsx b/web/src/components/TagList.tsx index bed7313f..123dc250 100644 --- a/web/src/components/TagList.tsx +++ b/web/src/components/TagList.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import useToggle from "react-use/lib/useToggle"; import { useFilterStore, useTagStore } from "@/store/module"; +import { useMemoList } from "@/store/v1"; import { useTranslate } from "@/utils/i18n"; import showCreateTagDialog from "./CreateTagDialog"; import Icon from "./Icon"; @@ -15,13 +16,14 @@ const TagList = () => { const t = useTranslate(); const filterStore = useFilterStore(); const tagStore = useTagStore(); + const memoList = useMemoList(); const tagsText = tagStore.state.tags; const filter = filterStore.state; const [tags, setTags] = useState([]); useEffect(() => { tagStore.fetchTags(); - }, []); + }, [memoList.size()]); useEffect(() => { const sortedTags = Array.from(tagsText).sort(); diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 5cdc9aba..9cdcf417 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -58,7 +58,7 @@ const Explore = () => { ))} {isRequesting ? ( -
+

{t("memo.fetching-data")}

) : isComplete ? ( @@ -69,7 +69,7 @@ const Explore = () => {
) ) : ( -
+
{t("memo.fetch-more")} diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index e3e003cb..e78cae17 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -68,7 +68,7 @@ const Home = () => { ))} {isRequesting ? ( -
+

{t("memo.fetching-data")}

) : isComplete ? ( @@ -79,7 +79,7 @@ const Home = () => {
) ) : ( -
+
{t("memo.fetch-more")} diff --git a/web/src/pages/UserProfile.tsx b/web/src/pages/UserProfile.tsx index 4d38b64d..c719adab 100644 --- a/web/src/pages/UserProfile.tsx +++ b/web/src/pages/UserProfile.tsx @@ -97,7 +97,7 @@ const UserProfile = () => { ))} {isRequesting ? ( -
+

{t("memo.fetching-data")}

) : isComplete ? ( @@ -108,7 +108,7 @@ const UserProfile = () => {
) ) : ( -
+
{t("memo.fetch-more")}