mirror of
https://github.com/usememos/memos.git
synced 2025-02-14 18:30:42 +01:00
chore: traverse nodes to upsert tags
This commit is contained in:
parent
c797099950
commit
f74fa97b4a
@ -90,3 +90,25 @@ func convertFromASTNode(rawNode ast.Node) *apiv2pb.Node {
|
|||||||
|
|
||||||
return 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"
|
apiv1 "github.com/usememos/memos/api/v1"
|
||||||
"github.com/usememos/memos/internal/log"
|
"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"
|
||||||
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||||
"github.com/usememos/memos/plugin/webhook"
|
"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")
|
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{
|
create := &store.Memo{
|
||||||
CreatorID: user.ID,
|
CreatorID: user.ID,
|
||||||
Content: request.Content,
|
Content: request.Content,
|
||||||
@ -45,6 +51,18 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe
|
|||||||
}
|
}
|
||||||
metric.Enqueue("memo create")
|
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)
|
memoMessage, err := s.convertMemoFromStore(ctx, memo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to convert memo")
|
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 {
|
for _, path := range request.UpdateMask.Paths {
|
||||||
if path == "content" {
|
if path == "content" {
|
||||||
update.Content = &request.Memo.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" {
|
} else if path == "visibility" {
|
||||||
visibility := convertVisibilityToStore(request.Memo.Visibility)
|
visibility := convertVisibilityToStore(request.Memo.Visibility)
|
||||||
update.Visibility = &visibility
|
update.Visibility = &visibility
|
||||||
|
@ -281,6 +281,7 @@ const MemoEditor = (props: Props) => {
|
|||||||
id: memo.id,
|
id: memo.id,
|
||||||
relations: state.relationList,
|
relations: state.relationList,
|
||||||
});
|
});
|
||||||
|
await memoStore.getOrFetchMemoById(memo.id, { skipCache: true });
|
||||||
if (onConfirm) {
|
if (onConfirm) {
|
||||||
onConfirm(memo.id);
|
onConfirm(memo.id);
|
||||||
}
|
}
|
||||||
@ -310,6 +311,7 @@ const MemoEditor = (props: Props) => {
|
|||||||
id: memo.id,
|
id: memo.id,
|
||||||
relations: state.relationList,
|
relations: state.relationList,
|
||||||
});
|
});
|
||||||
|
await memoStore.getOrFetchMemoById(memo.id, { skipCache: true });
|
||||||
if (onConfirm) {
|
if (onConfirm) {
|
||||||
onConfirm(memo.id);
|
onConfirm(memo.id);
|
||||||
}
|
}
|
||||||
@ -319,57 +321,15 @@ const MemoEditor = (props: Props) => {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error(error.response.data.message);
|
toast.error(error.response.data.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState((state) => {
|
setState((state) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isRequesting: false,
|
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) => {
|
const handleTagSelectorClick = useCallback((tag: string) => {
|
||||||
@ -419,18 +379,6 @@ const MemoEditor = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<Icon.Link className="w-5 h-5 mx-auto" />
|
<Icon.Link className="w-5 h-5 mx-auto" />
|
||||||
</IconButton>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<ResourceListView resourceList={state.resourceList} setResourceList={handleSetResourceList} />
|
<ResourceListView resourceList={state.resourceList} setResourceList={handleSetResourceList} />
|
||||||
@ -456,7 +404,7 @@ const MemoEditor = (props: Props) => {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 flex flex-row justify-end items-center">
|
<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")}
|
{t("editor.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import useToggle from "react-use/lib/useToggle";
|
import useToggle from "react-use/lib/useToggle";
|
||||||
import { useFilterStore, useTagStore } from "@/store/module";
|
import { useFilterStore, useTagStore } from "@/store/module";
|
||||||
|
import { useMemoList } from "@/store/v1";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import showCreateTagDialog from "./CreateTagDialog";
|
import showCreateTagDialog from "./CreateTagDialog";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
@ -15,13 +16,14 @@ const TagList = () => {
|
|||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const filterStore = useFilterStore();
|
const filterStore = useFilterStore();
|
||||||
const tagStore = useTagStore();
|
const tagStore = useTagStore();
|
||||||
|
const memoList = useMemoList();
|
||||||
const tagsText = tagStore.state.tags;
|
const tagsText = tagStore.state.tags;
|
||||||
const filter = filterStore.state;
|
const filter = filterStore.state;
|
||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tagStore.fetchTags();
|
tagStore.fetchTags();
|
||||||
}, []);
|
}, [memoList.size()]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sortedTags = Array.from(tagsText).sort();
|
const sortedTags = Array.from(tagsText).sort();
|
||||||
|
@ -58,7 +58,7 @@ const Explore = () => {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{isRequesting ? (
|
{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>
|
<p className="text-sm text-gray-400 italic">{t("memo.fetching-data")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : isComplete ? (
|
) : isComplete ? (
|
||||||
@ -69,7 +69,7 @@ const Explore = () => {
|
|||||||
</div>
|
</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}>
|
<span className="cursor-pointer text-sm italic text-gray-500 hover:text-green-600" onClick={fetchMemos}>
|
||||||
{t("memo.fetch-more")}
|
{t("memo.fetch-more")}
|
||||||
</span>
|
</span>
|
||||||
|
@ -68,7 +68,7 @@ const Home = () => {
|
|||||||
<MemoView key={`${memo.id}-${memo.updateTime}`} memo={memo} showVisibility showPinnedStyle showParent />
|
<MemoView key={`${memo.id}-${memo.updateTime}`} memo={memo} showVisibility showPinnedStyle showParent />
|
||||||
))}
|
))}
|
||||||
{isRequesting ? (
|
{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>
|
<p className="text-sm text-gray-400 italic">{t("memo.fetching-data")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : isComplete ? (
|
) : isComplete ? (
|
||||||
@ -79,7 +79,7 @@ const Home = () => {
|
|||||||
</div>
|
</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}>
|
<span className="cursor-pointer text-sm italic text-gray-500 hover:text-green-600" onClick={fetchMemos}>
|
||||||
{t("memo.fetch-more")}
|
{t("memo.fetch-more")}
|
||||||
</span>
|
</span>
|
||||||
|
@ -97,7 +97,7 @@ const UserProfile = () => {
|
|||||||
<MemoView key={memo.id} memo={memo} showVisibility showPinnedStyle showParent />
|
<MemoView key={memo.id} memo={memo} showVisibility showPinnedStyle showParent />
|
||||||
))}
|
))}
|
||||||
{isRequesting ? (
|
{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>
|
<p className="text-sm text-gray-400 italic">{t("memo.fetching-data")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : isComplete ? (
|
) : isComplete ? (
|
||||||
@ -108,7 +108,7 @@ const UserProfile = () => {
|
|||||||
</div>
|
</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}>
|
<span className="cursor-pointer text-sm italic text-gray-500 hover:text-green-600" onClick={fetchMemos}>
|
||||||
{t("memo.fetch-more")}
|
{t("memo.fetch-more")}
|
||||||
</span>
|
</span>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user