feat: update memo relations dialog

This commit is contained in:
Steven 2023-10-19 00:18:07 +08:00
parent 22d331d6c4
commit 21c70e7993
10 changed files with 190 additions and 15 deletions

View File

@ -194,9 +194,9 @@ func (s *APIV1Service) GetMemoList(c echo.Context) error {
if tag != "" { if tag != "" {
contentSearch = append(contentSearch, "#"+tag) contentSearch = append(contentSearch, "#"+tag)
} }
contentSlice := c.QueryParams()["content"] content := c.QueryParam("content")
if len(contentSlice) > 0 { if content != "" {
contentSearch = append(contentSearch, contentSlice...) contentSearch = append(contentSearch, content)
} }
findMemoMessage.ContentSearch = contentSearch findMemoMessage.ContentSearch = contentSearch

View File

@ -0,0 +1,134 @@
import { Button, Input } from "@mui/joy";
import { isNaN, unionBy } from "lodash-es";
import React, { useState } from "react";
import { toast } from "react-hot-toast";
import { memoServiceClient } from "@/grpcweb";
import { Memo } from "@/types/proto/api/v2/memo_service";
import { useTranslate } from "@/utils/i18n";
import { generateDialog } from "./Dialog";
import Icon from "./Icon";
interface Props extends DialogProps {
onCancel?: () => void;
onConfirm?: (memoIdList: number[]) => void;
}
const CreateMemoRelationDialog: React.FC<Props> = (props: Props) => {
const { destroy, onCancel, onConfirm } = props;
const t = useTranslate();
const [memoId, setMemoId] = useState<string>("");
const [memoList, setMemoList] = useState<Memo[]>([]);
const handleMemoIdInputKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Enter") {
handleSaveBtnClick();
}
};
const handleMemoIdChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
const memoId = event.target.value;
setMemoId(memoId.trim());
};
const handleSaveBtnClick = async () => {
const id = Number(memoId);
if (isNaN(id)) {
toast.error("Invalid memo id");
return;
}
try {
const { memo } = await memoServiceClient.getMemo({
id,
});
if (!memo) {
toast.error("Not found memo");
return;
}
setMemoId("");
setMemoList(unionBy([memo, ...memoList], "id"));
} catch (error: any) {
console.error(error);
toast.error(error.response.data.message);
}
};
const handleDeleteMemoRelation = async (memo: Memo) => {
setMemoList(memoList.filter((m) => m !== memo));
};
const handleCloseDialog = () => {
if (onCancel) {
onCancel();
}
destroy();
};
const handleConfirmBtnClick = async () => {
if (onConfirm) {
onConfirm(memoList.map((memo) => memo.id));
}
destroy();
};
return (
<>
<div className="dialog-header-container">
<p className="title-text">{"Relations"}</p>
<button className="btn close-btn" onClick={() => destroy()}>
<Icon.X />
</button>
</div>
<div className="dialog-content-container !w-80">
<Input
className="mb-2"
size="md"
placeholder={"Memo ID. e.g. 286"}
value={memoId}
onChange={handleMemoIdChanged}
onKeyDown={handleMemoIdInputKeyDown}
fullWidth
endDecorator={<Icon.Check onClick={handleSaveBtnClick} className="w-4 h-auto cursor-pointer hover:opacity-80" />}
/>
{memoList.length > 0 && (
<>
<div className="w-full flex flex-row justify-start items-start flex-wrap gap-x-2 gap-y-1">
{memoList.map((memo) => (
<div
className="max-w-[120px] text-sm mr-2 mt-1 cursor-pointer truncate opacity-80 dark:text-gray-300 hover:opacity-60 hover:line-through"
key={memo.id}
onClick={() => handleDeleteMemoRelation(memo)}
>
<span className="font-mono mr-1">#{memo.id}</span>
<span className="opacity-80">{memo.content}</span>
</div>
))}
</div>
</>
)}
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
<Button variant="plain" color="neutral" onClick={handleCloseDialog}>
{t("common.cancel")}
</Button>
<Button onClick={handleConfirmBtnClick} disabled={memoList.length === 0}>
{t("common.confirm")}
</Button>
</div>
</div>
</>
);
};
function showCreateMemoRelationDialog(props: Omit<Props, "destroy" | "hide">) {
generateDialog(
{
className: "create-memo-relation-dialog",
dialogName: "create-memo-relation-dialog",
},
CreateMemoRelationDialog,
props
);
}
export default showCreateMemoRelationDialog;

View File

@ -1,4 +1,5 @@
import { CssVarsProvider } from "@mui/joy"; import { CssVarsProvider } from "@mui/joy";
import classNames from "classnames";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
@ -11,6 +12,7 @@ import "@/less/base-dialog.less";
interface DialogConfig { interface DialogConfig {
dialogName: string; dialogName: string;
className?: string; className?: string;
containerClassName?: string;
clickSpaceDestroy?: boolean; clickSpaceDestroy?: boolean;
} }
@ -19,7 +21,7 @@ interface Props extends DialogConfig, DialogProps {
} }
const BaseDialog: React.FC<Props> = (props: Props) => { const BaseDialog: React.FC<Props> = (props: Props) => {
const { children, className, clickSpaceDestroy, dialogName, destroy } = props; const { children, className, containerClassName, clickSpaceDestroy, dialogName, destroy } = props;
const dialogStore = useDialogStore(); const dialogStore = useDialogStore();
const dialogContainerRef = useRef<HTMLDivElement>(null); const dialogContainerRef = useRef<HTMLDivElement>(null);
const dialogIndex = dialogStore.state.dialogStack.findIndex((item) => item === dialogName); const dialogIndex = dialogStore.state.dialogStack.findIndex((item) => item === dialogName);
@ -55,8 +57,8 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
}; };
return ( return (
<div className={`dialog-wrapper ${className ?? ""}`} onMouseDown={handleSpaceClicked}> <div className={classNames("dialog-wrapper", className)} onMouseDown={handleSpaceClicked}>
<div ref={dialogContainerRef} className="dialog-container" onMouseDown={(e) => e.stopPropagation()}> <div ref={dialogContainerRef} className={classNames("dialog-container", containerClassName)} onMouseDown={(e) => e.stopPropagation()}>
{children} {children}
</div> </div>
</div> </div>

View File

@ -83,12 +83,12 @@ const Header = () => {
return ( return (
<div <div
className={`fixed sm:sticky top-0 left-0 w-full sm:w-56 h-full shrink-0 pointer-events-none sm:pointer-events-auto z-10 ${ className={`fixed sm:sticky top-0 left-0 w-full sm:w-56 h-screen shrink-0 pointer-events-none sm:pointer-events-auto z-10 ${
showHeader && "pointer-events-auto" showHeader && "pointer-events-auto"
}`} }`}
> >
<div <div
className={`fixed top-0 left-0 w-full h-full opacity-0 pointer-events-none transition-opacity duration-300 sm:!hidden ${ className={`fixed top-0 left-0 w-full h-full max-h-screen opacity-0 pointer-events-none transition-opacity duration-300 sm:!hidden ${
showHeader && "opacity-60 pointer-events-auto" showHeader && "opacity-60 pointer-events-auto"
}`} }`}
onClick={() => layoutStore.setHeaderStatus(false)} onClick={() => layoutStore.setHeaderStatus(false)}

View File

@ -25,6 +25,7 @@ import "@/less/memo.less";
interface Props { interface Props {
memo: Memo; memo: Memo;
showVisibility?: boolean; showVisibility?: boolean;
showPinnedStyle?: boolean;
lazyRendering?: boolean; lazyRendering?: boolean;
} }
@ -229,7 +230,7 @@ const Memo: React.FC<Props> = (props: Props) => {
return ( return (
<> <>
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned && !readonly ? "pinned" : ""}`} ref={memoContainerRef}> <div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned && props.showPinnedStyle ? "pinned" : ""}`} ref={memoContainerRef}>
<div className="memo-top-wrapper"> <div className="memo-top-wrapper">
<div className="w-full max-w-[calc(100%-20px)] flex flex-row justify-start items-center mr-1"> <div className="w-full max-w-[calc(100%-20px)] flex flex-row justify-start items-center mr-1">
<span className="text-sm text-gray-400 select-none" onClick={handleGotoMemoDetailPage}> <span className="text-sm text-gray-400 select-none" onClick={handleGotoMemoDetailPage}>
@ -290,6 +291,12 @@ const Memo: React.FC<Props> = (props: Props) => {
<div className="flex flex-row justify-start items-center"> <div className="flex flex-row justify-start items-center">
{creator && ( {creator && (
<> <>
<Link className="flex flex-row justify-start items-center" to={`/m/${memo.id}`}>
<Tooltip title={"Identifier"} placement="top">
<span className="text-sm text-gray-500 dark:text-gray-400">#{memo.id}</span>
</Tooltip>
</Link>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
<Link to={`/u/${encodeURIComponent(memo.creatorUsername)}`}> <Link to={`/u/${encodeURIComponent(memo.creatorUsername)}`}>
<Tooltip title={"Creator"} placement="top"> <Tooltip title={"Creator"} placement="top">
<span className="flex flex-row justify-start items-center"> <span className="flex flex-row justify-start items-center">
@ -298,7 +305,7 @@ const Memo: React.FC<Props> = (props: Props) => {
</span> </span>
</Tooltip> </Tooltip>
</Link> </Link>
{memo.pinned && ( {memo.pinned && props.showPinnedStyle && (
<> <>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" /> <Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
<Tooltip title={"Pinned"} placement="top"> <Tooltip title={"Pinned"} placement="top">

View File

@ -31,7 +31,13 @@ const MemoEditorDialog: React.FC<Props> = ({ memoId, relationList, destroy }: Pr
</button> </button>
</div> </div>
<div className="flex flex-col justify-start items-start max-w-full w-[36rem]"> <div className="flex flex-col justify-start items-start max-w-full w-[36rem]">
<MemoEditor cacheKey={`memo-editor-${memoId}`} memoId={memoId} relationList={relationList} onConfirm={handleCloseBtnClick} /> <MemoEditor
className="border-none !p-0 -mb-2"
cacheKey={`memo-editor-${memoId}`}
memoId={memoId}
relationList={relationList}
onConfirm={handleCloseBtnClick}
/>
</div> </div>
</> </>
); );
@ -42,6 +48,7 @@ export default function showMemoEditorDialog(props: Pick<Props, "memoId" | "rela
{ {
className: "memo-editor-dialog", className: "memo-editor-dialog",
dialogName: "memo-editor-dialog", dialogName: "memo-editor-dialog",
containerClassName: "dark:!bg-zinc-700",
}, },
MemoEditorDialog, MemoEditorDialog,
props props

View File

@ -1,5 +1,5 @@
import { Select, Option, Button } from "@mui/joy"; import { Select, Option, Button } from "@mui/joy";
import { isNumber, last, uniq } from "lodash-es"; import { isNumber, last, uniq, uniqBy } from "lodash-es";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -10,6 +10,7 @@ import { getMatchedNodes } from "@/labs/marked";
import { useFilterStore, useGlobalStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "@/store/module"; import { useFilterStore, useGlobalStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "@/store/module";
import { Resource } from "@/types/proto/api/v2/resource_service"; import { Resource } from "@/types/proto/api/v2/resource_service";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import showCreateMemoRelationDialog from "../CreateMemoRelationDialog";
import showCreateResourceDialog from "../CreateResourceDialog"; import showCreateResourceDialog from "../CreateResourceDialog";
import Icon from "../Icon"; import Icon from "../Icon";
import VisibilityIcon from "../VisibilityIcon"; import VisibilityIcon from "../VisibilityIcon";
@ -181,6 +182,23 @@ const MemoEditor = (props: Props) => {
}); });
}; };
const handleAddMemoRelationBtnClick = () => {
showCreateMemoRelationDialog({
onConfirm: (memoIdList) => {
setState((prevState) => ({
...prevState,
relationList: uniqBy(
[
...memoIdList.map((id) => ({ memoId: memoId || UNKNOWN_ID, relatedMemoId: id, type: "REFERENCE" as MemoRelationType })),
...state.relationList,
].filter((relation) => relation.relatedMemoId !== (memoId || UNKNOWN_ID)),
"relatedMemoId"
),
}));
},
});
};
const handleSetResourceList = (resourceList: Resource[]) => { const handleSetResourceList = (resourceList: Resource[]) => {
setState((prevState) => ({ setState((prevState) => ({
...prevState, ...prevState,
@ -406,7 +424,10 @@ const MemoEditor = (props: Props) => {
<div className="flex flex-row justify-start items-center"> <div className="flex flex-row justify-start items-center">
<TagSelector onTagSelectorClick={(tag) => handleTagSelectorClick(tag)} /> <TagSelector onTagSelectorClick={(tag) => handleTagSelectorClick(tag)} />
<button 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"> <button 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">
<Icon.Paperclip className="w-5 h-5 mx-auto" onClick={handleUploadFileBtnClick} /> <Icon.Image className="w-5 h-5 mx-auto" onClick={handleUploadFileBtnClick} />
</button>
<button 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">
<Icon.Link className="w-5 h-5 mx-auto" onClick={handleAddMemoRelationBtnClick} />
</button> </button>
<button 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"> <button 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">
<Icon.CheckSquare className="w-5 h-5 mx-auto" onClick={handleCheckBoxBtnClick} /> <Icon.CheckSquare className="w-5 h-5 mx-auto" onClick={handleCheckBoxBtnClick} />

View File

@ -132,7 +132,7 @@ const MemoList: React.FC = () => {
return ( return (
<div className="flex flex-col justify-start items-start w-full max-w-full overflow-y-scroll pb-28 hide-scrollbar"> <div className="flex flex-col justify-start items-start w-full max-w-full overflow-y-scroll pb-28 hide-scrollbar">
{sortedMemos.map((memo) => ( {sortedMemos.map((memo) => (
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} lazyRendering showVisibility /> <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} lazyRendering showVisibility showPinnedStyle />
))} ))}
{isFetching ? ( {isFetching ? (
<div className="flex flex-col justify-start items-center w-full mt-2 mb-1"> <div className="flex flex-col justify-start items-center w-full mt-2 mb-1">

View File

@ -22,7 +22,7 @@
} }
.btn { .btn {
@apply flex flex-col justify-center items-center w-6 h-6 rounded hover:bg-gray-100 dark:hover:bg-zinc-700 hover:shadow; @apply flex flex-col justify-center items-center w-6 h-6 rounded hover:bg-gray-100 dark:hover:bg-zinc-900 hover:shadow;
} }
} }

View File

@ -132,6 +132,10 @@ const MemoDetail = () => {
<MemoRelationListView relationList={referenceRelations} /> <MemoRelationListView relationList={referenceRelations} />
<div className="w-full mt-4 flex flex-col sm:flex-row justify-start sm:justify-between sm:items-center gap-2"> <div className="w-full mt-4 flex flex-col sm:flex-row justify-start sm:justify-between sm:items-center gap-2">
<div className="flex flex-row justify-start items-center"> <div className="flex flex-row justify-start items-center">
<Tooltip title={"Identifier"} placement="top">
<span className="text-sm text-gray-500 dark:text-gray-400">#{memo.id}</span>
</Tooltip>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
<Link to={`/u/${encodeURIComponent(memo.creatorUsername)}`}> <Link to={`/u/${encodeURIComponent(memo.creatorUsername)}`}>
<Tooltip title={"Creator"} placement="top"> <Tooltip title={"Creator"} placement="top">
<span className="flex flex-row justify-start items-center"> <span className="flex flex-row justify-start items-center">