feat: customize system profile (#774)

* feat: system setting for customized profile

* chore: update

* feat: update frontend

* chore: update
This commit is contained in:
boojack 2022-12-18 21:18:30 +08:00 committed by GitHub
parent 55695f2189
commit b67ed1ee13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 259 additions and 56 deletions

View File

@ -14,4 +14,6 @@ type SystemStatus struct {
AdditionalStyle string `json:"additionalStyle"` AdditionalStyle string `json:"additionalStyle"`
// Additional script. // Additional script.
AdditionalScript string `json:"additionalScript"` AdditionalScript string `json:"additionalScript"`
// Customized server profile, including server name and external url.
CustomizedProfile CustomizedProfile `json:"customizedProfile"`
} }

View File

@ -14,8 +14,20 @@ const (
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle" SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
// SystemSettingAdditionalScriptName is the key type of additional script. // SystemSettingAdditionalScriptName is the key type of additional script.
SystemSettingAdditionalScriptName SystemSettingName = "additionalScript" SystemSettingAdditionalScriptName SystemSettingName = "additionalScript"
// SystemSettingCustomizedProfileName is the key type of customized server profile.
SystemSettingCustomizedProfileName SystemSettingName = "customizedProfile"
) )
// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
type CustomizedProfile struct {
// Name is the server name, default is `memos`
Name string `json:"name"`
// IconURL is the url of icon image.
IconURL string `json:"iconUrl"`
// ExternalURL is the external url of server. e.g. https://usermemos.com
ExternalURL string `json:"externalUrl"`
}
func (key SystemSettingName) String() string { func (key SystemSettingName) String() string {
switch key { switch key {
case SystemSettingAllowSignUpName: case SystemSettingAllowSignUpName:
@ -24,6 +36,8 @@ func (key SystemSettingName) String() string {
return "additionalStyle" return "additionalStyle"
case SystemSettingAdditionalScriptName: case SystemSettingAdditionalScriptName:
return "additionalScript" return "additionalScript"
case SystemSettingCustomizedProfileName:
return "customizedProfile"
} }
return "" return ""
} }
@ -75,6 +89,16 @@ func (upsert SystemSettingUpsert) Validate() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to unmarshal system setting additional script value") return fmt.Errorf("failed to unmarshal system setting additional script value")
} }
} else if upsert.Name == SystemSettingCustomizedProfileName {
value := CustomizedProfile{
Name: "memos",
IconURL: "",
ExternalURL: "",
}
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting customized profile value")
}
} else { } else {
return fmt.Errorf("invalid system setting name") return fmt.Errorf("invalid system setting name")
} }

View File

@ -46,6 +46,9 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
AllowSignUp: false, AllowSignUp: false,
AdditionalStyle: "", AdditionalStyle: "",
AdditionalScript: "", AdditionalScript: "",
CustomizedProfile: api.CustomizedProfile{
Name: "memos",
},
} }
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{}) systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
@ -54,7 +57,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
} }
for _, systemSetting := range systemSettingList { for _, systemSetting := range systemSettingList {
var value interface{} var value interface{}
err = json.Unmarshal([]byte(systemSetting.Value), &value) err := json.Unmarshal([]byte(systemSetting.Value), &value)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
} }
@ -65,6 +68,13 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
systemStatus.AdditionalStyle = value.(string) systemStatus.AdditionalStyle = value.(string)
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName { } else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
systemStatus.AdditionalScript = value.(string) systemStatus.AdditionalScript = value.(string)
} else if systemSetting.Name == api.SystemSettingCustomizedProfileName {
valueMap := value.(map[string]interface{})
systemStatus.CustomizedProfile = api.CustomizedProfile{
Name: valueMap["name"].(string),
IconURL: valueMap["iconUrl"].(string),
ExternalURL: valueMap["externalUrl"].(string),
}
} }
} }

View File

@ -55,6 +55,11 @@ const App = () => {
scriptEl.innerHTML = systemStatus.additionalScript; scriptEl.innerHTML = systemStatus.additionalScript;
document.head.appendChild(scriptEl); document.head.appendChild(scriptEl);
} }
// dynamic update metadata with customized profile.
document.title = systemStatus.customizedProfile.name;
const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
link.href = systemStatus.customizedProfile.iconUrl || "/logo.webp";
}, [systemStatus]); }, [systemStatus]);
useEffect(() => { useEffect(() => {

View File

@ -1,4 +1,4 @@
import { useEffect } from "react"; import { useEffect, useRef } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { ANIMATION_DURATION } from "../../helpers/consts"; import { ANIMATION_DURATION } from "../../helpers/consts";
@ -10,8 +10,8 @@ import "../../less/base-dialog.less";
interface DialogConfig { interface DialogConfig {
className: string; className: string;
clickSpaceDestroy?: boolean;
dialogName: string; dialogName: string;
clickSpaceDestroy?: boolean;
} }
interface Props extends DialogConfig, DialogProps { interface Props extends DialogConfig, DialogProps {
@ -21,13 +21,14 @@ interface Props extends DialogConfig, DialogProps {
const BaseDialog: React.FC<Props> = (props: Props) => { const BaseDialog: React.FC<Props> = (props: Props) => {
const { children, className, clickSpaceDestroy, dialogName, destroy } = props; const { children, className, clickSpaceDestroy, dialogName, destroy } = props;
const dialogStore = useDialogStore(); const dialogStore = useDialogStore();
const dialogContainerRef = useRef<HTMLDivElement>(null);
const dialogIndex = dialogStore.state.dialogStack.findIndex((item) => item === dialogName);
useEffect(() => { useEffect(() => {
dialogStore.pushDialogStack(dialogName); dialogStore.pushDialogStack(dialogName);
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (event.code === "Escape") { if (event.code === "Escape") {
if (dialogName === dialogStore.topDialogStack()) { if (dialogName === dialogStore.topDialogStack()) {
dialogStore.popDialogStack();
destroy(); destroy();
} }
} }
@ -37,9 +38,16 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
return () => { return () => {
document.body.removeEventListener("keydown", handleKeyDown); document.body.removeEventListener("keydown", handleKeyDown);
dialogStore.removeDialog(dialogName);
}; };
}, []); }, []);
useEffect(() => {
if (dialogIndex > 0 && dialogContainerRef.current) {
dialogContainerRef.current.style.marginTop = `${dialogIndex * 16}px`;
}
}, [dialogIndex]);
const handleSpaceClicked = () => { const handleSpaceClicked = () => {
if (clickSpaceDestroy) { if (clickSpaceDestroy) {
destroy(); destroy();
@ -48,7 +56,7 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
return ( return (
<div className={`dialog-wrapper ${className}`} onMouseDown={handleSpaceClicked}> <div className={`dialog-wrapper ${className}`} onMouseDown={handleSpaceClicked}>
<div className="dialog-container" onMouseDown={(e) => e.stopPropagation()}> <div ref={dialogContainerRef} className="dialog-container" onMouseDown={(e) => e.stopPropagation()}>
{children} {children}
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useUserStore } from "../store/module";
import Icon from "./Icon"; import Icon from "./Icon";
import { generateDialog } from "./Dialog"; import { generateDialog } from "./Dialog";
import MyAccountSection from "./Settings/MyAccountSection"; import MyAccountSection from "./Settings/MyAccountSection";
@ -7,7 +8,6 @@ import PreferencesSection from "./Settings/PreferencesSection";
import MemberSection from "./Settings/MemberSection"; import MemberSection from "./Settings/MemberSection";
import SystemSection from "./Settings/SystemSection"; import SystemSection from "./Settings/SystemSection";
import "../less/setting-dialog.less"; import "../less/setting-dialog.less";
import { useUserStore } from "../store/module";
type Props = DialogProps; type Props = DialogProps;
@ -67,7 +67,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
onClick={() => handleSectionSelectorItemClick("system")} onClick={() => handleSectionSelectorItemClick("system")}
className={`section-item ${state.selectedSection === "system" ? "selected" : ""}`} className={`section-item ${state.selectedSection === "system" ? "selected" : ""}`}
> >
<span className="icon-text">🧑🔧</span> {t("setting.system")} <span className="icon-text">🛠</span> {t("setting.system")}
</span> </span>
</div> </div>
</> </>

View File

@ -1,9 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, Switch, Textarea } from "@mui/joy"; import { Button, Switch, Textarea } from "@mui/joy";
import { useGlobalStore } from "../../store/module";
import * as api from "../../helpers/api"; import * as api from "../../helpers/api";
import toastHelper from "../Toast"; import toastHelper from "../Toast";
import "../../less/settings/system-section.less"; import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog";
import "@/less/settings/system-section.less";
interface State { interface State {
dbSize: number; dbSize: number;
@ -23,25 +25,28 @@ const formatBytes = (bytes: number) => {
const SystemSection = () => { const SystemSection = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const globalStore = useGlobalStore();
const systemStatus = globalStore.state.systemStatus;
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
dbSize: 0, dbSize: systemStatus.dbSize,
allowSignUp: false, allowSignUp: systemStatus.allowSignUp,
additionalStyle: "", additionalStyle: systemStatus.additionalStyle,
additionalScript: "", additionalScript: systemStatus.additionalScript,
}); });
useEffect(() => { useEffect(() => {
api.getSystemStatus().then(({ data }) => { globalStore.fetchSystemStatus();
const { data: status } = data;
setState({
dbSize: status.dbSize,
allowSignUp: status.allowSignUp,
additionalStyle: status.additionalStyle,
additionalScript: status.additionalScript,
});
});
}, []); }, []);
useEffect(() => {
setState({
dbSize: systemStatus.dbSize,
allowSignUp: systemStatus.allowSignUp,
additionalStyle: systemStatus.additionalStyle,
additionalScript: systemStatus.additionalScript,
});
}, [systemStatus]);
const handleAllowSignUpChanged = async (value: boolean) => { const handleAllowSignUpChanged = async (value: boolean) => {
setState({ setState({
...state, ...state,
@ -60,16 +65,14 @@ const SystemSection = () => {
}); });
}; };
const handleUpdateCustomizedProfileButtonClick = () => {
showUpdateCustomizedProfileDialog();
};
const handleVacuumBtnClick = async () => { const handleVacuumBtnClick = async () => {
try { try {
await api.vacuumDatabase(); await api.vacuumDatabase();
const { data: status } = (await api.getSystemStatus()).data; await globalStore.fetchSystemStatus();
setState({
dbSize: status.dbSize,
allowSignUp: status.allowSignUp,
additionalStyle: status.additionalStyle,
additionalScript: status.additionalScript,
});
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return; return;
@ -113,17 +116,23 @@ const SystemSection = () => {
return ( return (
<div className="section-container system-section-container"> <div className="section-container system-section-container">
<p className="title-text">{t("common.basic")}</p> <p className="title-text">{t("common.basic")}</p>
<label className="form-label"> <div className="form-label">
<div className="normal-text">
Server name: <span className="font-mono font-bold">{systemStatus.customizedProfile.name}</span>
</div>
<Button onClick={handleUpdateCustomizedProfileButtonClick}>Edit</Button>
</div>
<div className="form-label">
<span className="normal-text"> <span className="normal-text">
{t("setting.system-section.database-file-size")}: <span className="font-mono font-medium">{formatBytes(state.dbSize)}</span> {t("setting.system-section.database-file-size")}: <span className="font-mono font-bold">{formatBytes(state.dbSize)}</span>
</span> </span>
<Button onClick={handleVacuumBtnClick}>{t("common.vacuum")}</Button> <Button onClick={handleVacuumBtnClick}>{t("common.vacuum")}</Button>
</label> </div>
<p className="title-text">{t("sidebar.setting")}</p> <p className="title-text">{t("sidebar.setting")}</p>
<label className="form-label"> <div className="form-label">
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span> <span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} /> <Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
</label> </div>
<div className="form-label"> <div className="form-label">
<span className="normal-text">{t("setting.system-section.additional-style")}</span> <span className="normal-text">{t("setting.system-section.additional-style")}</span>
<Button onClick={handleSaveAdditionalStyle}>{t("common.save")}</Button> <Button onClick={handleSaveAdditionalStyle}>{t("common.save")}</Button>

View File

@ -0,0 +1,100 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useGlobalStore } from "../store/module";
import * as api from "../helpers/api";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
type Props = DialogProps;
const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
const { t } = useTranslation();
const globalStore = useGlobalStore();
const [state, setState] = useState<CustomizedProfile>(globalStore.state.systemStatus.customizedProfile);
useEffect(() => {
// do nth
}, []);
const handleCloseBtnClick = () => {
destroy();
};
const handleNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
setState((state) => {
return {
...state,
name: e.target.value as string,
};
});
};
const handleIconUrlChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
setState((state) => {
return {
...state,
iconUrl: e.target.value as string,
};
});
};
const handleSaveBtnClick = async () => {
if (state.name === "" || state.iconUrl === "") {
toastHelper.error(t("message.fill-all"));
return;
}
try {
await api.upsertSystemSetting({
name: "customizedProfile",
value: JSON.stringify(state),
});
await globalStore.fetchSystemStatus();
} catch (error) {
console.error(error);
return;
}
toastHelper.success("Succeed to update customized profile");
destroy();
};
return (
<>
<div className="dialog-header-container !w-64">
<p className="title-text">Customize server</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X />
</button>
</div>
<div className="dialog-content-container">
<p className="text-sm mb-1">
Server Name<span className="text-sm text-gray-400 ml-1">(Default is memos)</span>
</p>
<input type="text" className="input-text" value={state.name} onChange={handleNameChanged} />
<p className="text-sm mb-1 mt-2">Icon URL</p>
<input type="text" className="input-text" value={state.iconUrl} onChange={handleIconUrlChanged} />
<div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
<span className="btn-text" onClick={handleCloseBtnClick}>
{t("common.cancel")}
</span>
<span className="btn-primary" onClick={handleSaveBtnClick}>
{t("common.save")}
</span>
</div>
</div>
</>
);
};
function showUpdateCustomizedProfileDialog() {
generateDialog(
{
className: "update-customized-profile-dialog",
dialogName: "update-customized-profile-dialog",
},
UpdateCustomizedProfileDialog
);
}
export default showUpdateCustomizedProfileDialog;

View File

@ -14,11 +14,11 @@
@apply w-full flex flex-row justify-start items-center; @apply w-full flex flex-row justify-start items-center;
> .logo-img { > .logo-img {
@apply h-20 w-auto; @apply h-16 w-auto mr-2;
} }
> .logo-text { > .logo-text {
@apply text-6xl tracking-wide text-black dark:text-gray-200; @apply text-5xl tracking-wide text-black opacity-80 dark:text-gray-200;
} }
} }

View File

@ -11,7 +11,7 @@
@apply flex flex-row justify-start items-center; @apply flex flex-row justify-start items-center;
> .logo-img { > .logo-img {
@apply h-12 sm:h-14 w-auto mr-1; @apply h-12 sm:h-14 w-auto mr-2;
} }
> .title-text { > .title-text {

View File

@ -11,7 +11,7 @@
@apply flex flex-row justify-start items-center; @apply flex flex-row justify-start items-center;
> .logo-img { > .logo-img {
@apply h-12 sm:h-14 w-auto mr-1; @apply h-12 sm:h-14 w-auto mr-2;
} }
> .logo-text { > .logo-text {

View File

@ -23,8 +23,8 @@ const Auth = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
const userStore = useUserStore(); const userStore = useUserStore();
const systemStatus = globalStore.state.systemStatus;
const actionBtnLoadingState = useLoading(false); const actionBtnLoadingState = useLoading(false);
const systemStatus = globalStore.state.systemStatus;
const mode = systemStatus.profile.mode; const mode = systemStatus.profile.mode;
const [username, setUsername] = useState(mode === "dev" ? "demohero" : ""); const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
const [password, setPassword] = useState(mode === "dev" ? "secret" : ""); const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
@ -119,8 +119,8 @@ const Auth = () => {
<div className="auth-form-wrapper"> <div className="auth-form-wrapper">
<div className="page-header-container"> <div className="page-header-container">
<div className="title-container"> <div className="title-container">
<img className="logo-img" src="/logo.webp" alt="" /> <img className="logo-img" src={systemStatus.customizedProfile.iconUrl} alt="" />
<p className="logo-text">memos</p> <p className="logo-text">{systemStatus.customizedProfile.name}</p>
</div> </div>
<p className="slogan-text">{t("slogan")}</p> <p className="slogan-text">{t("slogan")}</p>
</div> </div>

View File

@ -2,7 +2,7 @@ import dayjs from "dayjs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useLocationStore, useMemoStore, useUserStore } from "../store/module"; import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module";
import { DEFAULT_MEMO_LIMIT } from "../helpers/consts"; import { DEFAULT_MEMO_LIMIT } from "../helpers/consts";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
import toastHelper from "../components/Toast"; import toastHelper from "../components/Toast";
@ -16,16 +16,18 @@ interface State {
const Explore = () => { const Explore = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const globalStore = useGlobalStore();
const locationStore = useLocationStore(); const locationStore = useLocationStore();
const userStore = useUserStore(); const userStore = useUserStore();
const memoStore = useMemoStore(); const memoStore = useMemoStore();
const user = userStore.state.user;
const location = locationStore.state;
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
memos: [], memos: [],
}); });
const [isComplete, setIsComplete] = useState<boolean>(false); const [isComplete, setIsComplete] = useState<boolean>(false);
const loadingState = useLoading(); const loadingState = useLoading();
const customizedProfile = globalStore.state.systemStatus.customizedProfile;
const user = userStore.state.user;
const location = locationStore.state;
useEffect(() => { useEffect(() => {
memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length).then((memos) => { memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length).then((memos) => {
@ -61,8 +63,8 @@ const Explore = () => {
<div className="page-container"> <div className="page-container">
<div className="page-header"> <div className="page-header">
<div className="title-container"> <div className="title-container">
<img className="logo-img" src="/logo.webp" alt="" /> <img className="logo-img" src={customizedProfile.iconUrl} alt="" />
<span className="title-text">memos</span> <span className="title-text">{customizedProfile.name}</span>
</div> </div>
<div className="action-button-container"> <div className="action-button-container">
{!loadingState.isLoading && user ? ( {!loadingState.isLoading && user ? (

View File

@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { UNKNOWN_ID } from "../helpers/consts"; import { UNKNOWN_ID } from "../helpers/consts";
import { useLocationStore, useMemoStore, useUserStore } from "../store/module"; import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
import toastHelper from "../components/Toast"; import toastHelper from "../components/Toast";
import MemoContent from "../components/MemoContent"; import MemoContent from "../components/MemoContent";
@ -17,17 +17,19 @@ interface State {
const MemoDetail = () => { const MemoDetail = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const params = useParams(); const params = useParams();
const globalStore = useGlobalStore();
const locationStore = useLocationStore(); const locationStore = useLocationStore();
const memoStore = useMemoStore(); const memoStore = useMemoStore();
const userStore = useUserStore(); const userStore = useUserStore();
const user = userStore.state.user;
const location = locationStore.state;
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
memo: { memo: {
id: UNKNOWN_ID, id: UNKNOWN_ID,
} as Memo, } as Memo,
}); });
const loadingState = useLoading(); const loadingState = useLoading();
const customizedProfile = globalStore.state.systemStatus.customizedProfile;
const user = userStore.state.user;
const location = locationStore.state;
useEffect(() => { useEffect(() => {
const memoId = Number(params.memoId); const memoId = Number(params.memoId);
@ -52,8 +54,8 @@ const MemoDetail = () => {
<div className="page-container"> <div className="page-container">
<div className="page-header"> <div className="page-header">
<div className="title-container"> <div className="title-container">
<img className="logo-img" src="/logo.webp" alt="" /> <img className="logo-img" src={customizedProfile.iconUrl} alt="" />
<p className="logo-text">memos</p> <p className="logo-text">{customizedProfile.name}</p>
</div> </div>
<div className="action-button-container"> <div className="action-button-container">
{!loadingState.isLoading && ( {!loadingState.isLoading && (

View File

@ -1,9 +1,9 @@
import store, { useAppSelector } from "..";
import { pushDialogStack, popDialogStack } from "../reducer/dialog";
import { last } from "lodash"; import { last } from "lodash";
import store, { useAppSelector } from "..";
import { pushDialogStack, popDialogStack, removeDialog } from "../reducer/dialog";
export const useDialogStore = () => { export const useDialogStore = () => {
const state = useAppSelector((state) => state.editor); const state = useAppSelector((state) => state.dialog);
return { return {
state, state,
@ -16,6 +16,9 @@ export const useDialogStore = () => {
popDialogStack: () => { popDialogStack: () => {
store.dispatch(popDialogStack()); store.dispatch(popDialogStack());
}, },
removeDialog: (dialogName: string) => {
store.dispatch(removeDialog(dialogName));
},
topDialogStack: () => { topDialogStack: () => {
return last(store.getState().dialog.dialogStack); return last(store.getState().dialog.dialogStack);
}, },

View File

@ -11,6 +11,11 @@ export const initialGlobalState = async () => {
allowSignUp: false, allowSignUp: false,
additionalStyle: "", additionalStyle: "",
additionalScript: "", additionalScript: "",
customizedProfile: {
name: "memos",
iconUrl: "/logo.webp",
externalUrl: "",
},
} as SystemStatus, } as SystemStatus,
}; };
@ -42,6 +47,11 @@ export const useGlobalStore = () => {
getState: () => { getState: () => {
return store.getState().global; return store.getState().global;
}, },
fetchSystemStatus: async () => {
const { data: systemStatus } = (await api.getSystemStatus()).data;
store.dispatch(setGlobalState({ systemStatus: systemStatus }));
return systemStatus;
},
setLocale: (locale: Locale) => { setLocale: (locale: Locale) => {
store.dispatch(setLocale(locale)); store.dispatch(setLocale(locale));
}, },

View File

@ -22,9 +22,16 @@ const dialogSlice = createSlice({
dialogStack: state.dialogStack.slice(0, state.dialogStack.length - 1), dialogStack: state.dialogStack.slice(0, state.dialogStack.length - 1),
}; };
}, },
removeDialog: (state, action: PayloadAction<string>) => {
const filterDialogStack = state.dialogStack.filter((dialogName) => dialogName !== action.payload);
return {
...state,
dialogStack: filterDialogStack,
};
},
}, },
}); });
export const { pushDialogStack, popDialogStack } = dialogSlice.actions; export const { pushDialogStack, popDialogStack, removeDialog } = dialogSlice.actions;
export default dialogSlice.reducer; export default dialogSlice.reducer;

View File

@ -21,11 +21,19 @@ const globalSlice = createSlice({
allowSignUp: false, allowSignUp: false,
additionalStyle: "", additionalStyle: "",
additionalScript: "", additionalScript: "",
customizedProfile: {
name: "memos",
iconUrl: "/logo.webp",
externalUrl: "",
},
}, },
} as State, } as State,
reducers: { reducers: {
setGlobalState: (_, action: PayloadAction<State>) => { setGlobalState: (state, action: PayloadAction<Partial<State>>) => {
return action.payload; return {
...state,
...action.payload,
};
}, },
setLocale: (state, action: PayloadAction<Locale>) => { setLocale: (state, action: PayloadAction<Locale>) => {
return { return {

View File

@ -3,6 +3,12 @@ interface Profile {
version: string; version: string;
} }
interface CustomizedProfile {
name: string;
iconUrl: string;
externalUrl: string;
}
interface SystemStatus { interface SystemStatus {
host?: User; host?: User;
profile: Profile; profile: Profile;
@ -11,6 +17,7 @@ interface SystemStatus {
allowSignUp: boolean; allowSignUp: boolean;
additionalStyle: string; additionalStyle: string;
additionalScript: string; additionalScript: string;
customizedProfile: CustomizedProfile;
} }
interface SystemSetting { interface SystemSetting {

View File

@ -1,3 +1,4 @@
import { resolve } from "path";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import legacy from "@vitejs/plugin-legacy"; import legacy from "@vitejs/plugin-legacy";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
@ -32,4 +33,9 @@ export default defineConfig({
}, },
}, },
}, },
resolve: {
alias: {
"@/": `${resolve(__dirname, "src")}/`,
},
},
}); });