mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: implement inline memo editor
This commit is contained in:
@ -15,6 +15,7 @@ interface Props {
|
|||||||
memo: Memo;
|
memo: Memo;
|
||||||
className?: string;
|
className?: string;
|
||||||
hiddenActions?: ("edit" | "archive" | "delete" | "share" | "pin")[];
|
hiddenActions?: ("edit" | "archive" | "delete" | "share" | "pin")[];
|
||||||
|
onEdit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemoActionMenu = (props: Props) => {
|
const MemoActionMenu = (props: Props) => {
|
||||||
@ -50,6 +51,12 @@ const MemoActionMenu = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEditMemoClick = () => {
|
const handleEditMemoClick = () => {
|
||||||
|
if (props.onEdit) {
|
||||||
|
props.onEdit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove me later.
|
||||||
showMemoEditorDialog({
|
showMemoEditorDialog({
|
||||||
memoName: memo.name,
|
memoName: memo.name,
|
||||||
cacheKey: `${memo.name}-${memo.updateTime}`,
|
cacheKey: `${memo.name}-${memo.updateTime}`,
|
||||||
|
@ -38,6 +38,7 @@ export interface Props {
|
|||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
memoPatchRef?: React.MutableRefObject<Partial<Memo>>;
|
memoPatchRef?: React.MutableRefObject<Partial<Memo>>;
|
||||||
onConfirm?: (memoName: string) => void;
|
onConfirm?: (memoName: string) => void;
|
||||||
|
onCancel?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -439,7 +440,12 @@ 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 gap-2">
|
||||||
|
{props.onCancel && (
|
||||||
|
<Button className="!font-normal" color="neutral" variant="plain" loading={state.isRequesting} onClick={props.onCancel}>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
className="!font-normal"
|
className="!font-normal"
|
||||||
disabled={!allowSave}
|
disabled={!allowSave}
|
||||||
|
@ -14,7 +14,7 @@ import { convertVisibilityToString } from "@/utils/memo";
|
|||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import MemoActionMenu from "./MemoActionMenu";
|
import MemoActionMenu from "./MemoActionMenu";
|
||||||
import MemoContent from "./MemoContent";
|
import MemoContent from "./MemoContent";
|
||||||
import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog";
|
import MemoEditor from "./MemoEditor";
|
||||||
import MemoReactionistView from "./MemoReactionListView";
|
import MemoReactionistView from "./MemoReactionListView";
|
||||||
import MemoRelationListView from "./MemoRelationListView";
|
import MemoRelationListView from "./MemoRelationListView";
|
||||||
import MemoResourceListView from "./MemoResourceListView";
|
import MemoResourceListView from "./MemoResourceListView";
|
||||||
@ -45,6 +45,7 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
|||||||
const workspaceMemoRelatedSetting =
|
const workspaceMemoRelatedSetting =
|
||||||
workspaceSettingStore.getWorkspaceSettingByKey(WorkspaceSettingKey.MEMO_RELATED).memoRelatedSetting ||
|
workspaceSettingStore.getWorkspaceSettingByKey(WorkspaceSettingKey.MEMO_RELATED).memoRelatedSetting ||
|
||||||
WorkspaceMemoRelatedSetting.fromPartial({});
|
WorkspaceMemoRelatedSetting.fromPartial({});
|
||||||
|
const [showEditor, setShowEditor] = useState<boolean>(false);
|
||||||
const [creator, setCreator] = useState(userStore.getUserByName(memo.creator));
|
const [creator, setCreator] = useState(userStore.getUserByName(memo.creator));
|
||||||
const memoContainerRef = useRef<HTMLDivElement>(null);
|
const memoContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE);
|
const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE);
|
||||||
@ -85,10 +86,7 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
if (workspaceMemoRelatedSetting.enableDoubleClickEdit) {
|
if (workspaceMemoRelatedSetting.enableDoubleClickEdit) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
showMemoEditorDialog({
|
setShowEditor(true);
|
||||||
memoName: memo.name,
|
|
||||||
cacheKey: `${memo.name}-${memo.updateTime}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -137,50 +135,73 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2">
|
{!showEditor && (
|
||||||
<div className="w-auto invisible group-hover:visible flex flex-row justify-between items-center gap-2">
|
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2">
|
||||||
{props.showVisibility && memo.visibility !== Visibility.PRIVATE && (
|
<div className="w-auto invisible group-hover:visible flex flex-row justify-between items-center gap-2">
|
||||||
<Tooltip title={t(`memo.visibility.${convertVisibilityToString(memo.visibility).toLowerCase()}` as any)} placement="top">
|
{props.showVisibility && memo.visibility !== Visibility.PRIVATE && (
|
||||||
<span className="flex justify-center items-center hover:opacity-70">
|
<Tooltip title={t(`memo.visibility.${convertVisibilityToString(memo.visibility).toLowerCase()}` as any)} placement="top">
|
||||||
<VisibilityIcon visibility={memo.visibility} />
|
<span className="flex justify-center items-center hover:opacity-70">
|
||||||
</span>
|
<VisibilityIcon visibility={memo.visibility} />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{currentUser && <ReactionSelector className="border-none w-auto h-auto" memo={memo} />}
|
||||||
|
</div>
|
||||||
|
{!isInMemoDetailPage && (
|
||||||
|
<Link
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-row justify-start items-center hover:opacity-70",
|
||||||
|
commentAmount === 0 && "invisible group-hover:visible",
|
||||||
|
)}
|
||||||
|
to={`/m/${memo.uid}#comments`}
|
||||||
|
unstable_viewTransition
|
||||||
|
>
|
||||||
|
<Icon.MessageCircleMore className="w-4 h-4 mx-auto text-gray-500 dark:text-gray-400" />
|
||||||
|
{commentAmount > 0 && <span className="text-xs text-gray-500 dark:text-gray-400">{commentAmount}</span>}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{props.showPinned && memo.pinned && (
|
||||||
|
<Tooltip title={t("common.pinned")} placement="top">
|
||||||
|
<Icon.Bookmark className="w-4 h-auto text-amber-500" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{currentUser && <ReactionSelector className="border-none w-auto h-auto" memo={memo} />}
|
{!readonly && (
|
||||||
|
<MemoActionMenu
|
||||||
|
className="-ml-1"
|
||||||
|
memo={memo}
|
||||||
|
hiddenActions={props.showPinned ? [] : ["pin"]}
|
||||||
|
onEdit={() => setShowEditor(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isInMemoDetailPage && (
|
)}
|
||||||
<Link
|
|
||||||
className={clsx(
|
|
||||||
"flex flex-row justify-start items-center hover:opacity-70",
|
|
||||||
commentAmount === 0 && "invisible group-hover:visible",
|
|
||||||
)}
|
|
||||||
to={`/m/${memo.uid}#comments`}
|
|
||||||
unstable_viewTransition
|
|
||||||
>
|
|
||||||
<Icon.MessageCircleMore className="w-4 h-4 mx-auto text-gray-500 dark:text-gray-400" />
|
|
||||||
{commentAmount > 0 && <span className="text-xs text-gray-500 dark:text-gray-400">{commentAmount}</span>}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{props.showPinned && memo.pinned && (
|
|
||||||
<Tooltip title={t("common.pinned")} placement="top">
|
|
||||||
<Icon.Bookmark className="w-4 h-auto text-amber-500" />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{!readonly && <MemoActionMenu className="-ml-1" memo={memo} hiddenActions={props.showPinned ? [] : ["pin"]} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<MemoContent
|
|
||||||
key={`${memo.name}-${memo.updateTime}`}
|
{showEditor ? (
|
||||||
memoName={memo.name}
|
<MemoEditor
|
||||||
nodes={memo.nodes}
|
autoFocus
|
||||||
readonly={readonly}
|
className="border-none !p-0 -mb-2"
|
||||||
onClick={handleMemoContentClick}
|
cacheKey={`inline-memo-editor-${memo.name}`}
|
||||||
onDoubleClick={handleMemoContentDoubleClick}
|
memoName={memo.name}
|
||||||
compact={props.compact && workspaceMemoRelatedSetting.enableAutoCompact}
|
onConfirm={() => setShowEditor(false)}
|
||||||
/>
|
onCancel={() => setShowEditor(false)}
|
||||||
<MemoResourceListView resources={memo.resources} />
|
/>
|
||||||
<MemoRelationListView memo={memo} relations={referencedMemos} />
|
) : (
|
||||||
<MemoReactionistView memo={memo} reactions={memo.reactions} />
|
<>
|
||||||
|
<MemoContent
|
||||||
|
key={`${memo.name}-${memo.updateTime}`}
|
||||||
|
memoName={memo.name}
|
||||||
|
nodes={memo.nodes}
|
||||||
|
readonly={readonly}
|
||||||
|
onClick={handleMemoContentClick}
|
||||||
|
onDoubleClick={handleMemoContentDoubleClick}
|
||||||
|
compact={props.compact && workspaceMemoRelatedSetting.enableAutoCompact}
|
||||||
|
/>
|
||||||
|
<MemoResourceListView resources={memo.resources} />
|
||||||
|
<MemoRelationListView memo={memo} relations={referencedMemos} />
|
||||||
|
<MemoReactionistView memo={memo} reactions={memo.reactions} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,7 @@ import { toast } from "react-hot-toast";
|
|||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import Icon from "@/components/Icon";
|
import Icon from "@/components/Icon";
|
||||||
import { MemoDetailSidebar, MemoDetailSidebarDrawer } from "@/components/MemoDetailSidebar";
|
import { MemoDetailSidebar, MemoDetailSidebarDrawer } from "@/components/MemoDetailSidebar";
|
||||||
import showMemoEditorDialog from "@/components/MemoEditor/MemoEditorDialog";
|
import MemoEditor from "@/components/MemoEditor";
|
||||||
import MemoView from "@/components/MemoView";
|
import MemoView from "@/components/MemoView";
|
||||||
import MobileHeader from "@/components/MobileHeader";
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
@ -27,6 +27,7 @@ const MemoDetail = () => {
|
|||||||
const uid = params.uid;
|
const uid = params.uid;
|
||||||
const memo = memoStore.getMemoByUid(uid || "");
|
const memo = memoStore.getMemoByUid(uid || "");
|
||||||
const [parentMemo, setParentMemo] = useState<Memo | undefined>(undefined);
|
const [parentMemo, setParentMemo] = useState<Memo | undefined>(undefined);
|
||||||
|
const [showCommentEditor, setShowCommentEditor] = useState(false);
|
||||||
const commentRelations =
|
const commentRelations =
|
||||||
memo?.relations.filter((relation) => relation.relatedMemo === memo.name && relation.type === MemoRelation_Type.COMMENT) || [];
|
memo?.relations.filter((relation) => relation.relatedMemo === memo.name && relation.type === MemoRelation_Type.COMMENT) || [];
|
||||||
const comments = commentRelations.map((relation) => memoStore.getMemoByName(relation.memo)).filter((memo) => memo) as any as Memo[];
|
const comments = commentRelations.map((relation) => memoStore.getMemoByName(relation.memo)).filter((memo) => memo) as any as Memo[];
|
||||||
@ -66,17 +67,13 @@ const MemoDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleShowCommentEditor = () => {
|
const handleShowCommentEditor = () => {
|
||||||
showMemoEditorDialog({
|
setShowCommentEditor(true);
|
||||||
placeholder: t("editor.add-your-comment-here"),
|
|
||||||
parentMemoName: memo.name,
|
|
||||||
onConfirm: handleCommentCreated,
|
|
||||||
cacheKey: `${memo.name}-${memo.updateTime}-comment`,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCommentCreated = async (memoCommentName: string) => {
|
const handleCommentCreated = async (memoCommentName: string) => {
|
||||||
await memoStore.getOrFetchMemoByName(memoCommentName);
|
await memoStore.getOrFetchMemoByName(memoCommentName);
|
||||||
await memoStore.getOrFetchMemoByName(memo.name, { skipCache: true });
|
await memoStore.getOrFetchMemoByName(memo.name, { skipCache: true });
|
||||||
|
setShowCommentEditor(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -145,6 +142,18 @@ const MemoDetail = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{showCommentEditor && (
|
||||||
|
<div className="w-full">
|
||||||
|
<MemoEditor
|
||||||
|
cacheKey={`${memo.name}-${memo.updateTime}-comment`}
|
||||||
|
placeholder={t("editor.add-your-comment-here")}
|
||||||
|
parentMemoName={memo.name}
|
||||||
|
autoFocus
|
||||||
|
onConfirm={handleCommentCreated}
|
||||||
|
onCancel={() => setShowCommentEditor(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{md && (
|
{md && (
|
||||||
|
Reference in New Issue
Block a user