diff --git a/web/src/components/CreateMemoRelationDialog.tsx b/web/src/components/CreateMemoRelationDialog.tsx deleted file mode 100644 index 8ad69bbd..00000000 --- a/web/src/components/CreateMemoRelationDialog.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { Autocomplete, AutocompleteOption, Button, Checkbox, Chip, IconButton } from "@mui/joy"; -import React, { useState } from "react"; -import { toast } from "react-hot-toast"; -import useDebounce from "react-use/lib/useDebounce"; -import { memoServiceClient } from "@/grpcweb"; -import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; -import useCurrentUser from "@/hooks/useCurrentUser"; -import { Memo } from "@/types/proto/api/v1/memo_service"; -import { useTranslate } from "@/utils/i18n"; -import { generateDialog } from "./Dialog"; -import Icon from "./Icon"; - -interface Props extends DialogProps { - onConfirm: (memos: Memo[], embedded?: boolean) => void; - // Custom filter function for filtering memos. - filter?: (memo: Memo) => boolean; -} - -const CreateMemoRelationDialog: React.FC = (props: Props) => { - const { destroy, onConfirm } = props; - const t = useTranslate(); - const user = useCurrentUser(); - const [searchText, setSearchText] = useState(""); - const [isFetching, setIsFetching] = useState(true); - const [fetchedMemos, setFetchedMemos] = useState([]); - const [selectedMemos, setSelectedMemos] = useState([]); - const [embedded, setEmbedded] = useState(true); - const filteredMemos = fetchedMemos.filter((memo) => !selectedMemos.includes(memo) && (!props.filter || props.filter(memo))); - - useDebounce( - async () => { - setIsFetching(true); - try { - const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `include_comments == true`]; - if (searchText) { - filters.push(`content_search == [${JSON.stringify(searchText)}]`); - } - const { memos } = await memoServiceClient.listMemos({ - pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, - filter: filters.length > 0 ? filters.join(" && ") : undefined, - }); - setFetchedMemos(memos); - } catch (error: any) { - console.error(error); - toast.error(error.response.data.message); - } - setIsFetching(false); - }, - 300, - [searchText], - ); - - const getHighlightedContent = (content: string) => { - const index = content.toLowerCase().indexOf(searchText.toLowerCase()); - if (index === -1) { - return content; - } - let before = content.slice(0, index); - if (before.length > 20) { - before = "..." + before.slice(before.length - 20); - } - const highlighted = content.slice(index, index + searchText.length); - let after = content.slice(index + searchText.length); - if (after.length > 20) { - after = after.slice(0, 20) + "..."; - } - - return ( - <> - {before} - {highlighted} - {after} - - ); - }; - - const handleCloseDialog = () => { - destroy(); - }; - - const handleConfirmBtnClick = async () => { - onConfirm(selectedMemos, embedded); - destroy(); - }; - - return ( - <> -
-

{t("reference.add-references")}

- destroy()}> - - -
-
- setSearchText(value.trim())} - getOptionKey={(memo) => memo.name} - getOptionLabel={(memo) => memo.content} - isOptionEqualToValue={(memo, value) => memo.name === value.name} - renderOption={(props, memo) => ( - -
-

{memo.displayTime?.toLocaleString()}

-

{searchText ? getHighlightedContent(memo.content) : memo.snippet}

-
-
- )} - renderTags={(memos) => - memos.map((memo) => ( - -
-

{memo.displayTime?.toLocaleString()}

- {memo.content} -
-
- )) - } - onChange={(_, value) => setSelectedMemos(value)} - /> -
- setEmbedded(e.target.checked)} /> -
-
- - -
-
- - ); -}; - -function showCreateMemoRelationDialog(props: Omit) { - generateDialog( - { - className: "create-memo-relation-dialog", - dialogName: "create-memo-relation-dialog", - }, - CreateMemoRelationDialog, - props, - ); -} - -export default showCreateMemoRelationDialog; diff --git a/web/src/components/MemoEditor/ActionButton/AddMemoRelationButton.tsx b/web/src/components/MemoEditor/ActionButton/AddMemoRelationButton.tsx deleted file mode 100644 index e8be41d5..00000000 --- a/web/src/components/MemoEditor/ActionButton/AddMemoRelationButton.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { IconButton } from "@mui/joy"; -import { uniqBy } from "lodash-es"; -import { useContext } from "react"; -import toast from "react-hot-toast"; -import showCreateMemoRelationDialog from "@/components/CreateMemoRelationDialog"; -import Icon from "@/components/Icon"; -import { MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service"; -import { Memo } from "@/types/proto/api/v1/memo_service"; -import { EditorRefActions } from "../Editor"; -import { MemoEditorContext } from "../types"; - -interface Props { - editorRef: React.RefObject; -} - -const AddMemoRelationButton = (props: Props) => { - const { editorRef } = props; - const context = useContext(MemoEditorContext); - - const handleAddMemoRelationBtnClick = () => { - showCreateMemoRelationDialog({ - onConfirm: (memos, embedded) => { - // If embedded mode is enabled, embed the memo instead of creating a relation. - if (embedded) { - if (!editorRef.current) { - toast.error("Failed to embed memo"); - return; - } - - const cursorPosition = editorRef.current.getCursorPosition(); - const prevValue = editorRef.current.getContent().slice(0, cursorPosition); - if (prevValue !== "" && !prevValue.endsWith("\n")) { - editorRef.current.insertText("\n"); - } - for (const memo of memos) { - editorRef.current.insertText(`![[memos/${memo.uid}]]\n`); - } - setTimeout(() => { - editorRef.current?.scrollToCursor(); - editorRef.current?.focus(); - }); - return; - } - - context.setRelationList( - uniqBy( - [ - ...memos.map((memo) => ({ - memo: context.memoName || "", - relatedMemo: memo.name, - type: MemoRelation_Type.REFERENCE, - })), - ...context.relationList, - ].filter((relation) => relation.relatedMemo !== context.memoName), - "relatedMemo", - ), - ); - }, - filter: (memo: Memo) => - memo.name !== context.memoName && !context.relationList.some((relation) => relation.relatedMemo === memo.name), - }); - }; - - return ( - - - - ); -}; - -export default AddMemoRelationButton; diff --git a/web/src/components/MemoEditor/ActionButton/AddMemoRelationPopover.tsx b/web/src/components/MemoEditor/ActionButton/AddMemoRelationPopover.tsx new file mode 100644 index 00000000..a2ece33f --- /dev/null +++ b/web/src/components/MemoEditor/ActionButton/AddMemoRelationPopover.tsx @@ -0,0 +1,185 @@ +import { Autocomplete, AutocompleteOption, Button, Checkbox, Chip, IconButton } from "@mui/joy"; +import { uniqBy } from "lodash-es"; +import React, { useContext, useState } from "react"; +import { toast } from "react-hot-toast"; +import useDebounce from "react-use/lib/useDebounce"; +import Icon from "@/components/Icon"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/Popover"; +import { memoServiceClient } from "@/grpcweb"; +import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service"; +import { Memo } from "@/types/proto/api/v1/memo_service"; +import { useTranslate } from "@/utils/i18n"; +import { EditorRefActions } from "../Editor"; +import { MemoEditorContext } from "../types"; + +interface Props { + editorRef: React.RefObject; +} + +const AddMemoRelationPopover = (props: Props) => { + const { editorRef } = props; + const t = useTranslate(); + const context = useContext(MemoEditorContext); + const user = useCurrentUser(); + const [searchText, setSearchText] = useState(""); + const [isFetching, setIsFetching] = useState(true); + const [fetchedMemos, setFetchedMemos] = useState([]); + const [selectedMemos, setSelectedMemos] = useState([]); + const [embedded, setEmbedded] = useState(true); + const [popoverOpen, setPopoverOpen] = useState(false); + + const filteredMemos = fetchedMemos.filter( + (memo) => + !selectedMemos.includes(memo) && + memo.name !== context.memoName && + !context.relationList.some((relation) => relation.relatedMemo === memo.name), + ); + + useDebounce( + async () => { + setIsFetching(true); + try { + const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`]; + if (searchText) { + filters.push(`content_search == [${JSON.stringify(searchText)}]`); + } + const { memos } = await memoServiceClient.listMemos({ + pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, + filter: filters.length > 0 ? filters.join(" && ") : undefined, + }); + setFetchedMemos(memos); + } catch (error: any) { + console.error(error); + toast.error(error.response.data.message); + } + setIsFetching(false); + }, + 300, + [searchText], + ); + + const getHighlightedContent = (content: string) => { + const index = content.toLowerCase().indexOf(searchText.toLowerCase()); + if (index === -1) { + return content; + } + let before = content.slice(0, index); + if (before.length > 20) { + before = "..." + before.slice(before.length - 20); + } + const highlighted = content.slice(index, index + searchText.length); + let after = content.slice(index + searchText.length); + if (after.length > 20) { + after = after.slice(0, 20) + "..."; + } + + return ( + <> + {before} + {highlighted} + {after} + + ); + }; + + const addMemoRelations = async () => { + // If embedded mode is enabled, embed the memo instead of creating a relation. + if (embedded) { + if (!editorRef.current) { + toast.error("Failed to embed memo"); + return; + } + + const cursorPosition = editorRef.current.getCursorPosition(); + const prevValue = editorRef.current.getContent().slice(0, cursorPosition); + if (prevValue !== "" && !prevValue.endsWith("\n")) { + editorRef.current.insertText("\n"); + } + for (const memo of selectedMemos) { + editorRef.current.insertText(`![[memos/${memo.uid}]]\n`); + } + setTimeout(() => { + editorRef.current?.scrollToCursor(); + editorRef.current?.focus(); + }); + } else { + context.setRelationList( + uniqBy( + [ + ...selectedMemos.map((memo) => ({ + memo: context.memoName || "", + relatedMemo: memo.name, + type: MemoRelation_Type.REFERENCE, + })), + ...context.relationList, + ].filter((relation) => relation.relatedMemo !== context.memoName), + "relatedMemo", + ), + ); + } + setSelectedMemos([]); + setPopoverOpen(false); + }; + + return ( + + + + + + + +
+ setSearchText(value.trim())} + getOptionKey={(memo) => memo.name} + getOptionLabel={(memo) => memo.content} + isOptionEqualToValue={(memo, value) => memo.name === value.name} + renderOption={(props, memo) => ( + +
+

{memo.displayTime?.toLocaleString()}

+

{searchText ? getHighlightedContent(memo.content) : memo.snippet}

+
+
+ )} + renderTags={(memos) => + memos.map((memo) => ( + +
+

{memo.displayTime?.toLocaleString()}

+ {memo.content} +
+
+ )) + } + onChange={(_, value) => setSelectedMemos(value)} + /> +
+ setEmbedded(e.target.checked)} /> +
+
+ +
+
+
+
+ ); +}; + +export default AddMemoRelationPopover; diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index 5ad0e155..6b54a736 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -20,7 +20,7 @@ import { useTranslate } from "@/utils/i18n"; import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/memo"; import Icon from "../Icon"; import VisibilityIcon from "../VisibilityIcon"; -import AddMemoRelationButton from "./ActionButton/AddMemoRelationButton"; +import AddMemoRelationPopover from "./ActionButton/AddMemoRelationPopover"; import MarkdownMenu from "./ActionButton/MarkdownMenu"; import TagSelector from "./ActionButton/TagSelector"; import UploadResourceButton from "./ActionButton/UploadResourceButton"; @@ -432,7 +432,7 @@ const MemoEditor = (props: Props) => { - + diff --git a/web/src/theme/index.ts b/web/src/theme/index.ts index d202c02f..22f89a45 100644 --- a/web/src/theme/index.ts +++ b/web/src/theme/index.ts @@ -22,6 +22,13 @@ const theme = extendTheme({ }, }, }, + JoyAutocomplete: { + styleOverrides: { + listbox: { + zIndex: 9999, + }, + }, + }, }, });