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
12 changed files with 158 additions and 36 deletions

View File

@ -10,6 +10,8 @@ type SystemStatus struct {
// System settings // System settings
// Allow sign up. // Allow sign up.
AllowSignUp bool `json:"allowSignUp"` AllowSignUp bool `json:"allowSignUp"`
// Disable public memos.
DisablePublicMemos bool `json:"disablePublicMemos"`
// Additional style. // Additional style.
AdditionalStyle string `json:"additionalStyle"` AdditionalStyle string `json:"additionalStyle"`
// Additional script. // Additional script.

View File

@ -17,6 +17,8 @@ const (
SystemSettingSecretSessionName SystemSettingName = "secretSessionName" SystemSettingSecretSessionName SystemSettingName = "secretSessionName"
// SystemSettingAllowSignUpName is the key type of allow signup setting. // SystemSettingAllowSignUpName is the key type of allow signup setting.
SystemSettingAllowSignUpName SystemSettingName = "allowSignUp" 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 is the key type of additional style.
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle" SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
// SystemSettingAdditionalScriptName is the key type of additional script. // SystemSettingAdditionalScriptName is the key type of additional script.
@ -51,6 +53,8 @@ func (key SystemSettingName) String() string {
return "secretSessionName" return "secretSessionName"
case SystemSettingAllowSignUpName: case SystemSettingAllowSignUpName:
return "allowSignUp" return "allowSignUp"
case SystemSettingDisablePublicMemosName:
return "disablePublicMemos"
case SystemSettingAdditionalStyleName: case SystemSettingAdditionalStyleName:
return "additionalStyle" return "additionalStyle"
case SystemSettingAdditionalScriptName: case SystemSettingAdditionalScriptName:
@ -65,6 +69,7 @@ func (key SystemSettingName) String() string {
var ( var (
SystemSettingAllowSignUpValue = []bool{true, false} SystemSettingAllowSignUpValue = []bool{true, false}
SystemSettingDisbalePublicMemosValue = []bool{true, false}
) )
type SystemSetting struct { type SystemSetting struct {
@ -100,6 +105,24 @@ func (upsert SystemSettingUpsert) Validate() error {
if invalid { if invalid {
return fmt.Errorf("invalid system setting allow signup value") 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 { } else if upsert.Name == SystemSettingAdditionalStyleName {
value := "" value := ""
err := json.Unmarshal([]byte(upsert.Value), &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 memoCreate.CreatorID = userID
memo, err := s.Store.CreateMemo(ctx, memoCreate) memo, err := s.Store.CreateMemo(ctx, memoCreate)
if err != nil { if err != nil {

View File

@ -46,6 +46,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
Profile: *s.Profile, Profile: *s.Profile,
DBSize: 0, DBSize: 0,
AllowSignUp: false, AllowSignUp: false,
DisablePublicMemos: false,
AdditionalStyle: "", AdditionalStyle: "",
AdditionalScript: "", AdditionalScript: "",
CustomizedProfile: api.CustomizedProfile{ CustomizedProfile: api.CustomizedProfile{
@ -75,6 +76,8 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
if systemSetting.Name == api.SystemSettingAllowSignUpName { if systemSetting.Name == api.SystemSettingAllowSignUpName {
systemStatus.AllowSignUp = value.(bool) systemStatus.AllowSignUp = value.(bool)
} else if systemSetting.Name == api.SystemSettingDisablePublicMemosName {
systemStatus.DisablePublicMemos = value.(bool)
} else if systemSetting.Name == api.SystemSettingAdditionalStyleName { } else if systemSetting.Name == api.SystemSettingAdditionalStyleName {
systemStatus.AdditionalStyle = value.(string) systemStatus.AdditionalStyle = value.(string)
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName { } else if systemSetting.Name == api.SystemSettingAdditionalScriptName {

View File

@ -4,7 +4,15 @@ import { useTranslation } from "react-i18next";
import { getMatchedNodes } from "../labs/marked"; import { getMatchedNodes } from "../labs/marked";
import { deleteMemoResource, upsertMemoResource } from "../helpers/api"; import { deleteMemoResource, upsertMemoResource } from "../helpers/api";
import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts"; 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 * as storage from "../helpers/storage";
import Icon from "./Icon"; import Icon from "./Icon";
import toastHelper from "./Toast"; import toastHelper from "./Toast";
@ -42,6 +50,9 @@ const MemoEditor = () => {
const memoStore = useMemoStore(); const memoStore = useMemoStore();
const tagStore = useTagStore(); const tagStore = useTagStore();
const resourceStore = useResourceStore(); const resourceStore = useResourceStore();
const {
state: { systemStatus },
} = useGlobalStore();
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
fullscreen: false, fullscreen: false,
@ -63,6 +74,12 @@ const MemoEditor = () => {
}; };
}); });
useEffect(() => {
if (systemStatus.disablePublicMemos) {
editorStore.setMemoVisibility("PRIVATE");
}
}, [systemStatus.disablePublicMemos]);
useEffect(() => { useEffect(() => {
const { editingMemoIdCache } = storage.get(["editingMemoIdCache"]); const { editingMemoIdCache } = storage.get(["editingMemoIdCache"]);
if (editingMemoIdCache) { if (editingMemoIdCache) {
@ -484,7 +501,9 @@ const MemoEditor = () => {
<Selector <Selector
className="visibility-selector" className="visibility-selector"
value={editorState.memoVisibility} value={editorState.memoVisibility}
tooltipTitle={t("memo.visibility.disabled")}
dataSource={memoVisibilityOptionSelectorItems} dataSource={memoVisibilityOptionSelectorItems}
disabled={systemStatus.disablePublicMemos}
handleValueChanged={handleMemoVisibilityOptionChanged} handleValueChanged={handleMemoVisibilityOptionChanged}
/> />
<div className="buttons-container"> <div className="buttons-container">

View File

@ -5,11 +5,14 @@ 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 showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog"; import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog";
import { useAppDispatch } from "../../store";
import { setGlobalState } from "../../store/reducer/global";
import "@/less/settings/system-section.less"; import "@/less/settings/system-section.less";
interface State { interface State {
dbSize: number; dbSize: number;
allowSignUp: boolean; allowSignUp: boolean;
disablePublicMemos: boolean;
additionalStyle: string; additionalStyle: string;
additionalScript: string; additionalScript: string;
} }
@ -32,8 +35,11 @@ const SystemSection = () => {
allowSignUp: systemStatus.allowSignUp, allowSignUp: systemStatus.allowSignUp,
additionalStyle: systemStatus.additionalStyle, additionalStyle: systemStatus.additionalStyle,
additionalScript: systemStatus.additionalScript, additionalScript: systemStatus.additionalScript,
disablePublicMemos: systemStatus.disablePublicMemos,
}); });
const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
globalStore.fetchSystemStatus(); globalStore.fetchSystemStatus();
}, []); }, []);
@ -44,6 +50,7 @@ const SystemSection = () => {
allowSignUp: systemStatus.allowSignUp, allowSignUp: systemStatus.allowSignUp,
additionalStyle: systemStatus.additionalStyle, additionalStyle: systemStatus.additionalStyle,
additionalScript: systemStatus.additionalScript, additionalScript: systemStatus.additionalScript,
disablePublicMemos: systemStatus.disablePublicMemos,
}); });
}, [systemStatus]); }, [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 () => { const handleSaveAdditionalScript = async () => {
try { try {
await api.upsertSystemSetting({ await api.upsertSystemSetting({
@ -133,6 +153,10 @@ const SystemSection = () => {
<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)} />
</div> </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"> <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

@ -2,6 +2,7 @@ import { memo, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useToggle from "../../hooks/useToggle"; import useToggle from "../../hooks/useToggle";
import Icon from "../Icon"; import Icon from "../Icon";
import { Tooltip } from "@mui/joy";
import "../../less/common/selector.less"; import "../../less/common/selector.less";
interface SelectorItem { interface SelectorItem {
@ -14,6 +15,8 @@ interface Props {
value: string; value: string;
dataSource: SelectorItem[]; dataSource: SelectorItem[];
handleValueChanged?: (value: string) => void; handleValueChanged?: (value: string) => void;
disabled?: boolean;
tooltipTitle?: string;
} }
const nullItem = { const nullItem = {
@ -22,7 +25,7 @@ const nullItem = {
}; };
const Selector: React.FC<Props> = (props: Props) => { 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 { t } = useTranslation();
const [showSelector, toggleSelectorStatus] = useToggle(false); const [showSelector, toggleSelectorStatus] = useToggle(false);
@ -58,17 +61,29 @@ const Selector: React.FC<Props> = (props: Props) => {
}; };
const handleCurrentValueClick = (event: React.MouseEvent) => { const handleCurrentValueClick = (event: React.MouseEvent) => {
if (disabled) return;
event.stopPropagation(); event.stopPropagation();
toggleSelectorStatus(); toggleSelectorStatus();
}; };
return ( return (
<Tooltip title={tooltipTitle} hidden={!disabled}>
<div className={`selector-wrapper ${className ?? ""} `} ref={selectorElRef}> <div className={`selector-wrapper ${className ?? ""} `} ref={selectorElRef}>
<div className={`current-value-container ${showSelector ? "active" : ""}`} onClick={handleCurrentValueClick}> <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> <span className="value-text">{currentItem.text}</span>
{!disabled && (
<span className="arrow-text"> <span className="arrow-text">
<Icon.ChevronDown className="icon-img" /> <Icon.ChevronDown className="icon-img" />
</span> </span>
)}
</div> </div>
<div className={`items-wrapper ${showSelector ? "" : "!hidden"}`}> <div className={`items-wrapper ${showSelector ? "" : "!hidden"}`}>
@ -91,6 +106,7 @@ const Selector: React.FC<Props> = (props: Props) => {
)} )}
</div> </div>
</div> </div>
</Tooltip>
); );
}; };

View File

@ -14,6 +14,10 @@
width: calc(100% - 20px); width: calc(100% - 20px);
} }
> .lock-text {
@apply flex flex-row justify-center items-center w-4 shrink-0 mr-1;
}
> .arrow-text { > .arrow-text {
@apply flex flex-row justify-center items-center w-4 shrink-0; @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; @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": { "visibility": {
"private": "Only visible to you", "private": "Only visible to you",
"protected": "Visible to members", "protected": "Visible to members",
"public": "Everyone can see" "public": "Everyone can see",
"disabled": "Public memos are disabled"
} }
}, },
"memo-list": { "memo-list": {
@ -180,6 +181,7 @@
}, },
"database-file-size": "Database File Size", "database-file-size": "Database File Size",
"allow-user-signup": "Allow user signup", "allow-user-signup": "Allow user signup",
"disable-public-memos": "Disable public memos",
"additional-style": "Additional style", "additional-style": "Additional style",
"additional-script": "Additional script", "additional-script": "Additional script",
"additional-style-placeholder": "Additional CSS codes", "additional-style-placeholder": "Additional CSS codes",

View File

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

View File

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

View File

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