sotre
This commit is contained in:
@@ -1,124 +1,185 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Component } from 'react';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { useStorageManager } from '../../api/storageManagerContext';
|
||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||
import { ArticleView } from '../../components/article';
|
||||
import BackendError from '../../components/BackendError';
|
||||
import { requireObject } from '../../utils/apiShape';
|
||||
import { requireDefined, requireObject } from '../../utils/apiShape';
|
||||
import { backendErrorFrom, isBackendFailure } from '../../utils/backendError';
|
||||
import { parseCategoryTreeResponse } from '../../store/categorySlice';
|
||||
import {
|
||||
artikelDetailPath,
|
||||
resolveArticleTab,
|
||||
} from './articleRoute';
|
||||
import withSkuRoute from './withSkuRoute';
|
||||
|
||||
export default function ArtikelSkuPage() {
|
||||
const { sku: skuParam, tab: tabParam } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const store = useStorageManager();
|
||||
const sku = skuParam ? decodeURIComponent(skuParam) : '';
|
||||
const [data, setData] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
class ArtikelSkuPage extends Component {
|
||||
static contextType = StorageManagerContext;
|
||||
|
||||
const retry = useCallback(() => {
|
||||
setReloadKey((key) => key + 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(e) {
|
||||
if (e.key !== 'Escape') return;
|
||||
e.preventDefault();
|
||||
navigate(-1);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => document.removeEventListener('keydown', onKeyDown);
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
if (!sku) {
|
||||
setError('Keine Artikelnr. angegeben');
|
||||
setData(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await store.article.get(sku);
|
||||
requireObject(res, 'GET /api/article/:sku');
|
||||
requireObject(res.article, 'article');
|
||||
if (!cancelled) {
|
||||
setData(res.article);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
setData(null);
|
||||
setError(backendErrorFrom(err));
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: null,
|
||||
categories: null,
|
||||
rootCategoryId: null,
|
||||
error: null,
|
||||
loading: true,
|
||||
reloadKey: 0,
|
||||
};
|
||||
}, [sku, reloadKey, store]);
|
||||
this._cancelled = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._cancelled = false;
|
||||
this.installEscape();
|
||||
void this.load();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { sku, tab: tabParam, navigate } = this.props;
|
||||
const { data, reloadKey } = this.state;
|
||||
|
||||
if (prevProps.sku !== sku || prevState.reloadKey !== reloadKey) {
|
||||
void this.load();
|
||||
return;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const resolved = resolveArticleTab(tabParam, data);
|
||||
if (tabParam !== resolved) {
|
||||
navigate(artikelDetailPath(data, resolved), { replace: true });
|
||||
}
|
||||
}, [data, tabParam, navigate]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (isBackendFailure(error)) {
|
||||
componentWillUnmount() {
|
||||
this._cancelled = true;
|
||||
this.removeEscape();
|
||||
}
|
||||
|
||||
installEscape() {
|
||||
this._onKeyDown = (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
e.preventDefault();
|
||||
this.props.navigate(-1);
|
||||
};
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
}
|
||||
|
||||
removeEscape() {
|
||||
if (this._onKeyDown) {
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
this._onKeyDown = null;
|
||||
}
|
||||
}
|
||||
|
||||
retry = () => {
|
||||
this.setState((s) => ({ reloadKey: s.reloadKey + 1 }));
|
||||
};
|
||||
|
||||
async load() {
|
||||
const { sku } = this.props;
|
||||
const store = this.context;
|
||||
|
||||
if (!sku) {
|
||||
this.setState({
|
||||
error: 'Keine Artikelnr. angegeben',
|
||||
data: null,
|
||||
categories: null,
|
||||
rootCategoryId: null,
|
||||
loading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._cancelled = false;
|
||||
this.setState({ loading: true, error: null });
|
||||
|
||||
try {
|
||||
const [res, treeData, companies, session] = await Promise.all([
|
||||
store.article.get(sku),
|
||||
store.categoryTreePromise,
|
||||
store.companyPromise,
|
||||
store.authMePromise,
|
||||
]);
|
||||
requireObject(res, 'GET /api/article/:sku');
|
||||
requireObject(res.article, 'article');
|
||||
const companyId = requireDefined(session.user?.selectedCompanyId, 'selectedCompanyId');
|
||||
const company = requireDefined(
|
||||
companies.find((c) => c.id === companyId),
|
||||
'company',
|
||||
);
|
||||
const parsed = parseCategoryTreeResponse(treeData, company.rootCategoryId);
|
||||
if (!this._cancelled) {
|
||||
this.setState({
|
||||
data: res.article,
|
||||
categories: parsed.categories,
|
||||
rootCategoryId: parsed.rootCategoryId,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (!this._cancelled) {
|
||||
this.setState({
|
||||
data: null,
|
||||
categories: null,
|
||||
rootCategoryId: null,
|
||||
error: backendErrorFrom(err),
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleArticleUpdated = (next) => {
|
||||
this.setState({ data: next });
|
||||
};
|
||||
|
||||
handleTabChange = (nextTab) => {
|
||||
const { data } = this.state;
|
||||
this.props.navigate(artikelDetailPath(data, nextTab));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tab: tabParam } = this.props;
|
||||
const { data, categories, rootCategoryId, error, loading } = this.state;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<BackendError
|
||||
error={error}
|
||||
onRetry={retry}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (isBackendFailure(error)) {
|
||||
return <BackendError error={error} onRetry={this.retry} />;
|
||||
}
|
||||
return (
|
||||
<Alert severity="error" sx={{ maxWidth: 640 }}>
|
||||
{typeof error === 'string' ? error : error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || categories == null || rootCategoryId == null) return null;
|
||||
|
||||
const tab = resolveArticleTab(tabParam, data);
|
||||
if (tabParam !== tab) return null;
|
||||
|
||||
return (
|
||||
<Alert severity="error" sx={{ maxWidth: 640 }}>
|
||||
{typeof error === 'string' ? error : error.message}
|
||||
</Alert>
|
||||
<ArticleView
|
||||
article={data}
|
||||
categories={categories}
|
||||
rootCategoryId={rootCategoryId}
|
||||
tab={tab}
|
||||
onTabChange={this.handleTabChange}
|
||||
onArticleUpdated={this.handleArticleUpdated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const tab = resolveArticleTab(tabParam, data);
|
||||
if (tabParam !== tab) return null;
|
||||
|
||||
return (
|
||||
<ArticleView
|
||||
article={data}
|
||||
tab={tab}
|
||||
onTabChange={(nextTab) => {
|
||||
navigate(artikelDetailPath(data, nextTab));
|
||||
}}
|
||||
onArticleUpdated={setData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withSkuRoute(ArtikelSkuPage);
|
||||
|
||||
13
src/pages/artikel/withSkuRoute.jsx
Normal file
13
src/pages/artikel/withSkuRoute.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
/** Injects `sku`, `tab`, and `navigate` from the article detail route. */
|
||||
export default function withSkuRoute(Wrapped) {
|
||||
function SkuRouteWrapper(props) {
|
||||
const navigate = useNavigate();
|
||||
const { sku: skuParam, tab } = useParams();
|
||||
const sku = skuParam ? decodeURIComponent(skuParam) : '';
|
||||
|
||||
return <Wrapped {...props} sku={sku} tab={tab} navigate={navigate} />;
|
||||
}
|
||||
return SkuRouteWrapper;
|
||||
}
|
||||
Reference in New Issue
Block a user