Files
dashfront/src/components/article/CategoryPickerDialog.jsx
2026-09-22 13:45:22 +02:00

281 lines
9.0 KiB
JavaScript

import { PureComponent } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import CloseIcon from '@mui/icons-material/Close';
import CheckIcon from '@mui/icons-material/Check';
import FolderIcon from '@mui/icons-material/Folder';
import Breadcrumbs from '@mui/material/Breadcrumbs';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import CategoryTree from '../category/CategoryTree';
import categoryAutomat from '../../automats/categoryAutomat';
import { getCategoryBreadcrumbs, categoryDisplayName } from '../../utils/categoryUtils';
const EMPTY_ARRAY = [];
export default class CategoryPickerDialog extends PureComponent {
constructor(props) {
super(props);
const categories = props.categories ?? categoryAutomat.state.categories;
const rootCategoryId = props.rootCategoryId ?? categoryAutomat.state.rootCategoryId;
const currentCategoryIds = props.currentCategoryIds ?? EMPTY_ARRAY;
let selectedId = null;
if (props.selectedId != null) {
selectedId = props.selectedId;
} else if (currentCategoryIds.length > 0) {
selectedId = currentCategoryIds[0];
} else if (rootCategoryId != null) {
selectedId = rootCategoryId;
}
this.state = {
categories,
rootCategoryId,
selectedId,
};
}
componentDidMount() {
if (this.props.categories === undefined || this.props.rootCategoryId === undefined) {
this._unsub = categoryAutomat.subscribe((s) => {
this.setState((prev) => ({
categories: this.props.categories ?? s.categories,
rootCategoryId: this.props.rootCategoryId ?? s.rootCategoryId,
}));
});
}
}
componentDidUpdate(prevProps) {
if (this.props.open && !prevProps.open) {
const currentCategoryIds = this.props.currentCategoryIds ?? EMPTY_ARRAY;
let nextSelectedId = null;
if (this.props.selectedId != null) {
nextSelectedId = this.props.selectedId;
} else if (currentCategoryIds.length > 0) {
nextSelectedId = currentCategoryIds[0];
} else if (this.state.rootCategoryId != null) {
nextSelectedId = this.state.rootCategoryId;
}
this.setState({ selectedId: nextSelectedId });
}
if (
this.props.categories !== prevProps.categories ||
this.props.rootCategoryId !== prevProps.rootCategoryId
) {
this.setState({
categories: this.props.categories ?? categoryAutomat.state.categories,
rootCategoryId: this.props.rootCategoryId ?? categoryAutomat.state.rootCategoryId,
});
}
}
componentWillUnmount() {
this._unsub?.();
}
handleSelect = (id) => {
this.setState({ selectedId: id });
};
handleDoubleClick = (id) => {
this.handleConfirmWithId(id);
};
handleConfirmWithId = (targetId) => {
if (targetId == null) return;
const { disabledIds = EMPTY_ARRAY, excludeIds = EMPTY_ARRAY, mode = 'assign', onSelect, onSelectCategory, onClose } = this.props;
if (disabledIds.some((id) => String(id) === String(targetId))) return;
if (excludeIds.some((id) => String(id) === String(targetId))) return;
const { categories } = this.state;
const targetNode = categories ? categories[String(targetId)] : null;
if (mode === 'single') {
onSelectCategory?.(targetId, targetNode);
onSelect?.(targetId, targetNode);
} else {
const currentCategoryIds = this.props.currentCategoryIds ?? EMPTY_ARRAY;
const ids = currentCategoryIds.filter((id) => String(id) !== String(targetId));
ids.push(targetId);
onSelectCategory?.(targetId, targetNode);
onSelect?.(ids);
}
onClose?.();
};
handleConfirm = () => {
this.handleConfirmWithId(this.state.selectedId);
};
render() {
const {
open,
onClose,
currentCategoryIds = EMPTY_ARRAY,
title,
confirmLabel,
mode = 'assign',
disabledIds = EMPTY_ARRAY,
excludeIds = EMPTY_ARRAY,
} = this.props;
const { categories, rootCategoryId, selectedId } = this.state;
const dialogTitle = title || (mode === 'single' ? 'Kategorie auswählen' : 'Kategorie zuweisen');
const actionLabel = confirmLabel || (mode === 'single' ? 'Auswählen' : 'Zuweisen');
const selectedNode = categories && selectedId != null ? categories[String(selectedId)] : null;
const isAlreadyAssigned =
mode !== 'single' && currentCategoryIds.some((id) => String(id) === String(selectedId));
const isSelectedDisabled =
selectedId == null ||
disabledIds.some((id) => String(id) === String(selectedId)) ||
excludeIds.some((id) => String(id) === String(selectedId));
const breadcrumbs =
categories && selectedId != null
? getCategoryBreadcrumbs(categories, selectedId, rootCategoryId)
: EMPTY_ARRAY;
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
slotProps={{
paper: {
sx: {
height: '75vh',
minHeight: '75vh',
maxHeight: 700,
display: 'flex',
flexDirection: 'column',
borderRadius: 2.5,
overflow: 'hidden',
},
},
}}
PaperProps={{
sx: {
height: '75vh',
minHeight: '75vh',
maxHeight: 700,
display: 'flex',
flexDirection: 'column',
borderRadius: 2.5,
overflow: 'hidden',
},
}}
>
{/* Header */}
<DialogTitle
sx={{
p: 2,
pb: 1.5,
borderBottom: 1,
borderColor: 'divider',
bgcolor: 'background.paper',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<FolderIcon color="primary" />
<Typography variant="h6" component="span" sx={{ fontWeight: 600 }}>
{dialogTitle}
</Typography>
</Box>
<IconButton size="small" onClick={onClose} aria-label="Schließen">
<CloseIcon fontSize="small" />
</IconButton>
</Box>
{/* Breadcrumb Path Preview */}
{breadcrumbs.length > 0 && (
<Breadcrumbs
separator={<NavigateNextIcon sx={{ fontSize: 14 }} />}
sx={{ mt: 1, '& .MuiBreadcrumbs-li': { fontSize: '0.8rem' } }}
>
{breadcrumbs.map((bc, idx) => {
const isLast = idx === breadcrumbs.length - 1;
return (
<Typography
key={bc.id}
variant="caption"
color={isLast ? 'primary.main' : 'text.secondary'}
fontWeight={isLast ? 600 : 400}
>
{categoryDisplayName(bc.name, bc.id, rootCategoryId)}
</Typography>
);
})}
</Breadcrumbs>
)}
</DialogTitle>
{/* Tree Content */}
<DialogContent sx={{ p: 0, flex: 1, minHeight: 0, overflow: 'hidden' }}>
<CategoryTree
categories={categories}
rootCategoryId={rootCategoryId}
selectedId={selectedId}
onSelect={this.handleSelect}
onDoubleClick={this.handleDoubleClick}
alreadyAssignedIds={currentCategoryIds}
disabledIds={disabledIds}
excludeIds={excludeIds}
allowAdd={true}
allowDelete={true}
allowReorder={true}
allowRename={true}
showSearch={true}
sx={{ height: '100%' }}
/>
</DialogContent>
{/* Footer / Actions */}
<DialogActions
sx={{
p: 2,
borderTop: 1,
borderColor: 'divider',
bgcolor: 'background.paper',
display: 'flex',
alignItems: 'center',
justifyContent: isAlreadyAssigned ? 'space-between' : 'flex-end',
}}
>
{isAlreadyAssigned && (
<Typography variant="caption" color="text.secondary">
(bereits zugewiesen)
</Typography>
)}
<Box sx={{ display: 'flex', gap: 1 }}>
<Button onClick={onClose} color="inherit">
Abbrechen
</Button>
<Button
variant="contained"
color="primary"
startIcon={<CheckIcon />}
onClick={this.handleConfirm}
disabled={isSelectedDisabled}
>
{actionLabel}
</Button>
</Box>
</DialogActions>
</Dialog>
);
}
}