import { Component, PureComponent } from 'react'; import { EditablePanelTile } from 'dashgui/panels'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; import categoryAutomat, { getCategoryState } from '../../../automats/categoryAutomat'; import artikelAutomat from '../../../automats/artikelAutomat'; import { getCategoryBreadcrumbs, categoryDisplayName } from '../../../utils/categoryUtils'; import { Box, Chip, IconButton, Typography } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import CategoryPickerDialog from '../CategoryPickerDialog'; import { notifyError } from '../../../utils/notify'; import { WHITE, } from '../../../colors'; const articleNameTextSx = { fontWeight: 700, lineHeight: 1.2, }; const articleNameFieldSx = { '& .MuiInputBase-input': { fontSize: '2.125rem', ...articleNameTextSx, }, }; const articleNameFieldHiddenSx = { ...articleNameFieldSx, userSelect: 'text', '& .MuiOutlinedInput-notchedOutline': { borderColor: 'transparent', }, '& .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline': { borderColor: 'transparent', }, '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: 'transparent', }, '& .MuiInputLabel-root': { visibility: 'hidden', }, }; const pillsContainerSx = { display: 'flex', flexWrap: 'wrap', gap: 0.5, mt: 'auto', }; const columnSx = { display: 'flex', flexDirection: 'column', flex: 1, alignSelf: 'stretch', }; function categoryIdsChanged(a, b) { const aa = a ?? []; const bb = b ?? []; return ( aa.length !== bb.length || [...aa].map(String).sort().join(',') !== [...bb].map(String).sort().join(',') ); } function draftDirty(draft, saved) { const nameDiff = draft.name.trim() !== String(saved.name ?? '').trim(); const seoDiff = String(draft.seo ?? '').trim() !== String(saved.seo ?? '').trim(); const catDiff = categoryIdsChanged(draft.categoryIds, saved.categoryIds); return nameDiff || seoDiff || catDiff; } function draftValid(draft) { return draft.name.trim().length > 0 && (draft.categoryIds?.length ?? 0) > 0; } const headerTileSx = { minHeight: 0, }; function buildCategoryLabel(categories, categoryId, rootCategoryId) { if (!categories || categoryId == null) return ''; const crumbs = getCategoryBreadcrumbs(categories, categoryId, rootCategoryId); if (crumbs.length === 0) return ''; return crumbs.map((c) => c.name).join(' | '); } function categoryLabels(categoryIds, categories, rootCategoryId) { return (categoryIds ?? []) .map((id) => ({ id, label: buildCategoryLabel(categories, id, rootCategoryId) })) .filter((c) => c.label); } class CategoryPills extends PureComponent { constructor(props) { super(props); const catState = getCategoryState(); this.state = { pickerOpen: false, targetPillId: null, categories: catState?.categories ?? categoryAutomat.state.categories, rootCategoryId: catState?.rootCategoryId ?? categoryAutomat.state.rootCategoryId, }; } componentDidMount() { this._unsubCat = categoryAutomat.subscribe((s) => { this.setState({ categories: s.categories, rootCategoryId: s.rootCategoryId, }); }); } componentWillUnmount() { this._unsubCat?.(); } handleRemoveCategory = (removeId) => { const { categoryIds, onCategoryIdsChange } = this.props; const remaining = categoryIds.filter((id) => String(id) !== String(removeId)); onCategoryIdsChange?.(remaining); }; handleOpenAddPicker = () => { this.setState({ pickerOpen: true, targetPillId: null }); }; handleOpenMovePicker = (pillId) => { this.setState({ pickerOpen: true, targetPillId: pillId }); }; handleClosePicker = () => { this.setState({ pickerOpen: false, targetPillId: null }); }; handleCategorySelect = (selectedCategoryId) => { const { categoryIds, onCategoryIdsChange } = this.props; const { targetPillId } = this.state; if (targetPillId != null) { const updated = categoryIds.map((id) => String(id) === String(targetPillId) ? selectedCategoryId : id ); const deduped = [...new Set(updated)]; onCategoryIdsChange?.(deduped); } else { if (!categoryIds.some((id) => String(id) === String(selectedCategoryId))) { onCategoryIdsChange?.([...categoryIds, selectedCategoryId]); } } }; render() { const { categoryIds, editMode } = this.props; const { pickerOpen, targetPillId, categories, rootCategoryId } = this.state; const labels = categoryLabels(categoryIds, categories, rootCategoryId); const targetNode = categories && targetPillId != null ? categories[String(targetPillId)] : null; const targetLabel = targetNode ? categoryDisplayName(targetNode.name, targetNode.id, rootCategoryId) : ''; const dialogTitle = targetPillId != null ? (targetLabel ? `Artikel aus Kategorie „${targetLabel}“ verschieben in…` : 'Artikel aus Kategorie verschieben in…') : 'Kategorie hinzufügen'; const confirmLabel = targetPillId != null ? 'Hierher verschieben' : 'Hinzufügen'; return ( <> {labels.map(({ id, label }) => ( this.handleOpenMovePicker(id) : undefined} onDelete={ editMode && categoryIds.length > 1 ? () => this.handleRemoveCategory(id) : undefined } sx={{ cursor: editMode ? 'pointer' : 'default', ...(editMode ? { '&:hover': { bgcolor: 'action.hover', }, } : {}), }} /> ))} {editMode ? ( ) : null} {editMode ? ( ) : null} ); } } class NameDisplay extends Component { render() { const { name, seo, categoryIds } = this.props; return ( {}} sx={articleNameFieldHiddenSx} /> {seo ? ( {seo} ) : null} {(categoryIds?.length ?? 0) > 0 ? ( ) : null} ); } } class NameEdit extends Component { constructor(props) { super(props); this.state = { name: props.name, seo: props.seo ?? '', categoryIds: [...(props.categoryIds ?? [])], }; } componentDidMount() { const { onNameDraftChange, onSeoDraftChange, onCategoryIdsDraftChange } = this.props; onNameDraftChange?.(this.state.name); onSeoDraftChange?.(this.state.seo); onCategoryIdsDraftChange?.(this.state.categoryIds); } handleNameChange = (name) => { this.setState({ name }); this.props.onNameDraftChange?.(name); }; handleSeoChange = (seo) => { this.setState({ seo }); this.props.onSeoDraftChange?.(seo); }; handleCategoryIdsChange = (categoryIds) => { this.setState({ categoryIds }); this.props.onCategoryIdsDraftChange?.(categoryIds); }; render() { const { name, seo, categoryIds } = this.state; return ( ); } } export default class ArticleViewName extends PureComponent { constructor(props) { super(props); this.state = { nameDraft: null, seoDraft: null, categoryIdsDraft: null, }; } handleNameDraftChange = (name) => { this.setState({ nameDraft: name }); }; handleSeoDraftChange = (seo) => { this.setState({ seoDraft: seo }); }; handleCategoryIdsDraftChange = (categoryIds) => { this.setState({ categoryIdsDraft: categoryIds }); }; handleSave = async () => { const { article: articleData, onArticleUpdated } = this.props; const { nameDraft, seoDraft, categoryIdsDraft } = this.state; const name = nameDraft != null ? nameDraft.trim() : String(articleData.name ?? '').trim(); const seo = seoDraft != null ? seoDraft.trim() : String(articleData.seo ?? articleData.seoDescription ?? '').trim(); const categoryIds = categoryIdsDraft ?? (articleData.categoryIds ?? []); const nameChanged = name !== String(articleData.name ?? '').trim(); const seoChanged = seo !== String(articleData.seo ?? articleData.seoDescription ?? '').trim(); const categoriesChanged = categoryIdsChanged(categoryIds, articleData.categoryIds); let article = null; if (nameChanged || seoChanged) { const updateBody = {}; if (nameChanged) updateBody.name = name; if (seoChanged) updateBody.seo = seo; const res = await artikelAutomat.actions.update(articleData.sku, updateBody); article = res.article; } if (categoriesChanged) { const catRes = await artikelAutomat.actions.setCategories(articleData.sku, categoryIds); article = catRes.article; } if (article) onArticleUpdated?.(article); this.setState({ nameDraft: null, seoDraft: null, categoryIdsDraft: null }); }; render() { const { article: articleData } = this.props; const { nameDraft, seoDraft, categoryIdsDraft } = this.state; const savedCategoryIds = articleData.categoryIds ?? []; const savedSeo = articleData.seo ?? articleData.seoDescription ?? ''; const value = { name: articleData.name ?? '', seo: savedSeo, categoryIds: [...savedCategoryIds], }; const draft = nameDraft != null && categoryIdsDraft != null ? { name: nameDraft, seo: seoDraft ?? savedSeo, categoryIds: categoryIdsDraft, } : null; return ( draft != null && draftDirty(draft, value)} canSave={() => draft != null && draftValid(draft)} display={ } edit={ } /> ); } }