276 lines
7.3 KiB
JavaScript
276 lines
7.3 KiB
JavaScript
import { Component } from 'react';
|
|
import { EditablePanelTile } from '../../panels';
|
|
import SingleLineTextEdit from '../../SingleLineTextEdit';
|
|
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
|
import { requireObject } from '../../../utils/apiShape';
|
|
import { getCategoryBreadcrumbs } from '../../../store/categorySlice';
|
|
import { Box, Chip, IconButton } from '@mui/material';
|
|
import AddIcon from '@mui/icons-material/Add';
|
|
import CategoryPickerDialog from '../CategoryPickerDialog';
|
|
|
|
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 draftDirty(draft, saved) {
|
|
const nameDiff = draft.name.trim() !== String(saved.name ?? '').trim();
|
|
const a = draft.categoryIds ?? [];
|
|
const b = saved.categoryIds ?? [];
|
|
const catDiff =
|
|
a.length !== b.length ||
|
|
[...a].map(String).sort().join(',') !== [...b].map(String).sort().join(',');
|
|
return nameDiff || 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 };
|
|
}
|
|
|
|
handleRemoveCategory = (removeId) => {
|
|
const { categoryIds, onCategoryIdsChange } = this.props;
|
|
const remaining = categoryIds.filter((id) => String(id) !== String(removeId));
|
|
onCategoryIdsChange?.(remaining);
|
|
};
|
|
|
|
handleOpenPicker = () => {
|
|
this.setState({ pickerOpen: true });
|
|
};
|
|
|
|
handleClosePicker = () => {
|
|
this.setState({ pickerOpen: false });
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
categoryLabels: labels,
|
|
categoryIds,
|
|
categories,
|
|
rootCategoryId,
|
|
sku,
|
|
editMode,
|
|
onCategoryIdsChange,
|
|
} = this.props;
|
|
const { pickerOpen } = this.state;
|
|
|
|
return (
|
|
<>
|
|
<Box sx={pillsContainerSx}>
|
|
{labels.map(({ id, label }) => (
|
|
<Chip
|
|
key={id}
|
|
label={label}
|
|
size="small"
|
|
variant="outlined"
|
|
onDelete={
|
|
editMode && categoryIds.length > 1
|
|
? () => this.handleRemoveCategory(id)
|
|
: undefined
|
|
}
|
|
/>
|
|
))}
|
|
{editMode ? (
|
|
<IconButton
|
|
size="small"
|
|
onClick={this.handleOpenPicker}
|
|
sx={{ ml: 0.5, width: 24, height: 24 }}
|
|
>
|
|
<AddIcon fontSize="small" />
|
|
</IconButton>
|
|
) : null}
|
|
</Box>
|
|
<CategoryPickerDialog
|
|
key={sku}
|
|
open={pickerOpen}
|
|
onClose={this.handleClosePicker}
|
|
categories={categories}
|
|
rootCategoryId={rootCategoryId}
|
|
currentCategoryIds={categoryIds}
|
|
onSelect={onCategoryIdsChange}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
class NameDisplay extends Component {
|
|
render() {
|
|
const { name, categoryIds, categories, rootCategoryId, sku } = this.props;
|
|
const labels = categoryLabels(categoryIds, categories, rootCategoryId);
|
|
return (
|
|
<Box sx={columnSx}>
|
|
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center' }}>
|
|
<SingleLineTextEdit
|
|
label="Name"
|
|
value={name}
|
|
readOnly
|
|
onChange={() => {}}
|
|
sx={articleNameFieldHiddenSx}
|
|
/>
|
|
</Box>
|
|
{labels.length > 0 ? (
|
|
<CategoryPills
|
|
categoryLabels={labels}
|
|
categoryIds={categoryIds}
|
|
categories={categories}
|
|
rootCategoryId={rootCategoryId}
|
|
sku={sku}
|
|
editMode={false}
|
|
/>
|
|
) : null}
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
class NameEdit extends Component {
|
|
render() {
|
|
const {
|
|
draft,
|
|
onChange,
|
|
inputRef,
|
|
categories,
|
|
rootCategoryId,
|
|
sku,
|
|
} = this.props;
|
|
const labels = categoryLabels(draft.categoryIds, categories, rootCategoryId);
|
|
return (
|
|
<Box sx={columnSx}>
|
|
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center' }}>
|
|
<SingleLineTextEdit
|
|
inputRef={inputRef}
|
|
label="Name"
|
|
value={draft.name}
|
|
onChange={(name) => onChange({ ...draft, name })}
|
|
sx={articleNameFieldSx}
|
|
/>
|
|
</Box>
|
|
<CategoryPills
|
|
categoryLabels={labels}
|
|
categoryIds={draft.categoryIds}
|
|
categories={categories}
|
|
rootCategoryId={rootCategoryId}
|
|
sku={sku}
|
|
editMode
|
|
onCategoryIdsChange={(categoryIds) => onChange({ ...draft, categoryIds })}
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default class ArticleViewName extends Component {
|
|
static contextType = StorageManagerContext;
|
|
|
|
handleSave = async (draft) => {
|
|
const { article: articleData, onArticleUpdated } = this.props;
|
|
const store = this.context;
|
|
const name = draft.name.trim();
|
|
const categoryIds = draft.categoryIds ?? [];
|
|
|
|
const [nameRes] = await Promise.all([
|
|
store.article.update(articleData.sku, { name }),
|
|
store.article.setCategories(articleData.sku, { categoryIds }),
|
|
]);
|
|
|
|
requireObject(nameRes, 'PATCH /api/article/:sku');
|
|
requireObject(nameRes.article, 'article');
|
|
onArticleUpdated?.(nameRes.article);
|
|
};
|
|
|
|
render() {
|
|
const { article: articleData, categories, rootCategoryId } = this.props;
|
|
const savedCategoryIds = articleData.categoryIds ?? [];
|
|
const value = { name: articleData.name ?? '', categoryIds: [...savedCategoryIds] };
|
|
|
|
return (
|
|
<EditablePanelTile
|
|
wide={3}
|
|
minHeight={0}
|
|
sx={headerTileSx}
|
|
value={value}
|
|
onSave={this.handleSave}
|
|
isDirty={draftDirty}
|
|
canSave={draftValid}
|
|
display={
|
|
<NameDisplay
|
|
name={value.name}
|
|
categoryIds={value.categoryIds}
|
|
categories={categories}
|
|
rootCategoryId={rootCategoryId}
|
|
sku={articleData.sku}
|
|
/>
|
|
}
|
|
edit={
|
|
<NameEdit
|
|
categories={categories}
|
|
rootCategoryId={rootCategoryId}
|
|
sku={articleData.sku}
|
|
/>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
}
|