281 lines
9.0 KiB
JavaScript
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>
|
|
);
|
|
}
|
|
}
|