chore: traverse nodes to upsert tags

This commit is contained in:
Steven 2024-01-02 08:56:30 +08:00
parent c797099950
commit f74fa97b4a
7 changed files with 71 additions and 65 deletions

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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) => {
>
<Icon.Link className="w-5 h-5 mx-auto" />
</IconButton>
<IconButton
className="flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer text-gray-600 dark:text-gray-400 hover:bg-gray-300 dark:hover:bg-zinc-800 hover:shadow"
onClick={handleCheckBoxBtnClick}
>
<Icon.CheckSquare className="w-5 h-5 mx-auto" />
</IconButton>
<IconButton
className="flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer text-gray-600 dark:text-gray-400 hover:bg-gray-300 dark:hover:bg-zinc-800 hover:shadow"
onClick={handleCodeBlockBtnClick}
>
<Icon.Code className="w-5 h-5 mx-auto" />
</IconButton>
</div>
</div>
<ResourceListView resourceList={state.resourceList} setResourceList={handleSetResourceList} />
@ -456,7 +404,7 @@ const MemoEditor = (props: Props) => {
</Select>
</div>
<div className="shrink-0 flex flex-row justify-end items-center">
<Button color="success" disabled={!allowSave} onClick={handleSaveBtnClick}>
<Button color="success" disabled={!allowSave} loading={state.isRequesting} onClick={handleSaveBtnClick}>
{t("editor.save")}
</Button>
</div>

View file

@ -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<Tag[]>([]);
useEffect(() => {
tagStore.fetchTags();
}, []);
}, [memoList.size()]);
useEffect(() => {
const sortedTags = Array.from(tagsText).sort();

View file

@ -58,7 +58,7 @@ const Explore = () => {
))}
{isRequesting ? (
<div className="flex flex-col justify-start items-center w-full my-8">
<div className="flex flex-col justify-start items-center w-full my-4">
<p className="text-sm text-gray-400 italic">{t("memo.fetching-data")}</p>
</div>
) : isComplete ? (
@ -69,7 +69,7 @@ const Explore = () => {
</div>
)
) : (
<div className="w-full flex flex-row justify-center items-center my-2">
<div className="w-full flex flex-row justify-center items-center my-4">
<span className="cursor-pointer text-sm italic text-gray-500 hover:text-green-600" onClick={fetchMemos}>
{t("memo.fetch-more")}
</span>

View file

@ -68,7 +68,7 @@ const Home = () => {
<MemoView key={`${memo.id}-${memo.updateTime}`} memo={memo} showVisibility showPinnedStyle showParent />
))}
{isRequesting ? (
<div className="flex flex-col justify-start items-center w-full my-8">
<div className="flex flex-col justify-start items-center w-full my-4">
<p className="text-sm text-gray-400 italic">{t("memo.fetching-data")}</p>
</div>
) : isComplete ? (
@ -79,7 +79,7 @@ const Home = () => {
</div>
)
) : (
<div className="w-full flex flex-row justify-center items-center my-2">
<div className="w-full flex flex-row justify-center items-center my-4">
<span className="cursor-pointer text-sm italic text-gray-500 hover:text-green-600" onClick={fetchMemos}>
{t("memo.fetch-more")}
</span>

View file

@ -97,7 +97,7 @@ const UserProfile = () => {
<MemoView key={memo.id} memo={memo} showVisibility showPinnedStyle showParent />
))}
{isRequesting ? (
<div className="flex flex-col justify-start items-center w-full my-8">
<div className="flex flex-col justify-start items-center w-full my-4">
<p className="text-sm text-gray-400 italic">{t("memo.fetching-data")}</p>
</div>
) : isComplete ? (
@ -108,7 +108,7 @@ const UserProfile = () => {
</div>
)
) : (
<div className="w-full flex flex-row justify-center items-center my-2">
<div className="w-full flex flex-row justify-center items-center my-4">
<span className="cursor-pointer text-sm italic text-gray-500 hover:text-green-600" onClick={fetchMemos}>
{t("memo.fetch-more")}
</span>