Files
dashfront/src/components/article/CategoryPickerDialog.jsx
seb 78be835388 Add category picker to article view with draft-based editing
- 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.
2026-08-03 08:57:32 +02:00

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>
);
}