mirror of
https://github.com/usememos/memos.git
synced 2024-12-25 22:51:29 +08:00
chore: traverse nodes to upsert tags
This commit is contained in:
parent
c797099950
commit
f74fa97b4a
7 changed files with 71 additions and 65 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue