2.4.1: Settings GPG, Localization, CLI DB migrate, minor improvements
This commit is contained in:
15
react/admin-panel/src/elements/hidden-file-upload.js
Normal file
15
react/admin-panel/src/elements/hidden-file-upload.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import {styled} from "@mui/material";
|
||||
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
clip: 'rect(0 0 0 0)',
|
||||
clipPath: 'inset(50%)',
|
||||
height: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
width: 1,
|
||||
});
|
||||
|
||||
export default VisuallyHiddenInput;
|
||||
@@ -39,6 +39,12 @@ const StyledStatBox = styled(Alert)((props) => ({
|
||||
},
|
||||
"& div:nth-of-type(1)": {
|
||||
padding: props.theme.spacing(2),
|
||||
"& span": {
|
||||
fontSize: "2.5em",
|
||||
},
|
||||
"& p": {
|
||||
fontSize: "1em",
|
||||
}
|
||||
},
|
||||
"& div:nth-of-type(2) > svg": {
|
||||
position: "absolute",
|
||||
@@ -49,8 +55,18 @@ const StyledStatBox = styled(Alert)((props) => ({
|
||||
},
|
||||
"& div:nth-of-type(3)": {
|
||||
backdropFilter: "brightness(70%)",
|
||||
textAlign: "right",
|
||||
padding: props.theme.spacing(0.5),
|
||||
"& a": {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "auto 30px",
|
||||
alignItems: "center",
|
||||
justifyContent: "end",
|
||||
textDecoration: "none",
|
||||
"& svg": {
|
||||
textAlign: "center",
|
||||
justifySelf: "center"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -60,7 +76,7 @@ const StatBox = (props) => <StyledStatBox variant={"filled"} icon={false}
|
||||
<Box>
|
||||
{!isNaN(props.count) ?
|
||||
<>
|
||||
<h2>{props.count}</h2>
|
||||
<span>{props.count}</span>
|
||||
<p>{props.text}</p>
|
||||
</> : <CircularProgress variant={"determinate"} />
|
||||
}
|
||||
@@ -68,7 +84,8 @@ const StatBox = (props) => <StyledStatBox variant={"filled"} icon={false}
|
||||
<Box>{props.icon}</Box>
|
||||
<Box>
|
||||
<Link to={props.link}>
|
||||
More info <ArrowCircleRight />
|
||||
<span>{props.L("admin.more_info")}</span>
|
||||
<ArrowCircleRight />
|
||||
</Link>
|
||||
</Box>
|
||||
</StyledStatBox>
|
||||
@@ -131,25 +148,25 @@ export default function Overview(props) {
|
||||
<StatBox color={"info"} count={stats?.userCount}
|
||||
text={L("admin.users_registered")}
|
||||
icon={<People/>}
|
||||
link={"/admin/users"}/>
|
||||
link={"/admin/users"} L={L}/>
|
||||
</Grid>
|
||||
<Grid item xs={6} lg={3}>
|
||||
<StatBox color={"success"} count={stats?.groupCount}
|
||||
text={L("admin.available_groups")}
|
||||
icon={<Groups/>}
|
||||
link={"/admin/groups"}/>
|
||||
link={"/admin/groups"} L={L}/>
|
||||
</Grid>
|
||||
<Grid item xs={6} lg={3}>
|
||||
<StatBox color={"warning"} count={stats?.pageCount}
|
||||
text={L("admin.routes_defined")}
|
||||
icon={<LibraryBooks/>}
|
||||
link={"/admin/routes"}/>
|
||||
link={"/admin/routes"} L={L}/>
|
||||
</Grid>
|
||||
<Grid item xs={6} lg={3}>
|
||||
<StatBox color={"error"} count={stats?.errorCount}
|
||||
text={L("admin.error_count")}
|
||||
icon={<BugReport />}
|
||||
link={"/admin/logs"}/>
|
||||
link={"/admin/logs"} L={L}/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box m={2} p={2} component={Paper}>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {CheckCircle, CloudUpload, ErrorOutline, Remove, Upload, VpnKey} from "@m
|
||||
import SpacedFormGroup from "../../elements/form-group";
|
||||
import ButtonBar from "../../elements/button-bar";
|
||||
import CollapseBox from "./collapse-box";
|
||||
import VisuallyHiddenInput from "../../elements/hidden-file-upload";
|
||||
|
||||
const GpgKeyField = styled(TextField)((props) => ({
|
||||
"& > div": {
|
||||
@@ -24,18 +25,6 @@ const GpgFingerprintBox = styled(Box)((props) => ({
|
||||
}
|
||||
}));
|
||||
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
clip: 'rect(0 0 0 0)',
|
||||
clipPath: 'inset(50%)',
|
||||
height: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
width: 1,
|
||||
});
|
||||
|
||||
export default function GpgBox(props) {
|
||||
|
||||
// meta
|
||||
@@ -87,7 +76,7 @@ export default function GpgBox(props) {
|
||||
data += event.target.result;
|
||||
if (reader.readyState === 2) {
|
||||
if (!data.match(/^-+\s*BEGIN/m)) {
|
||||
showDialog(L("Selected file is a not a GPG Public Key in ASCII format"), L("Error reading file"));
|
||||
showDialog(L("account.invalid_gpg_key"), L("account.error_reading_file"));
|
||||
return false;
|
||||
} else {
|
||||
callback(data);
|
||||
@@ -98,9 +87,7 @@ export default function GpgBox(props) {
|
||||
reader.readAsText(file);
|
||||
}, [showDialog]);
|
||||
|
||||
return <CollapseBox title={L("account.gpg_key")} {...other}
|
||||
|
||||
icon={<VpnKey />}>
|
||||
return <CollapseBox title={L("account.gpg_key")} icon={<VpnKey />} {...other}>
|
||||
{
|
||||
profile.gpgKey ? <Box>
|
||||
<GpgFingerprintBox mb={2}>
|
||||
@@ -150,8 +137,8 @@ export default function GpgBox(props) {
|
||||
variant="outlined"
|
||||
startIcon={<CloudUpload />}
|
||||
component={"label"}>
|
||||
Upload file
|
||||
<VisuallyHiddenInput type={"file"} onChange={e => {
|
||||
{L("general.upload_file")}
|
||||
<VisuallyHiddenInput type={"file"} onChange={e => {
|
||||
let file = e.target.files[0];
|
||||
getFileContents(file, (data) => {
|
||||
setGpgKey(data);
|
||||
|
||||
98
react/admin-panel/src/views/settings/input-gpg-key.js
Normal file
98
react/admin-panel/src/views/settings/input-gpg-key.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import {Box, IconButton, styled, TextField} from "@mui/material";
|
||||
import {Delete, Upload} from "@mui/icons-material";
|
||||
import React, {useCallback, useContext, useRef, useState} from "react";
|
||||
import {LocaleContext} from "shared/locale";
|
||||
import VisuallyHiddenInput from "../../elements/hidden-file-upload";
|
||||
|
||||
const StyledGpgKeyInput = styled(Box)((props) => ({
|
||||
display: "grid",
|
||||
gridTemplateColumns: "40px auto",
|
||||
"& button": {
|
||||
padding: 0,
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: props.theme.palette.grey[400],
|
||||
borderTopLeftRadius: 5,
|
||||
borderBottomLeftRadius: 5,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
backgroundColor: props.theme.palette.grey[300],
|
||||
},
|
||||
"& > div > div": {
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}
|
||||
}));
|
||||
|
||||
export default function GpgKeyInput(props) {
|
||||
|
||||
const { value, api, showDialog, onChange, ...other } = props;
|
||||
const {translate: L} = useContext(LocaleContext);
|
||||
const isConfigured = !!value;
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const onRemoveKey = useCallback(() => {
|
||||
api.settingsRemoveGPG().then(data => {
|
||||
if (!data.success) {
|
||||
showDialog(data.msg, L("settings.remove_gpg_key_error"));
|
||||
} else {
|
||||
onChange(null);
|
||||
}
|
||||
});
|
||||
}, [api, showDialog, onChange]);
|
||||
|
||||
const onImportGPG = useCallback((publicKey) => {
|
||||
api.settingsImportGPG(publicKey).then(data => {
|
||||
if (!data.success) {
|
||||
showDialog(data.msg, L("settings.import_gpg_key_error"));
|
||||
} else {
|
||||
onChange(data.gpgKey);
|
||||
}
|
||||
});
|
||||
}, [api, showDialog, onChange]);
|
||||
|
||||
const onOpenDialog = useCallback(() => {
|
||||
if (isConfigured) {
|
||||
showDialog(
|
||||
L("settings.remove_gpg_key_text"),
|
||||
L("settings.remove_gpg_key"),
|
||||
[L("general.cancel"), L("general.remove")],
|
||||
button => button === 1 ? onRemoveKey() : true
|
||||
);
|
||||
} else if (fileInputRef?.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
}, [showDialog, isConfigured, onRemoveKey, fileInputRef?.current]);
|
||||
|
||||
const getFileContents = useCallback((file, callback) => {
|
||||
let reader = new FileReader();
|
||||
let data = "";
|
||||
reader.onload = function(event) {
|
||||
data += event.target.result;
|
||||
if (reader.readyState === 2) {
|
||||
if (!data.match(/^-+\s*BEGIN/m)) {
|
||||
showDialog(L("account.invalid_gpg_key"), L("account.error_reading_file"));
|
||||
return false;
|
||||
} else {
|
||||
callback(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}, [showDialog]);
|
||||
|
||||
return <StyledGpgKeyInput {...other}>
|
||||
<IconButton onClick={onOpenDialog}>
|
||||
{ isConfigured ? <Delete color={"error"} /> : <Upload color={"success"} /> }
|
||||
</IconButton>
|
||||
<VisuallyHiddenInput ref={fileInputRef} type={"file"} onChange={e => {
|
||||
let file = e.target.files[0];
|
||||
getFileContents(file, (data) => {
|
||||
onImportGPG(data);
|
||||
});
|
||||
return false;
|
||||
}} />
|
||||
<TextField variant={"outlined"} size={"small"} disabled={true}
|
||||
value={value?.fingerprint ?? L("settings.no_gpg_key_configured")} />
|
||||
</StyledGpgKeyInput>
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import {LocaleContext} from "shared/locale";
|
||||
import {
|
||||
Box, Button,
|
||||
CircularProgress, FormControl,
|
||||
FormGroup, FormLabel, Grid, IconButton,
|
||||
FormLabel, Grid, IconButton,
|
||||
Paper,
|
||||
Tab,
|
||||
Table,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
RestartAlt,
|
||||
Save,
|
||||
Send,
|
||||
SettingsApplications, SmartToy, Storage
|
||||
SettingsApplications, SmartToy, Storage,
|
||||
} from "@mui/icons-material";
|
||||
import TIME_ZONES from "shared/time-zones";
|
||||
import ButtonBar from "../../elements/button-bar";
|
||||
@@ -34,10 +34,12 @@ import SettingsPasswordInput from "./input-password";
|
||||
import SettingsTextInput from "./input-text";
|
||||
import SettingsSelection from "./input-selection";
|
||||
import ViewContent from "../../elements/view-content";
|
||||
import GpgKeyInput from "./input-gpg-key";
|
||||
import SpacedFormGroup from "../../elements/form-group";
|
||||
|
||||
export default function SettingsView(props) {
|
||||
|
||||
// TODO: website-logo (?), mail_contact, mail_contact_gpg_key_id
|
||||
// TODO: website-logo (?), mail_contact_gpg_key_id
|
||||
|
||||
// meta
|
||||
const api = props.api;
|
||||
@@ -47,6 +49,7 @@ export default function SettingsView(props) {
|
||||
"general": [
|
||||
"base_url",
|
||||
"site_name",
|
||||
"mail_contact",
|
||||
"user_registration_enabled",
|
||||
"time_zone",
|
||||
"allowed_extensions",
|
||||
@@ -75,6 +78,8 @@ export default function SettingsView(props) {
|
||||
]
|
||||
};
|
||||
|
||||
const CUSTOM_KEYS = ["mail_contact_gpg_key"];
|
||||
|
||||
// data
|
||||
const [fetchSettings, setFetchSettings] = useState(true);
|
||||
const [settings, setSettings] = useState(null);
|
||||
@@ -94,8 +99,12 @@ export default function SettingsView(props) {
|
||||
}, [])).includes(key);
|
||||
}
|
||||
|
||||
const isCustom = (key) => {
|
||||
return CUSTOM_KEYS.includes(key);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
requestModules(props.api, ["general", "settings"], currentLocale).then(data => {
|
||||
requestModules(props.api, ["general", "settings", "account"], currentLocale).then(data => {
|
||||
if (!data.success) {
|
||||
showDialog("Error fetching translations: " + data.msg);
|
||||
}
|
||||
@@ -115,7 +124,9 @@ export default function SettingsView(props) {
|
||||
return obj;
|
||||
}, {})
|
||||
);
|
||||
setUncategorizedKeys(Object.keys(data.settings).filter(key => isUncategorized(key)));
|
||||
setUncategorizedKeys(Object.keys(data.settings)
|
||||
.filter(key => !isCustom(key))
|
||||
.filter(key => isUncategorized(key)));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -132,7 +143,15 @@ export default function SettingsView(props) {
|
||||
|
||||
const onSaveSettings = useCallback(() => {
|
||||
setSaving(true);
|
||||
api.saveSettings(settings).then(data => {
|
||||
|
||||
let settingsToSave = {...settings};
|
||||
for (const key of CUSTOM_KEYS) {
|
||||
if (settingsToSave.hasOwnProperty(key)) {
|
||||
delete settingsToSave[key];
|
||||
}
|
||||
}
|
||||
|
||||
api.saveSettings(settingsToSave).then(data => {
|
||||
setSaving(false);
|
||||
if (data.success) {
|
||||
showDialog(L("settings.save_settings_success"), L("general.success"));
|
||||
@@ -253,6 +272,13 @@ export default function SettingsView(props) {
|
||||
if (selectedTab === "general") {
|
||||
return [
|
||||
renderTextInput("site_name"),
|
||||
renderTextInput("mail_contact", false, {type: "email"}),
|
||||
<SpacedFormGroup key={"mail-contact-gpg-key"}>
|
||||
<FormLabel>{L("settings.mail_contact_gpg_key")}</FormLabel>
|
||||
<GpgKeyInput value={settings.mail_contact_gpg_key} api={api}
|
||||
showDialog={showDialog}
|
||||
onChange={value => setSettings({...settings, mail_contact_gpg_key: value})}/>
|
||||
</SpacedFormGroup>,
|
||||
renderTextInput("base_url"),
|
||||
renderTextValuesInput("trusted_domains"),
|
||||
renderCheckBox("user_registration_enabled"),
|
||||
@@ -269,7 +295,7 @@ export default function SettingsView(props) {
|
||||
renderPasswordInput("mail_password", !settings.mail_enabled),
|
||||
renderTextInput("mail_footer", !settings.mail_enabled),
|
||||
renderCheckBox("mail_async", !settings.mail_enabled),
|
||||
<FormGroup key={"mail-test"}>
|
||||
<SpacedFormGroup key={"mail-test"}>
|
||||
<FormLabel>{L("settings.send_test_email")}</FormLabel>
|
||||
<FormControl disabled={!settings.mail_enabled}>
|
||||
<Grid container spacing={1}>
|
||||
@@ -292,7 +318,7 @@ export default function SettingsView(props) {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</SpacedFormGroup>
|
||||
];
|
||||
} else if (selectedTab === "captcha") {
|
||||
let captchaOptions = {};
|
||||
|
||||
@@ -16,7 +16,7 @@ import * as React from "react";
|
||||
import ViewContent from "../../elements/view-content";
|
||||
import FormGroup from "../../elements/form-group";
|
||||
import ButtonBar from "../../elements/button-bar";
|
||||
import {RestartAlt, Save, Send} from "@mui/icons-material";
|
||||
import {Delete, RestartAlt, Save, Send} from "@mui/icons-material";
|
||||
import PasswordStrength from "shared/elements/password-strength";
|
||||
|
||||
const initialUser = {
|
||||
@@ -51,20 +51,20 @@ export default function UserEditView(props) {
|
||||
const [sendInvite, setSetInvite] = useState(isNewUser);
|
||||
|
||||
useEffect(() => {
|
||||
requestModules(props.api, ["general", "account"], currentLocale).then(data => {
|
||||
requestModules(api, ["general", "account"], currentLocale).then(data => {
|
||||
if (!data.success) {
|
||||
props.showDialog("Error fetching translations: " + data.msg);
|
||||
showDialog("Error fetching translations: " + data.msg);
|
||||
}
|
||||
});
|
||||
}, [currentLocale]);
|
||||
|
||||
const onFetchGroups = useCallback(() => {
|
||||
api.searchGroups(groupInput, user?.groups?.map(group => group.id)).then((res) => {
|
||||
if (res.success) {
|
||||
setGroups(res.groups);
|
||||
} else {
|
||||
showDialog(res.msg, L("account.search_groups_error"));
|
||||
}
|
||||
if (res.success) {
|
||||
setGroups(res.groups);
|
||||
} else {
|
||||
showDialog(res.msg, L("account.search_groups_error"));
|
||||
}
|
||||
});
|
||||
}, [api, showDialog, user?.groups, groupInput]);
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function UserEditView(props) {
|
||||
});
|
||||
} else {
|
||||
api.createUser(user.name, user.fullName, user.email, groupIds,
|
||||
user.password, user.passwordConfirm
|
||||
user.password, user.passwordConfirm
|
||||
).then(res => {
|
||||
setSaving(false);
|
||||
if (res.success) {
|
||||
@@ -143,6 +143,16 @@ export default function UserEditView(props) {
|
||||
setChanged(true);
|
||||
}, [user]);
|
||||
|
||||
const onDeleteUser = useCallback(() => {
|
||||
api.deleteUser(userId).then(res => {
|
||||
if (res.success) {
|
||||
navigate("/admin/users");
|
||||
} else {
|
||||
showDialog(res.msg, L("account.delete_user_error"));
|
||||
}
|
||||
});
|
||||
}, [api, showDialog, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewUser) {
|
||||
onFetchUser(true);
|
||||
@@ -163,118 +173,132 @@ export default function UserEditView(props) {
|
||||
<span key={"action"}>{isNewUser ? L("general.new") : L("general.edit")}</span>
|
||||
]}>
|
||||
<Grid container>
|
||||
<Grid item xs={12} mt={1} mb={1}>
|
||||
<Button variant={"outlined"} color={"error"} size={"small"}
|
||||
startIcon={<Delete />}
|
||||
disabled={isNewUser || !api.hasPermission("user/delete") || user.id === api.user.id}
|
||||
onClick={() => showDialog(
|
||||
L("account.delete_user_text"),
|
||||
L("account.delete_user_title"),
|
||||
[L("general.cancel"), L("general.confirm")],
|
||||
(buttonIndex) => buttonIndex === 1 ? onDeleteUser() : true)
|
||||
}
|
||||
>
|
||||
{L("general.delete")}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} lg={6}>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.name")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
value={user.name}
|
||||
onChange={e => onChangeValue("name", e.target.value)} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.full_name")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
value={user.fullName}
|
||||
onChange={e => onChangeValue("fullName", e.target.value)} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.email")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
value={user.email ?? ""}
|
||||
type={"email"}
|
||||
onChange={e => onChangeValue("email", e.target.value)} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.groups")}</FormLabel>
|
||||
<Autocomplete
|
||||
options={Object.values(groups || {})}
|
||||
getOptionLabel={group => group.name}
|
||||
getOptionKey={group => group.id}
|
||||
filterOptions={(options) => options}
|
||||
clearOnBlur={true}
|
||||
clearOnEscape
|
||||
freeSolo
|
||||
multiple
|
||||
value={user.groups}
|
||||
inputValue={groupInput}
|
||||
onChange={(e, v) => onChangeValue("groups", v)}
|
||||
onInputChange={e => setGroupInput((!e || e.target.value === 0) ? "" : e.target.value) }
|
||||
renderTags={(values, props) =>
|
||||
values.map((option, index) => {
|
||||
return <Chip label={option.name}
|
||||
style={{backgroundColor: option.color}}
|
||||
{...props({index})} />
|
||||
})
|
||||
}
|
||||
renderInput={(params) => <TextField {...params}
|
||||
onBlur={() => setGroupInput("")} />}
|
||||
/>
|
||||
</FormGroup>
|
||||
{ !isNewUser ?
|
||||
<>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.password")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
value={user.password}
|
||||
type={"password"}
|
||||
placeholder={"(" + L("general.unchanged") + ")"}
|
||||
onChange={e => onChangeValue("password", e.target.value)} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<MuiFormGroup>
|
||||
<FormControlLabel
|
||||
control={<Checkbox
|
||||
checked={!!user.active}
|
||||
onChange={(e, v) => onChangeValue("active", v)} />}
|
||||
label={L("account.active")} />
|
||||
</MuiFormGroup>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={<Checkbox
|
||||
checked={!!user.confirmed}
|
||||
onChange={(e, v) => onChangeValue("confirmed", v)} />}
|
||||
label={L("account.confirmed")} />
|
||||
</FormGroup>
|
||||
</> : <>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={<Checkbox
|
||||
checked={sendInvite}
|
||||
onChange={(e, v) => setSetInvite(v)} />}
|
||||
label={L("account.send_invite")} />
|
||||
</FormGroup>
|
||||
{!sendInvite && <>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.name")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
value={user.name}
|
||||
onChange={e => onChangeValue("name", e.target.value)} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.full_name")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
value={user.fullName}
|
||||
onChange={e => onChangeValue("fullName", e.target.value)} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.email")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
value={user.email ?? ""}
|
||||
type={"email"}
|
||||
onChange={e => onChangeValue("email", e.target.value)} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.groups")}</FormLabel>
|
||||
<Autocomplete
|
||||
options={Object.values(groups || {})}
|
||||
getOptionLabel={group => group.name}
|
||||
getOptionKey={group => group.id}
|
||||
filterOptions={(options) => options}
|
||||
clearOnBlur={true}
|
||||
clearOnEscape
|
||||
freeSolo
|
||||
multiple
|
||||
value={user.groups}
|
||||
inputValue={groupInput}
|
||||
onChange={(e, v) => onChangeValue("groups", v)}
|
||||
onInputChange={e => setGroupInput((!e || e.target.value === 0) ? "" : e.target.value) }
|
||||
renderTags={(values, props) =>
|
||||
values.map((option, index) => {
|
||||
return <Chip label={option.name}
|
||||
style={{backgroundColor: option.color}}
|
||||
{...props({index})} />
|
||||
})
|
||||
}
|
||||
renderInput={(params) => <TextField {...params}
|
||||
onBlur={() => setGroupInput("")} />}
|
||||
/>
|
||||
</FormGroup>
|
||||
{ !isNewUser ?
|
||||
<>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.password")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
value={user.password}
|
||||
type={"password"}
|
||||
placeholder={"(" + L("general.unchanged") + ")"}
|
||||
onChange={e => onChangeValue("password", e.target.value)} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<MuiFormGroup>
|
||||
<FormControlLabel
|
||||
control={<Checkbox
|
||||
checked={!!user.active}
|
||||
onChange={(e, v) => onChangeValue("active", v)} />}
|
||||
label={L("account.active")} />
|
||||
</MuiFormGroup>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.password_confirm")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
value={user.passwordConfirm}
|
||||
type={"password"}
|
||||
onChange={e => onChangeValue("passwordConfirm", e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControlLabel
|
||||
control={<Checkbox
|
||||
checked={!!user.confirmed}
|
||||
onChange={(e, v) => onChangeValue("confirmed", v)} />}
|
||||
label={L("account.confirmed")} />
|
||||
</FormGroup>
|
||||
<Box mb={2}>
|
||||
<PasswordStrength password={user.password} />
|
||||
</Box>
|
||||
</> : <>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={<Checkbox
|
||||
checked={sendInvite}
|
||||
onChange={(e, v) => setSetInvite(v)} />}
|
||||
label={L("account.send_invite")} />
|
||||
</FormGroup>
|
||||
{!sendInvite && <>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.password")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
value={user.password}
|
||||
type={"password"}
|
||||
onChange={e => onChangeValue("password", e.target.value)} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("account.password_confirm")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField size={"small"} variant={"outlined"}
|
||||
value={user.passwordConfirm}
|
||||
type={"password"}
|
||||
onChange={e => onChangeValue("passwordConfirm", e.target.value)} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<Box mb={2}>
|
||||
<PasswordStrength password={user.password} />
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ButtonBar>
|
||||
|
||||
@@ -302,6 +302,14 @@ export default class API {
|
||||
return this.apiCall("settings/set", { settings: settings });
|
||||
}
|
||||
|
||||
async settingsImportGPG(publicKey) {
|
||||
return this.apiCall("settings/importGPG", { publicKey: publicKey });
|
||||
}
|
||||
|
||||
async settingsRemoveGPG() {
|
||||
return this.apiCall("settings/removeGPG");
|
||||
}
|
||||
|
||||
/** MailAPI **/
|
||||
async sendTestMail(receiver) {
|
||||
return this.apiCall("mail/test", { receiver: receiver });
|
||||
@@ -396,8 +404,8 @@ export default class API {
|
||||
}
|
||||
|
||||
/** GPG API **/
|
||||
async uploadGPG(pubkey) {
|
||||
let res = await this.apiCall("gpgKey/import", { pubkey: pubkey });
|
||||
async uploadGPG(publicKey) {
|
||||
let res = await this.apiCall("gpgKey/import", { publicKey: publicKey });
|
||||
if (res.success) {
|
||||
this.user.gpgKey = res.gpgKey;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user