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:
Christopher 2023-02-13 17:07:31 +01:00 committed by GitHub
parent 28405f6d24
commit 4641e89c17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 158 additions and 36 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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">

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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;
}
}

View File

@ -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",

View File

@ -9,6 +9,7 @@ export const initialGlobalState = async () => {
appearance: "system" as Appearance,
systemStatus: {
allowSignUp: false,
disablePublicMemos: false,
additionalStyle: "",
additionalScript: "",
customizedProfile: {

View File

@ -19,6 +19,7 @@ const globalSlice = createSlice({
},
dbSize: 0,
allowSignUp: false,
disablePublicMemos: false,
additionalStyle: "",
additionalScript: "",
customizedProfile: {

View File

@ -18,6 +18,7 @@ interface SystemStatus {
dbSize: number;
// System settings
allowSignUp: boolean;
disablePublicMemos: boolean;
additionalStyle: string;
additionalScript: string;
customizedProfile: CustomizedProfile;