refactor: introducing use{Module}Store instead of service (#768)

* refactor: introducing `useEditorStore`

* refactor: update

* chore: update
This commit is contained in:
boojack 2022-12-18 15:25:18 +08:00 committed by GitHub
parent bd00fa798d
commit ef621a444f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 911 additions and 886 deletions

View File

@ -2,29 +2,30 @@ import { useColorScheme } from "@mui/joy";
import { useEffect, Suspense } from "react";
import { useTranslation } from "react-i18next";
import { RouterProvider } from "react-router-dom";
import { globalService, locationService } from "./services";
import { useAppSelector } from "./store";
import router from "./router";
import { useLocationStore, useGlobalStore } from "./store/module";
import * as storage from "./helpers/storage";
import { getSystemColorScheme } from "./helpers/utils";
import Loading from "./pages/Loading";
function App() {
const App = () => {
const { i18n } = useTranslation();
const { appearance, locale, systemStatus } = useAppSelector((state) => state.global);
const globalStore = useGlobalStore();
const locationStore = useLocationStore();
const { mode, setMode } = useColorScheme();
const { appearance, locale, systemStatus } = globalStore.state;
useEffect(() => {
locationService.updateStateWithLocation();
locationStore.updateStateWithLocation();
window.onpopstate = () => {
locationService.updateStateWithLocation();
locationStore.updateStateWithLocation();
};
}, []);
useEffect(() => {
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleColorSchemeChange = (e: MediaQueryListEvent) => {
if (globalService.getState().appearance === "system") {
if (globalStore.getState().appearance === "system") {
const mode = e.matches ? "dark" : "light";
setMode(mode);
}
@ -91,6 +92,6 @@ function App() {
<RouterProvider router={router} />
</Suspense>
);
}
};
export default App;

View File

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import { useAppSelector } from "../store";
import { useGlobalStore } from "../store/module";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import GitHubBadge from "./GitHubBadge";
@ -9,7 +9,8 @@ type Props = DialogProps;
const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
const { t } = useTranslation();
const profile = useAppSelector((state) => state.global.systemStatus.profile);
const globalStore = useGlobalStore();
const profile = globalStore.state.systemStatus.profile;
const handleCloseBtnClick = () => {
destroy();

View File

@ -1,15 +1,16 @@
import { Option, Select } from "@mui/joy";
import { useTranslation } from "react-i18next";
import { globalService, userService } from "../services";
import { useAppSelector } from "../store";
import { useGlobalStore, useUserStore } from "../store/module";
import Icon from "./Icon";
const appearanceList = ["system", "light", "dark"];
const AppearanceSelect = () => {
const user = useAppSelector((state) => state.user.user);
const appearance = useAppSelector((state) => state.global.appearance);
const { t } = useTranslation();
const globalStore = useGlobalStore();
const userStore = useUserStore();
const { appearance } = globalStore.state;
const user = userStore.state.user;
const getPrefixIcon = (apperance: Appearance) => {
const className = "w-4 h-auto";
@ -24,9 +25,9 @@ const AppearanceSelect = () => {
const handleSelectChange = async (appearance: Appearance) => {
if (user) {
await userService.upsertUserSetting("appearance", appearance);
await userStore.upsertUserSetting("appearance", appearance);
}
globalService.setAppearance(appearance);
globalStore.setAppearance(appearance);
};
return (

View File

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { useMemoStore } from "../store/module";
import * as utils from "../helpers/utils";
import useToggle from "../hooks/useToggle";
import { memoService } from "../services";
import toastHelper from "./Toast";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources";
@ -14,13 +14,13 @@ interface Props {
const ArchivedMemo: React.FC<Props> = (props: Props) => {
const { memo } = props;
const { t } = useTranslation();
const memoStore = useMemoStore();
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);
const handleDeleteMemoClick = async () => {
if (showConfirmDeleteBtn) {
try {
await memoService.deleteMemoById(memo.id);
await memoStore.deleteMemoById(memo.id);
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.message);
@ -32,11 +32,11 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
const handleRestoreMemoClick = async () => {
try {
await memoService.patchMemo({
await memoStore.patchMemo({
id: memo.id,
rowStatus: "NORMAL",
});
await memoService.fetchMemos();
await memoStore.fetchMemos();
toastHelper.info(t("message.restored-successfully"));
} catch (error: any) {
console.error(error);

View File

@ -1,8 +1,7 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useMemoStore } from "../store/module";
import useLoading from "../hooks/useLoading";
import { memoService } from "../services";
import { useAppSelector } from "../store";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
@ -14,12 +13,13 @@ type Props = DialogProps;
const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
const { t } = useTranslation();
const { destroy } = props;
const memos = useAppSelector((state) => state.memo.memos);
const memoStore = useMemoStore();
const memos = memoStore.state.memos;
const loadingState = useLoading();
const [archivedMemos, setArchivedMemos] = useState<Memo[]>([]);
useEffect(() => {
memoService
memoStore
.fetchArchivedMemos()
.then((result) => {
setArchivedMemos(result);

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useUserStore } from "../store/module";
import { validate, ValidatorConfig } from "../helpers/validator";
import { userService } from "../services";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
@ -20,6 +20,7 @@ interface Props extends DialogProps {
const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
const { user: propsUser, destroy } = props;
const { t } = useTranslation();
const userStore = useUserStore();
const [newPassword, setNewPassword] = useState("");
const [newPasswordAgain, setNewPasswordAgain] = useState("");
@ -60,7 +61,7 @@ const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
}
try {
await userService.patchUser({
await userStore.patchUser({
id: propsUser.id,
password: newPassword,
});

View File

@ -1,7 +1,7 @@
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { memoService } from "../services";
import { useMemoStore } from "../store/module";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
@ -14,11 +14,12 @@ interface Props extends DialogProps {
const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
const { t } = useTranslation();
const { destroy, memoId } = props;
const memoStore = useMemoStore();
const [createdAt, setCreatedAt] = useState("");
const maxDatetimeValue = dayjs().format("YYYY-MM-DDTHH:mm");
useEffect(() => {
memoService.getMemoById(memoId).then((memo) => {
memoStore.getMemoById(memoId).then((memo) => {
if (memo) {
const datetime = dayjs(memo.createdTs).format("YYYY-MM-DDTHH:mm");
setCreatedAt(datetime);
@ -48,7 +49,7 @@ const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
}
try {
await memoService.patchMemo({
await memoStore.patchMemo({
id: memoId,
createdTs,
});

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useUserStore } from "../store/module";
import { validate, ValidatorConfig } from "../helpers/validator";
import { userService } from "../services";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
@ -17,6 +17,7 @@ type Props = DialogProps;
const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
const { t } = useTranslation();
const userStore = useUserStore();
const [newPassword, setNewPassword] = useState("");
const [newPasswordAgain, setNewPasswordAgain] = useState("");
@ -57,8 +58,8 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
}
try {
const user = userService.getState().user as User;
await userService.patchUser({
const user = userStore.getState().user as User;
await userStore.patchUser({
id: user.id,
password: newPassword,
});

View File

@ -1,6 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { resourceService } from "../services";
import { useResourceStore } from "../store/module";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
@ -24,8 +24,9 @@ const validateFilename = (filename: string): boolean => {
};
const ChangeResourceFilenameDialog: React.FC<Props> = (props: Props) => {
const { t } = useTranslation();
const { destroy, resourceId, resourceFilename } = props;
const { t } = useTranslation();
const resourceStore = useResourceStore();
const [filename, setFilename] = useState<string>(resourceFilename);
const handleFilenameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -47,7 +48,7 @@ const ChangeResourceFilenameDialog: React.FC<Props> = (props: Props) => {
return;
}
try {
await resourceService.patchResource({
await resourceStore.patchResource({
id: resourceId,
filename: filename,
});

View File

@ -1,7 +1,7 @@
import dayjs from "dayjs";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { memoService, shortcutService } from "../services";
import { useMemoStore, useShortcutStore } from "../store/module";
import { filterConsts, getDefaultFilter, relationConsts } from "../helpers/filter";
import useLoading from "../hooks/useLoading";
import Icon from "./Icon";
@ -16,6 +16,7 @@ interface Props extends DialogProps {
const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
const { destroy, shortcutId } = props;
const shortcutStore = useShortcutStore();
const [title, setTitle] = useState<string>("");
const [filters, setFilters] = useState<Filter[]>([]);
const requestState = useLoading(false);
@ -23,7 +24,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
useEffect(() => {
if (shortcutId) {
const shortcutTemp = shortcutService.getShortcutById(shortcutId);
const shortcutTemp = shortcutStore.getShortcutById(shortcutId);
if (shortcutTemp) {
setTitle(shortcutTemp.title);
const temp = JSON.parse(shortcutTemp.payload);
@ -52,13 +53,13 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
}
try {
if (shortcutId) {
await shortcutService.patchShortcut({
await shortcutStore.patchShortcut({
id: shortcutId,
title,
payload: JSON.stringify(filters),
});
} else {
await shortcutService.createShortcut({
await shortcutStore.createShortcut({
title,
payload: JSON.stringify(filters),
});
@ -161,9 +162,9 @@ interface MemoFilterInputerProps {
const MemoFilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterInputerProps) => {
const { index, filter, handleFilterChange, handleFilterRemove } = props;
const { t } = useTranslation();
const memoStore = useMemoStore();
const [value, setValue] = useState<string>(filter.value.value);
const tags = Array.from(memoService.getState().tags);
const tags = Array.from(memoStore.getState().tags);
const { type } = filter;
const operatorDataSource = Object.values(filterConsts[type as FilterType].operators).map(({ text, value }) => ({ text: t(text), value }));

View File

@ -1,6 +1,6 @@
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppSelector } from "../store";
import { useMemoStore } from "../store/module";
import toImage from "../labs/html2image";
import useToggle from "../hooks/useToggle";
import { DAILY_TIMESTAMP } from "../helpers/consts";
@ -21,7 +21,8 @@ const weekdayChineseStrArray = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
const DailyReviewDialog: React.FC<Props> = (props: Props) => {
const { t } = useTranslation();
const memos = useAppSelector((state) => state.memo.memos);
const memoStore = useMemoStore();
const memos = memoStore.state.memos;
const [currentDateStamp, setCurrentDateStamp] = useState(utils.getDateStampByDate(utils.getDateString(props.currentDateStamp)));
const [showDatePicker, toggleShowDatePicker] = useToggle(false);
const memosElRef = useRef<HTMLDivElement>(null);

View File

@ -3,7 +3,7 @@ import dayjs from "dayjs";
import { memo, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { editorStateService, locationService, memoService, userService } from "../services";
import { useEditorStore, useLocationStore, useMemoStore, useUserStore } from "../store/module";
import Icon from "./Icon";
import toastHelper from "./Toast";
import MemoContent from "./MemoContent";
@ -30,9 +30,13 @@ const Memo: React.FC<Props> = (props: Props) => {
const { memo, highlightWord } = props;
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const editorStore = useEditorStore();
const locationStore = useLocationStore();
const userStore = useUserStore();
const memoStore = useMemoStore();
const [displayTimeStr, setDisplayTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.displayTs, i18n.language));
const memoContainerRef = useRef<HTMLDivElement>(null);
const isVisitorMode = userService.isVisitorMode();
const isVisitorMode = userStore.isVisitorMode();
useEffect(() => {
let intervalFlag: any = -1;
@ -59,9 +63,9 @@ const Memo: React.FC<Props> = (props: Props) => {
const handleTogglePinMemoBtnClick = async () => {
try {
if (memo.pinned) {
await memoService.unpinMemo(memo.id);
await memoStore.unpinMemo(memo.id);
} else {
await memoService.pinMemo(memo.id);
await memoStore.pinMemo(memo.id);
}
} catch (error) {
// do nth
@ -69,12 +73,12 @@ const Memo: React.FC<Props> = (props: Props) => {
};
const handleEditMemoClick = () => {
editorStateService.setEditMemoWithId(memo.id);
editorStore.setEditMemoWithId(memo.id);
};
const handleArchiveMemoClick = async () => {
try {
await memoService.patchMemo({
await memoStore.patchMemo({
id: memo.id,
rowStatus: "ARCHIVED",
});
@ -83,8 +87,8 @@ const Memo: React.FC<Props> = (props: Props) => {
toastHelper.error(error.response.data.message);
}
if (editorStateService.getState().editMemoId === memo.id) {
editorStateService.clearEditMemo();
if (editorStore.getState().editMemoId === memo.id) {
editorStore.clearEditMemo();
}
};
@ -97,14 +101,14 @@ const Memo: React.FC<Props> = (props: Props) => {
if (targetEl.className === "tag-span") {
const tagName = targetEl.innerText.slice(1);
const currTagQuery = locationService.getState().query?.tag;
const currTagQuery = locationStore.getState().query?.tag;
if (currTagQuery === tagName) {
locationService.setTagQuery(undefined);
locationStore.setTagQuery(undefined);
} else {
locationService.setTagQuery(tagName);
locationStore.setTagQuery(tagName);
}
} else if (targetEl.classList.contains("todo-block")) {
if (userService.isVisitorMode()) {
if (userStore.isVisitorMode()) {
return;
}
@ -128,7 +132,7 @@ const Memo: React.FC<Props> = (props: Props) => {
finalContent += `${tempList[i]}`;
}
}
await memoService.patchMemo({
await memoStore.patchMemo({
id: memo.id,
content: finalContent,
});
@ -151,7 +155,7 @@ const Memo: React.FC<Props> = (props: Props) => {
return;
}
editorStateService.setEditMemoWithId(memo.id);
editorStore.setEditMemoWithId(memo.id);
};
const handleMemoDisplayTimeClick = () => {
@ -159,11 +163,11 @@ const Memo: React.FC<Props> = (props: Props) => {
};
const handleMemoVisibilityClick = (visibility: Visibility) => {
const currVisibilityQuery = locationService.getState().query?.visibility;
const currVisibilityQuery = locationStore.getState().query?.visibility;
if (currVisibilityQuery === visibility) {
locationService.setMemoVisibilityQuery(undefined);
locationStore.setMemoVisibilityQuery(undefined);
} else {
locationService.setMemoVisibilityQuery(visibility);
locationStore.setMemoVisibilityQuery(visibility);
}
};

View File

@ -1,9 +1,9 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useUserStore } from "../store/module";
import { marked } from "../labs/marked";
import { highlightWithWord } from "../labs/highlighter";
import Icon from "./Icon";
import { useAppSelector } from "../store";
import "../less/memo-content.less";
export interface DisplayConfig {
@ -36,7 +36,8 @@ const MemoContent: React.FC<Props> = (props: Props) => {
return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content;
}, [content]);
const { t } = useTranslation();
const user = useAppSelector((state) => state.user.user);
const userStore = useUserStore();
const user = userStore.state.user;
const [state, setState] = useState<State>({
expandButtonStatus: -1,
@ -84,6 +85,9 @@ const MemoContent: React.FC<Props> = (props: Props) => {
setState({
expandButtonStatus: Number(expandButtonStatus) as ExpandButtonStatus,
});
if (!expandButtonStatus) {
memoContentContainerRef.current?.scrollIntoView();
}
};
return (

View File

@ -3,8 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next";
import { deleteMemoResource, upsertMemoResource } from "../helpers/api";
import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
import { editorStateService, locationService, memoService, resourceService } from "../services";
import { useAppSelector } from "../store";
import { useEditorStore, useLocationStore, useMemoStore, useResourceStore, useUserStore } from "../store/module";
import * as storage from "../helpers/storage";
import Icon from "./Icon";
import toastHelper from "./Toast";
@ -41,19 +40,25 @@ interface State {
const MemoEditor = () => {
const { t, i18n } = useTranslation();
const user = useAppSelector((state) => state.user.user as User);
const setting = user.setting;
const editorState = useAppSelector((state) => state.editor);
const tags = useAppSelector((state) => state.memo.tags);
const userStore = useUserStore();
const editorStore = useEditorStore();
const locationStore = useLocationStore();
const memoStore = useMemoStore();
const resourceStore = useResourceStore();
const [state, setState] = useState<State>({
isUploadingResource: false,
fullscreen: false,
shouldShowEmojiPicker: false,
});
const [allowSave, setAllowSave] = useState<boolean>(false);
const editorState = editorStore.state;
const prevEditorStateRef = useRef(editorState);
const editorRef = useRef<EditorRefActions>(null);
const tagSelectorRef = useRef<HTMLDivElement>(null);
const user = userStore.state.user as User;
const setting = user.setting;
const tags = memoStore.state.tags;
const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
return {
value: item.value,
@ -64,22 +69,22 @@ const MemoEditor = () => {
useEffect(() => {
const { editingMemoIdCache, editingMemoVisibilityCache } = storage.get(["editingMemoIdCache", "editingMemoVisibilityCache"]);
if (editingMemoIdCache) {
editorStateService.setEditMemoWithId(editingMemoIdCache);
editorStore.setEditMemoWithId(editingMemoIdCache);
}
if (editingMemoVisibilityCache) {
editorStateService.setMemoVisibility(editingMemoVisibilityCache as "PUBLIC" | "PROTECTED" | "PRIVATE");
editorStore.setMemoVisibility(editingMemoVisibilityCache as "PUBLIC" | "PROTECTED" | "PRIVATE");
} else {
editorStateService.setMemoVisibility(setting.memoVisibility);
editorStore.setMemoVisibility(setting.memoVisibility);
}
}, []);
useEffect(() => {
if (editorState.editMemoId) {
memoService.getMemoById(editorState.editMemoId ?? UNKNOWN_ID).then((memo) => {
memoStore.getMemoById(editorState.editMemoId ?? UNKNOWN_ID).then((memo) => {
if (memo) {
handleEditorFocus();
editorStateService.setMemoVisibility(memo.visibility);
editorStateService.setResourceList(memo.resourceList);
editorStore.setMemoVisibility(memo.visibility);
editorStore.setResourceList(memo.resourceList);
editorRef.current?.setContent(memo.content ?? "");
}
});
@ -180,8 +185,8 @@ const MemoEditor = () => {
}
}
if (uploadedResourceList.length > 0) {
const resourceList = editorStateService.getState().resourceList;
editorStateService.setResourceList([...resourceList, ...uploadedResourceList]);
const resourceList = editorStore.getState().resourceList;
editorStore.setResourceList([...resourceList, ...uploadedResourceList]);
}
};
@ -210,7 +215,7 @@ const MemoEditor = () => {
let resource = undefined;
try {
resource = await resourceService.upload(file);
resource = await resourceStore.upload(file);
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.message);
@ -233,26 +238,26 @@ const MemoEditor = () => {
}
try {
const { editMemoId } = editorStateService.getState();
const { editMemoId } = editorStore.getState();
if (editMemoId && editMemoId !== UNKNOWN_ID) {
const prevMemo = await memoService.getMemoById(editMemoId ?? UNKNOWN_ID);
const prevMemo = await memoStore.getMemoById(editMemoId ?? UNKNOWN_ID);
if (prevMemo) {
await memoService.patchMemo({
await memoStore.patchMemo({
id: prevMemo.id,
content,
visibility: editorState.memoVisibility,
resourceIdList: editorState.resourceList.map((resource) => resource.id),
});
}
editorStateService.clearEditMemo();
editorStore.clearEditMemo();
} else {
await memoService.createMemo({
await memoStore.createMemo({
content,
visibility: editorState.memoVisibility,
resourceIdList: editorState.resourceList.map((resource) => resource.id),
});
locationService.clearQuery();
locationStore.clearQuery();
}
} catch (error: any) {
console.error(error);
@ -265,7 +270,7 @@ const MemoEditor = () => {
fullscreen: false,
};
});
editorStateService.clearResourceList();
editorStore.clearResourceList();
setEditorContentCache("");
storage.remove(["editingMemoVisibilityCache"]);
editorRef.current?.setContent("");
@ -273,8 +278,8 @@ const MemoEditor = () => {
const handleCancelEdit = () => {
if (editorState.editMemoId) {
editorStateService.clearEditMemo();
editorStateService.clearResourceList();
editorStore.clearEditMemo();
editorStore.clearResourceList();
editorRef.current?.setContent("");
setEditorContentCache("");
storage.remove(["editingMemoVisibilityCache"]);
@ -338,7 +343,7 @@ const MemoEditor = () => {
}
}
}
editorStateService.setResourceList([...editorState.resourceList, ...resourceList]);
editorStore.setResourceList([...editorState.resourceList, ...resourceList]);
document.body.removeChild(inputEl);
};
inputEl.click();
@ -361,7 +366,7 @@ const MemoEditor = () => {
}, []);
const handleDeleteResource = async (resourceId: ResourceId) => {
editorStateService.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId));
editorStore.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId));
if (editorState.editMemoId) {
await deleteMemoResource(editorState.editMemoId, resourceId);
}
@ -369,7 +374,7 @@ const MemoEditor = () => {
const handleMemoVisibilityOptionChanged = async (value: string) => {
const visibilityValue = value as Visibility;
editorStateService.setMemoVisibility(visibilityValue);
editorStore.setMemoVisibility(visibilityValue);
setEditingMemoVisibilityCache(visibilityValue);
};

View File

@ -1,6 +1,5 @@
import { useTranslation } from "react-i18next";
import { useAppSelector } from "../store";
import { locationService, shortcutService } from "../services";
import { useLocationStore, useShortcutStore } from "../store/module";
import * as utils from "../helpers/utils";
import { getTextWithMemoType } from "../helpers/filter";
import Icon from "./Icon";
@ -8,10 +7,11 @@ import "../less/memo-filter.less";
const MemoFilter = () => {
const { t } = useTranslation();
useAppSelector((state) => state.shortcut.shortcuts);
const query = useAppSelector((state) => state.location.query);
const locationStore = useLocationStore();
const shortcutStore = useShortcutStore();
const query = locationStore.state.query;
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query;
const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null;
const shortcut = shortcutId ? shortcutStore.getShortcutById(shortcutId) : null;
const showFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut || visibility);
return (
@ -20,7 +20,7 @@ const MemoFilter = () => {
<div
className={"filter-item-container " + (shortcut ? "" : "!hidden")}
onClick={() => {
locationService.setMemoShortcut(undefined);
locationStore.setMemoShortcut(undefined);
}}
>
<Icon.Target className="icon-text" /> {shortcut?.title}
@ -28,7 +28,7 @@ const MemoFilter = () => {
<div
className={"filter-item-container " + (tagQuery ? "" : "!hidden")}
onClick={() => {
locationService.setTagQuery(undefined);
locationStore.setTagQuery(undefined);
}}
>
<Icon.Tag className="icon-text" /> {tagQuery}
@ -36,7 +36,7 @@ const MemoFilter = () => {
<div
className={"filter-item-container " + (memoType ? "" : "!hidden")}
onClick={() => {
locationService.setMemoTypeQuery(undefined);
locationStore.setMemoTypeQuery(undefined);
}}
>
<Icon.Box className="icon-text" /> {t(getTextWithMemoType(memoType as MemoSpecType))}
@ -44,7 +44,7 @@ const MemoFilter = () => {
<div
className={"filter-item-container " + (visibility ? "" : "!hidden")}
onClick={() => {
locationService.setMemoVisibilityQuery(undefined);
locationStore.setMemoVisibilityQuery(undefined);
}}
>
<Icon.Eye className="icon-text" /> {visibility}
@ -53,7 +53,7 @@ const MemoFilter = () => {
<div
className="filter-item-container"
onClick={() => {
locationService.setFromAndToQuery();
locationStore.setFromAndToQuery();
}}
>
<Icon.Calendar className="icon-text" /> {utils.getDateString(duration.from)} to {utils.getDateString(duration.to)}
@ -62,7 +62,7 @@ const MemoFilter = () => {
<div
className={"filter-item-container " + (textQuery ? "" : "!hidden")}
onClick={() => {
locationService.setTextQuery(undefined);
locationStore.setTextQuery(undefined);
}}
>
<Icon.Search className="icon-text" /> {textQuery}

View File

@ -1,10 +1,9 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { memoService, shortcutService } from "../services";
import { DEFAULT_MEMO_LIMIT } from "../services/memoService";
import { useAppSelector } from "../store";
import { useLocationStore, useMemoStore, useShortcutStore, useUserStore } from "../store/module";
import { TAG_REG, LINK_REG } from "../labs/marked/parser";
import * as utils from "../helpers/utils";
import { DEFAULT_MEMO_LIMIT } from "../helpers/consts";
import { checkShouldShowMemoWithFilters } from "../helpers/filter";
import toastHelper from "./Toast";
import Memo from "./Memo";
@ -12,14 +11,18 @@ import "../less/memo-list.less";
const MemoList = () => {
const { t } = useTranslation();
const query = useAppSelector((state) => state.location.query);
const memoDisplayTsOption = useAppSelector((state) => state.user.user?.setting.memoDisplayTsOption);
const { memos, isFetching } = useAppSelector((state) => state.memo);
const userStore = useUserStore();
const memoStore = useMemoStore();
const shortcutStore = useShortcutStore();
const locationStore = useLocationStore();
const query = locationStore.state.query;
const memoDisplayTsOption = userStore.state.user?.setting.memoDisplayTsOption;
const { memos, isFetching } = memoStore.state;
const [isComplete, setIsComplete] = useState<boolean>(false);
const [highlightWord, setHighlightWord] = useState<string | undefined>("");
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query ?? {};
const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null;
const shortcut = shortcutId ? shortcutStore.getShortcutById(shortcutId) : null;
const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut || visibility);
const shownMemos =
@ -84,7 +87,7 @@ const MemoList = () => {
const sortedMemos = pinnedMemos.concat(unpinnedMemos).filter((m) => m.rowStatus === "NORMAL");
useEffect(() => {
memoService
memoStore
.fetchMemos()
.then((fetchedMemos) => {
if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) {
@ -118,7 +121,7 @@ const MemoList = () => {
const handleFetchMoreClick = async () => {
try {
const fetchedMemos = await memoService.fetchMemos(DEFAULT_MEMO_LIMIT, memos.length);
const fetchedMemos = await memoStore.fetchMemos(DEFAULT_MEMO_LIMIT, memos.length);
if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) {
setIsComplete(true);
} else {

View File

@ -1,6 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { memoService, shortcutService } from "../services";
import { useAppSelector } from "../store";
import { useLocationStore, useMemoStore, useShortcutStore } from "../store/module";
import Icon from "./Icon";
import SearchBar from "./SearchBar";
import { toggleSidebar } from "./Sidebar";
@ -9,8 +8,11 @@ import "../less/memos-header.less";
let prevRequestTimestamp = Date.now();
const MemosHeader = () => {
const query = useAppSelector((state) => state.location.query);
const shortcuts = useAppSelector((state) => state.shortcut.shortcuts);
const locationStore = useLocationStore();
const memoStore = useMemoStore();
const shortcutStore = useShortcutStore();
const query = locationStore.state.query;
const shortcuts = shortcutStore.state.shortcuts;
const [titleText, setTitleText] = useState("MEMOS");
useEffect(() => {
@ -19,7 +21,7 @@ const MemosHeader = () => {
return;
}
const shortcut = shortcutService.getShortcutById(query?.shortcutId);
const shortcut = shortcutStore.getShortcutById(query?.shortcutId);
if (shortcut) {
setTitleText(shortcut.title);
}
@ -29,7 +31,7 @@ const MemosHeader = () => {
const now = Date.now();
if (now - prevRequestTimestamp > 1 * 1000) {
prevRequestTimestamp = now;
memoService.fetchMemos().catch(() => {
memoStore.fetchMemos().catch(() => {
// do nth
});
}

View File

@ -3,8 +3,7 @@ import copy from "copy-to-clipboard";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import useLoading from "../hooks/useLoading";
import { resourceService } from "../services";
import { useAppSelector } from "../store";
import { useResourceStore } from "../store/module";
import Icon from "./Icon";
import toastHelper from "./Toast";
import Dropdown from "./common/Dropdown";
@ -24,13 +23,14 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
const { destroy } = props;
const { t } = useTranslation();
const loadingState = useLoading();
const { resources } = useAppSelector((state) => state.resource);
const resourceStore = useResourceStore();
const resources = resourceStore.state.resources;
const [state, setState] = useState<State>({
isUploadingResource: false,
});
useEffect(() => {
resourceService
resourceStore
.fetchResourceList()
.catch((error) => {
console.error(error);
@ -66,7 +66,7 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
for (const file of inputEl.files) {
try {
await resourceService.upload(file);
await resourceStore.upload(file);
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.message);
@ -127,7 +127,7 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
style: "warning",
onConfirm: async () => {
for (const resource of unusedResources) {
await resourceService.deleteResourceById(resource.id);
await resourceStore.deleteResourceById(resource.id);
}
},
});
@ -144,7 +144,7 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
content: warningText,
style: "warning",
onConfirm: async () => {
await resourceService.deleteResourceById(resource.id);
await resourceStore.deleteResourceById(resource.id);
},
});
};

View File

@ -2,8 +2,7 @@ import { Checkbox, Tooltip } from "@mui/joy";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import useLoading from "../hooks/useLoading";
import { editorStateService, resourceService } from "../services";
import { useAppSelector } from "../store";
import { useEditorStore, useResourceStore } from "../store/module";
import Icon from "./Icon";
import toastHelper from "./Toast";
import { generateDialog } from "./Dialog";
@ -20,14 +19,15 @@ const ResourcesSelectorDialog: React.FC<Props> = (props: Props) => {
const { destroy } = props;
const { t } = useTranslation();
const loadingState = useLoading();
const { resources } = useAppSelector((state) => state.resource);
const editorState = useAppSelector((state) => state.editor);
const editorStore = useEditorStore();
const resourceStore = useResourceStore();
const resources = resourceStore.state.resources;
const [state, setState] = useState<State>({
checkedArray: [],
});
useEffect(() => {
resourceService
resourceStore
.fetchResourceList()
.catch((error) => {
console.error(error);
@ -39,7 +39,7 @@ const ResourcesSelectorDialog: React.FC<Props> = (props: Props) => {
}, []);
useEffect(() => {
const checkedResourceIdArray = editorState.resourceList.map((resource) => resource.id);
const checkedResourceIdArray = editorStore.state.resourceList.map((resource) => resource.id);
setState({
checkedArray: resources.map((resource) => {
return checkedResourceIdArray.includes(resource.id);
@ -75,7 +75,7 @@ const ResourcesSelectorDialog: React.FC<Props> = (props: Props) => {
const resourceList = resources.filter((_, index) => {
return state.checkedArray[index];
});
editorStateService.setResourceList(resourceList);
editorStore.setResourceList(resourceList);
destroy();
};

View File

@ -1,33 +1,33 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { locationService } from "../services";
import { useAppSelector } from "../store";
import { useLocationStore } from "../store/module";
import { memoSpecialTypes } from "../helpers/filter";
import Icon from "./Icon";
import "../less/search-bar.less";
const SearchBar = () => {
const { t } = useTranslation();
const memoType = useAppSelector((state) => state.location.query?.type);
const locationStore = useLocationStore();
const memoType = locationStore.state.query.type;
const [queryText, setQueryText] = useState("");
useEffect(() => {
const text = locationService.getState().query.text;
const text = locationStore.getState().query.text;
setQueryText(text === undefined ? "" : text);
}, [locationService.getState().query.text]);
}, [locationStore.getState().query.text]);
const handleMemoTypeItemClick = (type: MemoSpecType | undefined) => {
const { type: prevType } = locationService.getState().query ?? {};
const { type: prevType } = locationStore.getState().query ?? {};
if (type === prevType) {
type = undefined;
}
locationService.setMemoTypeQuery(type);
locationStore.setMemoTypeQuery(type);
};
const handleTextQueryInput = (event: React.FormEvent<HTMLInputElement>) => {
const text = event.currentTarget.value;
setQueryText(text);
locationService.setTextQuery(text.length === 0 ? undefined : text);
locationStore.setTextQuery(text.length === 0 ? undefined : text);
};
return (

View File

@ -1,6 +1,5 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppSelector } from "../store";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import MyAccountSection from "./Settings/MyAccountSection";
@ -8,6 +7,7 @@ import PreferencesSection from "./Settings/PreferencesSection";
import MemberSection from "./Settings/MemberSection";
import SystemSection from "./Settings/SystemSection";
import "../less/setting-dialog.less";
import { useUserStore } from "../store/module";
type Props = DialogProps;
@ -20,7 +20,8 @@ interface State {
const SettingDialog: React.FC<Props> = (props: Props) => {
const { destroy } = props;
const { t } = useTranslation();
const user = useAppSelector((state) => state.user.user);
const userStore = useUserStore();
const user = userStore.state.user;
const [state, setState] = useState<State>({
selectedSection: "my-account",
});

View File

@ -1,7 +1,6 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { userService } from "../../services";
import { useAppSelector } from "../../store";
import { useUserStore } from "../../store/module";
import * as api from "../../helpers/api";
import toastHelper from "../Toast";
import Dropdown from "../common/Dropdown";
@ -16,7 +15,8 @@ interface State {
const PreferencesSection = () => {
const { t } = useTranslation();
const currentUser = useAppSelector((state) => state.user.user);
const userStore = useUserStore();
const currentUser = userStore.state.user;
const [state, setState] = useState<State>({
createUserUsername: "",
createUserPassword: "",
@ -80,7 +80,7 @@ const PreferencesSection = () => {
content: `Are you sure to archive ${user.username}?`,
style: "warning",
onConfirm: async () => {
await userService.patchUser({
await userStore.patchUser({
id: user.id,
rowStatus: "ARCHIVED",
});
@ -90,7 +90,7 @@ const PreferencesSection = () => {
};
const handleRestoreUserClick = async (user: User) => {
await userService.patchUser({
await userStore.patchUser({
id: user.id,
rowStatus: "NORMAL",
});
@ -103,7 +103,7 @@ const PreferencesSection = () => {
content: `Are you sure to delete ${user.username}? THIS ACTION IS IRREVERSIABLE.❗️`,
style: "warning",
onConfirm: async () => {
await userService.deleteUser({
await userStore.deleteUser({
id: user.id,
});
fetchUserList();

View File

@ -1,6 +1,5 @@
import { useTranslation } from "react-i18next";
import { useAppSelector } from "../../store";
import { userService } from "../../services";
import { useUserStore } from "../../store/module";
import { showCommonDialog } from "../Dialog/CommonDialog";
import showChangePasswordDialog from "../ChangePasswordDialog";
import showUpdateAccountDialog from "../UpdateAccountDialog";
@ -8,7 +7,8 @@ import "../../less/settings/my-account-section.less";
const MyAccountSection = () => {
const { t } = useTranslation();
const user = useAppSelector((state) => state.user.user as User);
const userStore = useUserStore();
const user = userStore.state.user as User;
const openAPIRoute = `${window.location.origin}/api/memo?openId=${user.openId}`;
const handleResetOpenIdBtnClick = async () => {
@ -17,7 +17,7 @@ const MyAccountSection = () => {
content: "❗The existing API will be invalidated and a new one will be generated, are you sure you want to reset?",
style: "warning",
onConfirm: async () => {
await userService.patchUser({
await userStore.patchUser({
id: user.id,
resetOpenId: true,
});

View File

@ -1,7 +1,6 @@
import { Select, Switch, Option } from "@mui/joy";
import { useTranslation } from "react-i18next";
import { globalService, userService } from "../../services";
import { useAppSelector } from "../../store";
import { useGlobalStore, useUserStore } from "../../store/module";
import { VISIBILITY_SELECTOR_ITEMS, MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS } from "../../helpers/consts";
import Icon from "../Icon";
import AppearanceSelect from "../AppearanceSelect";
@ -44,7 +43,9 @@ const localeSelectorItems = [
const PreferencesSection = () => {
const { t } = useTranslation();
const { setting, localSetting } = useAppSelector((state) => state.user.user as User);
const globalStore = useGlobalStore();
const userStore = useUserStore();
const { setting, localSetting } = userStore.state.user as User;
const visibilitySelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
return {
value: item.value,
@ -60,20 +61,20 @@ const PreferencesSection = () => {
});
const handleLocaleChanged = async (value: string) => {
await userService.upsertUserSetting("locale", value);
globalService.setLocale(value as Locale);
await userStore.upsertUserSetting("locale", value);
globalStore.setLocale(value as Locale);
};
const handleDefaultMemoVisibilityChanged = async (value: string) => {
await userService.upsertUserSetting("memoVisibility", value);
await userStore.upsertUserSetting("memoVisibility", value);
};
const handleMemoDisplayTsOptionChanged = async (value: string) => {
await userService.upsertUserSetting("memoDisplayTsOption", value);
await userStore.upsertUserSetting("memoDisplayTsOption", value);
};
const handleIsFoldingEnabledChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
userService.upsertLocalSetting("enableFoldMemo", event.target.checked);
userStore.upsertLocalSetting("enableFoldMemo", event.target.checked);
};
return (

View File

@ -4,10 +4,10 @@ import { useTranslation } from "react-i18next";
import copy from "copy-to-clipboard";
import { toLower } from "lodash";
import toImage from "../labs/html2image";
import { useMemoStore, useUserStore } from "../store/module";
import { VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
import * as utils from "../helpers/utils";
import { getMemoStats } from "../helpers/api";
import { memoService, userService } from "../services";
import useLoading from "../hooks/useLoading";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
@ -29,7 +29,9 @@ interface State {
const ShareMemoDialog: React.FC<Props> = (props: Props) => {
const { memo: propsMemo, destroy } = props;
const { t } = useTranslation();
const user = userService.getState().user as User;
const userStore = useUserStore();
const memoStore = useMemoStore();
const user = userStore.state.user as User;
const [state, setState] = useState<State>({
memoAmount: 0,
memoVisibility: propsMemo.visibility,
@ -113,7 +115,7 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => {
...state,
memoVisibility: visibilityValue,
});
await memoService.patchMemo({
await memoStore.patchMemo({
id: memo.id,
visibility: visibilityValue,
});

View File

@ -1,7 +1,6 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { locationService, shortcutService } from "../services";
import { useAppSelector } from "../store";
import { useLocationStore, useShortcutStore } from "../store/module";
import * as utils from "../helpers/utils";
import useToggle from "../hooks/useToggle";
import useLoading from "../hooks/useLoading";
@ -11,10 +10,12 @@ import showCreateShortcutDialog from "./CreateShortcutDialog";
import "../less/shortcut-list.less";
const ShortcutList = () => {
const query = useAppSelector((state) => state.location.query);
const shortcuts = useAppSelector((state) => state.shortcut.shortcuts);
const loadingState = useLoading();
const { t } = useTranslation();
const locationStore = useLocationStore();
const shortcutStore = useShortcutStore();
const query = locationStore.state.query;
const shortcuts = shortcutStore.state.shortcuts;
const loadingState = useLoading();
const pinnedShortcuts = shortcuts
.filter((s) => s.rowStatus === "ARCHIVED")
@ -25,7 +26,7 @@ const ShortcutList = () => {
const sortedShortcuts = pinnedShortcuts.concat(unpinnedShortcuts);
useEffect(() => {
shortcutService
shortcutStore
.getMyAllShortcuts()
.catch(() => {
// do nth
@ -60,13 +61,15 @@ interface ShortcutContainerProps {
const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutContainerProps) => {
const { shortcut, isActive } = props;
const { t } = useTranslation();
const locationStore = useLocationStore();
const shortcutStore = useShortcutStore();
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);
const handleShortcutClick = () => {
if (isActive) {
locationService.setMemoShortcut(undefined);
locationStore.setMemoShortcut(undefined);
} else {
locationService.setMemoShortcut(shortcut.id);
locationStore.setMemoShortcut(shortcut.id);
}
};
@ -75,10 +78,10 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
if (showConfirmDeleteBtn) {
try {
await shortcutService.deleteShortcutById(shortcut.id);
if (locationService.getState().query?.shortcutId === shortcut.id) {
await shortcutStore.deleteShortcutById(shortcut.id);
if (locationStore.getState().query?.shortcutId === shortcut.id) {
// need clear shortcut filter
locationService.setMemoShortcut(undefined);
locationStore.setMemoShortcut(undefined);
}
} catch (error: any) {
console.error(error);
@ -102,7 +105,7 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
id: shortcut.id,
rowStatus: shortcut.rowStatus === "ARCHIVED" ? "NORMAL" : "ARCHIVED",
};
await shortcutService.patchShortcut(shortcutPatch);
await shortcutStore.patchShortcut(shortcutPatch);
} catch (error) {
// do nth
}

View File

@ -2,8 +2,7 @@ import { isUndefined } from "lodash-es";
import { useEffect } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { userService } from "../services";
import { useAppSelector } from "../store";
import { useLocationStore, useUserStore } from "../store/module";
import showDailyReviewDialog from "./DailyReviewDialog";
import showSettingDialog from "./SettingDialog";
import UserBanner from "./UserBanner";
@ -14,11 +13,13 @@ import "../less/siderbar.less";
const Sidebar = () => {
const { t } = useTranslation();
const location = useAppSelector((state) => state.location);
const userStore = useUserStore();
const locationStore = useLocationStore();
const query = locationStore.state.query;
useEffect(() => {
toggleSidebar(false);
}, [location.query]);
}, [query]);
const handleSettingBtnClick = () => {
showSettingDialog();
@ -34,7 +35,7 @@ const Sidebar = () => {
<button className="btn action-btn" onClick={() => showDailyReviewDialog()}>
<span className="icon">📅</span> {t("sidebar.daily-review")}
</button>
{!userService.isVisitorMode() && (
{!userStore.isVisitorMode() && (
<>
<Link to="/explore" className="btn action-btn">
<span className="icon">🏂</span> {t("common.explore")}
@ -45,7 +46,7 @@ const Sidebar = () => {
</>
)}
</div>
{!userService.isVisitorMode() && <ShortcutList />}
{!userStore.isVisitorMode() && <ShortcutList />}
<TagList />
</aside>
</>

View File

@ -1,7 +1,6 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppSelector } from "../store";
import { locationService, memoService, userService } from "../services";
import { useLocationStore, useMemoStore, useUserStore } from "../store/module";
import useToggle from "../hooks/useToggle";
import Icon from "./Icon";
import "../less/tag-list.less";
@ -14,13 +13,16 @@ interface Tag {
const TagList = () => {
const { t } = useTranslation();
const { memos, tags: tagsText } = useAppSelector((state) => state.memo);
const query = useAppSelector((state) => state.location.query);
const locationStore = useLocationStore();
const userStore = useUserStore();
const memoStore = useMemoStore();
const { memos, tags: tagsText } = memoStore.state;
const query = locationStore.state.query;
const [tags, setTags] = useState<Tag[]>([]);
useEffect(() => {
if (memos.length > 0) {
memoService.updateTagsState();
memoStore.updateTagsState();
}
}, [memos]);
@ -75,7 +77,7 @@ const TagList = () => {
{tags.map((t, idx) => (
<TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={query?.tag} />
))}
{!userService.isVisitorMode() && tags.length === 0 && <p className="tip-text">{t("tag-list.tip-text")}</p>}
{!userStore.isVisitorMode() && tags.length === 0 && <p className="tip-text">{t("tag-list.tip-text")}</p>}
</div>
</div>
);
@ -87,6 +89,7 @@ interface TagItemContainerProps {
}
const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContainerProps) => {
const locationStore = useLocationStore();
const { tag, tagQuery } = props;
const isActive = tagQuery === tag.text;
const hasSubTags = tag.subTags.length > 0;
@ -94,9 +97,9 @@ const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContain
const handleTagClick = () => {
if (isActive) {
locationService.setTagQuery(undefined);
locationStore.setTagQuery(undefined);
} else {
locationService.setTagQuery(tag.text);
locationStore.setTagQuery(tag.text);
}
};

View File

@ -1,12 +1,11 @@
import { isEqual } from "lodash";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppSelector } from "../store";
import { userService } from "../services";
import { useUserStore } from "../store/module";
import { validate, ValidatorConfig } from "../helpers/validator";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
import { validate, ValidatorConfig } from "../helpers/validator";
const validateConfig: ValidatorConfig = {
minLength: 4,
@ -25,7 +24,8 @@ interface State {
const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
const { t } = useTranslation();
const user = useAppSelector((state) => state.user.user as User);
const userStore = useUserStore();
const user = userStore.state.user as User;
const [state, setState] = useState<State>({
username: user.username,
nickname: user.nickname,
@ -78,7 +78,7 @@ const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
}
try {
const user = userService.getState().user as User;
const user = userStore.getState().user as User;
const userPatch: UserPatch = {
id: user.id,
};
@ -91,7 +91,7 @@ const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
if (!isEqual(user.email, state.email)) {
userPatch.email = state.email;
}
await userService.patchUser(userPatch);
await userStore.patchUser(userPatch);
toastHelper.info("Update succeed");
handleCloseBtnClick();
} catch (error: any) {

View File

@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { useAppSelector } from "../store";
import * as api from "../helpers/api";
import * as storage from "../helpers/storage";
import Icon from "./Icon";
import "../less/about-site-dialog.less";
import { useGlobalStore } from "../store/module";
interface State {
latestVersion: string;
@ -11,7 +11,8 @@ interface State {
}
const UpdateVersionBanner: React.FC = () => {
const profile = useAppSelector((state) => state.global.systemStatus.profile);
const globalStore = useGlobalStore();
const profile = globalStore.state.systemStatus.profile;
const [state, setState] = useState<State>({
latestVersion: "",
show: false,

View File

@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useAppSelector } from "../store";
import { locationService, userService } from "../services";
import { useLocationStore, useMemoStore, useUserStore } from "../store/module";
import { getMemoStats } from "../helpers/api";
import { DAILY_TIMESTAMP } from "../helpers/consts";
import * as utils from "../helpers/utils";
@ -28,19 +27,21 @@ interface DailyUsageStat {
}
const UsageHeatMap = () => {
const locationStore = useLocationStore();
const userStore = useUserStore();
const memoStore = useMemoStore();
const todayTimeStamp = utils.getDateStampByDate(Date.now());
const todayDay = new Date(todayTimeStamp).getDay() + 1;
const nullCell = new Array(7 - todayDay).fill(0);
const usedDaysAmount = (tableConfig.width - 1) * tableConfig.height + todayDay;
const beginDayTimestamp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP;
const { memos } = useAppSelector((state) => state.memo);
const memos = memoStore.state.memos;
const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestamp));
const [currentStat, setCurrentStat] = useState<DailyUsageStat | null>(null);
const containerElRef = useRef<HTMLDivElement>(null);
useEffect(() => {
getMemoStats(userService.getCurrentUserId())
getMemoStats(userStore.getCurrentUserId())
.then(({ data: { data } }) => {
const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestamp);
for (const record of data) {
@ -84,11 +85,11 @@ const UsageHeatMap = () => {
}, []);
const handleUsageStatItemClick = useCallback((item: DailyUsageStat) => {
if (locationService.getState().query?.duration?.from === item.timestamp) {
locationService.setFromAndToQuery();
if (locationStore.getState().query?.duration?.from === item.timestamp) {
locationStore.setFromAndToQuery();
setCurrentStat(null);
} else if (item.count > 0) {
locationService.setFromAndToQuery(item.timestamp, item.timestamp + DAILY_TIMESTAMP);
locationStore.setFromAndToQuery(item.timestamp, item.timestamp + DAILY_TIMESTAMP);
setCurrentStat(item);
}
}, []);

View File

@ -1,11 +1,9 @@
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useLocationStore, useMemoStore, useUserStore } from "../store/module";
import { getMemoStats } from "../helpers/api";
import * as utils from "../helpers/utils";
import userService from "../services/userService";
import { locationService } from "../services";
import { useAppSelector } from "../store";
import Icon from "./Icon";
import Dropdown from "./common/Dropdown";
import showResourcesDialog from "./ResourcesDialog";
@ -16,12 +14,15 @@ import "../less/user-banner.less";
const UserBanner = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { user, owner } = useAppSelector((state) => state.user);
const { memos, tags } = useAppSelector((state) => state.memo);
const locationStore = useLocationStore();
const userStore = useUserStore();
const memoStore = useMemoStore();
const { user, owner } = userStore.state;
const { memos, tags } = memoStore.state;
const [username, setUsername] = useState("Memos");
const [memoAmount, setMemoAmount] = useState(0);
const [createdDays, setCreatedDays] = useState(0);
const isVisitorMode = userService.isVisitorMode();
const isVisitorMode = userStore.isVisitorMode();
useEffect(() => {
if (isVisitorMode) {
@ -37,7 +38,7 @@ const UserBanner = () => {
}, [isVisitorMode, user, owner]);
useEffect(() => {
getMemoStats(userService.getCurrentUserId())
getMemoStats(userStore.getCurrentUserId())
.then(({ data: { data } }) => {
setMemoAmount(data.length);
})
@ -47,7 +48,7 @@ const UserBanner = () => {
}, [memos]);
const handleUsernameClick = useCallback(() => {
locationService.clearQuery();
locationStore.clearQuery();
}, []);
const handleResourcesBtnClick = () => {
@ -78,7 +79,7 @@ const UserBanner = () => {
actionsClassName="min-w-36"
actions={
<>
{!userService.isVisitorMode() && (
{!userStore.isVisitorMode() && (
<>
<button
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
@ -100,7 +101,7 @@ const UserBanner = () => {
>
<span className="mr-1">🤠</span> {t("common.about")}
</button>
{!userService.isVisitorMode() && (
{!userStore.isVisitorMode() && (
<button
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
onClick={handleSignOutBtnClick}

View File

@ -18,4 +18,8 @@ export const MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS = [
{ text: "updated_ts", value: "updated_ts" },
];
// space width for tab action in editor
export const TAB_SPACE_WIDTH = 2;
// default fetch memo amount
export const DEFAULT_MEMO_LIMIT = 30;

View File

@ -2,11 +2,10 @@ import { Option, Select } from "@mui/joy";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useAppSelector } from "../store";
import { useGlobalStore, useUserStore } from "../store/module";
import * as api from "../helpers/api";
import { validate, ValidatorConfig } from "../helpers/validator";
import useLoading from "../hooks/useLoading";
import { globalService, userService } from "../services";
import Icon from "../components/Icon";
import toastHelper from "../components/Toast";
import AppearanceSelect from "../components/AppearanceSelect";
@ -22,14 +21,16 @@ const validateConfig: ValidatorConfig = {
const Auth = () => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const systemStatus = useAppSelector((state) => state.global.systemStatus);
const globalStore = useGlobalStore();
const userStore = useUserStore();
const systemStatus = globalStore.state.systemStatus;
const actionBtnLoadingState = useLoading(false);
const mode = systemStatus.profile.mode;
const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
useEffect(() => {
userService.doSignOut().catch();
userStore.doSignOut().catch();
}, []);
const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -62,7 +63,7 @@ const Auth = () => {
try {
actionBtnLoadingState.setLoading();
await api.signin(username, password);
const user = await userService.doSignIn();
const user = await userStore.doSignIn();
if (user) {
navigate("/");
} else {
@ -95,7 +96,7 @@ const Auth = () => {
try {
actionBtnLoadingState.setLoading();
await api.signup(username, password, role);
const user = await userService.doSignIn();
const user = await userStore.doSignIn();
if (user) {
navigate("/");
} else {
@ -109,7 +110,7 @@ const Auth = () => {
};
const handleLocaleItemClick = (locale: Locale) => {
globalService.setLocale(locale);
globalStore.setLocale(locale);
};
return (

View File

@ -2,9 +2,8 @@ import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { memoService } from "../services";
import { DEFAULT_MEMO_LIMIT } from "../services/memoService";
import { useAppSelector } from "../store";
import { useLocationStore, useMemoStore, useUserStore } from "../store/module";
import { DEFAULT_MEMO_LIMIT } from "../helpers/consts";
import useLoading from "../hooks/useLoading";
import toastHelper from "../components/Toast";
import MemoContent from "../components/MemoContent";
@ -17,8 +16,11 @@ interface State {
const Explore = () => {
const { t, i18n } = useTranslation();
const user = useAppSelector((state) => state.user.user);
const location = useAppSelector((state) => state.location);
const locationStore = useLocationStore();
const userStore = useUserStore();
const memoStore = useMemoStore();
const user = userStore.state.user;
const location = locationStore.state;
const [state, setState] = useState<State>({
memos: [],
});
@ -26,7 +28,7 @@ const Explore = () => {
const loadingState = useLoading();
useEffect(() => {
memoService.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length).then((memos) => {
memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length).then((memos) => {
if (memos.length < DEFAULT_MEMO_LIMIT) {
setIsComplete(true);
}
@ -39,7 +41,7 @@ const Explore = () => {
const handleFetchMoreClick = async () => {
try {
const fetchedMemos = await memoService.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length);
const fetchedMemos = await memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length);
if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) {
setIsComplete(true);
} else {

View File

@ -1,8 +1,7 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { globalService, userService } from "../services";
import { useAppSelector } from "../store";
import { useGlobalStore, useUserStore } from "../store/module";
import toastHelper from "../components/Toast";
import Sidebar from "../components/Sidebar";
import MemosHeader from "../components/MemosHeader";
@ -15,12 +14,14 @@ import "../less/home.less";
function Home() {
const { t } = useTranslation();
const location = useLocation();
const user = useAppSelector((state) => state.user.user);
const globalStore = useGlobalStore();
const userStore = useUserStore();
const user = userStore.state.user;
useEffect(() => {
const { owner } = userService.getState();
const { owner } = userStore.getState();
if (userService.isVisitorMode()) {
if (userStore.isVisitorMode()) {
if (!owner) {
toastHelper.error(t("message.user-not-found"));
}
@ -29,7 +30,7 @@ function Home() {
useEffect(() => {
if (user?.setting.locale) {
globalService.setLocale(user.setting.locale);
globalStore.setLocale(user.setting.locale);
}
}, [user?.setting.locale]);
@ -43,11 +44,11 @@ function Home() {
<main className="memos-wrapper">
<MemosHeader />
<div className="memos-editor-wrapper">
{!userService.isVisitorMode() && <MemoEditor />}
{!userStore.isVisitorMode() && <MemoEditor />}
<MemoFilter />
</div>
<MemoList />
{userService.isVisitorMode() && (
{userStore.isVisitorMode() && (
<div className="addition-btn-container">
{user ? (
<button className="btn" onClick={() => (window.location.href = "/")}>

View File

@ -2,9 +2,8 @@ import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useParams } from "react-router-dom";
import { memoService } from "../services";
import { UNKNOWN_ID } from "../helpers/consts";
import { useAppSelector } from "../store";
import { useLocationStore, useMemoStore, useUserStore } from "../store/module";
import useLoading from "../hooks/useLoading";
import toastHelper from "../components/Toast";
import MemoContent from "../components/MemoContent";
@ -18,8 +17,11 @@ interface State {
const MemoDetail = () => {
const { t, i18n } = useTranslation();
const params = useParams();
const user = useAppSelector((state) => state.user.user);
const location = useAppSelector((state) => state.location);
const locationStore = useLocationStore();
const memoStore = useMemoStore();
const userStore = useUserStore();
const user = userStore.state.user;
const location = locationStore.state;
const [state, setState] = useState<State>({
memo: {
id: UNKNOWN_ID,
@ -30,7 +32,7 @@ const MemoDetail = () => {
useEffect(() => {
const memoId = Number(params.memoId);
if (memoId && !isNaN(memoId)) {
memoService
memoStore
.fetchMemoById(memoId)
.then((memo) => {
setState({

View File

@ -1,7 +1,8 @@
import { createBrowserRouter, redirect } from "react-router-dom";
import { lazy } from "react";
import { isNullorUndefined } from "../helpers/utils";
import { globalService, userService } from "../services";
import store from "../store";
import { initialGlobalState, initialUserState } from "../store/module";
const Auth = lazy(() => import("../pages/Auth"));
const Explore = lazy(() => import("../pages/Explore"));
@ -14,7 +15,7 @@ const router = createBrowserRouter([
element: <Auth />,
loader: async () => {
try {
await globalService.initialState();
await initialGlobalState();
} catch (error) {
// do nth
}
@ -26,13 +27,13 @@ const router = createBrowserRouter([
element: <Home />,
loader: async () => {
try {
await globalService.initialState();
await userService.initialState();
await initialGlobalState();
await initialUserState();
} catch (error) {
// do nth
}
const { host, user } = userService.getState();
const { host, user } = store.getState().user;
if (isNullorUndefined(host)) {
return redirect("/auth");
} else if (isNullorUndefined(user)) {
@ -46,13 +47,13 @@ const router = createBrowserRouter([
element: <Home />,
loader: async () => {
try {
await globalService.initialState();
await userService.initialState();
await initialGlobalState();
await initialUserState();
} catch (error) {
// do nth
}
const { host } = userService.getState();
const { host } = store.getState().user;
if (isNullorUndefined(host)) {
return redirect("/auth");
}
@ -64,13 +65,13 @@ const router = createBrowserRouter([
element: <Explore />,
loader: async () => {
try {
await globalService.initialState();
await userService.initialState();
await initialGlobalState();
await initialUserState();
} catch (error) {
// do nth
}
const { host } = userService.getState();
const { host } = store.getState().user;
if (isNullorUndefined(host)) {
return redirect("/auth");
}
@ -82,13 +83,13 @@ const router = createBrowserRouter([
element: <MemoDetail />,
loader: async () => {
try {
await globalService.initialState();
await userService.initialState();
await initialGlobalState();
await initialUserState();
} catch (error) {
// do nth
}
const { host } = userService.getState();
const { host } = store.getState().user;
if (isNullorUndefined(host)) {
return redirect("/auth");
}

View File

@ -1,6 +0,0 @@
# Services
What should service do?
- request data api and throw error;
- dispatch state actions;

View File

@ -1,30 +0,0 @@
import store from "../store";
import { setEditMemoId, setMemoVisibility, setResourceList } from "../store/modules/editor";
const editorStateService = {
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));
},
clearResourceList: () => {
store.dispatch(setResourceList([]));
},
};
export default editorStateService;

View File

@ -1,51 +0,0 @@
import store from "../store";
import * as api from "../helpers/api";
import * as storage from "../helpers/storage";
import { setAppearance, setGlobalState, setLocale } from "../store/modules/global";
const globalService = {
getState: () => {
return store.getState().global;
},
initialState: async () => {
const defaultGlobalState = {
locale: "en" as Locale,
appearance: "system" as Appearance,
systemStatus: {
allowSignUp: false,
additionalStyle: "",
additionalScript: "",
} as SystemStatus,
};
const { locale: storageLocale, appearance: storageAppearance } = storage.get(["locale", "appearance"]);
if (storageLocale) {
defaultGlobalState.locale = storageLocale;
}
if (storageAppearance) {
defaultGlobalState.appearance = storageAppearance;
}
try {
const { data } = (await api.getSystemStatus()).data;
if (data) {
defaultGlobalState.systemStatus = data;
}
} catch (error) {
// do nth
}
store.dispatch(setGlobalState(defaultGlobalState));
},
setLocale: (locale: Locale) => {
store.dispatch(setLocale(locale));
},
setAppearance: (appearance: Appearance) => {
store.dispatch(setAppearance(appearance));
},
};
export default globalService;

View File

@ -1,9 +0,0 @@
import globalService from "./globalService";
import editorStateService from "./editorStateService";
import locationService from "./locationService";
import memoService from "./memoService";
import shortcutService from "./shortcutService";
import userService from "./userService";
import resourceService from "./resourceService";
export { globalService, editorStateService, locationService, memoService, shortcutService, userService, resourceService };

View File

@ -1,131 +0,0 @@
import { stringify } from "qs";
import store from "../store";
import { setQuery, setPathname, Query, updateStateWithLocation, updatePathnameStateWithLocation } from "../store/modules/location";
const updateLocationUrl = (method: "replace" | "push" = "replace") => {
// avoid pathname confusion when entering from non-home page
store.dispatch(updatePathnameStateWithLocation());
const { query, pathname, hash } = store.getState().location;
let queryString = stringify(query);
if (queryString) {
queryString = "?" + queryString;
} else {
queryString = "";
}
if (method === "replace") {
window.history.replaceState(null, "", pathname + hash + queryString);
} else {
window.history.pushState(null, "", pathname + hash + queryString);
}
store.dispatch(updateStateWithLocation());
};
const locationService = {
getState: () => {
return store.getState().location;
},
updateStateWithLocation: () => {
store.dispatch(updateStateWithLocation());
},
setPathname: (pathname: string) => {
store.dispatch(setPathname(pathname));
updateLocationUrl();
},
pushHistory: (pathname: string) => {
store.dispatch(setPathname(pathname));
updateLocationUrl("push");
},
replaceHistory: (pathname: string) => {
store.dispatch(setPathname(pathname));
updateLocationUrl("replace");
},
setQuery: (query: Query) => {
store.dispatch(setQuery(query));
updateLocationUrl();
},
clearQuery: () => {
store.dispatch(
setQuery({
tag: undefined,
type: undefined,
duration: undefined,
text: undefined,
shortcutId: undefined,
visibility: undefined,
})
);
updateLocationUrl();
},
setMemoTypeQuery: (type?: MemoSpecType) => {
store.dispatch(
setQuery({
type: type,
})
);
updateLocationUrl();
},
setMemoShortcut: (shortcutId?: ShortcutId) => {
store.dispatch(
setQuery({
shortcutId: shortcutId,
})
);
updateLocationUrl();
},
setTextQuery: (text?: string) => {
store.dispatch(
setQuery({
text: text,
})
);
updateLocationUrl();
},
setTagQuery: (tag?: string) => {
store.dispatch(
setQuery({
tag: tag,
})
);
updateLocationUrl();
},
setFromAndToQuery: (from?: number, to?: number) => {
let duration = undefined;
if (from && to && from < to) {
duration = {
from,
to,
};
}
store.dispatch(
setQuery({
duration,
})
);
updateLocationUrl();
},
setMemoVisibilityQuery: (visibility?: Visibility) => {
store.dispatch(
setQuery({
visibility: visibility,
})
);
updateLocationUrl();
},
};
export default locationService;

View File

@ -1,143 +0,0 @@
import { uniqBy } from "lodash";
import * as api from "../helpers/api";
import { createMemo, deleteMemo, patchMemo, setIsFetching, setMemos, setTags } from "../store/modules/memo";
import store from "../store";
import userService from "./userService";
export const DEFAULT_MEMO_LIMIT = 30;
const convertResponseModelMemo = (memo: Memo): Memo => {
return {
...memo,
createdTs: memo.createdTs * 1000,
updatedTs: memo.updatedTs * 1000,
displayTs: memo.displayTs * 1000,
};
};
const memoService = {
getState: () => {
return store.getState().memo;
},
fetchMemos: async (limit = DEFAULT_MEMO_LIMIT, offset = 0) => {
store.dispatch(setIsFetching(true));
const memoFind: MemoFind = {
rowStatus: "NORMAL",
limit,
offset,
};
if (userService.isVisitorMode()) {
memoFind.creatorId = userService.getUserIdFromPath();
}
const { data } = (await api.getMemoList(memoFind)).data;
const fetchedMemos = data.map((m) => convertResponseModelMemo(m));
if (offset === 0) {
store.dispatch(setMemos([]));
}
const memos = memoService.getState().memos;
store.dispatch(setMemos(uniqBy(memos.concat(fetchedMemos), "id")));
store.dispatch(setIsFetching(false));
return fetchedMemos;
},
fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => {
const memoFind: MemoFind = {
rowStatus: "NORMAL",
limit,
offset,
};
const { data } = (await api.getAllMemos(memoFind)).data;
const memos = data.map((m) => convertResponseModelMemo(m));
return memos;
},
fetchArchivedMemos: async () => {
const memoFind: MemoFind = {
rowStatus: "ARCHIVED",
};
if (userService.isVisitorMode()) {
memoFind.creatorId = userService.getUserIdFromPath();
}
const { data } = (await api.getMemoList(memoFind)).data;
const archivedMemos = data.map((m) => {
return convertResponseModelMemo(m);
});
return archivedMemos;
},
fetchMemoById: async (memoId: MemoId) => {
const { data } = (await api.getMemoById(memoId)).data;
const memo = convertResponseModelMemo(data);
return memo;
},
getMemoById: async (memoId: MemoId) => {
for (const m of memoService.getState().memos) {
if (m.id === memoId) {
return m;
}
}
return await memoService.fetchMemoById(memoId);
},
updateTagsState: async () => {
const tagFind: TagFind = {};
if (userService.isVisitorMode()) {
tagFind.creatorId = userService.getUserIdFromPath();
}
const { data } = (await api.getTagList(tagFind)).data;
store.dispatch(setTags(data));
},
getLinkedMemos: async (memoId: MemoId): Promise<Memo[]> => {
const { memos } = memoService.getState();
const regex = new RegExp(`[@(.+?)](${memoId})`);
return memos.filter((m) => m.content.match(regex));
},
createMemo: async (memoCreate: MemoCreate) => {
const { data } = (await api.createMemo(memoCreate)).data;
const memo = convertResponseModelMemo(data);
store.dispatch(createMemo(memo));
return memo;
},
patchMemo: async (memoPatch: MemoPatch): Promise<Memo> => {
const { data } = (await api.patchMemo(memoPatch)).data;
const memo = convertResponseModelMemo(data);
store.dispatch(patchMemo(memo));
return memo;
},
pinMemo: async (memoId: MemoId) => {
await api.pinMemo(memoId);
store.dispatch(
patchMemo({
id: memoId,
pinned: true,
})
);
},
unpinMemo: async (memoId: MemoId) => {
await api.unpinMemo(memoId);
store.dispatch(
patchMemo({
id: memoId,
pinned: false,
})
);
},
deleteMemoById: async (memoId: MemoId) => {
await api.deleteMemo(memoId);
store.dispatch(deleteMemo(memoId));
},
};
export default memoService;

View File

@ -1,54 +0,0 @@
import * as api from "../helpers/api";
import store from "../store";
import { patchResource, setResources, deleteResource } from "../store/modules/resource";
const convertResponseModelResource = (resource: Resource): Resource => {
return {
...resource,
createdTs: resource.createdTs * 1000,
updatedTs: resource.updatedTs * 1000,
};
};
const resourceService = {
getState: () => {
return store.getState().resource;
},
async fetchResourceList(): Promise<Resource[]> {
const { data } = (await api.getResourceList()).data;
const resourceList = data.map((m) => convertResponseModelResource(m));
store.dispatch(setResources(resourceList));
return resourceList;
},
async upload(file: File): Promise<Resource> {
const { name: filename, size } = file;
if (size > 64 << 20) {
return Promise.reject("overload max size: 8MB");
}
const formData = new FormData();
formData.append("file", file, filename);
const { data } = (await api.uploadFile(formData)).data;
const resource = convertResponseModelResource(data);
const resourceList = resourceService.getState().resources;
store.dispatch(setResources([resource, ...resourceList]));
return resource;
},
async deleteResourceById(id: ResourceId) {
await api.deleteResourceById(id);
store.dispatch(deleteResource(id));
},
async patchResource(resourcePatch: ResourcePatch): Promise<Resource> {
const { data } = (await api.patchResource(resourcePatch)).data;
const resource = convertResponseModelResource(data);
store.dispatch(patchResource(resource));
return resource;
},
};
export default resourceService;

View File

@ -1,52 +0,0 @@
import * as api from "../helpers/api";
import store from "../store/";
import { createShortcut, deleteShortcut, patchShortcut, setShortcuts } from "../store/modules/shortcut";
const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => {
return {
...shortcut,
createdTs: shortcut.createdTs * 1000,
updatedTs: shortcut.updatedTs * 1000,
};
};
const shortcutService = {
getState: () => {
return store.getState().shortcut;
},
getMyAllShortcuts: async () => {
const { data } = (await api.getShortcutList()).data;
const shortcuts = data.map((s) => convertResponseModelShortcut(s));
store.dispatch(setShortcuts(shortcuts));
},
getShortcutById: (id: ShortcutId) => {
for (const s of shortcutService.getState().shortcuts) {
if (s.id === id) {
return s;
}
}
return null;
},
createShortcut: async (shortcutCreate: ShortcutCreate) => {
const { data } = (await api.createShortcut(shortcutCreate)).data;
const shortcut = convertResponseModelShortcut(data);
store.dispatch(createShortcut(shortcut));
},
patchShortcut: async (shortcutPatch: ShortcutPatch) => {
const { data } = (await api.patchShortcut(shortcutPatch)).data;
const shortcut = convertResponseModelShortcut(data);
store.dispatch(patchShortcut(shortcut));
},
deleteShortcutById: async (shortcutId: ShortcutId) => {
await api.deleteShortcutById(shortcutId);
store.dispatch(deleteShortcut(shortcutId));
},
};
export default shortcutService;

View File

@ -1,148 +0,0 @@
import { globalService, locationService } from ".";
import * as api from "../helpers/api";
import * as storage from "../helpers/storage";
import { UNKNOWN_ID } from "../helpers/consts";
import store from "../store";
import { setUser, patchUser, setHost, setOwner } from "../store/modules/user";
import { getSystemColorScheme } from "../helpers/utils";
const defaultSetting: Setting = {
locale: "en",
appearance: getSystemColorScheme(),
memoVisibility: "PRIVATE",
memoDisplayTsOption: "created_ts",
};
const defaultLocalSetting: LocalSetting = {
enableFoldMemo: true,
};
export const convertResponseModelUser = (user: User): User => {
const setting: Setting = {
...defaultSetting,
};
const { localSetting: storageLocalSetting } = storage.get(["localSetting"]);
const localSetting: LocalSetting = {
...defaultLocalSetting,
...storageLocalSetting,
};
if (user.userSettingList) {
for (const userSetting of user.userSettingList) {
(setting as any)[userSetting.key] = JSON.parse(userSetting.value);
}
}
return {
...user,
setting,
localSetting,
createdTs: user.createdTs * 1000,
updatedTs: user.updatedTs * 1000,
};
};
const userService = {
getState: () => {
return store.getState().user;
},
initialState: async () => {
const { systemStatus } = globalService.getState();
if (systemStatus.host) {
store.dispatch(setHost(convertResponseModelUser(systemStatus.host)));
}
const ownerUserId = userService.getUserIdFromPath();
if (ownerUserId) {
const { data: owner } = (await api.getUserById(ownerUserId)).data;
if (owner) {
store.dispatch(setOwner(convertResponseModelUser(owner)));
}
}
const { data } = (await api.getMyselfUser()).data;
if (data) {
const user = convertResponseModelUser(data);
store.dispatch(setUser(user));
if (user.setting.locale) {
globalService.setLocale(user.setting.locale);
}
if (user.setting.appearance) {
globalService.setAppearance(user.setting.appearance);
}
}
},
getCurrentUserId: () => {
if (userService.isVisitorMode()) {
return userService.getUserIdFromPath() || UNKNOWN_ID;
} else {
return userService.getState().user?.id || UNKNOWN_ID;
}
},
isVisitorMode: () => {
return !(userService.getUserIdFromPath() === undefined);
},
getUserIdFromPath: () => {
const userIdRegex = /^\/u\/(\d+).*/;
const result = locationService.getState().pathname.match(userIdRegex);
if (result && result.length === 2) {
return Number(result[1]);
}
return undefined;
},
doSignIn: async () => {
const { data: user } = (await api.getMyselfUser()).data;
if (user) {
store.dispatch(setUser(convertResponseModelUser(user)));
} else {
userService.doSignOut();
}
return user;
},
doSignOut: async () => {
store.dispatch(setUser());
await api.signout();
},
getUserById: async (userId: UserId) => {
const { data: user } = (await api.getUserById(userId)).data;
if (user) {
return convertResponseModelUser(user);
} else {
return undefined;
}
},
upsertUserSetting: async (key: keyof Setting, value: any) => {
await api.upsertUserSetting({
key: key as any,
value: JSON.stringify(value),
});
await userService.doSignIn();
},
upsertLocalSetting: async (key: keyof LocalSetting, value: any) => {
storage.set({ localSetting: { [key]: value } });
store.dispatch(patchUser({ localSetting: { [key]: value } }));
},
patchUser: async (userPatch: UserPatch): Promise<void> => {
const { data } = (await api.patchUser(userPatch)).data;
if (userPatch.id === store.getState().user.user?.id) {
const user = convertResponseModelUser(data);
store.dispatch(patchUser(user));
}
},
deleteUser: async (userDelete: UserDelete) => {
await api.deleteUser(userDelete);
},
};
export default userService;

View File

@ -1,12 +1,12 @@
import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import globalReducer from "./modules/global";
import userReducer from "./modules/user";
import memoReducer from "./modules/memo";
import editorReducer from "./modules/editor";
import shortcutReducer from "./modules/shortcut";
import locationReducer from "./modules/location";
import resourceReducer from "./modules/resource";
import globalReducer from "./reducer/global";
import userReducer from "./reducer/user";
import memoReducer from "./reducer/memo";
import editorReducer from "./reducer/editor";
import shortcutReducer from "./reducer/shortcut";
import locationReducer from "./reducer/location";
import resourceReducer from "./reducer/resource";
const store = configureStore({
reducer: {

View File

@ -0,0 +1,28 @@
import store, { useAppSelector } from "..";
import { setEditMemoId, setMemoVisibility, 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));
},
clearResourceList: () => {
store.dispatch(setResourceList([]));
},
};
};

View File

@ -0,0 +1,52 @@
import * as api from "../../helpers/api";
import * as storage from "../../helpers/storage";
import store, { useAppSelector } from "../";
import { setAppearance, setGlobalState, setLocale } from "../reducer/global";
export const initialGlobalState = async () => {
const defaultGlobalState = {
locale: "en" as Locale,
appearance: "system" as Appearance,
systemStatus: {
allowSignUp: false,
additionalStyle: "",
additionalScript: "",
} as SystemStatus,
};
const { locale: storageLocale, appearance: storageAppearance } = storage.get(["locale", "appearance"]);
if (storageLocale) {
defaultGlobalState.locale = storageLocale;
}
if (storageAppearance) {
defaultGlobalState.appearance = storageAppearance;
}
try {
const { data } = (await api.getSystemStatus()).data;
if (data) {
defaultGlobalState.systemStatus = data;
}
} catch (error) {
// do nth
}
store.dispatch(setGlobalState(defaultGlobalState));
};
export const useGlobalStore = () => {
const state = useAppSelector((state) => state.global);
return {
state,
getState: () => {
return store.getState().global;
},
setLocale: (locale: Locale) => {
store.dispatch(setLocale(locale));
},
setAppearance: (appearance: Appearance) => {
store.dispatch(setAppearance(appearance));
},
};
};

View File

@ -0,0 +1,7 @@
export * from "./editor";
export * from "./global";
export * from "./location";
export * from "./memo";
export * from "./resource";
export * from "./shortcut";
export * from "./user";

View File

@ -0,0 +1,122 @@
import { stringify } from "qs";
import store, { useAppSelector } from "../";
import { setQuery, setPathname, Query, updateStateWithLocation, updatePathnameStateWithLocation } from "../reducer/location";
const updateLocationUrl = (method: "replace" | "push" = "replace") => {
// avoid pathname confusion when entering from non-home page
store.dispatch(updatePathnameStateWithLocation());
const { query, pathname, hash } = store.getState().location;
let queryString = stringify(query);
if (queryString) {
queryString = "?" + queryString;
} else {
queryString = "";
}
if (method === "replace") {
window.history.replaceState(null, "", pathname + hash + queryString);
} else {
window.history.pushState(null, "", pathname + hash + queryString);
}
store.dispatch(updateStateWithLocation());
};
export const useLocationStore = () => {
const state = useAppSelector((state) => state.location);
return {
state,
getState: () => {
return store.getState().location;
},
updateStateWithLocation: () => {
store.dispatch(updateStateWithLocation());
},
setPathname: (pathname: string) => {
store.dispatch(setPathname(pathname));
updateLocationUrl();
},
pushHistory: (pathname: string) => {
store.dispatch(setPathname(pathname));
updateLocationUrl("push");
},
replaceHistory: (pathname: string) => {
store.dispatch(setPathname(pathname));
updateLocationUrl("replace");
},
setQuery: (query: Query) => {
store.dispatch(setQuery(query));
updateLocationUrl();
},
clearQuery: () => {
store.dispatch(
setQuery({
tag: undefined,
type: undefined,
duration: undefined,
text: undefined,
shortcutId: undefined,
visibility: undefined,
})
);
updateLocationUrl();
},
setMemoTypeQuery: (type?: MemoSpecType) => {
store.dispatch(
setQuery({
type: type,
})
);
updateLocationUrl();
},
setMemoShortcut: (shortcutId?: ShortcutId) => {
store.dispatch(
setQuery({
shortcutId: shortcutId,
})
);
updateLocationUrl();
},
setTextQuery: (text?: string) => {
store.dispatch(
setQuery({
text: text,
})
);
updateLocationUrl();
},
setTagQuery: (tag?: string) => {
store.dispatch(
setQuery({
tag: tag,
})
);
updateLocationUrl();
},
setFromAndToQuery: (from?: number, to?: number) => {
let duration = undefined;
if (from && to && from < to) {
duration = {
from,
to,
};
}
store.dispatch(
setQuery({
duration,
})
);
updateLocationUrl();
},
setMemoVisibilityQuery: (visibility?: Visibility) => {
store.dispatch(
setQuery({
visibility: visibility,
})
);
updateLocationUrl();
},
};
};

View File

@ -0,0 +1,135 @@
import { uniqBy } from "lodash";
import * as api from "../../helpers/api";
import { DEFAULT_MEMO_LIMIT } from "../../helpers/consts";
import { useUserStore } from "./";
import store, { useAppSelector } from "../";
import { createMemo, deleteMemo, patchMemo, setIsFetching, setMemos, setTags } from "../reducer/memo";
const convertResponseModelMemo = (memo: Memo): Memo => {
return {
...memo,
createdTs: memo.createdTs * 1000,
updatedTs: memo.updatedTs * 1000,
displayTs: memo.displayTs * 1000,
};
};
export const useMemoStore = () => {
const state = useAppSelector((state) => state.memo);
const userStore = useUserStore();
const fetchMemoById = async (memoId: MemoId) => {
const { data } = (await api.getMemoById(memoId)).data;
const memo = convertResponseModelMemo(data);
return memo;
};
return {
state,
getState: () => {
return store.getState().memo;
},
fetchMemos: async (limit = DEFAULT_MEMO_LIMIT, offset = 0) => {
store.dispatch(setIsFetching(true));
const memoFind: MemoFind = {
rowStatus: "NORMAL",
limit,
offset,
};
if (userStore.isVisitorMode()) {
memoFind.creatorId = userStore.getUserIdFromPath();
}
const { data } = (await api.getMemoList(memoFind)).data;
const fetchedMemos = data.map((m) => convertResponseModelMemo(m));
if (offset === 0) {
store.dispatch(setMemos([]));
}
const memos = state.memos;
store.dispatch(setMemos(uniqBy(memos.concat(fetchedMemos), "id")));
store.dispatch(setIsFetching(false));
return fetchedMemos;
},
fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => {
const memoFind: MemoFind = {
rowStatus: "NORMAL",
limit,
offset,
};
const { data } = (await api.getAllMemos(memoFind)).data;
const memos = data.map((m) => convertResponseModelMemo(m));
return memos;
},
fetchArchivedMemos: async () => {
const memoFind: MemoFind = {
rowStatus: "ARCHIVED",
};
if (userStore.isVisitorMode()) {
memoFind.creatorId = userStore.getUserIdFromPath();
}
const { data } = (await api.getMemoList(memoFind)).data;
const archivedMemos = data.map((m) => {
return convertResponseModelMemo(m);
});
return archivedMemos;
},
fetchMemoById,
getMemoById: async (memoId: MemoId) => {
for (const m of state.memos) {
if (m.id === memoId) {
return m;
}
}
return await fetchMemoById(memoId);
},
updateTagsState: async () => {
const tagFind: TagFind = {};
if (userStore.isVisitorMode()) {
tagFind.creatorId = userStore.getUserIdFromPath();
}
const { data } = (await api.getTagList(tagFind)).data;
store.dispatch(setTags(data));
},
getLinkedMemos: async (memoId: MemoId): Promise<Memo[]> => {
const regex = new RegExp(`[@(.+?)](${memoId})`);
return state.memos.filter((m) => m.content.match(regex));
},
createMemo: async (memoCreate: MemoCreate) => {
const { data } = (await api.createMemo(memoCreate)).data;
const memo = convertResponseModelMemo(data);
store.dispatch(createMemo(memo));
return memo;
},
patchMemo: async (memoPatch: MemoPatch): Promise<Memo> => {
const { data } = (await api.patchMemo(memoPatch)).data;
const memo = convertResponseModelMemo(data);
store.dispatch(patchMemo(memo));
return memo;
},
pinMemo: async (memoId: MemoId) => {
await api.pinMemo(memoId);
store.dispatch(
patchMemo({
id: memoId,
pinned: true,
})
);
},
unpinMemo: async (memoId: MemoId) => {
await api.unpinMemo(memoId);
store.dispatch(
patchMemo({
id: memoId,
pinned: false,
})
);
},
deleteMemoById: async (memoId: MemoId) => {
await api.deleteMemo(memoId);
store.dispatch(deleteMemo(memoId));
},
};
};

View File

@ -0,0 +1,53 @@
import store, { useAppSelector } from "../";
import { patchResource, setResources, deleteResource } from "../reducer/resource";
import * as api from "../../helpers/api";
const convertResponseModelResource = (resource: Resource): Resource => {
return {
...resource,
createdTs: resource.createdTs * 1000,
updatedTs: resource.updatedTs * 1000,
};
};
export const useResourceStore = () => {
const state = useAppSelector((state) => state.resource);
return {
state,
getState: () => {
return store.getState().resource;
},
async fetchResourceList(): Promise<Resource[]> {
const { data } = (await api.getResourceList()).data;
const resourceList = data.map((m) => convertResponseModelResource(m));
store.dispatch(setResources(resourceList));
return resourceList;
},
async upload(file: File): Promise<Resource> {
const { name: filename, size } = file;
if (size > 64 << 20) {
return Promise.reject("overload max size: 8MB");
}
const formData = new FormData();
formData.append("file", file, filename);
const { data } = (await api.uploadFile(formData)).data;
const resource = convertResponseModelResource(data);
const resourceList = state.resources;
store.dispatch(setResources([resource, ...resourceList]));
return resource;
},
async deleteResourceById(id: ResourceId) {
await api.deleteResourceById(id);
store.dispatch(deleteResource(id));
},
async patchResource(resourcePatch: ResourcePatch): Promise<Resource> {
const { data } = (await api.patchResource(resourcePatch)).data;
const resource = convertResponseModelResource(data);
store.dispatch(patchResource(resource));
return resource;
},
};
};

View File

@ -0,0 +1,49 @@
import store, { useAppSelector } from "../";
import { createShortcut, deleteShortcut, patchShortcut, setShortcuts } from "../reducer/shortcut";
import * as api from "../../helpers/api";
const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => {
return {
...shortcut,
createdTs: shortcut.createdTs * 1000,
updatedTs: shortcut.updatedTs * 1000,
};
};
export const useShortcutStore = () => {
const state = useAppSelector((state) => state.shortcut);
return {
state,
getState: () => {
return store.getState().shortcut;
},
getMyAllShortcuts: async () => {
const { data } = (await api.getShortcutList()).data;
const shortcuts = data.map((s) => convertResponseModelShortcut(s));
store.dispatch(setShortcuts(shortcuts));
},
getShortcutById: (id: ShortcutId) => {
for (const s of state.shortcuts) {
if (s.id === id) {
return s;
}
}
return null;
},
createShortcut: async (shortcutCreate: ShortcutCreate) => {
const { data } = (await api.createShortcut(shortcutCreate)).data;
const shortcut = convertResponseModelShortcut(data);
store.dispatch(createShortcut(shortcut));
},
patchShortcut: async (shortcutPatch: ShortcutPatch) => {
const { data } = (await api.patchShortcut(shortcutPatch)).data;
const shortcut = convertResponseModelShortcut(data);
store.dispatch(patchShortcut(shortcut));
},
deleteShortcutById: async (shortcutId: ShortcutId) => {
await api.deleteShortcutById(shortcutId);
store.dispatch(deleteShortcut(shortcutId));
},
};
};

View File

@ -0,0 +1,151 @@
import store, { useAppSelector } from "..";
import * as api from "../../helpers/api";
import * as storage from "../../helpers/storage";
import { UNKNOWN_ID } from "../../helpers/consts";
import { getSystemColorScheme } from "../../helpers/utils";
import { setAppearance, setLocale } from "../reducer/global";
import { setUser, patchUser, setHost, setOwner } from "../reducer/user";
const defaultSetting: Setting = {
locale: "en",
appearance: getSystemColorScheme(),
memoVisibility: "PRIVATE",
memoDisplayTsOption: "created_ts",
};
const defaultLocalSetting: LocalSetting = {
enableFoldMemo: true,
};
export const convertResponseModelUser = (user: User): User => {
const setting: Setting = {
...defaultSetting,
};
const { localSetting: storageLocalSetting } = storage.get(["localSetting"]);
const localSetting: LocalSetting = {
...defaultLocalSetting,
...storageLocalSetting,
};
if (user.userSettingList) {
for (const userSetting of user.userSettingList) {
(setting as any)[userSetting.key] = JSON.parse(userSetting.value);
}
}
return {
...user,
setting,
localSetting,
createdTs: user.createdTs * 1000,
updatedTs: user.updatedTs * 1000,
};
};
export const initialUserState = async () => {
const { systemStatus } = store.getState().global;
if (systemStatus.host) {
store.dispatch(setHost(convertResponseModelUser(systemStatus.host)));
}
const ownerUserId = getUserIdFromPath();
if (ownerUserId) {
const { data: owner } = (await api.getUserById(ownerUserId)).data;
if (owner) {
store.dispatch(setOwner(convertResponseModelUser(owner)));
}
}
const { data } = (await api.getMyselfUser()).data;
if (data) {
const user = convertResponseModelUser(data);
store.dispatch(setUser(user));
if (user.setting.locale) {
store.dispatch(setLocale(user.setting.locale));
}
if (user.setting.appearance) {
store.dispatch(setAppearance(user.setting.appearance));
}
}
};
const getUserIdFromPath = () => {
const { pathname } = store.getState().location;
const userIdRegex = /^\/u\/(\d+).*/;
const result = pathname.match(userIdRegex);
if (result && result.length === 2) {
return Number(result[1]);
}
return undefined;
};
const doSignIn = async () => {
const { data: user } = (await api.getMyselfUser()).data;
if (user) {
store.dispatch(setUser(convertResponseModelUser(user)));
} else {
doSignOut();
}
return user;
};
const doSignOut = async () => {
store.dispatch(setUser());
await api.signout();
};
export const useUserStore = () => {
const state = useAppSelector((state) => state.user);
const isVisitorMode = () => {
return !(getUserIdFromPath() === undefined);
};
return {
state,
getState: () => {
return store.getState().user;
},
isVisitorMode,
getUserIdFromPath,
doSignIn,
doSignOut,
getCurrentUserId: () => {
if (isVisitorMode()) {
return getUserIdFromPath() || UNKNOWN_ID;
} else {
return state.user?.id || UNKNOWN_ID;
}
},
getUserById: async (userId: UserId) => {
const { data: user } = (await api.getUserById(userId)).data;
if (user) {
return convertResponseModelUser(user);
} else {
return undefined;
}
},
upsertUserSetting: async (key: keyof Setting, value: any) => {
await api.upsertUserSetting({
key: key as any,
value: JSON.stringify(value),
});
await doSignIn();
},
upsertLocalSetting: async (key: keyof LocalSetting, value: any) => {
storage.set({ localSetting: { [key]: value } });
store.dispatch(patchUser({ localSetting: { [key]: value } }));
},
patchUser: async (userPatch: UserPatch): Promise<void> => {
const { data } = (await api.patchUser(userPatch)).data;
if (userPatch.id === store.getState().user.user?.id) {
const user = convertResponseModelUser(data);
store.dispatch(patchUser(user));
}
},
deleteUser: async (userDelete: UserDelete) => {
await api.deleteUser(userDelete);
},
};
};

View File

@ -1,9 +1,9 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface State {
editMemoId?: MemoId;
memoVisibility: Visibility;
resourceList: Resource[];
editMemoId?: MemoId;
}
const editorSlice = createSlice({