Show article history in a table via GET /article/{sku}/history.
This commit is contained in:
@@ -106,8 +106,21 @@ export class ApiClient {
|
|||||||
exportZip: (query = {}) => this.#blob('GET', '/api/article/export.zip', { query }),
|
exportZip: (query = {}) => this.#blob('GET', '/api/article/export.zip', { query }),
|
||||||
/** GET /api/article/{sku} */
|
/** GET /api/article/{sku} */
|
||||||
get: (sku) => this.#json('GET', `/api/article/${enc(sku)}`),
|
get: (sku) => this.#json('GET', `/api/article/${enc(sku)}`),
|
||||||
/** PATCH /api/article/{sku} */
|
/** GET /api/article/{sku}/history → { history: [{ id, date, text }, ...] } */
|
||||||
update: (sku, body) => this.#json('PATCH', `/api/article/${enc(sku)}`, { body }),
|
history: (sku) => this.#json('GET', `/api/article/${enc(sku)}/history`),
|
||||||
|
/** PATCH /api/article/{sku} — body: at least one of { name, unitamount, unitSi } */
|
||||||
|
update: async (sku, body) => {
|
||||||
|
const nameChange = body != null && Object.hasOwn(body, 'name');
|
||||||
|
if (nameChange) {
|
||||||
|
this.setArticleIndexName(sku, body.name);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await this.#json('PATCH', `/api/article/${enc(sku)}`, { body });
|
||||||
|
} catch (err) {
|
||||||
|
if (nameChange) this.clearArticleIndex();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
/** DELETE /api/article/{sku} */
|
/** DELETE /api/article/{sku} */
|
||||||
delete: (sku) => this.#empty('DELETE', `/api/article/${enc(sku)}`),
|
delete: (sku) => this.#empty('DELETE', `/api/article/${enc(sku)}`),
|
||||||
/** PUT /api/article/{sku}/prices */
|
/** PUT /api/article/{sku}/prices */
|
||||||
|
|||||||
@@ -4,16 +4,25 @@ import { ApiClient } from './ApiClient';
|
|||||||
/**
|
/**
|
||||||
* Stale-while-revalidate cache: readers keep the current promise; update()
|
* Stale-while-revalidate cache: readers keep the current promise; update()
|
||||||
* swaps only after a successful reload. Failed updates leave the cache intact.
|
* swaps only after a successful reload. Failed updates leave the cache intact.
|
||||||
|
* replace() writes local data without a round-trip.
|
||||||
*/
|
*/
|
||||||
function createCache(label, load) {
|
function createCache(label, load) {
|
||||||
let promise = load().catch((err) => {
|
/** Settled value, or `undefined` if not loaded yet / last load failed. */
|
||||||
console.error(`[storageMgr] ${label} failed`, err);
|
let current;
|
||||||
throw err;
|
let promise = load()
|
||||||
});
|
.then((data) => {
|
||||||
|
current = data;
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`[storageMgr] ${label} failed`, err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
async function update() {
|
async function update() {
|
||||||
try {
|
try {
|
||||||
const data = await load();
|
const data = await load();
|
||||||
|
current = data;
|
||||||
promise = Promise.resolve(data);
|
promise = Promise.resolve(data);
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -22,11 +31,36 @@ function createCache(label, load) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replace(data) {
|
||||||
|
current = data;
|
||||||
|
promise = Promise.resolve(data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
current = undefined;
|
||||||
|
promise = load()
|
||||||
|
.then((data) => {
|
||||||
|
current = data;
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`[storageMgr] ${label} failed`, err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
update,
|
update,
|
||||||
|
replace,
|
||||||
|
clear,
|
||||||
get promise() {
|
get promise() {
|
||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
|
/** Loaded value, or `undefined` if the initial load has not settled successfully. */
|
||||||
|
get value() {
|
||||||
|
return current;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,15 +70,33 @@ function createCache(label, load) {
|
|||||||
export class StorageManager extends ApiClient {
|
export class StorageManager extends ApiClient {
|
||||||
/** @type {EventSource | null} */
|
/** @type {EventSource | null} */
|
||||||
#events = null;
|
#events = null;
|
||||||
|
/** @type {ReturnType<typeof createCache> | null} */
|
||||||
|
#articleIndexCache = null;
|
||||||
|
|
||||||
constructor(baseUrl = window.location.origin) {
|
constructor(baseUrl = window.location.origin) {
|
||||||
super(baseUrl);
|
super(baseUrl);
|
||||||
this.#initCaches();
|
this.#initCaches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Patch local articleIndex name if already loaded; no-op otherwise. */
|
||||||
|
setArticleIndexName(sku, name) {
|
||||||
|
const articles = this.#articleIndexCache.value;
|
||||||
|
if (!articles) return;
|
||||||
|
this.#articleIndexCache.replace(
|
||||||
|
articles.map((a) => (String(a.sku) === String(sku) ? { ...a, name } : a)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drop local articleIndex if loaded (forces reload on next read). */
|
||||||
|
clearArticleIndex() {
|
||||||
|
if (!this.#articleIndexCache.value) return;
|
||||||
|
this.#articleIndexCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
#initCaches() {
|
#initCaches() {
|
||||||
const authMeCache = createCache('GET /auth/me', () => this.#loadAuthMe());
|
const authMeCache = createCache('GET /auth/me', () => this.#loadAuthMe());
|
||||||
const articleIndexCache = createCache('GET /articleIndex', () => this.#loadArticleIndex());
|
this.#articleIndexCache = createCache('GET /articleIndex', () => this.#loadArticleIndex());
|
||||||
|
const articleIndexCache = this.#articleIndexCache;
|
||||||
const brandCache = createCache('GET /brand', () => this.#loadBrands());
|
const brandCache = createCache('GET /brand', () => this.#loadBrands());
|
||||||
const unitCache = createCache('GET /unit', () => this.#loadUnits());
|
const unitCache = createCache('GET /unit', () => this.#loadUnits());
|
||||||
const companyCache = createCache('GET /company', () => this.#loadCompanies());
|
const companyCache = createCache('GET /company', () => this.#loadCompanies());
|
||||||
@@ -87,12 +139,12 @@ export class StorageManager extends ApiClient {
|
|||||||
const es = new EventSource(this.urlFor('/api/events'), { withCredentials: true });
|
const es = new EventSource(this.urlFor('/api/events'), { withCredentials: true });
|
||||||
es.addEventListener('category', () => {
|
es.addEventListener('category', () => {
|
||||||
void this.updateCategoryTree().catch((err) => {
|
void this.updateCategoryTree().catch((err) => {
|
||||||
console.error('[storageMgr] category event refresh failed', err);
|
console.error('[storageMgr] category SSE refresh failed', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
es.addEventListener('articles', () => {
|
es.addEventListener('articles', () => {
|
||||||
void this.updateArticleIndex().catch((err) => {
|
void this.updateArticleIndex().catch((err) => {
|
||||||
console.error('[storageMgr] articles event refresh failed', err);
|
console.error('[storageMgr] articles SSE refresh failed', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
es.onerror = () => {
|
es.onerror = () => {
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Tab from '@mui/material/Tab';
|
import Tab from '@mui/material/Tab';
|
||||||
import Tabs from '@mui/material/Tabs';
|
import Tabs from '@mui/material/Tabs';
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||||
import CategoryIcon from '@mui/icons-material/Category';
|
import CategoryIcon from '@mui/icons-material/Category';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
|
||||||
import HistoryIcon from '@mui/icons-material/History';
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
import LabelIcon from '@mui/icons-material/Label';
|
import LabelIcon from '@mui/icons-material/Label';
|
||||||
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||||
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
|
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
|
||||||
import PushPinIcon from '@mui/icons-material/PushPin';
|
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
|
||||||
import {
|
import {
|
||||||
ARTICLE_TAB,
|
ARTICLE_TAB,
|
||||||
DEFAULT_ARTICLE_TAB,
|
DEFAULT_ARTICLE_TAB,
|
||||||
@@ -22,9 +18,15 @@ import {
|
|||||||
readPreferredArticleTab,
|
readPreferredArticleTab,
|
||||||
writePreferredArticleTab,
|
writePreferredArticleTab,
|
||||||
} from '../../pages/artikel/articleRoute';
|
} from '../../pages/artikel/articleRoute';
|
||||||
import { notifyError } from '../../utils/notify';
|
|
||||||
import { PanelTile } from '../panels';
|
import { PanelTile } from '../panels';
|
||||||
import { PANEL_TILE_BORDER_RADIUS, responsiveColumnSpan } from '../panels/panelTileLayout';
|
import { PANEL_TILE_BORDER_RADIUS, responsiveColumnSpan } from '../panels/panelTileLayout';
|
||||||
|
import ArticleAttributesTab from './tabs/ArticleAttributesTab';
|
||||||
|
import ArticleCompoundTab from './tabs/ArticleCompoundTab';
|
||||||
|
import ArticleHistoryTab from './tabs/ArticleHistoryTab';
|
||||||
|
import ArticleLabelTab from './tabs/ArticleLabelTab';
|
||||||
|
import ArticleSeoTab from './tabs/ArticleSeoTab';
|
||||||
|
import ArticleSettingsTab from './tabs/ArticleSettingsTab';
|
||||||
|
import ArticleSupplierTab from './tabs/ArticleSupplierTab';
|
||||||
|
|
||||||
const ALWAYS_TABS = [
|
const ALWAYS_TABS = [
|
||||||
{ id: ARTICLE_TAB.history, label: 'History', color: '#fed7aa', Icon: HistoryIcon },
|
{ id: ARTICLE_TAB.history, label: 'History', color: '#fed7aa', Icon: HistoryIcon },
|
||||||
@@ -67,42 +69,14 @@ function visibleTabs(article) {
|
|||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PanelValue({ label, children }) {
|
|
||||||
if (children == null || children === '') return null;
|
|
||||||
return (
|
|
||||||
<Box sx={{ mb: 1 }}>
|
|
||||||
{label ? (
|
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
|
||||||
{label}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
<Typography variant="body2" component="div">
|
|
||||||
{children}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Placeholder({ label }) {
|
|
||||||
return (
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{label}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArticleTileTab extends Component {
|
class ArticleTileTab extends Component {
|
||||||
static contextType = StorageManagerContext;
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
busy: false,
|
|
||||||
preferredTab: readPreferredArticleTab(),
|
preferredTab: readPreferredArticleTab(),
|
||||||
};
|
};
|
||||||
this.handleTabChange = this.handleTabChange.bind(this);
|
this.handleTabChange = this.handleTabChange.bind(this);
|
||||||
this.handlePinDefault = this.handlePinDefault.bind(this);
|
this.handlePinDefault = this.handlePinDefault.bind(this);
|
||||||
this.handleDeleteArticle = this.handleDeleteArticle.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTabChange(_event, tab) {
|
handleTabChange(_event, tab) {
|
||||||
@@ -116,69 +90,28 @@ class ArticleTileTab extends Component {
|
|||||||
this.setState({ preferredTab: tab });
|
this.setState({ preferredTab: tab });
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDeleteArticle() {
|
|
||||||
const { article: articleData, onArticleDeleted } = this.props;
|
|
||||||
if (this.state.busy) return;
|
|
||||||
if (!window.confirm(`Artikel „${articleData.name ?? articleData.sku}“ wirklich löschen?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ busy: true });
|
|
||||||
try {
|
|
||||||
await this.context.article.delete(articleData.sku);
|
|
||||||
onArticleDeleted?.(articleData);
|
|
||||||
} catch (err) {
|
|
||||||
notifyError(err);
|
|
||||||
this.setState({ busy: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSeo() {
|
|
||||||
const { article: articleData } = this.props;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PanelValue label="Slug">{articleData.slug}</PanelValue>
|
|
||||||
<PanelValue label="SEO-Titel">{articleData.seoTitle}</PanelValue>
|
|
||||||
<PanelValue label="SEO-Beschreibung">{articleData.seoDescription}</PanelValue>
|
|
||||||
<PanelValue label="Beschreibung">{articleData.description}</PanelValue>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSettings() {
|
|
||||||
const { busy } = this.state;
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
color="error"
|
|
||||||
variant="contained"
|
|
||||||
startIcon={<DeleteIcon />}
|
|
||||||
disabled={busy}
|
|
||||||
onClick={() => void this.handleDeleteArticle()}
|
|
||||||
>
|
|
||||||
Artikel löschen
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPanel() {
|
renderPanel() {
|
||||||
|
const { article: articleData, onArticleDeleted } = this.props;
|
||||||
switch (this.props.tab) {
|
switch (this.props.tab) {
|
||||||
case ARTICLE_TAB.history:
|
case ARTICLE_TAB.history:
|
||||||
return <Placeholder label="History" />;
|
return <ArticleHistoryTab sku={articleData.sku} />;
|
||||||
case ARTICLE_TAB.supplier:
|
case ARTICLE_TAB.supplier:
|
||||||
return <Placeholder label="Supplier" />;
|
return <ArticleSupplierTab />;
|
||||||
case ARTICLE_TAB.seo:
|
case ARTICLE_TAB.seo:
|
||||||
return this.renderSeo();
|
return <ArticleSeoTab article={articleData} />;
|
||||||
case ARTICLE_TAB.label:
|
case ARTICLE_TAB.label:
|
||||||
return <Placeholder label="Label" />;
|
return <ArticleLabelTab />;
|
||||||
case ARTICLE_TAB.attributes:
|
case ARTICLE_TAB.attributes:
|
||||||
return <Placeholder label="Attribute und Merkmale" />;
|
return <ArticleAttributesTab />;
|
||||||
case ARTICLE_TAB.compound:
|
case ARTICLE_TAB.compound:
|
||||||
return <Placeholder label="Compound Editor" />;
|
return <ArticleCompoundTab />;
|
||||||
case ARTICLE_TAB.settings:
|
case ARTICLE_TAB.settings:
|
||||||
return this.renderSettings();
|
return (
|
||||||
|
<ArticleSettingsTab
|
||||||
|
article={articleData}
|
||||||
|
onArticleDeleted={onArticleDeleted}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/components/article/tabs/ArticleAttributesTab.jsx
Normal file
12
src/components/article/tabs/ArticleAttributesTab.jsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
export default class ArticleAttributesTab extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Attribute und Merkmale
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/components/article/tabs/ArticleCompoundTab.jsx
Normal file
12
src/components/article/tabs/ArticleCompoundTab.jsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
export default class ArticleCompoundTab extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Compound Editor
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/components/article/tabs/ArticleHistoryTab.jsx
Normal file
71
src/components/article/tabs/ArticleHistoryTab.jsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableCell from '@mui/material/TableCell';
|
||||||
|
import TableHead from '@mui/material/TableHead';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
||||||
|
import { notifyError } from '../../../utils/notify';
|
||||||
|
|
||||||
|
function formatDate(iso) {
|
||||||
|
return new Date(iso).toLocaleString('de-DE');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ArticleHistoryTab extends Component {
|
||||||
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { history: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
void this.context.article
|
||||||
|
.history(this.props.sku)
|
||||||
|
.then((res) => this.setState({ history: res.history }))
|
||||||
|
.catch(notifyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { history } = this.state;
|
||||||
|
if (history == null) return null;
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
'& td': { borderBottom: 'none', bgcolor: 'transparent' },
|
||||||
|
'& th': { bgcolor: 'transparent' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
borderBottom: '1px solid rgba(0, 0, 0, 0.35)',
|
||||||
|
width: '1%',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Datum
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
sx={{ borderBottom: '1px solid rgba(0, 0, 0, 0.35)' }}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{history.map((entry) => (
|
||||||
|
<TableRow key={entry.id}>
|
||||||
|
<TableCell
|
||||||
|
sx={{ verticalAlign: 'top', whiteSpace: 'nowrap', width: '1%' }}
|
||||||
|
>
|
||||||
|
{formatDate(entry.date)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ whiteSpace: 'pre-wrap' }}>{entry.text}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/components/article/tabs/ArticleLabelTab.jsx
Normal file
12
src/components/article/tabs/ArticleLabelTab.jsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
export default class ArticleLabelTab extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Label
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/components/article/tabs/ArticleSeoTab.jsx
Normal file
33
src/components/article/tabs/ArticleSeoTab.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
function PanelValue({ label, children }) {
|
||||||
|
if (children == null || children === '') return null;
|
||||||
|
return (
|
||||||
|
<Box sx={{ mb: 1 }}>
|
||||||
|
{label ? (
|
||||||
|
<Typography variant="caption" color="text.secondary" display="block">
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<Typography variant="body2" component="div">
|
||||||
|
{children}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ArticleSeoTab extends Component {
|
||||||
|
render() {
|
||||||
|
const { article: articleData } = this.props;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PanelValue label="Slug">{articleData.slug}</PanelValue>
|
||||||
|
<PanelValue label="SEO-Titel">{articleData.seoTitle}</PanelValue>
|
||||||
|
<PanelValue label="SEO-Beschreibung">{articleData.seoDescription}</PanelValue>
|
||||||
|
<PanelValue label="Beschreibung">{articleData.description}</PanelValue>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/components/article/tabs/ArticleSettingsTab.jsx
Normal file
51
src/components/article/tabs/ArticleSettingsTab.jsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
||||||
|
import { notifyError } from '../../../utils/notify';
|
||||||
|
|
||||||
|
export default class ArticleSettingsTab extends Component {
|
||||||
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { busy: false };
|
||||||
|
this.handleDeleteArticle = this.handleDeleteArticle.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDeleteArticle() {
|
||||||
|
const { article: articleData, onArticleDeleted } = this.props;
|
||||||
|
if (this.state.busy) return;
|
||||||
|
if (!window.confirm(`Artikel „${articleData.name ?? articleData.sku}“ wirklich löschen?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ busy: true });
|
||||||
|
try {
|
||||||
|
await this.context.article.delete(articleData.sku);
|
||||||
|
onArticleDeleted?.(articleData);
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ busy: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { busy } = this.state;
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
color="error"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<DeleteIcon />}
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => void this.handleDeleteArticle()}
|
||||||
|
>
|
||||||
|
Artikel löschen
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/components/article/tabs/ArticleSupplierTab.jsx
Normal file
12
src/components/article/tabs/ArticleSupplierTab.jsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
export default class ArticleSupplierTab extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Supplier
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user