mirror of
https://github.com/usememos/memos.git
synced 2025-02-14 18:30:42 +01:00
chore: update memo relation dialog
This commit is contained in:
parent
f654d3c90e
commit
08ac60cc70
@ -1,8 +1,10 @@
|
|||||||
import { Button, IconButton, Input } from "@mui/joy";
|
import { Autocomplete, AutocompleteOption, Button, Chip, IconButton } from "@mui/joy";
|
||||||
import { isNaN, unionBy } from "lodash-es";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
import useDebounce from "react-use/lib/useDebounce";
|
||||||
import { memoServiceClient } from "@/grpcweb";
|
import { memoServiceClient } from "@/grpcweb";
|
||||||
|
import { getDateTimeString } from "@/helpers/datetime";
|
||||||
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import { Memo } from "@/types/proto/api/v2/memo_service";
|
import { Memo } from "@/types/proto/api/v2/memo_service";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
@ -16,46 +18,58 @@ interface Props extends DialogProps {
|
|||||||
const CreateMemoRelationDialog: React.FC<Props> = (props: Props) => {
|
const CreateMemoRelationDialog: React.FC<Props> = (props: Props) => {
|
||||||
const { destroy, onCancel, onConfirm } = props;
|
const { destroy, onCancel, onConfirm } = props;
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const [memoId, setMemoId] = useState<string>("");
|
const user = useCurrentUser();
|
||||||
const [memoList, setMemoList] = useState<Memo[]>([]);
|
const [searchText, setSearchText] = useState<string>("");
|
||||||
|
const [isFetching, setIsFetching] = useState<boolean>(true);
|
||||||
|
const [fetchedMemos, setFetchedMemos] = useState<Memo[]>([]);
|
||||||
|
const [selectedMemos, setSelectedMemos] = useState<Memo[]>([]);
|
||||||
|
const filteredMemos = fetchedMemos.filter((memo) => !selectedMemos.includes(memo));
|
||||||
|
|
||||||
const handleMemoIdInputKeyDown = (event: React.KeyboardEvent) => {
|
useDebounce(
|
||||||
if (event.key === "Enter") {
|
async () => {
|
||||||
handleSaveBtnClick();
|
setIsFetching(true);
|
||||||
}
|
try {
|
||||||
};
|
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`];
|
||||||
|
if (searchText) {
|
||||||
const handleMemoIdChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
|
filters.push(`content_search == [${JSON.stringify(searchText)}]`);
|
||||||
const memoId = event.target.value;
|
}
|
||||||
setMemoId(memoId.trim());
|
const { memos } = await memoServiceClient.listMemos({
|
||||||
};
|
limit: 10,
|
||||||
|
filter: filters.length > 0 ? filters.join(" && ") : undefined,
|
||||||
const handleSaveBtnClick = async () => {
|
});
|
||||||
const id = Number(memoId);
|
setFetchedMemos(memos);
|
||||||
if (isNaN(id)) {
|
} catch (error: any) {
|
||||||
toast.error("Invalid memo id");
|
console.error(error);
|
||||||
return;
|
toast.error(error.response.data.message);
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { memo } = await memoServiceClient.getMemo({
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
if (!memo) {
|
|
||||||
toast.error("Not found memo");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
setIsFetching(false);
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
[searchText]
|
||||||
|
);
|
||||||
|
|
||||||
setMemoId("");
|
const getHighlightedContent = (content: string) => {
|
||||||
setMemoList(unionBy([memo, ...memoList], "id"));
|
const index = content.toLowerCase().indexOf(searchText.toLowerCase());
|
||||||
} catch (error: any) {
|
if (index === -1) {
|
||||||
console.error(error);
|
return content;
|
||||||
toast.error(error.response.data.message);
|
}
|
||||||
|
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) + "...";
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteMemoRelation = async (memo: Memo) => {
|
return (
|
||||||
setMemoList(memoList.filter((m) => m !== memo));
|
<>
|
||||||
|
{before}
|
||||||
|
<mark className="font-medium">{highlighted}</mark>
|
||||||
|
{after}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseDialog = () => {
|
const handleCloseDialog = () => {
|
||||||
@ -67,7 +81,7 @@ const CreateMemoRelationDialog: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
const handleConfirmBtnClick = async () => {
|
const handleConfirmBtnClick = async () => {
|
||||||
if (onConfirm) {
|
if (onConfirm) {
|
||||||
onConfirm(memoList.map((memo) => memo.id));
|
onConfirm(selectedMemos.map((memo) => memo.id));
|
||||||
}
|
}
|
||||||
destroy();
|
destroy();
|
||||||
};
|
};
|
||||||
@ -81,37 +95,49 @@ const CreateMemoRelationDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container !w-80">
|
<div className="dialog-content-container !w-80">
|
||||||
<Input
|
<Autocomplete
|
||||||
className="mb-2"
|
className="w-full"
|
||||||
size="md"
|
size="md"
|
||||||
placeholder={"Input memo ID. e.g. 26"}
|
clearOnBlur
|
||||||
value={memoId}
|
disableClearable
|
||||||
onChange={handleMemoIdChanged}
|
placeholder={"Search content"}
|
||||||
onKeyDown={handleMemoIdInputKeyDown}
|
noOptionsText={"No memos found"}
|
||||||
fullWidth
|
options={filteredMemos}
|
||||||
endDecorator={<Icon.Check onClick={handleSaveBtnClick} className="w-4 h-auto cursor-pointer hover:opacity-80" />}
|
loading={isFetching}
|
||||||
/>
|
inputValue={searchText}
|
||||||
{memoList.length > 0 && (
|
value={selectedMemos}
|
||||||
<>
|
multiple
|
||||||
<div className="w-full flex flex-row justify-start items-start flex-wrap gap-2 mt-1">
|
onInputChange={(_, value) => setSearchText(value.trim())}
|
||||||
{memoList.map((memo) => (
|
getOptionKey={(option) => option.name}
|
||||||
<div
|
getOptionLabel={(option) => option.content}
|
||||||
className="max-w-[50%] text-sm px-2 py-1 flex flex-row justify-start items-center border rounded-md cursor-pointer truncate opacity-80 text-gray-600 dark:text-gray-400 dark:border-zinc-700 dark:bg-zinc-900 hover:opacity-60 hover:line-through"
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
key={memo.name}
|
renderOption={(props, option) => (
|
||||||
onClick={() => handleDeleteMemoRelation(memo)}
|
<AutocompleteOption {...props}>
|
||||||
>
|
<div className="w-full flex flex-col justify-start items-start">
|
||||||
<span className="max-w-full text-ellipsis whitespace-nowrap overflow-hidden">{memo.content}</span>
|
<p className="text-xs text-gray-400 select-none">{getDateTimeString(option.displayTime)}</p>
|
||||||
<Icon.X className="opacity-60 w-4 h-auto shrink-0 ml-1" />
|
<p className="mt-0.5 text-sm leading-5 line-clamp-2">
|
||||||
|
{searchText ? getHighlightedContent(option.content) : option.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</AutocompleteOption>
|
||||||
|
)}
|
||||||
|
renderTags={(memos) =>
|
||||||
|
memos.map((memo) => (
|
||||||
|
<Chip key={memo.name} className="!max-w-full !rounded" variant="outlined" color="neutral">
|
||||||
|
<div className="w-full flex flex-col justify-start items-start">
|
||||||
|
<p className="text-xs text-gray-400 select-none">{getDateTimeString(memo.displayTime)}</p>
|
||||||
|
<span className="w-full text-sm leading-5 truncate">{memo.content}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</Chip>
|
||||||
</div>
|
))
|
||||||
</>
|
}
|
||||||
)}
|
onChange={(_, value) => setSelectedMemos(value)}
|
||||||
|
/>
|
||||||
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
|
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
|
||||||
<Button variant="plain" color="neutral" onClick={handleCloseDialog}>
|
<Button variant="plain" color="neutral" onClick={handleCloseDialog}>
|
||||||
{t("common.cancel")}
|
{t("common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirmBtnClick} disabled={memoList.length === 0}>
|
<Button onClick={handleConfirmBtnClick} disabled={selectedMemos.length === 0}>
|
||||||
{t("common.confirm")}
|
{t("common.confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,7 +43,7 @@ const RelationListView = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<Icon.Link className="w-4 h-auto shrink-0 opacity-80" />
|
<Icon.Link className="w-4 h-auto shrink-0 opacity-80" />
|
||||||
<span className="mx-1 max-w-full text-ellipsis whitespace-nowrap overflow-hidden">{memo.content}</span>
|
<span className="mx-1 max-w-full text-ellipsis whitespace-nowrap overflow-hidden">{memo.content}</span>
|
||||||
<Icon.X className="w-4 h-auto cursor-pointer opacity-60 hover:opacity-100" />
|
<Icon.X className="w-4 h-auto cursor-pointer shrink-0 opacity-60 hover:opacity-100" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -34,10 +34,10 @@ const Archived = () => {
|
|||||||
const filters = [`creator == "${user.name}"`, "row_status == 'ARCHIVED'"];
|
const filters = [`creator == "${user.name}"`, "row_status == 'ARCHIVED'"];
|
||||||
const contentSearch: string[] = [];
|
const contentSearch: string[] = [];
|
||||||
if (tagQuery) {
|
if (tagQuery) {
|
||||||
contentSearch.push(`"#${tagQuery}"`);
|
contentSearch.push(JSON.stringify(`#${tagQuery}`));
|
||||||
}
|
}
|
||||||
if (textQuery) {
|
if (textQuery) {
|
||||||
contentSearch.push(`"${textQuery}"`);
|
contentSearch.push(JSON.stringify(textQuery));
|
||||||
}
|
}
|
||||||
if (contentSearch.length > 0) {
|
if (contentSearch.length > 0) {
|
||||||
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
|
@ -32,10 +32,10 @@ const Explore = () => {
|
|||||||
const filters = [`row_status == "NORMAL"`, `visibilities == [${user ? "'PUBLIC', 'PROTECTED'" : "'PUBLIC'"}]`];
|
const filters = [`row_status == "NORMAL"`, `visibilities == [${user ? "'PUBLIC', 'PROTECTED'" : "'PUBLIC'"}]`];
|
||||||
const contentSearch: string[] = [];
|
const contentSearch: string[] = [];
|
||||||
if (tagQuery) {
|
if (tagQuery) {
|
||||||
contentSearch.push(`"#${tagQuery}"`);
|
contentSearch.push(JSON.stringify(`#${tagQuery}`));
|
||||||
}
|
}
|
||||||
if (textQuery) {
|
if (textQuery) {
|
||||||
contentSearch.push(`"${textQuery}"`);
|
contentSearch.push(JSON.stringify(textQuery));
|
||||||
}
|
}
|
||||||
if (contentSearch.length > 0) {
|
if (contentSearch.length > 0) {
|
||||||
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
@ -56,7 +56,7 @@ const Explore = () => {
|
|||||||
<div className="relative w-full h-auto flex flex-col justify-start items-start px-4 sm:px-6">
|
<div className="relative w-full h-auto flex flex-col justify-start items-start px-4 sm:px-6">
|
||||||
<MemoFilter className="px-2 pb-2" />
|
<MemoFilter className="px-2 pb-2" />
|
||||||
{sortedMemos.map((memo) => (
|
{sortedMemos.map((memo) => (
|
||||||
<MemoView key={memo.id} memo={memo} showCreator />
|
<MemoView key={`${memo.id}-${memo.displayTime}`} memo={memo} showCreator />
|
||||||
))}
|
))}
|
||||||
{isRequesting ? (
|
{isRequesting ? (
|
||||||
<div className="flex flex-col justify-start items-center w-full my-4">
|
<div className="flex flex-col justify-start items-center w-full my-4">
|
||||||
|
@ -42,10 +42,10 @@ const Home = () => {
|
|||||||
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`];
|
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`];
|
||||||
const contentSearch: string[] = [];
|
const contentSearch: string[] = [];
|
||||||
if (tagQuery) {
|
if (tagQuery) {
|
||||||
contentSearch.push(`"#${tagQuery}"`);
|
contentSearch.push(JSON.stringify(`#${tagQuery}`));
|
||||||
}
|
}
|
||||||
if (textQuery) {
|
if (textQuery) {
|
||||||
contentSearch.push(`"${textQuery}"`);
|
contentSearch.push(JSON.stringify(textQuery));
|
||||||
}
|
}
|
||||||
if (contentSearch.length > 0) {
|
if (contentSearch.length > 0) {
|
||||||
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
@ -73,7 +73,7 @@ const Home = () => {
|
|||||||
<div className="flex flex-col justify-start items-start w-full max-w-full pb-28">
|
<div className="flex flex-col justify-start items-start w-full max-w-full pb-28">
|
||||||
<MemoFilter className="px-2 pb-2" />
|
<MemoFilter className="px-2 pb-2" />
|
||||||
{sortedMemos.map((memo) => (
|
{sortedMemos.map((memo) => (
|
||||||
<MemoView key={`${memo.id}-${memo.updateTime}`} memo={memo} showVisibility showPinned />
|
<MemoView key={`${memo.id}-${memo.displayTime}`} memo={memo} showVisibility showPinned />
|
||||||
))}
|
))}
|
||||||
{isRequesting ? (
|
{isRequesting ? (
|
||||||
<div className="flex flex-col justify-start items-center w-full my-4">
|
<div className="flex flex-col justify-start items-center w-full my-4">
|
||||||
|
@ -198,7 +198,7 @@ const MemoDetail = () => {
|
|||||||
<span className="text-gray-400 text-sm ml-0.5">({comments.length})</span>
|
<span className="text-gray-400 text-sm ml-0.5">({comments.length})</span>
|
||||||
</div>
|
</div>
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => (
|
||||||
<MemoView key={comment.id} memo={comment} showCreator />
|
<MemoView key={`${memo.id}-${memo.displayTime}`} memo={comment} showCreator />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -69,10 +69,10 @@ const Timeline = () => {
|
|||||||
const filters = [`row_status == "NORMAL"`];
|
const filters = [`row_status == "NORMAL"`];
|
||||||
const contentSearch: string[] = [];
|
const contentSearch: string[] = [];
|
||||||
if (tagQuery) {
|
if (tagQuery) {
|
||||||
contentSearch.push(`"#${tagQuery}"`);
|
contentSearch.push(JSON.stringify(`#${tagQuery}`));
|
||||||
}
|
}
|
||||||
if (textQuery) {
|
if (textQuery) {
|
||||||
contentSearch.push(`"${textQuery}"`);
|
contentSearch.push(JSON.stringify(textQuery));
|
||||||
}
|
}
|
||||||
if (contentSearch.length > 0) {
|
if (contentSearch.length > 0) {
|
||||||
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
@ -90,10 +90,10 @@ const Timeline = () => {
|
|||||||
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`];
|
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`];
|
||||||
const contentSearch: string[] = [];
|
const contentSearch: string[] = [];
|
||||||
if (tagQuery) {
|
if (tagQuery) {
|
||||||
contentSearch.push(`"#${tagQuery}"`);
|
contentSearch.push(JSON.stringify(`#${tagQuery}`));
|
||||||
}
|
}
|
||||||
if (textQuery) {
|
if (textQuery) {
|
||||||
contentSearch.push(`"${textQuery}"`);
|
contentSearch.push(JSON.stringify(textQuery));
|
||||||
}
|
}
|
||||||
if (contentSearch.length > 0) {
|
if (contentSearch.length > 0) {
|
||||||
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
@ -159,7 +159,7 @@ const Timeline = () => {
|
|||||||
<div className={classNames("flex flex-col justify-start items-start", md ? "w-[calc(100%-8rem)]" : "w-full")}>
|
<div className={classNames("flex flex-col justify-start items-start", md ? "w-[calc(100%-8rem)]" : "w-full")}>
|
||||||
{group.memos.map((memo, index) => (
|
{group.memos.map((memo, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${memo.id}-${memo.createTime}`}
|
key={`${memo.id}-${memo.displayTime}`}
|
||||||
className={classNames("relative w-full flex flex-col justify-start items-start pl-4 sm:pl-10 pt-0")}
|
className={classNames("relative w-full flex flex-col justify-start items-start pl-4 sm:pl-10 pt-0")}
|
||||||
>
|
>
|
||||||
<MemoView className="!border !border-gray-100 dark:!border-zinc-700" memo={memo} />
|
<MemoView className="!border !border-gray-100 dark:!border-zinc-700" memo={memo} />
|
||||||
|
@ -67,10 +67,10 @@ const UserProfile = () => {
|
|||||||
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`];
|
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`];
|
||||||
const contentSearch: string[] = [];
|
const contentSearch: string[] = [];
|
||||||
if (tagQuery) {
|
if (tagQuery) {
|
||||||
contentSearch.push(`"#${tagQuery}"`);
|
contentSearch.push(JSON.stringify(`#${tagQuery}`));
|
||||||
}
|
}
|
||||||
if (textQuery) {
|
if (textQuery) {
|
||||||
contentSearch.push(`"${textQuery}"`);
|
contentSearch.push(JSON.stringify(textQuery));
|
||||||
}
|
}
|
||||||
if (contentSearch.length > 0) {
|
if (contentSearch.length > 0) {
|
||||||
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
@ -107,7 +107,7 @@ const UserProfile = () => {
|
|||||||
</div>
|
</div>
|
||||||
<MemoFilter className="px-2 pb-3" />
|
<MemoFilter className="px-2 pb-3" />
|
||||||
{sortedMemos.map((memo) => (
|
{sortedMemos.map((memo) => (
|
||||||
<MemoView key={memo.id} memo={memo} showVisibility showPinned />
|
<MemoView key={`${memo.id}-${memo.displayTime}`} memo={memo} showVisibility showPinned />
|
||||||
))}
|
))}
|
||||||
{isRequesting ? (
|
{isRequesting ? (
|
||||||
<div className="flex flex-col justify-start items-center w-full my-4">
|
<div className="flex flex-col justify-start items-center w-full my-4">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user