settings frontend bugfix + refactoring
This commit is contained in:
parent
3888e7fcde
commit
b274cd4ad2
@ -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
react/admin-panel/src/elements/form-group.js
Normal file
7
react/admin-panel/src/elements/form-group.js
Normal file
@ -0,0 +1,7 @@
|
||||
import {FormGroup, styled} from "@mui/material";
|
||||
|
||||
const SpacedFormGroup = styled(FormGroup)((props) => ({
|
||||
marginBottom: props.theme.spacing(2)
|
||||
}));
|
||||
|
||||
export default SpacedFormGroup;
|
@ -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
react/admin-panel/src/views/settings/input-check-box.js
Normal file
21
react/admin-panel/src/views/settings/input-check-box.js
Normal file
@ -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
react/admin-panel/src/views/settings/input-number.js
Normal file
22
react/admin-panel/src/views/settings/input-number.js
Normal file
@ -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
react/admin-panel/src/views/settings/input-password.js
Normal file
22
react/admin-panel/src/views/settings/input-password.js
Normal file
@ -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
react/admin-panel/src/views/settings/input-selection.js
Normal file
25
react/admin-panel/src/views/settings/input-selection.js
Normal file
@ -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
react/admin-panel/src/views/settings/input-text-values.js
Normal file
49
react/admin-panel/src/views/settings/input-text-values.js
Normal file
@ -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
react/admin-panel/src/views/settings/input-text.js
Normal file
20
react/admin-panel/src/views/settings/input-text.js
Normal file
@ -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>
|
||||
}
|
@ -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 = () => {
|
@ -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 };
|
Loading…
Reference in New Issue
Block a user