diff --git a/src/api/thumb.js b/src/api/thumb.js
index b14d864..80af253 100644
--- a/src/api/thumb.js
+++ b/src/api/thumb.js
@@ -12,4 +12,10 @@ export const thumb = {
return `/api/thumb/${encodeURIComponent(fullHash)}`;
},
// #endregion
+
+ // #region GET /api/smallthumb/:fullHash — smallUrl
+ smallUrl(fullHash) {
+ return `/api/smallthumb/${encodeURIComponent(fullHash)}`;
+ },
+ // #endregion
};
diff --git a/src/components/article/ArticleNameDialog.jsx b/src/components/article/ArticleNameDialog.jsx
index 1f9afce..9e3e665 100644
--- a/src/components/article/ArticleNameDialog.jsx
+++ b/src/components/article/ArticleNameDialog.jsx
@@ -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,
diff --git a/src/components/article/ArticlePhoto.jsx b/src/components/article/ArticlePhoto.jsx
index 85e5414..30477b1 100644
--- a/src/components/article/ArticlePhoto.jsx
+++ b/src/components/article/ArticlePhoto.jsx
@@ -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 (
) : null}
- {fullHash ? (
+ {imgSrc ? (
p.fullHash === selectedHash)) ||
+ primary;
+ const otherPhotos = photos.filter((p) => p.fullHash !== activePhoto?.fullHash);
return (
<>
@@ -47,7 +68,86 @@ class ArticlePhotoTile extends PureComponent {
busy: false,
}}
>
-
+ {otherPhotos.length === 0 ? (
+
+ ) : (
+
+
+
+ {/* Smallthumbs of all other images beside the main image */}
+
+ {otherPhotos.map((p) => (
+ {
+ 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',
+ },
+ }}
+ >
+
+
+ ))}
+
+
+ )}
{
- 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
) {
diff --git a/src/components/article/boxes/ArticleViewLists.jsx b/src/components/article/boxes/ArticleViewLists.jsx
index 0a2b55e..fe93ed0 100644
--- a/src/components/article/boxes/ArticleViewLists.jsx
+++ b/src/components/article/boxes/ArticleViewLists.jsx
@@ -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 (
{activeCreatorMode === 'parent' ? (
@@ -1624,7 +1626,36 @@ export default class ArticleViewLists extends PureComponent {
- {selectableAttributes.length === 0 ? (
+ {attributesStatus === 'loading' ? (
+
+
+
+ Merkmale werden geladen…
+
+
+ ) : !attributes || attributes.length === 0 ? (
+
+
+ Im System sind noch keine Merkmale angelegt.
+
+
+ Zu den Merkmalen →
+
+
+ ) : selectableAttributes.length === 0 ? (
Alle vorhandenen Merkmale sind diesem Artikel bereits zugewiesen.
diff --git a/src/components/article/boxes/ArticleViewName.jsx b/src/components/article/boxes/ArticleViewName.jsx
index 1abd021..50e49fb 100644
--- a/src/components/article/boxes/ArticleViewName.jsx
+++ b/src/components/article/boxes/ArticleViewName.jsx
@@ -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;
diff --git a/src/components/article/tabs/ArticleAttributesTab.jsx b/src/components/article/tabs/ArticleAttributesTab.jsx
index 983b186..c77f530 100644
--- a/src/components/article/tabs/ArticleAttributesTab.jsx
+++ b/src/components/article/tabs/ArticleAttributesTab.jsx
@@ -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 {
+ {merkmalOptions.length === 0 && (
+
+
+ Im System sind noch keine Merkmale angelegt.
+
+
+ Zu den Merkmalen →
+
+
+ )}
+
{/* Unified Assigned Merkmale List */}
{assignedGroups.length > 0 && (
diff --git a/src/components/category/CategoryTree.jsx b/src/components/category/CategoryTree.jsx
index 0d43f96..c179d79 100644
--- a/src/components/category/CategoryTree.jsx
+++ b/src/components/category/CategoryTree.jsx
@@ -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 {
diff --git a/src/routes/artikel.jsx b/src/routes/artikel.jsx
index 95d1417..20986bf 100644
--- a/src/routes/artikel.jsx
+++ b/src/routes/artikel.jsx
@@ -20,6 +20,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import articleSearchAutomat from '../automats/articleSearchAutomat';
+import articleIndexAutomat from '../automats/articleIndexAutomat';
import BackendError from '../components/BackendError';
import { formatPrice, getArticleGross } from '../utils/price';
import { thumb } from '../api';
@@ -50,6 +51,7 @@ import navigationAutomat, {
import categoryAutomat from '../automats/categoryAutomat';
import companyAutomat from '../automats/companyAutomat';
import { BrandLabel, BrandThumb } from '../components/brand';
+import { ArticleNameDialog } from '../components/article';
import { SCRIM_LABEL } from '../colors';
// #endregion
@@ -282,6 +284,7 @@ function initialSearchState(searchParams) {
categoryFilterOpen: false,
brandFilterOpen: false,
supplierFilterOpen: false,
+ newArticleOpen: false,
error: automatResult.error,
categoryFacets: automatResult.categoryFacets,
brandFacets: automatResult.brandFacets,
@@ -354,6 +357,42 @@ class ArtikelSearchPage extends Component {
}));
};
+ handleOpenNewArticle = () => {
+ this.setState({ newArticleOpen: true });
+ };
+
+ handleCloseNewArticle = () => {
+ this.setState({ newArticleOpen: false });
+ };
+
+ handleCreateArticle = async ({ names }) => {
+ const filterParam = this.props.searchParams.get('filter') ?? '';
+ const categories = parseFilter(filterParam);
+ const parentId =
+ categories[0] ||
+ categoryAutomat.state.rootCategoryId ||
+ companyAutomat.state.current?.rootCategoryId ||
+ companyAutomat.state.current?.rootcategory_id ||
+ 1;
+ const list = (names ?? []).filter(Boolean);
+ if (list.length <= 1) {
+ const created = await categoryAutomat.actions.createArticle({
+ parentId,
+ name: list[0] ?? '',
+ });
+ void articleIndexAutomat.actions.reload().catch(() => {});
+ if (created) {
+ this.props.navigate(artikelDetailPath(created));
+ }
+ return;
+ }
+ const createdList = await categoryAutomat.actions.createArticles({ parentId, names: list });
+ void articleIndexAutomat.actions.reload().catch(() => {});
+ if (createdList?.[0]) {
+ this.props.navigate(artikelDetailPath(createdList[0]));
+ }
+ };
+
handleOpenArticle = (row) => {
this.persistQuery();
this.props.navigate(artikelDetailPath(row));
@@ -548,6 +587,7 @@ class ArtikelSearchPage extends Component {
categoryFilterOpen,
brandFilterOpen,
supplierFilterOpen,
+ newArticleOpen,
error,
totalCount,
categoryFacets,
@@ -696,6 +736,13 @@ class ArtikelSearchPage extends Component {
>
Lieferanten
+
{categories.map((catId) => {
const label = getCategoryBreadcrumbLabel(
@@ -1047,6 +1094,12 @@ class ArtikelSearchPage extends Component {
+
>
);
}