u
This commit is contained in:
@@ -31,7 +31,7 @@ class ArticleNameDialog extends Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
mode: SINGLE_MODE,
|
||||
name: '',
|
||||
name: props.initialName ?? '',
|
||||
bulkText: '',
|
||||
busy: false,
|
||||
error: null,
|
||||
@@ -46,7 +46,7 @@ class ArticleNameDialog extends Component {
|
||||
if (this.props.open && !prevProps.open) {
|
||||
this.setState({
|
||||
mode: SINGLE_MODE,
|
||||
name: '',
|
||||
name: this.props.initialName ?? '',
|
||||
bulkText: '',
|
||||
busy: false,
|
||||
error: null,
|
||||
|
||||
@@ -9,14 +9,17 @@ import {
|
||||
class ArticlePhoto extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { thumbLoaded: false };
|
||||
this.state = { thumbLoaded: false, smallThumbError: false };
|
||||
this.handleThumbLoad = this.handleThumbLoad.bind(this);
|
||||
this.handleThumbError = this.handleThumbError.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.photo?.fullHash !== this.props.photo?.fullHash) {
|
||||
this.setState({ thumbLoaded: false });
|
||||
if (
|
||||
prevProps.photo?.fullHash !== this.props.photo?.fullHash ||
|
||||
prevProps.useSmallThumb !== this.props.useSmallThumb
|
||||
) {
|
||||
this.setState({ thumbLoaded: false, smallThumbError: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +28,11 @@ class ArticlePhoto extends Component {
|
||||
}
|
||||
|
||||
handleThumbError() {
|
||||
this.setState({ thumbLoaded: false });
|
||||
if (this.props.useSmallThumb && !this.state.smallThumbError) {
|
||||
this.setState({ smallThumbError: true, thumbLoaded: false });
|
||||
} else {
|
||||
this.setState({ thumbLoaded: false });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -34,9 +41,11 @@ class ArticlePhoto extends Component {
|
||||
size = 200,
|
||||
fill = false,
|
||||
objectFit = fill ? 'cover' : 'contain',
|
||||
useSmallThumb = false,
|
||||
sx,
|
||||
...rest
|
||||
} = this.props;
|
||||
const { thumbLoaded } = this.state;
|
||||
const { thumbLoaded, smallThumbError } = this.state;
|
||||
const fullHash = photo?.fullHash;
|
||||
const hasPreview = photo?.thumbHash != null;
|
||||
const aspectRatio =
|
||||
@@ -44,8 +53,15 @@ class ArticlePhoto extends Component {
|
||||
? `${photo.thumbW} / ${photo.thumbH}`
|
||||
: '1 / 1';
|
||||
|
||||
const imgSrc = fullHash
|
||||
? useSmallThumb && !smallThumbError
|
||||
? thumb.smallUrl(fullHash)
|
||||
: thumb.url(fullHash)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...rest}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
...(fill
|
||||
@@ -78,10 +94,10 @@ class ArticlePhoto extends Component {
|
||||
<ThumbHashImage thumbHash={photo.thumbHash} />
|
||||
</Box>
|
||||
) : null}
|
||||
{fullHash ? (
|
||||
{imgSrc ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={thumb.url(fullHash)}
|
||||
src={imgSrc}
|
||||
alt=""
|
||||
onLoad={this.handleThumbLoad}
|
||||
onError={this.handleThumbError}
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
import { PureComponent } from 'react';
|
||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||
import Box from '@mui/material/Box';
|
||||
import { PanelTile } from 'dashgui/panels';
|
||||
import { primaryPhoto } from '../../utils/articleDetail';
|
||||
import { sortedPhotos, primaryPhoto } from '../../utils/articleDetail';
|
||||
import ArticlePhoto from './ArticlePhoto';
|
||||
import ArticlePhotosDialog from './ArticlePhotosDialog';
|
||||
|
||||
class ArticlePhotoTile extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { editOpen: false };
|
||||
this.state = {
|
||||
editOpen: false,
|
||||
selectedHash: null,
|
||||
};
|
||||
this.handleEditOpen = this.handleEditOpen.bind(this);
|
||||
this.handleEditClose = this.handleEditClose.bind(this);
|
||||
this.handleSelectPhoto = this.handleSelectPhoto.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.article?.sku !== this.props.article?.sku) {
|
||||
this.setState({ selectedHash: null });
|
||||
}
|
||||
}
|
||||
|
||||
handleEditOpen() {
|
||||
@@ -21,6 +32,10 @@ class ArticlePhotoTile extends PureComponent {
|
||||
this.setState({ editOpen: false });
|
||||
}
|
||||
|
||||
handleSelectPhoto(fullHash) {
|
||||
this.setState({ selectedHash: fullHash });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
article: articleData,
|
||||
@@ -30,8 +45,14 @@ class ArticlePhotoTile extends PureComponent {
|
||||
sx,
|
||||
color,
|
||||
} = this.props;
|
||||
const { editOpen } = this.state;
|
||||
const photo = primaryPhoto(articleData);
|
||||
const { editOpen, selectedHash } = this.state;
|
||||
|
||||
const photos = sortedPhotos(articleData);
|
||||
const primary = primaryPhoto(articleData);
|
||||
const activePhoto =
|
||||
(selectedHash && photos.find((p) => p.fullHash === selectedHash)) ||
|
||||
primary;
|
||||
const otherPhotos = photos.filter((p) => p.fullHash !== activePhoto?.fullHash);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -47,7 +68,86 @@ class ArticlePhotoTile extends PureComponent {
|
||||
busy: false,
|
||||
}}
|
||||
>
|
||||
<ArticlePhoto photo={photo} />
|
||||
{otherPhotos.length === 0 ? (
|
||||
<ArticlePhoto photo={activePhoto} />
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<ArticlePhoto photo={activePhoto} />
|
||||
|
||||
{/* Smallthumbs of all other images beside the main image */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 0.75,
|
||||
maxHeight: 200,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
mt: '32px',
|
||||
py: 0.25,
|
||||
px: 0.25,
|
||||
flexShrink: 0,
|
||||
scrollbarWidth: 'thin',
|
||||
'&::-webkit-scrollbar': { width: 4 },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
bgcolor: 'action.hover',
|
||||
borderRadius: 2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{otherPhotos.map((p) => (
|
||||
<Box
|
||||
key={p.fullHash}
|
||||
component="button"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
this.handleSelectPhoto(p.fullHash);
|
||||
}}
|
||||
sx={{
|
||||
p: 0,
|
||||
m: 0,
|
||||
width: 40,
|
||||
height: 40,
|
||||
flexShrink: 0,
|
||||
cursor: 'pointer',
|
||||
borderRadius: 1,
|
||||
overflow: 'hidden',
|
||||
border: '2px solid transparent',
|
||||
bgcolor: 'transparent',
|
||||
outline: 'none',
|
||||
display: 'block',
|
||||
transition: 'all 0.15s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.08)',
|
||||
borderColor: 'primary.main',
|
||||
boxShadow: 2,
|
||||
},
|
||||
'&:focus-visible': {
|
||||
borderColor: 'primary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArticlePhoto
|
||||
photo={p}
|
||||
size={40}
|
||||
useSmallThumb
|
||||
objectFit="cover"
|
||||
sx={{ width: '100%', height: '100%', borderRadius: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</PanelTile>
|
||||
<ArticlePhotosDialog
|
||||
open={editOpen}
|
||||
|
||||
@@ -43,14 +43,12 @@ export default class CategoryPickerDialog extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.categories === undefined || this.props.rootCategoryId === undefined) {
|
||||
this._unsub = categoryAutomat.subscribe((s) => {
|
||||
this.setState(() => ({
|
||||
categories: this.props.categories ?? s.categories,
|
||||
rootCategoryId: this.props.rootCategoryId ?? s.rootCategoryId,
|
||||
}));
|
||||
this._unsub = categoryAutomat.subscribe((s) => {
|
||||
this.setState({
|
||||
categories: s.categories,
|
||||
rootCategoryId: s.rootCategoryId ?? this.props.rootCategoryId,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -64,10 +62,12 @@ export default class CategoryPickerDialog extends PureComponent {
|
||||
} else if (this.state.rootCategoryId != null) {
|
||||
nextSelectedId = this.state.rootCategoryId;
|
||||
}
|
||||
this.setState({ selectedId: nextSelectedId });
|
||||
}
|
||||
|
||||
if (
|
||||
this.setState({
|
||||
selectedId: nextSelectedId,
|
||||
categories: categoryAutomat.state.categories ?? this.props.categories,
|
||||
rootCategoryId: categoryAutomat.state.rootCategoryId ?? this.props.rootCategoryId,
|
||||
});
|
||||
} else if (
|
||||
this.props.categories !== prevProps.categories ||
|
||||
this.props.rootCategoryId !== prevProps.rootCategoryId
|
||||
) {
|
||||
|
||||
@@ -123,6 +123,7 @@ export default class ArticleViewLists extends PureComponent {
|
||||
|
||||
this.state = {
|
||||
attributes: initialAttributes,
|
||||
attributesStatus: companyAutomat.state.attributesStatus || 'idle',
|
||||
assignedAttributes: [],
|
||||
allArticles: [],
|
||||
busy: false,
|
||||
@@ -170,10 +171,11 @@ export default class ArticleViewLists extends PureComponent {
|
||||
return {
|
||||
company: state.current,
|
||||
attributes: state.attributes,
|
||||
attributesStatus: state.attributesStatus,
|
||||
selectedAttributeId: nextSelectedId,
|
||||
};
|
||||
});
|
||||
}, ['current', 'attributes']);
|
||||
}, ['current', 'attributes', 'attributesStatus']);
|
||||
|
||||
this.unsubArtikel = artikelAutomat.subscribe(() => {
|
||||
if (this._cancelled) return;
|
||||
@@ -1602,7 +1604,7 @@ export default class ArticleViewLists extends PureComponent {
|
||||
}
|
||||
|
||||
renderStandardView(selectableAttributes, assignedAttributeIds) {
|
||||
const { busy, activeCreatorMode, selectedAttributeId } = this.state;
|
||||
const { busy, activeCreatorMode, selectedAttributeId, attributes, attributesStatus } = this.state;
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{activeCreatorMode === 'parent' ? (
|
||||
@@ -1624,7 +1626,36 @@ export default class ArticleViewLists extends PureComponent {
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{selectableAttributes.length === 0 ? (
|
||||
{attributesStatus === 'loading' ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
|
||||
<CircularProgress size={14} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Merkmale werden geladen…
|
||||
</Typography>
|
||||
</Box>
|
||||
) : !attributes || attributes.length === 0 ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, py: 0.5 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Im System sind noch keine Merkmale angelegt.
|
||||
</Typography>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/artikel/merkmale"
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
fontWeight: 600,
|
||||
textDecoration: 'none',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
'&:hover': { textDecoration: 'underline' },
|
||||
}}
|
||||
>
|
||||
Zu den Merkmalen →
|
||||
</Link>
|
||||
</Box>
|
||||
) : selectableAttributes.length === 0 ? (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Alle vorhandenen Merkmale sind diesem Artikel bereits zugewiesen.
|
||||
</Typography>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, PureComponent } from 'react';
|
||||
import { EditablePanelTile } from 'dashgui/panels';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import { getCategoryState } from '../../../automats/categoryAutomat';
|
||||
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';
|
||||
@@ -92,10 +92,29 @@ function categoryLabels(categoryIds, categories, rootCategoryId) {
|
||||
.filter((c) => c.label);
|
||||
}
|
||||
|
||||
class CategoryPills extends Component {
|
||||
class CategoryPills extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { pickerOpen: false, targetPillId: null };
|
||||
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) => {
|
||||
@@ -135,8 +154,7 @@ class CategoryPills extends Component {
|
||||
|
||||
render() {
|
||||
const { categoryIds, editMode } = this.props;
|
||||
const { pickerOpen, targetPillId } = this.state;
|
||||
const { categories, rootCategoryId } = getCategoryState() ?? {};
|
||||
const { pickerOpen, targetPillId, categories, rootCategoryId } = this.state;
|
||||
const labels = categoryLabels(categoryIds, categories, rootCategoryId);
|
||||
|
||||
const targetNode = categories && targetPillId != null ? categories[String(targetPillId)] : null;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import Link from '@mui/material/Link';
|
||||
import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
@@ -562,6 +564,27 @@ export default class ArticleAttributesTab extends PureComponent {
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{merkmalOptions.length === 0 && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Im System sind noch keine Merkmale angelegt.
|
||||
</Typography>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/artikel/merkmale"
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
fontWeight: 600,
|
||||
textDecoration: 'none',
|
||||
'&:hover': { textDecoration: 'underline' },
|
||||
}}
|
||||
>
|
||||
Zu den Merkmalen →
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Unified Assigned Merkmale List */}
|
||||
{assignedGroups.length > 0 && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
@@ -416,14 +416,12 @@ export default class CategoryTree extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.categories === undefined || this.props.rootCategoryId === undefined) {
|
||||
this._unsub = categoryAutomat.subscribe((s) => {
|
||||
this.setState({
|
||||
categories: this.props.categories ?? s.categories,
|
||||
rootCategoryId: this.props.rootCategoryId ?? s.rootCategoryId,
|
||||
});
|
||||
this._unsub = categoryAutomat.subscribe((s) => {
|
||||
this.setState({
|
||||
categories: s.categories,
|
||||
rootCategoryId: s.rootCategoryId ?? this.props.rootCategoryId,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -463,7 +461,14 @@ export default class CategoryTree extends PureComponent {
|
||||
name,
|
||||
});
|
||||
enqueueSnackbar(`Kategorie „${name}“ angelegt.`, { variant: 'success' });
|
||||
this.setState({ addDialogOpen: false });
|
||||
const nextCategories = categoryAutomat.state.categories ?? {
|
||||
...(this.state.categories || {}),
|
||||
[String(created.id)]: created,
|
||||
};
|
||||
this.setState({
|
||||
addDialogOpen: false,
|
||||
categories: nextCategories,
|
||||
});
|
||||
this.props.onSelect?.(created.id, created);
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
@@ -489,12 +494,19 @@ export default class CategoryTree extends PureComponent {
|
||||
if (!name || !renameNode) return;
|
||||
this.setState({ isSubmitting: true });
|
||||
try {
|
||||
await categoryAutomat.actions.renameCategory({
|
||||
const updated = await categoryAutomat.actions.renameCategory({
|
||||
categoryId: renameNode.id,
|
||||
name,
|
||||
});
|
||||
enqueueSnackbar(`Kategorie in „${name}“ umbenannt.`, { variant: 'success' });
|
||||
this.setState({ renameDialogOpen: false });
|
||||
const nextCategories = categoryAutomat.state.categories ?? {
|
||||
...(this.state.categories || {}),
|
||||
[String(updated.id)]: updated,
|
||||
};
|
||||
this.setState({
|
||||
renameDialogOpen: false,
|
||||
categories: nextCategories,
|
||||
});
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
} finally {
|
||||
@@ -524,7 +536,10 @@ export default class CategoryTree extends PureComponent {
|
||||
if (this.props.selectedId != null && String(this.props.selectedId) === String(deleteNode.id)) {
|
||||
this.props.onSelect?.(deleteNode.parentId ?? rootCategoryId);
|
||||
}
|
||||
this.setState({ deleteDialogOpen: false });
|
||||
this.setState({
|
||||
deleteDialogOpen: false,
|
||||
categories: categoryAutomat.state.categories,
|
||||
});
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user