feat: memo editor dialog (#1772)

* feat: memo editor dialog

* chore: update mark

* chore: update
This commit is contained in:
boojack 2023-05-30 20:23:26 +08:00 committed by GitHub
parent 25ce36e495
commit dd8c10743d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 215 additions and 404 deletions

View File

@ -1,17 +1,19 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { NavLink, useLocation } from "react-router-dom"; import { NavLink, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLayoutStore, useUserStore } from "@/store/module"; import { useGlobalStore, useLayoutStore, useUserStore } from "@/store/module";
import { resolution } from "@/utils/layout"; import { resolution } from "@/utils/layout";
import Icon from "./Icon"; import Icon from "./Icon";
import UserBanner from "./UserBanner";
import showSettingDialog from "./SettingDialog"; import showSettingDialog from "./SettingDialog";
import showAskAIDialog from "./AskAIDialog"; import showAskAIDialog from "./AskAIDialog";
import showArchivedMemoDialog from "./ArchivedMemoDialog"; import showArchivedMemoDialog from "./ArchivedMemoDialog";
import showAboutSiteDialog from "./AboutSiteDialog"; import showAboutSiteDialog from "./AboutSiteDialog";
import UserBanner from "./UserBanner"; import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog";
const Header = () => { const Header = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const globalStore = useGlobalStore();
const location = useLocation(); const location = useLocation();
const userStore = useUserStore(); const userStore = useUserStore();
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
@ -57,7 +59,7 @@ const Header = () => {
className={({ isActive }) => className={({ isActive }) =>
`${ `${
isActive && "bg-white dark:bg-zinc-700 shadow" isActive && "bg-white dark:bg-zinc-700 shadow"
} px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` } px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700`
} }
> >
<> <>
@ -70,7 +72,7 @@ const Header = () => {
className={({ isActive }) => className={({ isActive }) =>
`${ `${
isActive && "bg-white dark:bg-zinc-700 shadow" isActive && "bg-white dark:bg-zinc-700 shadow"
} px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` } px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700`
} }
> >
<> <>
@ -83,7 +85,7 @@ const Header = () => {
className={({ isActive }) => className={({ isActive }) =>
`${ `${
isActive && "bg-white dark:bg-zinc-700 shadow" isActive && "bg-white dark:bg-zinc-700 shadow"
} px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` } px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700`
} }
> >
<> <>
@ -98,7 +100,7 @@ const Header = () => {
className={({ isActive }) => className={({ isActive }) =>
`${ `${
isActive && "bg-white dark:bg-zinc-700 shadow" isActive && "bg-white dark:bg-zinc-700 shadow"
} px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` } px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700`
} }
> >
<> <>
@ -109,25 +111,35 @@ const Header = () => {
<> <>
<button <button
id="header-ask-ai" id="header-ask-ai"
className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700" className="px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
onClick={() => showAskAIDialog()} onClick={() => showAskAIDialog()}
> >
<Icon.Bot className="mr-3 w-6 h-auto opacity-70" /> {t("ask-ai.title")} <Icon.Bot className="mr-3 w-6 h-auto opacity-70" /> {t("ask-ai.title")}
</button> </button>
<button <button
id="header-archived-memo" id="header-archived-memo"
className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700" className="px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
onClick={() => showArchivedMemoDialog()} onClick={() => showArchivedMemoDialog()}
> >
<Icon.Archive className="mr-3 w-6 h-auto opacity-70" /> {t("common.archived")} <Icon.Archive className="mr-3 w-6 h-auto opacity-70" /> {t("common.archived")}
</button> </button>
<button <button
id="header-settings" id="header-settings"
className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700" className="px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
onClick={() => showSettingDialog()} onClick={() => showSettingDialog()}
> >
<Icon.Settings className="mr-3 w-6 h-auto opacity-70" /> {t("common.settings")} <Icon.Settings className="mr-3 w-6 h-auto opacity-70" /> {t("common.settings")}
</button> </button>
{globalStore.isDev() && (
<div className="pr-3 pl-1 w-full">
<button
className="mt-2 w-full py-3 rounded-full flex flex-row justify-center items-center bg-green-600 font-medium text-white dark:opacity-80 hover:shadow hover:opacity-90"
onClick={() => showMemoEditorDialog()}
>
<Icon.Edit3 className="w-4 h-auto mr-1" /> New
</button>
</div>
)}
</> </>
)} )}
{isVisitorMode && ( {isVisitorMode && (
@ -138,7 +150,7 @@ const Header = () => {
className={({ isActive }) => className={({ isActive }) =>
`${ `${
isActive && "bg-white dark:bg-zinc-700 shadow" isActive && "bg-white dark:bg-zinc-700 shadow"
} px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700` } px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700`
} }
> >
<> <>
@ -147,7 +159,7 @@ const Header = () => {
</NavLink> </NavLink>
<button <button
id="header-about" id="header-about"
className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700" className="px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
onClick={() => showAboutSiteDialog()} onClick={() => showAboutSiteDialog()}
> >
<Icon.CupSoda className="mr-3 w-6 h-auto opacity-70" /> {t("common.about")} <Icon.CupSoda className="mr-3 w-6 h-auto opacity-70" /> {t("common.about")}

View File

@ -3,9 +3,9 @@ import { memo, useEffect, 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";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useEditorStore, useFilterStore, useMemoStore, useUserStore } from "@/store/module"; import { useFilterStore, useMemoStore, useUserStore } from "@/store/module";
import { getRelativeTimeString } from "@/helpers/datetime";
import { UNKNOWN_ID } from "@/helpers/consts"; import { UNKNOWN_ID } from "@/helpers/consts";
import { getRelativeTimeString } from "@/helpers/datetime";
import { useMemoCacheStore } from "@/store/zustand"; import { useMemoCacheStore } from "@/store/zustand";
import Tooltip from "./kit/Tooltip"; import Tooltip from "./kit/Tooltip";
import Divider from "./kit/Divider"; import Divider from "./kit/Divider";
@ -17,6 +17,7 @@ import MemoRelationListView from "./MemoRelationListView";
import showShareMemo from "./ShareMemoDialog"; import showShareMemo from "./ShareMemoDialog";
import showPreviewImageDialog from "./PreviewImageDialog"; import showPreviewImageDialog from "./PreviewImageDialog";
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog"; import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog";
import "@/less/memo.less"; import "@/less/memo.less";
interface Props { interface Props {
@ -28,7 +29,6 @@ interface Props {
const Memo: React.FC<Props> = (props: Props) => { const Memo: React.FC<Props> = (props: Props) => {
const { memo, readonly, showRelatedMemos } = props; const { memo, readonly, showRelatedMemos } = props;
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const editorStore = useEditorStore();
const filterStore = useFilterStore(); const filterStore = useFilterStore();
const userStore = useUserStore(); const userStore = useUserStore();
const memoStore = useMemoStore(); const memoStore = useMemoStore();
@ -78,7 +78,21 @@ const Memo: React.FC<Props> = (props: Props) => {
}; };
const handleEditMemoClick = () => { const handleEditMemoClick = () => {
editorStore.setEditMemoWithId(memo.id); showMemoEditorDialog({
memoId: memo.id,
});
};
const handleMarkMemoClick = () => {
showMemoEditorDialog({
relationList: [
{
memoId: UNKNOWN_ID,
relatedMemoId: memo.id,
type: "REFERENCE",
},
],
});
}; };
const handleArchiveMemoClick = async () => { const handleArchiveMemoClick = async () => {
@ -91,10 +105,6 @@ const Memo: React.FC<Props> = (props: Props) => {
console.error(error); console.error(error);
toast.error(error.response.data.message); toast.error(error.response.data.message);
} }
if (editorStore.getState().editMemoId === memo.id) {
editorStore.clearEditMemo();
}
}; };
const handleDeleteMemoClick = async () => { const handleDeleteMemoClick = async () => {
@ -180,7 +190,7 @@ const Memo: React.FC<Props> = (props: Props) => {
return; return;
} }
editorStore.setEditMemoWithId(memo.id); handleEditMemoClick();
}; };
const handleMemoCreatedTimeClick = (e: React.MouseEvent) => { const handleMemoCreatedTimeClick = (e: React.MouseEvent) => {
@ -199,15 +209,6 @@ const Memo: React.FC<Props> = (props: Props) => {
} }
}; };
const handleMarkMemo = () => {
const relation: MemoRelation = {
memoId: UNKNOWN_ID,
relatedMemoId: memo.id,
type: "REFERENCE",
};
editorStore.setRelationList(uniqWith([...editorStore.state.relationList, relation], isEqual));
};
return ( return (
<> <>
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned && !readonly ? "pinned" : ""}`} ref={memoContainerRef}> <div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned && !readonly ? "pinned" : ""}`} ref={memoContainerRef}>
@ -253,7 +254,7 @@ const Memo: React.FC<Props> = (props: Props) => {
<Icon.Share className="w-4 h-auto mr-2" /> <Icon.Share className="w-4 h-auto mr-2" />
{t("common.share")} {t("common.share")}
</span> </span>
<span className="btn" onClick={handleMarkMemo}> <span className="btn" onClick={handleMarkMemoClick}>
<Icon.Link className="w-4 h-auto mr-2" /> <Icon.Link className="w-4 h-auto mr-2" />
Mark Mark
</span> </span>

View File

@ -1,17 +1,20 @@
import { toLower } from "lodash-es"; import { toLower } from "lodash-es";
import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts"; import { VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts";
import { useEditorStore, useGlobalStore } from "@/store/module"; import { useGlobalStore } from "@/store/module";
import Selector from "@/components/kit/Selector"; import Selector from "@/components/kit/Selector";
const MemoVisibilitySelector = () => { interface Props {
value: Visibility;
onChange: (value: Visibility) => void;
}
const MemoVisibilitySelector = (props: Props) => {
const { value, onChange } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const editorStore = useEditorStore();
const { const {
state: { systemStatus }, state: { systemStatus },
} = useGlobalStore(); } = useGlobalStore();
const editorState = editorStore.state;
const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => { const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
return { return {
value: item.value, value: item.value,
@ -19,15 +22,8 @@ const MemoVisibilitySelector = () => {
}; };
}); });
useEffect(() => { const handleMemoVisibilityOptionChanged = async (visibility: string) => {
if (systemStatus.disablePublicMemos) { onChange(visibility as Visibility);
editorStore.setMemoVisibility("PRIVATE");
}
}, [systemStatus.disablePublicMemos, editorState.memoVisibility]);
const handleMemoVisibilityOptionChanged = async (value: string) => {
const visibilityValue = value as Visibility;
editorStore.setMemoVisibility(visibilityValue);
}; };
return ( return (
@ -35,7 +31,7 @@ const MemoVisibilitySelector = () => {
className="visibility-selector" className="visibility-selector"
disabled={systemStatus.disablePublicMemos} disabled={systemStatus.disablePublicMemos}
tooltipTitle={t("memo.visibility.disabled")} tooltipTitle={t("memo.visibility.disabled")}
value={editorState.memoVisibility} value={value}
dataSource={memoVisibilityOptionSelectorItems} dataSource={memoVisibilityOptionSelectorItems}
handleValueChanged={handleMemoVisibilityOptionChanged} handleValueChanged={handleMemoVisibilityOptionChanged}
/> />

View File

@ -1,41 +0,0 @@
import { useTranslation } from "react-i18next";
import { useEditorStore } from "@/store/module";
import Icon from "@/components/Icon";
import showCreateResourceDialog from "@/components/CreateResourceDialog";
import showResourcesSelectorDialog from "@/components/ResourcesSelectorDialog";
const ResourceSelector = () => {
const { t } = useTranslation();
const editorStore = useEditorStore();
const handleUploadFileBtnClick = () => {
showCreateResourceDialog({
onConfirm: (resourceList) => {
editorStore.setResourceList([...editorStore.state.resourceList, ...resourceList]);
},
});
};
return (
<div className="action-btn relative group">
<Icon.FileText className="icon-img" />
<div className="hidden flex-col justify-start items-start absolute top-6 left-0 mt-1 p-1 z-1 rounded w-auto overflow-auto font-mono shadow bg-zinc-200 dark:bg-zinc-600 group-hover:flex">
<div
className="w-full flex text-black dark:text-gray-300 cursor-pointer rounded text-sm leading-6 px-2 truncate hover:bg-zinc-300 dark:hover:bg-zinc-700 shrink-0"
onClick={handleUploadFileBtnClick}
>
<Icon.Plus className="w-4 mr-1" />
<span>{t("common.create")}</span>
</div>
<div
className="w-full flex text-black dark:text-gray-300 cursor-pointer rounded text-sm leading-6 px-2 truncate hover:bg-zinc-300 dark:hover:bg-zinc-700 shrink-0"
onClick={showResourcesSelectorDialog}
>
<Icon.Database className="w-4 mr-1" />
<span>{t("editor.resources")}</span>
</div>
</div>
</div>
);
};
export default ResourceSelector;

View File

@ -0,0 +1,39 @@
import { generateDialog } from "../Dialog";
import Icon from "../Icon";
import MemoEditor from ".";
interface Props extends DialogProps {
memoId?: MemoId;
relationList?: MemoRelation[];
}
const MemoEditorDialog: React.FC<Props> = ({ memoId, relationList, destroy }: Props) => {
const handleCloseBtnClick = () => {
destroy();
};
return (
<>
<div className="dialog-header-container">
<p className="title-text flex items-center">MEMOS</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X />
</button>
</div>
<div className="flex flex-col justify-start items-start max-w-full w-[36rem]">
<MemoEditor memoId={memoId} relationList={relationList} onConfirm={handleCloseBtnClick} />
</div>
</>
);
};
export default function showMemoEditorDialog(props: Pick<Props, "memoId" | "relationList"> = {}): void {
generateDialog(
{
className: "memo-editor-dialog",
dialogName: "memo-editor-dialog",
},
MemoEditorDialog,
props
);
}

View File

@ -1,17 +1,20 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useEditorStore } from "@/store/module";
import { useMemoCacheStore } from "@/store/zustand"; import { useMemoCacheStore } from "@/store/zustand";
import Icon from "../Icon"; import Icon from "../Icon";
interface Props {
relationList: MemoRelation[];
setRelationList: (relationList: MemoRelation[]) => void;
}
interface FormatedMemoRelation extends MemoRelation { interface FormatedMemoRelation extends MemoRelation {
relatedMemo: Memo; relatedMemo: Memo;
} }
const RelationListView = () => { const RelationListView = (props: Props) => {
const editorStore = useEditorStore(); const { relationList, setRelationList } = props;
const memoCacheStore = useMemoCacheStore(); const memoCacheStore = useMemoCacheStore();
const [formatedMemoRelationList, setFormatedMemoRelationList] = useState<FormatedMemoRelation[]>([]); const [formatedMemoRelationList, setFormatedMemoRelationList] = useState<FormatedMemoRelation[]>([]);
const relationList = editorStore.state.relationList;
useEffect(() => { useEffect(() => {
const fetchRelatedMemoList = async () => { const fetchRelatedMemoList = async () => {
@ -30,7 +33,7 @@ const RelationListView = () => {
const handleDeleteRelation = async (memoRelation: FormatedMemoRelation) => { const handleDeleteRelation = async (memoRelation: FormatedMemoRelation) => {
const newRelationList = relationList.filter((relation) => relation.relatedMemoId !== memoRelation.relatedMemoId); const newRelationList = relationList.filter((relation) => relation.relatedMemoId !== memoRelation.relatedMemoId);
editorStore.setRelationList(newRelationList); setRelationList(newRelationList);
}; };
return ( return (

View File

@ -1,20 +1,23 @@
import { useEditorStore } from "@/store/module";
import Icon from "../Icon"; import Icon from "../Icon";
import ResourceIcon from "../ResourceIcon"; import ResourceIcon from "../ResourceIcon";
const ResourceListView = () => { interface Props {
const editorStore = useEditorStore(); resourceList: Resource[];
const editorState = editorStore.state; setResourceList: (resourceList: Resource[]) => void;
}
const ResourceListView = (props: Props) => {
const { resourceList, setResourceList } = props;
const handleDeleteResource = async (resourceId: ResourceId) => { const handleDeleteResource = async (resourceId: ResourceId) => {
editorStore.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId)); setResourceList(resourceList.filter((resource) => resource.id !== resourceId));
}; };
return ( return (
<> <>
{editorState.resourceList && editorState.resourceList.length > 0 && ( {resourceList.length > 0 && (
<div className="w-full flex flex-row justify-start flex-wrap gap-2 mt-2"> <div className="w-full flex flex-row justify-start flex-wrap gap-2 mt-2">
{editorState.resourceList.map((resource) => { {resourceList.map((resource) => {
return ( return (
<div <div
key={resource.id} key={resource.id}

View File

@ -5,13 +5,13 @@ import { useTranslation } from "react-i18next";
import { getMatchedNodes } from "@/labs/marked"; import { getMatchedNodes } from "@/labs/marked";
import { upsertMemoResource } from "@/helpers/api"; import { upsertMemoResource } from "@/helpers/api";
import { TAB_SPACE_WIDTH, UNKNOWN_ID } from "@/helpers/consts"; import { TAB_SPACE_WIDTH, UNKNOWN_ID } from "@/helpers/consts";
import { useEditorStore, useFilterStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "@/store/module"; import { useFilterStore, useGlobalStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "@/store/module";
import storage from "@/helpers/storage"; import storage from "@/helpers/storage";
import { clearContentQueryParam, getContentQueryParam } from "@/helpers/utils"; import { clearContentQueryParam, getContentQueryParam } from "@/helpers/utils";
import Icon from "../Icon"; import Icon from "../Icon";
import Editor, { EditorRefActions } from "./Editor"; import Editor, { EditorRefActions } from "./Editor";
import showCreateResourceDialog from "../CreateResourceDialog";
import TagSelector from "./ActionButton/TagSelector"; import TagSelector from "./ActionButton/TagSelector";
import ResourceSelector from "./ActionButton/ResourceSelector";
import MemoVisibilitySelector from "./ActionButton/MemoVisibilitySelector"; import MemoVisibilitySelector from "./ActionButton/MemoVisibilitySelector";
import ResourceListView from "./ResourceListView"; import ResourceListView from "./ResourceListView";
import RelationListView from "./RelationListView"; import RelationListView from "./RelationListView";
@ -24,73 +24,75 @@ const getInitialContent = (): string => {
return getContentQueryParam() ?? storage.get(["editorContentCache"]).editorContentCache ?? ""; return getContentQueryParam() ?? storage.get(["editorContentCache"]).editorContentCache ?? "";
}; };
const setEditorContentCache = (content: string) => { interface Props {
storage.set({ className?: string;
editorContentCache: content, memoId?: MemoId;
}); relationList?: MemoRelation[];
}; onConfirm?: () => void;
}
interface State { interface State {
memoVisibility: Visibility;
resourceList: Resource[];
relationList: MemoRelation[];
fullscreen: boolean; fullscreen: boolean;
isUploadingResource: boolean; isUploadingResource: boolean;
isRequesting: boolean; isRequesting: boolean;
} }
const MemoEditor = () => { const MemoEditor = (props: Props) => {
const { className, memoId, onConfirm } = props;
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const {
state: { systemStatus },
} = useGlobalStore();
const userStore = useUserStore(); const userStore = useUserStore();
const editorStore = useEditorStore();
const filterStore = useFilterStore(); const filterStore = useFilterStore();
const memoStore = useMemoStore(); const memoStore = useMemoStore();
const tagStore = useTagStore(); const tagStore = useTagStore();
const resourceStore = useResourceStore(); const resourceStore = useResourceStore();
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
memoVisibility: "PRIVATE",
resourceList: [],
relationList: props.relationList ?? [],
fullscreen: false, fullscreen: false,
isUploadingResource: false, isUploadingResource: false,
isRequesting: false, isRequesting: false,
}); });
const [allowSave, setAllowSave] = useState<boolean>(false); const [hasContent, setHasContent] = useState<boolean>(false);
const [isInIME, setIsInIME] = useState(false); const [isInIME, setIsInIME] = useState(false);
const editorState = editorStore.state;
const prevEditorStateRef = useRef(editorState);
const editorRef = useRef<EditorRefActions>(null); const editorRef = useRef<EditorRefActions>(null);
const user = userStore.state.user as User; const user = userStore.state.user as User;
const setting = user.setting; const setting = user.setting;
useEffect(() => { useEffect(() => {
const { editingMemoIdCache } = storage.get(["editingMemoIdCache"]); let visibility = setting.memoVisibility;
if (editingMemoIdCache) { if (systemStatus.disablePublicMemos && visibility === "PUBLIC") {
editorStore.setEditMemoWithId(editingMemoIdCache); visibility = "PRIVATE";
} else {
editorStore.setMemoVisibility(setting.memoVisibility);
} }
}, []); setState((prevState) => ({
...prevState,
memoVisibility: visibility,
}));
}, [setting.memoVisibility, systemStatus.disablePublicMemos]);
useEffect(() => { useEffect(() => {
if (editorState.editMemoId) { if (memoId) {
memoStore.getMemoById(editorState.editMemoId ?? UNKNOWN_ID).then((memo) => { memoStore.getMemoById(memoId ?? UNKNOWN_ID).then((memo) => {
if (memo) { if (memo) {
handleEditorFocus(); handleEditorFocus();
editorStore.setMemoVisibility(memo.visibility); setState((prevState) => ({
editorStore.setResourceList(memo.resourceList); ...prevState,
editorStore.setRelationList(memo.relationList); memoVisibility: memo.visibility,
resourceList: memo.resourceList,
relationList: memo.relationList,
}));
editorRef.current?.setContent(memo.content ?? ""); editorRef.current?.setContent(memo.content ?? "");
} }
}); });
storage.set({
editingMemoIdCache: editorState.editMemoId,
});
} else {
storage.remove(["editingMemoIdCache"]);
} }
}, [memoId]);
prevEditorStateRef.current = editorState;
}, [editorState.editMemoId]);
useEffect(() => {
handleEditorFocus();
}, [editorStore.state.relationList]);
const handleKeyDown = (event: React.KeyboardEvent) => { const handleKeyDown = (event: React.KeyboardEvent) => {
if (!editorRef.current) { if (!editorRef.current) {
@ -159,6 +161,38 @@ const MemoEditor = () => {
} }
}; };
const handleMemoVisibilityChange = (visibility: Visibility) => {
setState((prevState) => ({
...prevState,
memoVisibility: visibility,
}));
};
const handleUploadFileBtnClick = () => {
showCreateResourceDialog({
onConfirm: (resourceList) => {
setState((prevState) => ({
...prevState,
resourceList: [...prevState.resourceList, ...resourceList],
}));
},
});
};
const handleSetResourceList = (resourceList: Resource[]) => {
setState((prevState) => ({
...prevState,
resourceList,
}));
};
const handleSetRelationList = (relationList: MemoRelation[]) => {
setState((prevState) => ({
...prevState,
relationList,
}));
};
const handleUploadResource = async (file: File) => { const handleUploadResource = async (file: File) => {
setState((state) => { setState((state) => {
return { return {
@ -190,14 +224,16 @@ const MemoEditor = () => {
const resource = await handleUploadResource(file); const resource = await handleUploadResource(file);
if (resource) { if (resource) {
uploadedResourceList.push(resource); uploadedResourceList.push(resource);
if (editorState.editMemoId) { if (memoId) {
await upsertMemoResource(editorState.editMemoId, resource.id); await upsertMemoResource(memoId, resource.id);
} }
} }
} }
if (uploadedResourceList.length > 0) { if (uploadedResourceList.length > 0) {
const resourceList = editorStore.getState().resourceList; setState((prevState) => ({
editorStore.setResourceList([...resourceList, ...uploadedResourceList]); ...prevState,
resourceList: [...prevState.resourceList, ...uploadedResourceList],
}));
} }
}; };
@ -215,6 +251,10 @@ const MemoEditor = () => {
} }
}; };
const handleContentChange = (content: string) => {
setHasContent(content !== "");
};
const handleSaveBtnClick = async () => { const handleSaveBtnClick = async () => {
if (state.isRequesting) { if (state.isRequesting) {
return; return;
@ -228,26 +268,24 @@ const MemoEditor = () => {
}); });
const content = editorRef.current?.getContent() ?? ""; const content = editorRef.current?.getContent() ?? "";
try { try {
const { editMemoId } = editorStore.getState(); if (memoId && memoId !== UNKNOWN_ID) {
if (editMemoId && editMemoId !== UNKNOWN_ID) { const prevMemo = await memoStore.getMemoById(memoId ?? UNKNOWN_ID);
const prevMemo = await memoStore.getMemoById(editMemoId ?? UNKNOWN_ID);
if (prevMemo) { if (prevMemo) {
await memoStore.patchMemo({ await memoStore.patchMemo({
id: prevMemo.id, id: prevMemo.id,
content, content,
visibility: editorState.memoVisibility, visibility: state.memoVisibility,
resourceIdList: editorState.resourceList.map((resource) => resource.id), resourceIdList: state.resourceList.map((resource) => resource.id),
relationList: editorState.relationList, relationList: state.relationList,
}); });
} }
editorStore.clearEditMemo();
} else { } else {
await memoStore.createMemo({ await memoStore.createMemo({
content, content,
visibility: editorState.memoVisibility, visibility: state.memoVisibility,
resourceIdList: editorState.resourceList.map((resource) => resource.id), resourceIdList: state.resourceList.map((resource) => resource.id),
relationList: editorState.relationList, relationList: state.relationList,
}); });
filterStore.clearFilter(); filterStore.clearFilter();
} }
@ -275,28 +313,18 @@ const MemoEditor = () => {
fullscreen: false, fullscreen: false,
}; };
}); });
editorStore.setResourceList([]); setState((prevState) => ({
editorStore.setRelationList([]); ...prevState,
setEditorContentCache(""); resourceList: [],
relationList: [],
}));
editorRef.current?.setContent(""); editorRef.current?.setContent("");
clearContentQueryParam(); clearContentQueryParam();
}; if (onConfirm) {
onConfirm();
const handleCancelEdit = () => {
if (editorState.editMemoId) {
editorStore.clearEditMemo();
editorStore.setResourceList([]);
editorStore.setRelationList([]);
editorRef.current?.setContent("");
setEditorContentCache("");
} }
}; };
const handleContentChange = (content: string) => {
setAllowSave(content !== "");
setEditorContentCache(content);
};
const handleCheckBoxBtnClick = () => { const handleCheckBoxBtnClick = () => {
if (!editorRef.current) { if (!editorRef.current) {
return; return;
@ -344,12 +372,6 @@ const MemoEditor = () => {
editorRef.current?.focus(); editorRef.current?.focus();
}; };
const handleEditorBlur = () => {
// do nothing
};
const isEditing = Boolean(editorState.editMemoId && editorState.editMemoId !== UNKNOWN_ID);
const editorConfig = useMemo( const editorConfig = useMemo(
() => ({ () => ({
className: `memo-editor`, className: `memo-editor`,
@ -362,14 +384,15 @@ const MemoEditor = () => {
[state.fullscreen, i18n.language] [state.fullscreen, i18n.language]
); );
const allowSave = (hasContent || state.resourceList.length > 0) && !state.isUploadingResource && !state.isRequesting;
return ( return (
<div <div
className={`memo-editor-container ${isEditing ? "edit-ing" : ""} ${state.fullscreen ? "fullscreen" : ""}`} className={`${className} memo-editor-container ${state.fullscreen ? "fullscreen" : ""}`}
tabIndex={0} tabIndex={0}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onDrop={handleDropEvent} onDrop={handleDropEvent}
onFocus={handleEditorFocus} onFocus={handleEditorFocus}
onBlur={handleEditorBlur}
onCompositionStart={() => setIsInIME(true)} onCompositionStart={() => setIsInIME(true)}
onCompositionEnd={() => setIsInIME(false)} onCompositionEnd={() => setIsInIME(false)}
> >
@ -383,25 +406,20 @@ const MemoEditor = () => {
<button className="action-btn"> <button className="action-btn">
<Icon.Code className="icon-img" onClick={handleCodeBlockBtnClick} /> <Icon.Code className="icon-img" onClick={handleCodeBlockBtnClick} />
</button> </button>
<ResourceSelector /> <button className="action-btn">
<Icon.Image className="icon-img" onClick={handleUploadFileBtnClick} />
</button>
<button className="action-btn" onClick={handleFullscreenBtnClick}> <button className="action-btn" onClick={handleFullscreenBtnClick}>
{state.fullscreen ? <Icon.Minimize className="icon-img" /> : <Icon.Maximize className="icon-img" />} {state.fullscreen ? <Icon.Minimize className="icon-img" /> : <Icon.Maximize className="icon-img" />}
</button> </button>
</div> </div>
</div> </div>
<ResourceListView /> <ResourceListView resourceList={state.resourceList} setResourceList={handleSetResourceList} />
<RelationListView /> <RelationListView relationList={state.relationList} setRelationList={handleSetRelationList} />
<div className="editor-footer-container"> <div className="editor-footer-container">
<MemoVisibilitySelector /> <MemoVisibilitySelector value={state.memoVisibility} onChange={handleMemoVisibilityChange} />
<div className="buttons-container"> <div className="buttons-container">
<button className={`action-btn cancel-btn ${isEditing ? "" : "!hidden"}`} onClick={handleCancelEdit}> <button className="action-btn confirm-btn" disabled={!allowSave} onClick={handleSaveBtnClick}>
{t("editor.cancel-edit")}
</button>
<button
className="action-btn confirm-btn"
disabled={!(allowSave || editorState.resourceList.length > 0) || state.isUploadingResource || state.isRequesting}
onClick={handleSaveBtnClick}
>
{t("editor.save")} {t("editor.save")}
</button> </button>
</div> </div>

View File

@ -1,138 +0,0 @@
import { Button, Checkbox } from "@mui/joy";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import useLoading from "@/hooks/useLoading";
import { useEditorStore, useResourceStore } from "@/store/module";
import { getResourceUrl } from "@/utils/resource";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import showPreviewImageDialog from "./PreviewImageDialog";
import "@/less/resources-selector-dialog.less";
type Props = DialogProps;
interface State {
checkedArray: boolean[];
}
const ResourcesSelectorDialog: React.FC<Props> = (props: Props) => {
const { destroy } = props;
const { t } = useTranslation();
const loadingState = useLoading();
const editorStore = useEditorStore();
const resourceStore = useResourceStore();
const resources = resourceStore.state.resources;
const [state, setState] = useState<State>({
checkedArray: [],
});
useEffect(() => {
resourceStore
.fetchResourceList()
.catch((error) => {
console.error(error);
toast.error(error.response.data.message);
})
.finally(() => {
loadingState.setFinish();
});
}, []);
useEffect(() => {
const checkedResourceIdArray = editorStore.state.resourceList.map((resource) => resource.id);
setState({
checkedArray: resources.map((resource) => {
return checkedResourceIdArray.includes(resource.id);
}),
});
}, [resources]);
const handlePreviewBtnClick = (resource: Resource) => {
const resourceUrl = getResourceUrl(resource);
if (resource.type.startsWith("image")) {
showPreviewImageDialog(
resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)),
resources.findIndex((r) => r.id === resource.id)
);
} else {
window.open(resourceUrl);
}
};
const handleCheckboxChange = (index: number) => {
const newCheckedArr = state.checkedArray;
newCheckedArr[index] = !newCheckedArr[index];
setState({
checkedArray: newCheckedArr,
});
};
const handleConfirmBtnClick = () => {
const resourceList = resources.filter((_, index) => {
return state.checkedArray[index];
});
editorStore.setResourceList(resourceList);
destroy();
};
return (
<>
<div className="dialog-header-container">
<p className="title-text">{t("common.resources")}</p>
<button className="btn close-btn" onClick={destroy}>
<Icon.X className="icon-img" />
</button>
</div>
<div className="dialog-content-container">
{loadingState.isLoading ? (
<div className="loading-text-container">
<p className="tip-text">{t("resource.fetching-data")}</p>
</div>
) : (
<div className="resource-table-container">
<div className="fields-container">
<span className="field-text name-text">{t("common.name")}</span>
<span className="field-text type-text">Type</span>
<span></span>
</div>
{resources.length === 0 ? (
<p className="tip-text">{t("resource.no-resources")}</p>
) : (
resources.map((resource, index) => (
<div key={resource.id} className="resource-container">
<span className="field-text name-text cursor-pointer" onClick={() => handlePreviewBtnClick(resource)}>
{resource.filename}
</span>
<span className="field-text type-text">{resource.type}</span>
<div className="flex justify-end">
<Checkbox checked={state.checkedArray[index]} onChange={() => handleCheckboxChange(index)} />
</div>
</div>
))
)}
</div>
)}
<div className="flex justify-between w-full mt-2 px-2">
<span className="text-sm font-mono text-gray-400 leading-8">
{t("message.count-selected-resources")}: {state.checkedArray.filter((checked) => checked).length}
</span>
<div className="flex flex-row justify-start items-center">
<Button onClick={handleConfirmBtnClick}>{t("common.confirm")}</Button>
</div>
</div>
</div>
</>
);
};
export default function showResourcesSelectorDialog() {
generateDialog(
{
className: "resources-selector-dialog",
dialogName: "resources-selector-dialog",
},
ResourcesSelectorDialog,
{}
);
}

View File

@ -22,10 +22,6 @@
} }
} }
&.edit-ing {
@apply border-blue-500;
}
> .memo-editor { > .memo-editor {
@apply mt-4 flex flex-col justify-start items-start relative w-full h-auto bg-inherit dark:text-gray-200; @apply mt-4 flex flex-col justify-start items-start relative w-full h-auto bg-inherit dark:text-gray-200;
} }

View File

@ -3,7 +3,6 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import globalReducer from "./reducer/global"; import globalReducer from "./reducer/global";
import userReducer from "./reducer/user"; import userReducer from "./reducer/user";
import memoReducer from "./reducer/memo"; import memoReducer from "./reducer/memo";
import editorReducer from "./reducer/editor";
import shortcutReducer from "./reducer/shortcut"; import shortcutReducer from "./reducer/shortcut";
import filterReducer from "./reducer/filter"; import filterReducer from "./reducer/filter";
import resourceReducer from "./reducer/resource"; import resourceReducer from "./reducer/resource";
@ -17,7 +16,6 @@ const store = configureStore({
user: userReducer, user: userReducer,
memo: memoReducer, memo: memoReducer,
tag: tagReducer, tag: tagReducer,
editor: editorReducer,
shortcut: shortcutReducer, shortcut: shortcutReducer,
filter: filterReducer, filter: filterReducer,
resource: resourceReducer, resource: resourceReducer,

View File

@ -1,28 +0,0 @@
import store, { useAppSelector } from "..";
import { setEditMemoId, setMemoVisibility, setRelationList, setResourceList } from "../reducer/editor";
export const useEditorStore = () => {
const state = useAppSelector((state) => state.editor);
return {
state,
getState: () => {
return store.getState().editor;
},
setEditMemoWithId: (editMemoId: MemoId) => {
store.dispatch(setEditMemoId(editMemoId));
},
clearEditMemo: () => {
store.dispatch(setEditMemoId());
},
setMemoVisibility: (memoVisibility: Visibility) => {
store.dispatch(setMemoVisibility(memoVisibility));
},
setResourceList: (resourceList: Resource[]) => {
store.dispatch(setResourceList(resourceList));
},
setRelationList: (relationList: MemoRelation[]) => {
store.dispatch(setRelationList(relationList));
},
};
};

View File

@ -1,4 +1,3 @@
export * from "./editor";
export * from "./global"; export * from "./global";
export * from "./filter"; export * from "./filter";
export * from "./memo"; export * from "./memo";

View File

@ -1,47 +0,0 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface State {
memoVisibility: Visibility;
resourceList: Resource[];
relationList: MemoRelation[];
editMemoId?: MemoId;
}
const editorSlice = createSlice({
name: "editor",
initialState: {
memoVisibility: "PRIVATE",
resourceList: [],
relationList: [],
} as State,
reducers: {
setEditMemoId: (state, action: PayloadAction<Option<MemoId>>) => {
return {
...state,
editMemoId: action.payload,
};
},
setMemoVisibility: (state, action: PayloadAction<Visibility>) => {
return {
...state,
memoVisibility: action.payload,
};
},
setResourceList: (state, action: PayloadAction<Resource[]>) => {
return {
...state,
resourceList: action.payload,
};
},
setRelationList: (state, action: PayloadAction<MemoRelation[]>) => {
return {
...state,
relationList: action.payload,
};
},
},
});
export const { setEditMemoId, setMemoVisibility, setResourceList, setRelationList } = editorSlice.actions;
export default editorSlice.reducer;