Browse Source

settings frontend bugfix + refactoring

Roman 3 weeks ago
parent
commit
b274cd4ad2

+ 1 - 1
react/admin-panel/src/AdminDashboard.jsx

@@ -21,7 +21,7 @@ const LogView = lazy(() => import("./views/log-view"));
 const AccessControlList = lazy(() => import("./views/access-control-list"));
 const RouteListView = lazy(() => import("./views/route/route-list"));
 const RouteEditView = lazy(() => import("./views/route/route-edit"));
-const SettingsView = lazy(() => import("./views/settings"));
+const SettingsView = lazy(() => import("./views/settings/settings"));
 const ProfileView = lazy(() => import("./views/profile/profile"));
 
 export default function AdminDashboard(props) {

+ 7 - 0
react/admin-panel/src/elements/form-group.js

@@ -0,0 +1,7 @@
+import {FormGroup, styled} from "@mui/material";
+
+const SpacedFormGroup = styled(FormGroup)((props) =>  ({
+    marginBottom: props.theme.spacing(2)
+}));
+
+export default SpacedFormGroup;

+ 17 - 20
react/admin-panel/src/views/profile/profile.js

@@ -27,6 +27,7 @@ import MfaTotp from "./mfa-totp";
 import MfaFido from "./mfa-fido";
 import Dialog from "shared/elements/dialog";
 import PasswordStrength from "shared/elements/password-strength";
+import SpacedFormGroup from "../../elements/form-group";
 
 const GpgKeyField = styled(TextField)((props) => ({
     "& > div": {
@@ -46,10 +47,6 @@ const GpgFingerprintBox = styled(Box)((props) => ({
     }
 }));
 
-const ProfileFormGroup = styled(FormGroup)((props) =>  ({
-    marginBottom: props.theme.spacing(2)
-}));
-
 const MFAOptions = styled(Box)((props) => ({
     "& > div": {
         borderColor: props.theme.palette.divider,
@@ -231,7 +228,7 @@ export default function ProfileView(props) {
             </div>
         </div>
         <div className={"content"}>
-            <ProfileFormGroup>
+            <SpacedFormGroup>
                 <FormLabel>{L("account.username")}</FormLabel>
                 <FormControl>
                     <TextField variant={"outlined"}
@@ -239,8 +236,8 @@ export default function ProfileView(props) {
                         value={profile.name}
                         onChange={e => setProfile({...profile, name: e.target.value })} />
                 </FormControl>
-            </ProfileFormGroup>
-            <ProfileFormGroup>
+            </SpacedFormGroup>
+            <SpacedFormGroup>
                 <FormLabel>{L("account.full_name")}</FormLabel>
                 <FormControl>
                     <TextField variant={"outlined"}
@@ -248,12 +245,12 @@ export default function ProfileView(props) {
                                value={profile.fullName ?? ""}
                                onChange={e => setProfile({...profile, fullName: e.target.value })} />
                 </FormControl>
-            </ProfileFormGroup>
+            </SpacedFormGroup>
 
             <CollapseBox title={L("account.change_password")} open={openedTab === "password"}
                          onToggle={() => setOpenedTab(openedTab === "password" ? "" : "password")}
                          icon={<Password />}>
-                <ProfileFormGroup>
+                <SpacedFormGroup>
                     <FormLabel>{L("account.password_old")}</FormLabel>
                     <FormControl>
                         <TextField variant={"outlined"}
@@ -263,8 +260,8 @@ export default function ProfileView(props) {
                                    value={changePassword.old}
                                    onChange={e => setChangePassword({...changePassword, old: e.target.value })} />
                     </FormControl>
-                </ProfileFormGroup>
-                <ProfileFormGroup>
+                </SpacedFormGroup>
+                <SpacedFormGroup>
                     <FormLabel>{L("account.password_new")}</FormLabel>
                     <FormControl>
                         <TextField variant={"outlined"}
@@ -273,8 +270,8 @@ export default function ProfileView(props) {
                                    value={changePassword.new}
                                    onChange={e => setChangePassword({...changePassword, new: e.target.value })} />
                     </FormControl>
-                </ProfileFormGroup>
-                <ProfileFormGroup>
+                </SpacedFormGroup>
+                <SpacedFormGroup>
                     <FormLabel>{L("account.password_confirm")}</FormLabel>
                     <FormControl>
                         <TextField variant={"outlined"}
@@ -283,7 +280,7 @@ export default function ProfileView(props) {
                                    value={changePassword.confirm}
                                    onChange={e => setChangePassword({...changePassword, confirm: e.target.value })} />
                     </FormControl>
-                </ProfileFormGroup>
+                </SpacedFormGroup>
                 <Box className={"w-50"}>
                     <PasswordStrength password={changePassword.new} minLength={6} />
                 </Box>
@@ -303,7 +300,7 @@ export default function ProfileView(props) {
                                     {profile.gpgKey.fingerprint}
                                 </code>
                             </GpgFingerprintBox>
-                            <ProfileFormGroup>
+                            <SpacedFormGroup>
                                 <FormLabel>{L("account.password")}</FormLabel>
                                 <FormControl>
                                     <TextField variant={"outlined"} size={"small"}
@@ -312,7 +309,7 @@ export default function ProfileView(props) {
                                                placeholder={L("account.password")}
                                     />
                                 </FormControl>
-                            </ProfileFormGroup>
+                            </SpacedFormGroup>
                             <Button startIcon={isGpgKeyRemoving ? <CircularProgress size={12} /> : <Remove />}
                                     color={"secondary"} onClick={onRemoveGpgKey}
                                     variant={"outlined"} size={"small"}
@@ -321,7 +318,7 @@ export default function ProfileView(props) {
                             </Button>
                     </Box> :
                     <Box>
-                        <ProfileFormGroup>
+                        <SpacedFormGroup>
                             <FormLabel>{L("account.gpg_key")}</FormLabel>
                             <GpgKeyField value={gpgKey} multiline={true} rows={8}
                                          disabled={isGpgKeyUploading || !api.hasPermission("user/importGPG")}
@@ -334,7 +331,7 @@ export default function ProfileView(props) {
                                              });
                                              return false;
                                          }}/>
-                        </ProfileFormGroup>
+                        </SpacedFormGroup>
                         <ButtonBar>
                             <Button size={"small"}
                                 variant={"outlined"}
@@ -372,7 +369,7 @@ export default function ProfileView(props) {
                             }
                             {L("account.2fa_type_" + profile.twoFactorToken.type)}
                         </GpgFingerprintBox>
-                        <ProfileFormGroup>
+                        <SpacedFormGroup>
                             <FormLabel>{L("account.password")}</FormLabel>
                             <FormControl>
                                 <TextField variant={"outlined"} size={"small"}
@@ -381,7 +378,7 @@ export default function ProfileView(props) {
                                            placeholder={L("account.password")}
                                 />
                             </FormControl>
-                        </ProfileFormGroup>
+                        </SpacedFormGroup>
                         <Button startIcon={is2FARemoving ? <CircularProgress size={12} /> : <Remove />}
                                 color={"secondary"} onClick={onRemove2FA}
                                 variant={"outlined"} size={"small"}

+ 21 - 0
react/admin-panel/src/views/settings/input-check-box.js

@@ -0,0 +1,21 @@
+import {Checkbox, FormControlLabel} from "@mui/material";
+import SpacedFormGroup from "../../elements/form-group";
+import {parseBool} from "shared/util";
+import {useContext} from "react";
+import {LocaleContext} from "shared/locale";
+
+export default function SettingsCheckBox(props) {
+
+    const {key_name, value, onChangeValue, disabled, ...other} = props;
+    const {translate: L} = useContext(LocaleContext);
+
+    return <SpacedFormGroup {...other}>
+        <FormControlLabel
+            disabled={disabled}
+            control={<Checkbox
+                disabled={disabled}
+                checked={parseBool(value)}
+                onChange={(e, v) => onChangeValue(v)} />}
+            label={L("settings." + key_name)} />
+    </SpacedFormGroup>
+}

+ 22 - 0
react/admin-panel/src/views/settings/input-number.js

@@ -0,0 +1,22 @@
+import {FormControl, FormLabel, TextField} from "@mui/material";
+import SpacedFormGroup from "../../elements/form-group";
+import {useContext} from "react";
+import {LocaleContext} from "shared/locale";
+
+export default function SettingsNumberInput(props) {
+
+    const {key_name, value, minValue, maxValue, onChangeValue, disabled, ...other} = props;
+    const {translate: L} = useContext(LocaleContext);
+
+    return <SpacedFormGroup {...other}>
+        <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
+        <FormControl>
+            <TextField size={"small"} variant={"outlined"}
+                       type={"number"}
+                       disabled={disabled}
+                       inputProps={{min: minValue, max: maxValue}}
+                       value={value}
+                       onChange={e => onChangeValue(e.target.value)} />
+        </FormControl>
+    </SpacedFormGroup>
+}

+ 22 - 0
react/admin-panel/src/views/settings/input-password.js

@@ -0,0 +1,22 @@
+import SpacedFormGroup from "../../elements/form-group";
+import {FormControl, FormLabel, TextField} from "@mui/material";
+import {useContext} from "react";
+import {LocaleContext} from "shared/locale";
+
+export default function SettingsPasswordInput(props) {
+
+    const {key_name, value, onChangeValue, disabled, ...other} = props;
+    const {translate: L} = useContext(LocaleContext);
+
+    return <SpacedFormGroup {...other}>
+        <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
+        <FormControl>
+            <TextField size={"small"} variant={"outlined"}
+                       type={"password"}
+                       disabled={disabled}
+                       placeholder={"(" + L("general.unchanged") + ")"}
+                       value={value}
+                       onChange={e => onChangeValue(e.target.value)} />
+        </FormControl>
+    </SpacedFormGroup>
+}

+ 25 - 0
react/admin-panel/src/views/settings/input-selection.js

@@ -0,0 +1,25 @@
+import {FormControl, FormLabel, Select} from "@mui/material";
+import SpacedFormGroup from "../../elements/form-group";
+import {useContext} from "react";
+import {LocaleContext} from "shared/locale";
+
+export default function SettingsSelection(props) {
+
+    const {key_name, value, options, onChangeValue, disabled, ...other} = props;
+    const {translate: L} = useContext(LocaleContext);
+
+    return <SpacedFormGroup {...other}>
+        <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
+        <FormControl>
+            <Select native value={value}
+                    disabled={disabled}
+                    size={"small"} onChange={e => onChangeValue(e.target.value)}>
+                {options.map(option => <option
+                    key={"option-" + option}
+                    value={option}>
+                    {option}
+                </option>)}
+            </Select>
+        </FormControl>
+    </SpacedFormGroup>
+}

+ 49 - 0
react/admin-panel/src/views/settings/input-text-values.js

@@ -0,0 +1,49 @@
+import {Autocomplete, Chip, FormLabel, TextField} from "@mui/material";
+import SpacedFormGroup from "../../elements/form-group";
+import {useCallback, useContext, useState} from "react";
+import {LocaleContext} from "shared/locale";
+
+export default function SettingsTextValues(props) {
+
+    const {key_name, value, options, onChangeValue, disabled, ...other} = props;
+    const {translate: L} = useContext(LocaleContext);
+
+    const [textInput, setTextInput] = useState("");
+
+    const onFinishTyping = useCallback(() => {
+        setTextInput("");
+        const newValue = textInput?.trim();
+        if (newValue) {
+            onChangeValue(value ? [...value, newValue] : [newValue]);
+        }
+    }, [textInput, value]);
+
+    return <SpacedFormGroup {...other}>
+        <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
+        <Autocomplete
+            clearIcon={false}
+            options={[]}
+            freeSolo
+            multiple
+            value={value || []}
+            inputValue={textInput}
+            onChange={(e, v) => onChangeValue(v)}
+            onInputChange={e => setTextInput(e.target.value.trim())}
+            renderTags={(values, props) =>
+                values.map((option, index) => (
+                    <Chip label={option} {...props({ index })} />
+                ))
+            }
+            renderInput={(params) => <TextField
+                {...params}
+                onKeyDown={e => {
+                    if (["Enter", "Tab", ",", " "].includes(e.key)) {
+                        e.preventDefault();
+                        e.stopPropagation();
+                        onFinishTyping();
+                    }
+                }}
+                onBlur={onFinishTyping} />}
+        />
+    </SpacedFormGroup>
+}

+ 20 - 0
react/admin-panel/src/views/settings/input-text.js

@@ -0,0 +1,20 @@
+import SpacedFormGroup from "../../elements/form-group";
+import {FormControl, FormLabel, TextField} from "@mui/material";
+import {useContext} from "react";
+import {LocaleContext} from "shared/locale";
+
+export default function SettingsTextInput(props) {
+
+    const {key_name, value, onChangeValue, disabled, ...other} = props;
+    const {translate: L} = useContext(LocaleContext);
+
+    return <SpacedFormGroup {...other}>
+        <FormLabel disabled={!!disabled}>{L("settings." + key_name)}</FormLabel>
+        <FormControl>
+            <TextField size={"small"} variant={"outlined"}
+                       disabled={!!disabled}
+                       value={value}
+                       onChange={e => onChangeValue(e.target.value)} />
+        </FormControl>
+    </SpacedFormGroup>
+}

+ 27 - 104
react/admin-panel/src/views/settings.js → react/admin-panel/src/views/settings/settings.js

@@ -1,10 +1,10 @@
 import {useCallback, useContext, useEffect, useState} from "react";
 import {LocaleContext} from "shared/locale";
 import {
-    Box, Button, Checkbox,
-    CircularProgress, FormControl, FormControlLabel,
+    Box, Button,
+    CircularProgress, FormControl,
     FormGroup, FormLabel, Grid, IconButton,
-    Paper, Select, styled,
+    Paper,
     Tab,
     Table,
     TableBody,
@@ -13,8 +13,6 @@ import {
     TableContainer,
     TableRow,
     Tabs, TextField,
-    Autocomplete,
-    Chip
 } from "@mui/material";
 import {Link} from "react-router-dom";
 import {
@@ -29,11 +27,14 @@ import {
     SettingsApplications
 } from "@mui/icons-material";
 import TIME_ZONES from "shared/time-zones";
-import ButtonBar from "../elements/button-bar";
-
-const SettingsFormGroup = styled(FormGroup)((props) => ({
-    marginBottom: props.theme.spacing(1),
-}));
+import ButtonBar from "../../elements/button-bar";
+import {parseBool} from "shared/util";
+import SettingsTextValues from "./input-text-values";
+import SettingsCheckBox from "./input-check-box";
+import SettingsNumberInput from "./input-number";
+import SettingsPasswordInput from "./input-password";
+import SettingsTextInput from "./input-text";
+import SettingsSelection from "./input-selection";
 
 export default function SettingsView(props) {
 
@@ -71,7 +72,6 @@ export default function SettingsView(props) {
     // data
     const [fetchSettings, setFetchSettings] = useState(true);
     const [settings, setSettings] = useState(null);
-    const [extra, setExtra] = useState({});
     const [uncategorizedKeys, setUncategorizedKeys] = useState([]);
 
     // ui
@@ -197,118 +197,41 @@ export default function SettingsView(props) {
         setFetchSettings(true);
         setNewKey("");
         setChanged(false);
-        setExtra({});
     }, []);
 
-    const parseBool = (v) => v !== undefined && (v === true || v === 1 || ["true", "1", "yes"].includes(v.toString().toLowerCase()));
+    const getInputProps = (key_name, disabled = false, props = {}) => {
+        return {
+            key: "form-" + key_name,
+            key_name: key_name,
+            value: settings[key_name],
+            disabled: disabled,
+            onChangeValue: v => setSettings({...settings, [key_name]: v}),
+            ...props
+        };
+    }
 
     const renderTextInput = (key_name, disabled=false, props={}) => {
-        return <SettingsFormGroup key={"form-" + key_name} {...props}>
-            <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
-            <FormControl>
-                <TextField size={"small"} variant={"outlined"}
-                           disabled={disabled}
-                           value={settings[key_name]}
-                           onChange={e => onChangeValue(key_name, e.target.value)} />
-            </FormControl>
-        </SettingsFormGroup>
+        return <SettingsTextInput {...getInputProps(key_name, disabled, props)} />
     }
 
     const renderPasswordInput = (key_name, disabled=false, props={}) => {
-        return <SettingsFormGroup key={"form-" + key_name} {...props}>
-            <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
-            <FormControl>
-                <TextField size={"small"} variant={"outlined"}
-                           type={"password"}
-                           disabled={disabled}
-                           placeholder={"(" + L("general.unchanged") + ")"}
-                           value={settings[key_name]}
-                           onChange={e => onChangeValue(key_name, e.target.value)} />
-            </FormControl>
-        </SettingsFormGroup>
+        return <SettingsPasswordInput {...getInputProps(key_name, disabled, props)} />
     }
 
     const renderNumberInput = (key_name, minValue, maxValue, disabled=false, props={}) => {
-        return <SettingsFormGroup key={"form-" + key_name} {...props}>
-            <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
-            <FormControl>
-                <TextField size={"small"} variant={"outlined"}
-                           type={"number"}
-                           disabled={disabled}
-                           inputProps={{min: minValue, max: maxValue}}
-                           value={settings[key_name]}
-                           onChange={e => onChangeValue(key_name, e.target.value)} />
-            </FormControl>
-        </SettingsFormGroup>
+        return <SettingsNumberInput minValue={minValue} maxValue={maxValue} {...getInputProps(key_name, disabled, props)} />
     }
 
     const renderCheckBox = (key_name, disabled=false, props={}) => {
-        return <SettingsFormGroup key={"form-" + key_name} {...props}>
-            <FormControlLabel
-                disabled={disabled}
-                control={<Checkbox
-                    disabled={disabled}
-                    checked={parseBool(settings[key_name])}
-                    onChange={(e, v) => onChangeValue(key_name, v)} />}
-                label={L("settings." + key_name)} />
-        </SettingsFormGroup>
+        return <SettingsCheckBox {...getInputProps(key_name, disabled, props)} />
     }
 
     const renderSelection = (key_name, options, disabled=false, props={}) => {
-        return <SettingsFormGroup key={"form-" + key_name} {...props}>
-            <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
-            <FormControl>
-                <Select native value={settings[key_name]}
-                        disabled={disabled}
-                        size={"small"} onChange={e => onChangeValue(key_name, e.target.value)}>
-                        {options.map(option => <option
-                            key={"option-" + option}
-                            value={option}>
-                                {option}
-                        </option>)}
-                </Select>
-            </FormControl>
-        </SettingsFormGroup>
+        return <SettingsSelection options={options} {...getInputProps(key_name, disabled, props)} />
     }
 
     const renderTextValuesInput = (key_name, disabled=false, props={}) => {
-
-        const finishTyping = () => {
-            console.log("finishTyping", key_name);
-            setExtra({...extra, [key_name]: ""});
-            if (extra[key_name]) {
-                setSettings({...settings, [key_name]: [...settings[key_name], extra[key_name]]});
-            }
-        }
-
-        return <SettingsFormGroup key={"form-" + key_name} {...props}>
-            <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
-            <Autocomplete
-                clearIcon={false}
-                options={[]}
-                freeSolo
-                multiple
-                value={settings[key_name]}
-                onChange={(e, v) => setSettings({...settings, [key_name]: v})}
-                renderTags={(values, props) =>
-                    values.map((option, index) => (
-                        <Chip label={option} {...props({ index })} />
-                    ))
-                }
-                renderInput={(params) => <TextField
-                    {...params}
-                    value={extra[key_name] ?? ""}
-                    onChange={e => setExtra({...extra, [key_name]: e.target.value.trim()})}
-                    onKeyDown={e => {
-                        if (["Enter", "Tab", " "].includes(e.key)) {
-                            e.preventDefault();
-                            e.stopPropagation();
-                            finishTyping();
-                        }
-                    }}
-                    onBlur={finishTyping} />}
-            />
-        </SettingsFormGroup>
+        return <SettingsTextValues {...getInputProps(key_name, disabled, props)} />
     }
 
     const renderTab = () => {

+ 6 - 1
react/shared/util.js

@@ -109,7 +109,12 @@ const isInt = (value) => {
         !isNaN(parseInt(value, 10));
 }
 
+
+const parseBool = (v) => v !== undefined &&
+    (v === true || v === 1 || ["true", "1", "yes"].includes(v.toString().toLowerCase()));
+
+
 export { humanReadableSize, removeParameter, getParameter, getCookie,
     encodeText, decodeText, getBaseUrl,
     formatDate, formatDateTime, formatDistance,
-    upperFirstChars, isInt, createDownload };
+    upperFirstChars, isInt, parseBool, createDownload };