- Add setCategories API method for PUT /api/article/:sku/categories - Create CategoryPickerDialog component for selecting categories - Add CategoryPills component with add/remove functionality - Category changes are now draft-based (like name editing) - Save button commits both name and category changes together - Delete icons hidden when only one category remains - Plus button only visible in edit mode Implements category assignment flow similar to name editing pattern.
159 lines
5.0 KiB
JavaScript
159 lines
5.0 KiB
JavaScript
import { useState } 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 List from '@mui/material/List';
|
|
import ListItemButton from '@mui/material/ListItemButton';
|
|
import ListItemText from '@mui/material/ListItemText';
|
|
import Collapse from '@mui/material/Collapse';
|
|
import ExpandLess from '@mui/icons-material/ExpandLess';
|
|
import ExpandMore from '@mui/icons-material/ExpandMore';
|
|
import CheckIcon from '@mui/icons-material/Check';
|
|
import FolderIcon from '@mui/icons-material/Folder';
|
|
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
|
import Box from '@mui/material/Box';
|
|
import { getCategoryChildren, categoryDisplayName } from '../../store/categorySlice';
|
|
function CategoryNode({
|
|
categories,
|
|
categoryId,
|
|
rootCategoryId,
|
|
selectedId,
|
|
onSelect,
|
|
expandedIds,
|
|
onToggleExpand,
|
|
depth = 0,
|
|
}) {
|
|
const node = categories[String(categoryId)];
|
|
if (!node) return null;
|
|
|
|
const children = getCategoryChildren(categories, categoryId);
|
|
const hasChildren = children.length > 0;
|
|
const isSelected = selectedId != null && String(selectedId) === String(categoryId);
|
|
const isExpanded = expandedIds.has(categoryId);
|
|
const label = categoryDisplayName(node.name, node.id, rootCategoryId);
|
|
|
|
return (
|
|
<>
|
|
<ListItemButton
|
|
selected={isSelected}
|
|
onClick={() => onSelect(categoryId)}
|
|
sx={{ pl: 1 + depth * 1.5 }}
|
|
>
|
|
<Box sx={{ minWidth: 24, display: 'flex', alignItems: 'center' }}>
|
|
{hasChildren ? (
|
|
isExpanded ? <FolderOpenIcon fontSize="small" /> : <FolderIcon fontSize="small" />
|
|
) : null}
|
|
</Box>
|
|
<ListItemText primary={label} />
|
|
{isSelected && <CheckIcon fontSize="small" color="primary" sx={{ ml: 1 }} />}
|
|
{hasChildren && (
|
|
isExpanded ? (
|
|
<ExpandLess onClick={(e) => { e.stopPropagation(); onToggleExpand(categoryId, false); }} />
|
|
) : (
|
|
<ExpandMore onClick={(e) => { e.stopPropagation(); onToggleExpand(categoryId, true); }} />
|
|
)
|
|
)}
|
|
</ListItemButton>
|
|
{hasChildren && (
|
|
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
|
<List disablePadding>
|
|
{children.map((child) => (
|
|
<CategoryNode
|
|
key={child.id}
|
|
categories={categories}
|
|
categoryId={child.id}
|
|
rootCategoryId={rootCategoryId}
|
|
selectedId={selectedId}
|
|
onSelect={onSelect}
|
|
expandedIds={expandedIds}
|
|
onToggleExpand={onToggleExpand}
|
|
depth={depth + 1}
|
|
/>
|
|
))}
|
|
</List>
|
|
</Collapse>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function buildDefaultExpanded(categories, rootCategoryId, currentCategoryIds) {
|
|
const ids = new Set();
|
|
if (!categories || !rootCategoryId) return ids;
|
|
for (const catId of currentCategoryIds ?? []) {
|
|
let id = catId;
|
|
while (id != null) {
|
|
const node = categories[String(id)];
|
|
if (!node) break;
|
|
ids.add(id);
|
|
if (node.parentId == null || node.parentId === 0) break;
|
|
id = node.parentId;
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
export default function CategoryPickerDialog({
|
|
open,
|
|
onClose,
|
|
categories,
|
|
rootCategoryId,
|
|
currentCategoryIds,
|
|
onSelect,
|
|
}) {
|
|
const [selectedId, setSelectedId] = useState(null);
|
|
const [expandedIds, setExpandedIds] = useState(
|
|
() => buildDefaultExpanded(categories, rootCategoryId, currentCategoryIds),
|
|
);
|
|
|
|
const handleToggleExpand = (id, expand) => {
|
|
setExpandedIds((prev) => {
|
|
const next = new Set(prev);
|
|
if (expand) next.add(id);
|
|
else next.delete(id);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const handleConfirm = () => {
|
|
if (selectedId == null) return;
|
|
const categoryIds = currentCategoryIds.filter((id) => String(id) !== String(selectedId));
|
|
categoryIds.push(selectedId);
|
|
onSelect?.(categoryIds);
|
|
onClose();
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
|
<DialogTitle>Kategorie zuweisen</DialogTitle>
|
|
<DialogContent dividers sx={{ p: 0 }}>
|
|
{categories && rootCategoryId != null ? (
|
|
<List dense disablePadding>
|
|
<CategoryNode
|
|
categories={categories}
|
|
categoryId={rootCategoryId}
|
|
rootCategoryId={rootCategoryId}
|
|
selectedId={selectedId}
|
|
onSelect={setSelectedId}
|
|
expandedIds={expandedIds}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
</List>
|
|
) : (
|
|
<Box sx={{ p: 2, textAlign: 'center', color: 'text.secondary' }}>
|
|
Lade Kategorien…
|
|
</Box>
|
|
)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={onClose}>Abbrechen</Button>
|
|
<Button onClick={handleConfirm} disabled={selectedId == null} variant="contained">
|
|
Zuweisen
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
}
|