This commit is contained in:
seb
2026-08-10 03:13:53 +02:00
parent 7a1c6e2d94
commit 8bbd6fade1
8 changed files with 682 additions and 172 deletions

View File

@@ -1,4 +1,14 @@
import NameDialog from '../NameDialog';
import { Component } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Alert from '@mui/material/Alert';
import BackendError from '../BackendError';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { backendErrorFrom, isBackendFailure } from '../../utils/backendError';
const TITLES = {
create: 'Neue Kategorie',
@@ -10,21 +20,206 @@ const SUBMIT_LABELS = {
edit: 'Speichern',
};
export default function CategoryNameDialog({
open,
mode = 'create',
initialName,
onClose,
onSubmit,
}) {
return (
<NameDialog
open={open}
title={TITLES[mode]}
submitLabel={SUBMIT_LABELS[mode]}
initialName={initialName}
onClose={onClose}
onSubmit={onSubmit}
/>
);
const BULK_MODE = 'bulk';
const SINGLE_MODE = 'single';
/** Split a bulk textarea into non-empty, trimmed names (one per line). */
function parseBulkNames(text) {
return String(text ?? '')
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
}
/**
* Category name dialog.
* Single mode: one name (create/edit). Bulk mode ("Sammelanlegen", create
* only): one name per line. Props: open, mode ('create' | 'edit'), initialName,
* onClose, onSubmit({ names: string[] }).
*/
class CategoryNameDialog extends Component {
constructor(props) {
super(props);
this.state = {
mode: SINGLE_MODE,
name: '',
bulkText: '',
busy: false,
error: null,
};
this.handleClose = this.handleClose.bind(this);
this.handleSingleSubmit = this.handleSingleSubmit.bind(this);
this.handleBulkSubmit = this.handleBulkSubmit.bind(this);
this.openBulk = this.openBulk.bind(this);
}
componentDidUpdate(prevProps) {
if (this.props.open && !prevProps.open) {
this.setState({
mode: SINGLE_MODE,
name: this.props.initialName ?? '',
bulkText: '',
busy: false,
error: null,
});
}
}
handleClose() {
if (this.state.busy) return;
this.props.onClose();
}
async handleSingleSubmit(event) {
event.preventDefault();
const name = this.state.name.trim();
if (!name) {
this.setState({ error: 'Bitte Name eingeben.' });
return;
}
this.setState({ busy: true, error: null });
try {
await this.props.onSubmit({ names: [name] });
this.props.onClose();
} catch (err) {
this.setState({ error: backendErrorFrom(err), busy: false });
}
}
async handleBulkSubmit(event) {
event.preventDefault();
const names = parseBulkNames(this.state.bulkText);
if (names.length === 0) {
this.setState({ error: 'Bitte mindestens einen Namen eingeben.' });
return;
}
this.setState({ busy: true, error: null });
try {
await this.props.onSubmit({ names });
this.props.onClose();
} catch (err) {
this.setState({ error: backendErrorFrom(err), busy: false });
}
}
openBulk() {
if (this.state.busy) return;
this.setState({ mode: BULK_MODE, error: null });
}
render() {
const { open, mode = 'create' } = this.props;
const { mode: formMode, name, bulkText, busy, error } = this.state;
const isBulk = formMode === BULK_MODE;
const canBulk = mode === 'create';
const canSubmitSingle = name.trim() !== '';
const bulkNames = parseBulkNames(bulkText);
const canSubmitBulk = bulkNames.length > 0;
return (
<Dialog open={open} onClose={this.handleClose} maxWidth="xs" fullWidth>
<DialogTitle>{TITLES[mode]}</DialogTitle>
{!isBulk && (
<Box component="form" onSubmit={this.handleSingleSubmit} noValidate>
<DialogContent>
{error ? (
isBackendFailure(error) ? (
<BackendError error={error} sx={{ mb: 2 }} />
) : (
<Alert severity="error" sx={{ mb: 2 }}>
{typeof error === 'string' ? error : error.message}
</Alert>
)
) : null}
<SingleLineTextEdit
required
autoFocus
label="Name"
value={name}
disabled={busy}
onChange={(nextName) => this.setState({ name: nextName })}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
{canBulk && (
<Button type="button" onClick={this.openBulk} disabled={busy}>
Sammelanlegen
</Button>
)}
<Box sx={{ flexGrow: 1 }} />
<Button type="button" onClick={this.handleClose} disabled={busy}>
Abbrechen
</Button>
<Button
type="submit"
variant="contained"
disabled={busy || !canSubmitSingle}
>
{SUBMIT_LABELS[mode]}
</Button>
</DialogActions>
</Box>
)}
{isBulk && (
<Box component="form" onSubmit={this.handleBulkSubmit} noValidate>
<DialogContent>
{error ? (
isBackendFailure(error) ? (
<BackendError error={error} sx={{ mb: 2 }} />
) : (
<Alert severity="error" sx={{ mb: 2 }}>
{typeof error === 'string' ? error : error.message}
</Alert>
)
) : null}
<SingleLineTextEdit
autoFocus
multiline
minRows={6}
label="Namen (einer pro Zeile)"
placeholder={'Kategorie A\nKategorie B\nKategorie C'}
value={bulkText}
disabled={busy}
onChange={(nextText) => this.setState({ bulkText: nextText })}
helperText={
bulkNames.length > 0
? `${bulkNames.length} Kategorien werden angelegt.`
: 'Ein Name pro Zeile eingeben.'
}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button
type="button"
onClick={() => this.setState({ mode: SINGLE_MODE, error: null })}
disabled={busy}
>
Einzel
</Button>
<Box sx={{ flexGrow: 1 }} />
<Button type="button" onClick={this.handleClose} disabled={busy}>
Abbrechen
</Button>
<Button
type="submit"
variant="contained"
disabled={busy || !canSubmitBulk}
>
Anlegen ({bulkNames.length})
</Button>
</DialogActions>
</Box>
)}
</Dialog>
);
}
}
export default CategoryNameDialog;

View File

@@ -1,7 +1,10 @@
import { Component } from 'react';
import { connect } from 'react-redux';
import { ActionTile } from '../tiles';
import { createCategory as createCategoryThunk } from '../../store/categorySlice';
import {
createCategories as createCategoriesThunk,
createCategory as createCategoryThunk,
} from '../../store/categorySlice';
import CategoryNameDialog from './CategoryNameDialog';
class NewCategoryTile extends Component {
@@ -21,9 +24,14 @@ class NewCategoryTile extends Component {
this.setState({ open: false });
}
async handleSubmit({ name }) {
async handleSubmit({ names }) {
const { parentId } = this.props;
await this.props.createCategory({ parentId, name }).unwrap();
const list = (names ?? []).filter(Boolean);
if (list.length <= 1) {
await this.props.createCategory({ parentId, name: list[0] ?? '' }).unwrap();
return;
}
await this.props.createCategories({ parentId, names: list }).unwrap();
}
render() {
@@ -49,6 +57,7 @@ class NewCategoryTile extends Component {
const mapDispatchToProps = {
createCategory: createCategoryThunk,
createCategories: createCategoriesThunk,
};
export default connect(null, mapDispatchToProps)(NewCategoryTile);