Add Kundennummer/Nummernkreise settings and settings dialog.
Add number & setting API endpoints, StorageManager caches, and a Settings dialog (tabs for settings keys and number ranges) reachable from the user menu. Show Kundennummer and creation date in the Kunde list, and refactor Neuer Kunde to show an auto-generated number with group select.
This commit is contained in:
@@ -9,9 +9,11 @@ import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import BusinessIcon from '@mui/icons-material/Business';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import { StorageManagerContext } from '../api/storageManagerContext';
|
||||
import { logout } from '../store/authSlice';
|
||||
import CompanySettingsDialog from './CompanySettingsDialog';
|
||||
import SettingsDialog from './settings/SettingsDialog';
|
||||
import { notifyError } from '../utils/notify';
|
||||
|
||||
class UserMenu extends Component {
|
||||
@@ -22,12 +24,15 @@ class UserMenu extends Component {
|
||||
this.state = {
|
||||
anchorEl: null,
|
||||
settingsOpen: false,
|
||||
numbersOpen: false,
|
||||
user: null,
|
||||
};
|
||||
this.handleOpenMenu = this.handleOpenMenu.bind(this);
|
||||
this.handleCloseMenu = this.handleCloseMenu.bind(this);
|
||||
this.handleOpenSettings = this.handleOpenSettings.bind(this);
|
||||
this.handleCloseSettings = this.handleCloseSettings.bind(this);
|
||||
this.handleOpenNumbers = this.handleOpenNumbers.bind(this);
|
||||
this.handleCloseNumbers = this.handleCloseNumbers.bind(this);
|
||||
this.handleLogout = this.handleLogout.bind(this);
|
||||
}
|
||||
|
||||
@@ -60,6 +65,14 @@ class UserMenu extends Component {
|
||||
this.setState({ settingsOpen: false });
|
||||
}
|
||||
|
||||
handleOpenNumbers() {
|
||||
this.setState({ anchorEl: null, numbersOpen: true });
|
||||
}
|
||||
|
||||
handleCloseNumbers() {
|
||||
this.setState({ numbersOpen: false });
|
||||
}
|
||||
|
||||
async handleLogout() {
|
||||
this.setState({ anchorEl: null });
|
||||
try {
|
||||
@@ -71,7 +84,7 @@ class UserMenu extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, anchorEl, settingsOpen } = this.state;
|
||||
const { user, anchorEl, settingsOpen, numbersOpen } = this.state;
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
return (
|
||||
@@ -101,6 +114,12 @@ class UserMenu extends Component {
|
||||
</ListItemIcon>
|
||||
<ListItemText>Unternehmen</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem disabled={!user?.selectedCompanyId} onClick={this.handleOpenNumbers}>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Einstellungen</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={this.handleLogout}>
|
||||
<ListItemIcon>
|
||||
<LogoutIcon fontSize="small" />
|
||||
@@ -109,6 +128,7 @@ class UserMenu extends Component {
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<CompanySettingsDialog open={settingsOpen} onClose={this.handleCloseSettings} />
|
||||
<SettingsDialog open={numbersOpen} onClose={this.handleCloseNumbers} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
286
src/components/settings/NumberSettingsPanel.jsx
Normal file
286
src/components/settings/NumberSettingsPanel.jsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import { Component } from 'react';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import BackendError from '../BackendError';
|
||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
|
||||
function parsePadding(value) {
|
||||
const n = Number(value);
|
||||
return Number.isInteger(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
function parseNext(value) {
|
||||
const n = Number(value);
|
||||
return Number.isInteger(n) && n >= 0 ? n : null;
|
||||
}
|
||||
|
||||
export default class NumberSettingsPanel extends Component {
|
||||
static contextType = StorageManagerContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
status: 'loading',
|
||||
error: null,
|
||||
numbers: [],
|
||||
// id → { prefix, padding, next, busy, saveError }
|
||||
drafts: {},
|
||||
};
|
||||
this.handleRetry = this.handleRetry.bind(this);
|
||||
this.handleSave = this.handleSave.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
void this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const numbers = requireArray(
|
||||
await (this.context.numbers ?? this.context.numberPromise),
|
||||
'numbers',
|
||||
);
|
||||
const drafts = Object.fromEntries(
|
||||
numbers.map((row) => [
|
||||
row.id,
|
||||
{
|
||||
prefix: row.prefix ?? '',
|
||||
padding: row.padding != null ? String(row.padding) : '',
|
||||
next: row.next != null ? String(row.next) : '',
|
||||
busy: false,
|
||||
saveError: null,
|
||||
},
|
||||
]),
|
||||
);
|
||||
this.setState({ status: 'ready', numbers, drafts });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
this.setState({ status: 'failed', error: backendErrorFrom(err) });
|
||||
}
|
||||
}
|
||||
|
||||
handleRetry() {
|
||||
void this.load();
|
||||
}
|
||||
|
||||
handleChange(id, field, value) {
|
||||
this.setState((prev) => ({
|
||||
drafts: {
|
||||
...prev.drafts,
|
||||
[id]: { ...prev.drafts[id], [field]: value, saveError: null },
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async handleSave(id) {
|
||||
const draft = this.state.drafts[id];
|
||||
if (!draft || draft.busy) return;
|
||||
const row = this.state.numbers.find((r) => r.id === id);
|
||||
if (!row) return;
|
||||
|
||||
const body = {};
|
||||
let invalid = false;
|
||||
let errorMessage = null;
|
||||
|
||||
if (draft.prefix !== (row.prefix ?? '')) {
|
||||
body.prefix = draft.prefix;
|
||||
}
|
||||
if (draft.padding !== (row.padding != null ? String(row.padding) : '')) {
|
||||
const padding = parsePadding(draft.padding);
|
||||
if (padding == null) {
|
||||
invalid = true;
|
||||
errorMessage = 'Stellenzahl muss eine positive ganze Zahl sein.';
|
||||
} else {
|
||||
body.padding = padding;
|
||||
}
|
||||
}
|
||||
if (draft.next !== (row.next != null ? String(row.next) : '')) {
|
||||
const next = parseNext(draft.next);
|
||||
if (next == null) {
|
||||
invalid = true;
|
||||
errorMessage = 'Nächste Nummer muss eine nicht-negative ganze Zahl sein.';
|
||||
} else {
|
||||
body.next = next;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
return;
|
||||
}
|
||||
if (invalid) {
|
||||
this.setState((prev) => ({
|
||||
drafts: {
|
||||
...prev.drafts,
|
||||
[id]: { ...prev.drafts[id], saveError: errorMessage },
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState((prev) => ({
|
||||
drafts: {
|
||||
...prev.drafts,
|
||||
[id]: { ...prev.drafts[id], busy: true, saveError: null },
|
||||
},
|
||||
}));
|
||||
try {
|
||||
const data = await this.context.number.update(id, body);
|
||||
requireObject(data, 'PATCH /api/number');
|
||||
requireObject(data.number, 'number');
|
||||
const updated = requireDefined(data.number, 'number');
|
||||
|
||||
const numbers = this.state.numbers.map((n) => (n.id === id ? { ...n, ...body } : n));
|
||||
await this.context.updateNumber();
|
||||
this.setState({
|
||||
numbers,
|
||||
drafts: {
|
||||
...this.state.drafts,
|
||||
[id]: {
|
||||
prefix: updated.prefix ?? '',
|
||||
padding: updated.padding != null ? String(updated.padding) : '',
|
||||
next: updated.next != null ? String(updated.next) : '',
|
||||
busy: false,
|
||||
saveError: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
enqueueSnackbar('Nummernkreis gespeichert.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
if (this._unmounted) return;
|
||||
this.setState((prev) => ({
|
||||
drafts: {
|
||||
...prev.drafts,
|
||||
[id]: { ...prev.drafts[id], busy: false, saveError: backendErrorFrom(err) },
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, error, numbers, drafts } = this.state;
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return <BackendError error={error} onRetry={this.handleRetry} />;
|
||||
}
|
||||
if (numbers.length === 0) {
|
||||
return (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Keine Nummernkreise.
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Typ</TableCell>
|
||||
<TableCell>Präfix</TableCell>
|
||||
<TableCell>Stellenzahl</TableCell>
|
||||
<TableCell>Nächste Nummer</TableCell>
|
||||
<TableCell align="right" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{numbers.map((row) => {
|
||||
const draft = drafts[row.id];
|
||||
if (!draft) return null;
|
||||
return (
|
||||
<TableRow key={row.id} hover>
|
||||
<TableCell>
|
||||
<Typography variant="body2">{row.type}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
value={draft.prefix}
|
||||
disabled={draft.busy}
|
||||
onChange={(e) => this.handleChange(row.id, 'prefix', e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
type="number"
|
||||
value={draft.padding}
|
||||
disabled={draft.busy}
|
||||
slotProps={{ htmlInput: { min: 1, step: 1 } }}
|
||||
onChange={(e) => this.handleChange(row.id, 'padding', e.target.value)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
type="number"
|
||||
value={draft.next}
|
||||
disabled={draft.busy}
|
||||
slotProps={{ htmlInput: { min: 0, step: 1 } }}
|
||||
onChange={(e) => this.handleChange(row.id, 'next', e.target.value)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={0}
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
>
|
||||
{draft.saveError ? (
|
||||
<Box sx={{ maxWidth: 220, mr: 1 }}>
|
||||
<Alert severity="error" sx={{ py: 0 }}>
|
||||
{typeof draft.saveError === 'string'
|
||||
? draft.saveError
|
||||
: draft.saveError.message}
|
||||
</Alert>
|
||||
</Box>
|
||||
) : null}
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Speichern"
|
||||
color="primary"
|
||||
disabled={draft.busy}
|
||||
onClick={() => void this.handleSave(row.id)}
|
||||
>
|
||||
<SaveOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
76
src/components/settings/SettingsDialog.jsx
Normal file
76
src/components/settings/SettingsDialog.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Component } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import NumbersIcon from '@mui/icons-material/Numbers';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import NumberSettingsPanel from './NumberSettingsPanel';
|
||||
import SettingsKeysPanel from './SettingsKeysPanel';
|
||||
|
||||
const TAB_KEYS = {
|
||||
keys: 'keys',
|
||||
numbers: 'numbers',
|
||||
};
|
||||
|
||||
const TABS = [
|
||||
{ id: TAB_KEYS.keys, label: 'Einstellungen', icon: <TuneIcon />, Panel: SettingsKeysPanel },
|
||||
{ id: TAB_KEYS.numbers, label: 'Nummernkreise', icon: <NumbersIcon />, Panel: NumberSettingsPanel },
|
||||
];
|
||||
|
||||
export default class SettingsDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { tab: TAB_KEYS.keys };
|
||||
this.handleTabChange = this.handleTabChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { open } = this.props;
|
||||
if (!open && prevProps.open) {
|
||||
this.setState({ tab: TAB_KEYS.keys });
|
||||
}
|
||||
}
|
||||
|
||||
handleTabChange(_event, value) {
|
||||
this.setState({ tab: value });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { open, onClose } = this.props;
|
||||
const { tab } = this.state;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Einstellungen</DialogTitle>
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={this.handleTabChange}
|
||||
variant="fullWidth"
|
||||
sx={{ px: 3 }}
|
||||
>
|
||||
{TABS.map((t) => (
|
||||
<Tab key={t.id} icon={t.icon} iconPosition="start" label={t.label} value={t.id} />
|
||||
))}
|
||||
</Tabs>
|
||||
<DialogContent dividers>
|
||||
<Box sx={{ display: 'grid' }}>
|
||||
{TABS.map((t) => (
|
||||
<Box
|
||||
key={t.id}
|
||||
sx={{
|
||||
gridArea: '1 / 1',
|
||||
visibility: t.id === tab ? undefined : 'hidden',
|
||||
}}
|
||||
>
|
||||
<t.Panel />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
237
src/components/settings/SettingsKeysPanel.jsx
Normal file
237
src/components/settings/SettingsKeysPanel.jsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import { Component } from 'react';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import BackendError from '../BackendError';
|
||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
|
||||
/**
|
||||
* Settings that may be set via PUT /api/company/setting/{key}.
|
||||
* Only these keys are accepted by the backend.
|
||||
*/
|
||||
const SETTABLE_KEYS = [
|
||||
'ui.theme',
|
||||
'keys.tavily',
|
||||
'keys.openrouter',
|
||||
'labelprinter.addr',
|
||||
'labelprinter.type',
|
||||
];
|
||||
|
||||
const KEY_LABELS = {
|
||||
'ui.theme': 'UI-Theme',
|
||||
'keys.tavily': 'Tavily API-Key',
|
||||
'keys.openrouter': 'OpenRouter API-Key',
|
||||
'labelprinter.addr': 'Labeldrucker-Adresse',
|
||||
'labelprinter.type': 'Labeldrucker-Typ',
|
||||
};
|
||||
|
||||
function keyLabel(key) {
|
||||
return KEY_LABELS[key] ?? key;
|
||||
}
|
||||
|
||||
export default class SettingsKeysPanel extends Component {
|
||||
static contextType = StorageManagerContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
status: 'loading',
|
||||
error: null,
|
||||
settings: [],
|
||||
// key → { value, busy, saveError }
|
||||
drafts: {},
|
||||
};
|
||||
this.handleRetry = this.handleRetry.bind(this);
|
||||
this.handleSave = this.handleSave.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
void this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const data = await this.context.setting.list();
|
||||
requireObject(data, 'GET /api/company/setting');
|
||||
const settings = requireArray(data.settings, 'settings');
|
||||
const drafts = Object.fromEntries(
|
||||
settings.map((s) => [
|
||||
s.key,
|
||||
{
|
||||
value: s.value ?? '',
|
||||
busy: false,
|
||||
saveError: null,
|
||||
},
|
||||
]),
|
||||
);
|
||||
this.setState({ status: 'ready', settings, drafts });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
this.setState({ status: 'failed', error: backendErrorFrom(err) });
|
||||
}
|
||||
}
|
||||
|
||||
handleRetry() {
|
||||
void this.load();
|
||||
}
|
||||
|
||||
handleChange(key, field, value) {
|
||||
this.setState((prev) => ({
|
||||
drafts: {
|
||||
...prev.drafts,
|
||||
[key]: { ...prev.drafts[key], [field]: value, saveError: null },
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async handleSave(key) {
|
||||
const draft = this.state.drafts[key];
|
||||
if (!draft || draft.busy) return;
|
||||
const row = this.state.settings.find((s) => s.key === key);
|
||||
if (!row) return;
|
||||
|
||||
const body = {};
|
||||
if (draft.value !== (row.value ?? '')) {
|
||||
body.value = draft.value;
|
||||
}
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState((prev) => ({
|
||||
drafts: {
|
||||
...prev.drafts,
|
||||
[key]: { ...prev.drafts[key], busy: true, saveError: null },
|
||||
},
|
||||
}));
|
||||
try {
|
||||
const data = await this.context.setting.upsert(key, body);
|
||||
requireObject(data, 'PUT /api/company/setting/:key');
|
||||
requireObject(data.setting, 'setting');
|
||||
const updated = requireDefined(data.setting, 'setting');
|
||||
|
||||
const settings = this.state.settings.map((s) =>
|
||||
s.key === key ? { ...s, ...body } : s,
|
||||
);
|
||||
this.setState({
|
||||
settings,
|
||||
drafts: {
|
||||
...this.state.drafts,
|
||||
[key]: {
|
||||
value: updated.value ?? '',
|
||||
busy: false,
|
||||
saveError: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
enqueueSnackbar('Einstellung gespeichert.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
if (this._unmounted) return;
|
||||
this.setState((prev) => ({
|
||||
drafts: {
|
||||
...prev.drafts,
|
||||
[key]: { ...prev.drafts[key], busy: false, saveError: backendErrorFrom(err) },
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, error, settings, drafts } = this.state;
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return <BackendError error={error} onRetry={this.handleRetry} />;
|
||||
}
|
||||
if (settings.length === 0) {
|
||||
return (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Keine Einstellungen.
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableBody>
|
||||
{SETTABLE_KEYS.map((key) => {
|
||||
const row = settings.find((s) => s.key === key);
|
||||
const draft = row ? drafts[row.key] : null;
|
||||
if (!row || !draft) return null;
|
||||
return (
|
||||
<TableRow key={row.key} hover>
|
||||
<TableCell>
|
||||
<Typography variant="body2">{keyLabel(row.key)}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
value={draft.value}
|
||||
disabled={draft.busy}
|
||||
onChange={(e) => this.handleChange(row.key, 'value', e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={0}
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
>
|
||||
{draft.saveError ? (
|
||||
<Box sx={{ maxWidth: 220, mr: 1 }}>
|
||||
<Alert severity="error" sx={{ py: 0 }}>
|
||||
{typeof draft.saveError === 'string'
|
||||
? draft.saveError
|
||||
: draft.saveError.message}
|
||||
</Alert>
|
||||
</Box>
|
||||
) : null}
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Speichern"
|
||||
color="primary"
|
||||
disabled={draft.busy}
|
||||
onClick={() => void this.handleSave(row.key)}
|
||||
>
|
||||
<SaveOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
3
src/components/settings/index.js
Normal file
3
src/components/settings/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as SettingsDialog } from './SettingsDialog';
|
||||
export { default as SettingsKeysPanel } from './SettingsKeysPanel';
|
||||
export { default as NumberSettingsPanel } from './NumberSettingsPanel';
|
||||
Reference in New Issue
Block a user