403 lines
12 KiB
JavaScript
403 lines
12 KiB
JavaScript
import { Component, PureComponent } from 'react';
|
|
import { EditablePanelTile } from 'dashgui/panels';
|
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
|
import { 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 Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = { pickerOpen: false, targetPillId: null };
|
|
}
|
|
|
|
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 } = this.state;
|
|
const { categories, rootCategoryId } = getCategoryState() ?? {};
|
|
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 (
|
|
<>
|
|
<Box sx={pillsContainerSx}>
|
|
{labels.map(({ id, label }) => (
|
|
<Chip
|
|
key={id}
|
|
label={label}
|
|
size="small"
|
|
variant="outlined"
|
|
onClick={editMode ? () => this.handleOpenMovePicker(id) : undefined}
|
|
onDelete={
|
|
editMode && categoryIds.length > 1
|
|
? () => this.handleRemoveCategory(id)
|
|
: undefined
|
|
}
|
|
sx={{
|
|
cursor: editMode ? 'pointer' : 'default',
|
|
...(editMode
|
|
? {
|
|
'&:hover': {
|
|
bgcolor: 'action.hover',
|
|
},
|
|
}
|
|
: {}),
|
|
}}
|
|
/>
|
|
))}
|
|
{editMode ? (
|
|
<IconButton
|
|
size="small"
|
|
onClick={this.handleOpenAddPicker}
|
|
sx={{ ml: 0.5, width: 24, height: 24 }}
|
|
>
|
|
<AddIcon fontSize="small" />
|
|
</IconButton>
|
|
) : null}
|
|
</Box>
|
|
{editMode ? (
|
|
<CategoryPickerDialog
|
|
open={pickerOpen}
|
|
onClose={this.handleClosePicker}
|
|
categories={categories}
|
|
rootCategoryId={rootCategoryId}
|
|
currentCategoryIds={categoryIds}
|
|
selectedId={targetPillId ?? rootCategoryId}
|
|
title={dialogTitle}
|
|
confirmLabel={confirmLabel}
|
|
mode="single"
|
|
onSelectCategory={this.handleCategorySelect}
|
|
/>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
class NameDisplay extends Component {
|
|
render() {
|
|
const { name, seo, categoryIds } = this.props;
|
|
return (
|
|
<Box sx={columnSx}>
|
|
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
|
<SingleLineTextEdit
|
|
label="Name"
|
|
value={name}
|
|
readOnly
|
|
onChange={() => {}}
|
|
sx={articleNameFieldHiddenSx}
|
|
/>
|
|
{seo ? (
|
|
<Box sx={{ px: 1.75, mt: -0.5, userSelect: 'text' }}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{seo}
|
|
</Typography>
|
|
</Box>
|
|
) : null}
|
|
</Box>
|
|
{(categoryIds?.length ?? 0) > 0 ? (
|
|
<CategoryPills categoryIds={categoryIds} editMode={false} />
|
|
) : null}
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
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 (
|
|
<Box sx={columnSx}>
|
|
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
|
<SingleLineTextEdit
|
|
label="Name"
|
|
value={name}
|
|
onChange={this.handleNameChange}
|
|
sx={articleNameFieldSx}
|
|
/>
|
|
<Box sx={{ mt: 1 }}>
|
|
<SingleLineTextEdit
|
|
label="SEO"
|
|
value={seo}
|
|
size="small"
|
|
onChange={this.handleSeoChange}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
<CategoryPills
|
|
categoryIds={categoryIds}
|
|
editMode
|
|
onCategoryIdsChange={this.handleCategoryIdsChange}
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
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 (
|
|
<EditablePanelTile
|
|
wide={3}
|
|
minHeight={0}
|
|
sx={headerTileSx}
|
|
color={WHITE}
|
|
value={value}
|
|
onSave={this.handleSave}
|
|
onError={notifyError}
|
|
isDirty={() => draft != null && draftDirty(draft, value)}
|
|
canSave={() => draft != null && draftValid(draft)}
|
|
display={
|
|
<NameDisplay
|
|
name={value.name}
|
|
seo={value.seo}
|
|
categoryIds={value.categoryIds}
|
|
/>
|
|
}
|
|
edit={
|
|
<NameEdit
|
|
name={value.name}
|
|
seo={value.seo}
|
|
categoryIds={value.categoryIds}
|
|
onNameDraftChange={this.handleNameDraftChange}
|
|
onSeoDraftChange={this.handleSeoDraftChange}
|
|
onCategoryIdsDraftChange={this.handleCategoryIdsDraftChange}
|
|
/>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
}
|