mirror of
https://github.com/usememos/memos.git
synced 2025-02-14 18:30:42 +01:00
feat(system): support for disabling public memos (#1003)
* feat(system): support for disabling public memos * fix(web/editor): set visibility to private on disabled public memos * feat(server/memo): find/check if public memos are disabled * fix(server/memo): handle error for finding system error * fix(server/memo): unmarshal visiblity when getting system settings * chore(web): move side effect imports to end * Update memo.go --------- Co-authored-by: boojack <stevenlgtm@gmail.com>
This commit is contained in:
parent
28405f6d24
commit
4641e89c17
@ -10,6 +10,8 @@ type SystemStatus struct {
|
||||
// System settings
|
||||
// Allow sign up.
|
||||
AllowSignUp bool `json:"allowSignUp"`
|
||||
// Disable public memos.
|
||||
DisablePublicMemos bool `json:"disablePublicMemos"`
|
||||
// Additional style.
|
||||
AdditionalStyle string `json:"additionalStyle"`
|
||||
// Additional script.
|
||||
|
@ -17,6 +17,8 @@ const (
|
||||
SystemSettingSecretSessionName SystemSettingName = "secretSessionName"
|
||||
// SystemSettingAllowSignUpName is the key type of allow signup setting.
|
||||
SystemSettingAllowSignUpName SystemSettingName = "allowSignUp"
|
||||
// SystemSettingsDisablePublicMemos is the key type of disable public memos setting.
|
||||
SystemSettingDisablePublicMemosName SystemSettingName = "disablePublicMemos"
|
||||
// SystemSettingAdditionalStyleName is the key type of additional style.
|
||||
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
|
||||
// SystemSettingAdditionalScriptName is the key type of additional script.
|
||||
@ -51,6 +53,8 @@ func (key SystemSettingName) String() string {
|
||||
return "secretSessionName"
|
||||
case SystemSettingAllowSignUpName:
|
||||
return "allowSignUp"
|
||||
case SystemSettingDisablePublicMemosName:
|
||||
return "disablePublicMemos"
|
||||
case SystemSettingAdditionalStyleName:
|
||||
return "additionalStyle"
|
||||
case SystemSettingAdditionalScriptName:
|
||||
@ -64,7 +68,8 @@ func (key SystemSettingName) String() string {
|
||||
}
|
||||
|
||||
var (
|
||||
SystemSettingAllowSignUpValue = []bool{true, false}
|
||||
SystemSettingAllowSignUpValue = []bool{true, false}
|
||||
SystemSettingDisbalePublicMemosValue = []bool{true, false}
|
||||
)
|
||||
|
||||
type SystemSetting struct {
|
||||
@ -100,6 +105,24 @@ func (upsert SystemSettingUpsert) Validate() error {
|
||||
if invalid {
|
||||
return fmt.Errorf("invalid system setting allow signup value")
|
||||
}
|
||||
} else if upsert.Name == SystemSettingDisablePublicMemosName {
|
||||
value := false
|
||||
err := json.Unmarshal([]byte(upsert.Value), &value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal system setting disable public memos value")
|
||||
}
|
||||
|
||||
invalid := true
|
||||
for _, v := range SystemSettingDisbalePublicMemosValue {
|
||||
if value == v {
|
||||
invalid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if invalid {
|
||||
return fmt.Errorf("invalid system setting disable public memos value")
|
||||
}
|
||||
} else if upsert.Name == SystemSettingAdditionalStyleName {
|
||||
value := ""
|
||||
err := json.Unmarshal([]byte(upsert.Value), &value)
|
||||
|
@ -53,6 +53,25 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
}
|
||||
}
|
||||
|
||||
// Find system settings
|
||||
disablePublicMemosSystemSettingKey := api.SystemSettingDisablePublicMemosName
|
||||
disablePublicMemosSystemSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
|
||||
Name: &disablePublicMemosSystemSettingKey,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
|
||||
}
|
||||
if disablePublicMemosSystemSetting != nil {
|
||||
disablePublicMemosValue := false
|
||||
err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemosValue)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
|
||||
}
|
||||
if disablePublicMemosValue {
|
||||
memoCreate.Visibility = api.Private
|
||||
}
|
||||
}
|
||||
|
||||
memoCreate.CreatorID = userID
|
||||
memo, err := s.Store.CreateMemo(ctx, memoCreate)
|
||||
if err != nil {
|
||||
|
@ -42,12 +42,13 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
systemStatus := api.SystemStatus{
|
||||
Host: hostUser,
|
||||
Profile: *s.Profile,
|
||||
DBSize: 0,
|
||||
AllowSignUp: false,
|
||||
AdditionalStyle: "",
|
||||
AdditionalScript: "",
|
||||
Host: hostUser,
|
||||
Profile: *s.Profile,
|
||||
DBSize: 0,
|
||||
AllowSignUp: false,
|
||||
DisablePublicMemos: false,
|
||||
AdditionalStyle: "",
|
||||
AdditionalScript: "",
|
||||
CustomizedProfile: api.CustomizedProfile{
|
||||
Name: "memos",
|
||||
LogoURL: "",
|
||||
@ -75,6 +76,8 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
||||
|
||||
if systemSetting.Name == api.SystemSettingAllowSignUpName {
|
||||
systemStatus.AllowSignUp = value.(bool)
|
||||
} else if systemSetting.Name == api.SystemSettingDisablePublicMemosName {
|
||||
systemStatus.DisablePublicMemos = value.(bool)
|
||||
} else if systemSetting.Name == api.SystemSettingAdditionalStyleName {
|
||||
systemStatus.AdditionalStyle = value.(string)
|
||||
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
|
||||
|
@ -4,7 +4,15 @@ import { useTranslation } from "react-i18next";
|
||||
import { getMatchedNodes } from "../labs/marked";
|
||||
import { deleteMemoResource, upsertMemoResource } from "../helpers/api";
|
||||
import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
|
||||
import { useEditorStore, useLocationStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "../store/module";
|
||||
import {
|
||||
useEditorStore,
|
||||
useGlobalStore,
|
||||
useLocationStore,
|
||||
useMemoStore,
|
||||
useResourceStore,
|
||||
useTagStore,
|
||||
useUserStore,
|
||||
} from "../store/module";
|
||||
import * as storage from "../helpers/storage";
|
||||
import Icon from "./Icon";
|
||||
import toastHelper from "./Toast";
|
||||
@ -42,6 +50,9 @@ const MemoEditor = () => {
|
||||
const memoStore = useMemoStore();
|
||||
const tagStore = useTagStore();
|
||||
const resourceStore = useResourceStore();
|
||||
const {
|
||||
state: { systemStatus },
|
||||
} = useGlobalStore();
|
||||
|
||||
const [state, setState] = useState<State>({
|
||||
fullscreen: false,
|
||||
@ -63,6 +74,12 @@ const MemoEditor = () => {
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (systemStatus.disablePublicMemos) {
|
||||
editorStore.setMemoVisibility("PRIVATE");
|
||||
}
|
||||
}, [systemStatus.disablePublicMemos]);
|
||||
|
||||
useEffect(() => {
|
||||
const { editingMemoIdCache } = storage.get(["editingMemoIdCache"]);
|
||||
if (editingMemoIdCache) {
|
||||
@ -484,7 +501,9 @@ const MemoEditor = () => {
|
||||
<Selector
|
||||
className="visibility-selector"
|
||||
value={editorState.memoVisibility}
|
||||
tooltipTitle={t("memo.visibility.disabled")}
|
||||
dataSource={memoVisibilityOptionSelectorItems}
|
||||
disabled={systemStatus.disablePublicMemos}
|
||||
handleValueChanged={handleMemoVisibilityOptionChanged}
|
||||
/>
|
||||
<div className="buttons-container">
|
||||
|
@ -5,11 +5,14 @@ import { useGlobalStore } from "../../store/module";
|
||||
import * as api from "../../helpers/api";
|
||||
import toastHelper from "../Toast";
|
||||
import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog";
|
||||
import { useAppDispatch } from "../../store";
|
||||
import { setGlobalState } from "../../store/reducer/global";
|
||||
import "@/less/settings/system-section.less";
|
||||
|
||||
interface State {
|
||||
dbSize: number;
|
||||
allowSignUp: boolean;
|
||||
disablePublicMemos: boolean;
|
||||
additionalStyle: string;
|
||||
additionalScript: string;
|
||||
}
|
||||
@ -32,8 +35,11 @@ const SystemSection = () => {
|
||||
allowSignUp: systemStatus.allowSignUp,
|
||||
additionalStyle: systemStatus.additionalStyle,
|
||||
additionalScript: systemStatus.additionalScript,
|
||||
disablePublicMemos: systemStatus.disablePublicMemos,
|
||||
});
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
globalStore.fetchSystemStatus();
|
||||
}, []);
|
||||
@ -44,6 +50,7 @@ const SystemSection = () => {
|
||||
allowSignUp: systemStatus.allowSignUp,
|
||||
additionalStyle: systemStatus.additionalStyle,
|
||||
additionalScript: systemStatus.additionalScript,
|
||||
disablePublicMemos: systemStatus.disablePublicMemos,
|
||||
});
|
||||
}, [systemStatus]);
|
||||
|
||||
@ -100,6 +107,19 @@ const SystemSection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDisablePublicMemosChanged = async (value: boolean) => {
|
||||
setState({
|
||||
...state,
|
||||
disablePublicMemos: value,
|
||||
});
|
||||
// Update global store immediately as MemoEditor/Selector is dependent on this value.
|
||||
dispatch(setGlobalState({ systemStatus: { ...systemStatus, disablePublicMemos: value } }));
|
||||
await api.upsertSystemSetting({
|
||||
name: "disablePublicMemos",
|
||||
value: JSON.stringify(value),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveAdditionalScript = async () => {
|
||||
try {
|
||||
await api.upsertSystemSetting({
|
||||
@ -133,6 +153,10 @@ const SystemSection = () => {
|
||||
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
|
||||
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
||||
</div>
|
||||
<div className="form-label">
|
||||
<span className="normal-text">{t("setting.system-section.disable-public-memos")}</span>
|
||||
<Switch checked={state.disablePublicMemos} onChange={(event) => handleDisablePublicMemosChanged(event.target.checked)} />
|
||||
</div>
|
||||
<div className="form-label">
|
||||
<span className="normal-text">{t("setting.system-section.additional-style")}</span>
|
||||
<Button onClick={handleSaveAdditionalStyle}>{t("common.save")}</Button>
|
||||
|
@ -2,6 +2,7 @@ import { memo, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useToggle from "../../hooks/useToggle";
|
||||
import Icon from "../Icon";
|
||||
import { Tooltip } from "@mui/joy";
|
||||
import "../../less/common/selector.less";
|
||||
|
||||
interface SelectorItem {
|
||||
@ -14,6 +15,8 @@ interface Props {
|
||||
value: string;
|
||||
dataSource: SelectorItem[];
|
||||
handleValueChanged?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
tooltipTitle?: string;
|
||||
}
|
||||
|
||||
const nullItem = {
|
||||
@ -22,7 +25,7 @@ const nullItem = {
|
||||
};
|
||||
|
||||
const Selector: React.FC<Props> = (props: Props) => {
|
||||
const { className, dataSource, handleValueChanged, value } = props;
|
||||
const { className, dataSource, handleValueChanged, value, disabled, tooltipTitle } = props;
|
||||
const { t } = useTranslation();
|
||||
const [showSelector, toggleSelectorStatus] = useToggle(false);
|
||||
|
||||
@ -58,39 +61,52 @@ const Selector: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
const handleCurrentValueClick = (event: React.MouseEvent) => {
|
||||
if (disabled) return;
|
||||
event.stopPropagation();
|
||||
toggleSelectorStatus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`selector-wrapper ${className ?? ""}`} ref={selectorElRef}>
|
||||
<div className={`current-value-container ${showSelector ? "active" : ""}`} onClick={handleCurrentValueClick}>
|
||||
<span className="value-text">{currentItem.text}</span>
|
||||
<span className="arrow-text">
|
||||
<Icon.ChevronDown className="icon-img" />
|
||||
</span>
|
||||
</div>
|
||||
<Tooltip title={tooltipTitle} hidden={!disabled}>
|
||||
<div className={`selector-wrapper ${className ?? ""} `} ref={selectorElRef}>
|
||||
<div
|
||||
className={`current-value-container ${showSelector ? "active" : ""} ${disabled && "selector-disabled"}`}
|
||||
onClick={handleCurrentValueClick}
|
||||
>
|
||||
{disabled && (
|
||||
<span className="lock-text">
|
||||
<Icon.Lock className="icon-img" />
|
||||
</span>
|
||||
)}
|
||||
<span className="value-text">{currentItem.text}</span>
|
||||
{!disabled && (
|
||||
<span className="arrow-text">
|
||||
<Icon.ChevronDown className="icon-img" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`items-wrapper ${showSelector ? "" : "!hidden"}`}>
|
||||
{dataSource.length > 0 ? (
|
||||
dataSource.map((d) => {
|
||||
return (
|
||||
<div
|
||||
className={`item-container ${d.value === value ? "selected" : ""}`}
|
||||
key={d.value}
|
||||
onClick={() => {
|
||||
handleItemClick(d);
|
||||
}}
|
||||
>
|
||||
{d.text}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="tip-text">{t("common.null")}</p>
|
||||
)}
|
||||
<div className={`items-wrapper ${showSelector ? "" : "!hidden"}`}>
|
||||
{dataSource.length > 0 ? (
|
||||
dataSource.map((d) => {
|
||||
return (
|
||||
<div
|
||||
className={`item-container ${d.value === value ? "selected" : ""}`}
|
||||
key={d.value}
|
||||
onClick={() => {
|
||||
handleItemClick(d);
|
||||
}}
|
||||
>
|
||||
{d.text}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="tip-text">{t("common.null")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -14,6 +14,10 @@
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
> .lock-text {
|
||||
@apply flex flex-row justify-center items-center w-4 shrink-0 mr-1;
|
||||
}
|
||||
|
||||
> .arrow-text {
|
||||
@apply flex flex-row justify-center items-center w-4 shrink-0;
|
||||
|
||||
@ -41,4 +45,11 @@
|
||||
@apply px-3 py-1 text-sm text-gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
> .selector-disabled {
|
||||
@apply cursor-not-allowed;
|
||||
@apply pointer-events-none;
|
||||
|
||||
@apply bg-gray-200 dark:bg-zinc-700 dark:border-zinc-600 text-gray-400;
|
||||
}
|
||||
}
|
||||
|
@ -101,7 +101,8 @@
|
||||
"visibility": {
|
||||
"private": "Only visible to you",
|
||||
"protected": "Visible to members",
|
||||
"public": "Everyone can see"
|
||||
"public": "Everyone can see",
|
||||
"disabled": "Public memos are disabled"
|
||||
}
|
||||
},
|
||||
"memo-list": {
|
||||
@ -180,6 +181,7 @@
|
||||
},
|
||||
"database-file-size": "Database File Size",
|
||||
"allow-user-signup": "Allow user signup",
|
||||
"disable-public-memos": "Disable public memos",
|
||||
"additional-style": "Additional style",
|
||||
"additional-script": "Additional script",
|
||||
"additional-style-placeholder": "Additional CSS codes",
|
||||
|
@ -9,6 +9,7 @@ export const initialGlobalState = async () => {
|
||||
appearance: "system" as Appearance,
|
||||
systemStatus: {
|
||||
allowSignUp: false,
|
||||
disablePublicMemos: false,
|
||||
additionalStyle: "",
|
||||
additionalScript: "",
|
||||
customizedProfile: {
|
||||
|
@ -19,6 +19,7 @@ const globalSlice = createSlice({
|
||||
},
|
||||
dbSize: 0,
|
||||
allowSignUp: false,
|
||||
disablePublicMemos: false,
|
||||
additionalStyle: "",
|
||||
additionalScript: "",
|
||||
customizedProfile: {
|
||||
|
1
web/src/types/modules/system.d.ts
vendored
1
web/src/types/modules/system.d.ts
vendored
@ -18,6 +18,7 @@ interface SystemStatus {
|
||||
dbSize: number;
|
||||
// System settings
|
||||
allowSignUp: boolean;
|
||||
disablePublicMemos: boolean;
|
||||
additionalStyle: string;
|
||||
additionalScript: string;
|
||||
customizedProfile: CustomizedProfile;
|
||||
|
Loading…
x
Reference in New Issue
Block a user