diff --git a/src/components/brand/ArtikelMarke.jsx b/src/components/brand/ArtikelMarke.jsx index d9e4cad..d0f7f6a 100644 --- a/src/components/brand/ArtikelMarke.jsx +++ b/src/components/brand/ArtikelMarke.jsx @@ -6,13 +6,18 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import CopyrightIcon from '@mui/icons-material/Copyright'; import ImageSearchIcon from '@mui/icons-material/ImageSearch'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; +import { enqueueSnackbar } from 'notistack'; +import brandAutomat from '../../automats/brandAutomat'; +import { notifyError } from '../../utils/notify'; import BrandThumb from './BrandThumb'; +import BrandImageSearchDialog from './BrandImageSearchDialog'; class ArtikelMarke extends PureComponent { constructor(props) { super(props); this.state = { draft: props.brand?.name ?? '', + logoOpen: false, }; } @@ -32,17 +37,27 @@ class ArtikelMarke extends PureComponent { }; handleBlur = () => { - const { onRename, brand } = this.props; + const { brand } = this.props; const draft = this.state.draft.trim(); if (!draft) { this.setState({ draft: brand?.name ?? '' }); return; } if (draft !== brand?.name) { - onRename?.(brand, draft); + void this.renameBrand(brand, draft); } }; + async renameBrand(brand, name) { + try { + await brandAutomat.actions.rename(brand.id, { name }); + enqueueSnackbar('Marke gespeichert.', { variant: 'success' }); + } catch (err) { + this.setState({ draft: brand?.name ?? '' }); + notifyError(err); + } + } + handleKeyDown = (event) => { const { brand } = this.props; if (event.key === 'Enter') { @@ -57,21 +72,33 @@ class ArtikelMarke extends PureComponent { }; handleSearchLogo = () => { - const { onSearchLogo, brand } = this.props; - onSearchLogo?.(brand); + this.setState({ logoOpen: true }); + }; + + handleLogoClose = () => { + this.setState({ logoOpen: false }); }; handleDelete = () => { - const { onDelete, brand } = this.props; - onDelete?.(brand); + const { brand } = this.props; + if (!window.confirm(`Marke „${brand.name}" wirklich löschen?`)) return; + void (async () => { + try { + await brandAutomat.actions.remove(brand.id); + enqueueSnackbar('Marke gelöscht.', { variant: 'success' }); + } catch (err) { + notifyError(err); + } + })(); }; render() { const { brand, disabled, sx } = this.props; - const { draft } = this.state; + const { draft, logoOpen } = this.state; const name = brand?.name ?? ''; return ( + <> + {logoOpen ? ( + + ) : null} + ); } } diff --git a/src/components/brand/ArtikelMarkeNew.jsx b/src/components/brand/ArtikelMarkeNew.jsx new file mode 100644 index 0000000..2c1e309 --- /dev/null +++ b/src/components/brand/ArtikelMarkeNew.jsx @@ -0,0 +1,61 @@ +import { PureComponent } from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; +import { enqueueSnackbar } from 'notistack'; +import brandAutomat from '../../automats/brandAutomat'; +import { notifyError } from '../../utils/notify'; + +// Own component so typing a new brand name never rerenders ArtikelMarkenPage +// (and its FillSpace measurement). Holds the draft locally and performs the +// automat mutation itself — no callbacks. +class ArtikelMarkeNew extends PureComponent { + constructor(props) { + super(props); + this.state = { newName: '' }; + } + + async submit() { + const name = this.state.newName.trim(); + if (!name) return; + try { + const row = await brandAutomat.actions.create({ name }); + enqueueSnackbar(`Marke „${row.name || name}" angelegt.`, { variant: 'success' }); + this.setState({ newName: '' }); + } catch (err) { + notifyError(err); + } + } + + render() { + const { rowRef } = this.props; + const { newName } = this.state; + return ( + + this.setState({ newName: next })} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + void this.submit(); + } + }} + sx={{ flex: 1 }} + /> + + + ); + } +} + +export default ArtikelMarkeNew; diff --git a/src/components/brand/index.js b/src/components/brand/index.js index 23375fa..d819fee 100644 --- a/src/components/brand/index.js +++ b/src/components/brand/index.js @@ -2,4 +2,5 @@ export { default as BrandThumb, extractBrandThumbInfo } from './BrandThumb'; export { default as BrandLabel } from './BrandLabel'; export { default as BrandImageSearchDialog } from './BrandImageSearchDialog'; export { default as ArtikelMarke } from './ArtikelMarke'; +export { default as ArtikelMarkeNew } from './ArtikelMarkeNew'; export { default as ArtikelMarkenTable } from './ArtikelMarkenTable'; diff --git a/src/routes/artikel/marken.jsx b/src/routes/artikel/marken.jsx index ab8e8df..0207474 100644 --- a/src/routes/artikel/marken.jsx +++ b/src/routes/artikel/marken.jsx @@ -1,69 +1,22 @@ import { PureComponent, createRef } from 'react'; import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import BackendError from '../../components/BackendError'; -import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; import FillSpace from 'dashgui/FillSpace'; import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; import { setLastArticleSku } from '../../automats'; import brandAutomat from '../../automats/brandAutomat'; -import { enqueueSnackbar } from 'notistack'; import { - BrandImageSearchDialog, ArtikelMarke, + ArtikelMarkeNew, } from '../../components/brand'; import { PURPLE_PANEL, } from '../../colors'; -class ArtikelMarkeNew extends PureComponent { - constructor(props) { - super(props); - this.state = { newName: '' }; - } - - async submit() { - const name = this.state.newName.trim(); - if (!name) return; - await this.props.onCreate?.(name); - this.setState({ newName: '' }); - } - - render() { - const { rowRef } = this.props; - const { newName } = this.state; - return ( - - this.setState({ newName: next })} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); - void this.submit(); - } - }} - sx={{ flex: 1 }} - /> - - - ); - } -} - class ArtikelMarkenPage extends PureComponent { constructor(props) { super(props); @@ -71,23 +24,14 @@ class ArtikelMarkenPage extends PureComponent { status: brandAutomat.state.status, error: brandAutomat.state.error ? backendErrorFrom({ message: brandAutomat.state.error }) : null, brands: brandAutomat.state.brands, - searchBrand: null, }; - this._cancelled = false; this.createRowRef = createRef(); - this.handleCreate = this.handleCreate.bind(this); this.handleRetry = this.handleRetry.bind(this); - this.handleRename = this.handleRename.bind(this); - this.handleDelete = this.handleDelete.bind(this); - this.handleSearchLogo = this.handleSearchLogo.bind(this); } componentDidMount() { setLastArticleSku(null); - this._cancelled = false; - // Mirror the automat's brands slice directly — the automat owns - // fetching (auth/SSE), the component never triggers loads. this.unsub = brandAutomat.subscribe(this, (state) => ({ brands: state.brands, status: state.status, @@ -96,7 +40,6 @@ class ArtikelMarkenPage extends PureComponent { } componentWillUnmount() { - this._cancelled = true; this.unsub?.(); } @@ -105,52 +48,6 @@ class ArtikelMarkenPage extends PureComponent { void brandAutomat.actions.load().catch((err) => notifyError(err)); } - handleSearchLogo(brand) { - this.setState({ searchBrand: brand }); - } - - async handleCreate(name) { - const trimmed = String(name ?? '').trim(); - if (!trimmed) return; - - try { - const row = await brandAutomat.actions.create({ name: trimmed }); - if (this._cancelled) return; - enqueueSnackbar(`Marke „${row.name || trimmed}" angelegt.`, { variant: 'success' }); - } catch (err) { - if (this._cancelled) return; - notifyError(err); - throw err; - } - } - - async handleRename(brand, draft) { - const nextName = String(draft ?? '').trim(); - if (!nextName || nextName === brand.name) return; - - try { - await brandAutomat.actions.rename(brand.id, { name: nextName }); - if (this._cancelled) return; - enqueueSnackbar('Marke gespeichert.', { variant: 'success' }); - } catch (err) { - if (this._cancelled) return; - notifyError(err); - } - } - - async handleDelete(brand) { - if (!window.confirm(`Marke „${brand.name}" wirklich löschen?`)) return; - - try { - await brandAutomat.actions.remove(brand.id); - if (this._cancelled) return; - enqueueSnackbar('Marke gelöscht.', { variant: 'success' }); - } catch (err) { - if (this._cancelled) return; - notifyError(err); - } - } - render() { const { status, error, brands } = this.state; @@ -176,24 +73,11 @@ class ArtikelMarkenPage extends PureComponent { minPageSize={8} minHeight={0} items={brands} - renderItem={(brand) => ( - - )} + renderItem={ArtikelMarke} /> - + - - this.setState({ searchBrand: null })} - /> ); }