This commit is contained in:
seb
2026-08-05 08:40:12 +02:00
parent a9e942df7a
commit 66f88e395b
12 changed files with 176 additions and 140 deletions

View File

@@ -215,18 +215,22 @@ class StorageManager {
Object.defineProperties(this, {
authMePromise: { get: () => authMeCache.promise },
updateAuthMe: { value: authMeCache.update },
articleIndexPromise: { get: () => articleIndexCache.promise },
updateArticleIndex: { value: articleIndexCache.update },
/** @deprecated use updateArticleIndex */
update: { value: articleIndexCache.update },
brandPromise: { get: () => brandCache.promise },
updateBrand: { value: brandCache.update },
unitPromise: { get: () => unitCache.promise },
updateUnit: { value: unitCache.update },
companyPromise: { get: () => companyCache.promise },
updateCompany: { value: companyCache.update },
categoryTreePromise: { get: () => categoryTreeCache.promise },
updateCategoryTree: { value: categoryTreeCache.update },
posterminalPromise: { get: () => posterminalCache.promise },
updatePosterminal: { value: posterminalCache.update },
});

View File

@@ -1,9 +1,11 @@
import { Component } from 'react';
import Box from '@mui/material/Box';
import { thumb } from '../../api';
import { StorageManagerContext } from '../../api/storageManager';
import ThumbHashImage from './ThumbHashImage';
class ArticlePhoto extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = { thumbLoaded: false };
@@ -78,7 +80,7 @@ class ArticlePhoto extends Component {
{fullHash ? (
<Box
component="img"
src={thumb.url(fullHash)}
src={this.context.thumb.url(fullHash)}
alt=""
onLoad={this.handleThumbLoad}
onError={this.handleThumbError}

View File

@@ -1,7 +1,7 @@
import { Component, createRef } from 'react';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import { article } from '../../api';
import { StorageManagerContext } from '../../api/storageManager';
import { PanelTile } from '../panels';
import { requireObject } from '../../utils/apiShape';
import { notifyError } from '../../utils/notify';
@@ -12,6 +12,8 @@ import ArticlePhoto from './ArticlePhoto';
import ArticlePhotosDialog from './ArticlePhotosDialog';
class ArticlePhotoTile extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.fileInputRef = createRef();
@@ -29,7 +31,7 @@ class ArticlePhotoTile extends Component {
try {
const file = await prepareArticlePhoto(rawBlob);
const uploadStarted = performance.now();
const res = await article.uploadImage(articleData.sku, file);
const res = await this.context.article.addImage(articleData.sku, file);
console.log('[article-photo] upload', {
uploadMs: Math.round(performance.now() - uploadStarted),
bytes: file.size,

View File

@@ -11,7 +11,7 @@ import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { article } from '../../api';
import { StorageManagerContext } from '../../api/storageManager';
import { requireArray, requireObject } from '../../utils/apiShape';
import {
articleWithPhotoOrder,
@@ -41,6 +41,8 @@ const overlayIconSx = {
};
class ArticlePhotosDialog extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.fileInputRef = createRef();
@@ -62,7 +64,7 @@ class ArticlePhotosDialog extends Component {
try {
const file = await prepareArticlePhoto(rawBlob);
const uploadStarted = performance.now();
const res = await article.uploadImage(articleData.sku, file);
const res = await this.context.article.addImage(articleData.sku, file);
console.log('[article-photo] upload', {
uploadMs: Math.round(performance.now() - uploadStarted),
bytes: file.size,
@@ -98,7 +100,7 @@ class ArticlePhotosDialog extends Component {
const { article: articleData, onArticleUpdated } = this.props;
this.setState({ busy: true });
try {
await article.deleteImage(articleData.sku, fullHash);
await this.context.article.deleteImage(articleData.sku, fullHash);
onArticleUpdated?.(articleWithoutPhoto(articleData, fullHash));
} catch (err) {
notifyError(err);
@@ -121,7 +123,7 @@ class ArticlePhotosDialog extends Component {
this.setState({ busy: true });
try {
const res = await article.orderImages(articleData.sku, fullHashes);
const res = await this.context.article.orderImages(articleData.sku, { fullHashes });
requireObject(res, 'PUT /api/article/:sku/images/order');
requireArray(res.fullHashes, 'fullHashes');
onArticleUpdated?.(articleWithPhotoOrder(articleData, res.fullHashes));

View File

@@ -14,7 +14,7 @@ import LocalShippingIcon from '@mui/icons-material/LocalShipping';
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
import PushPinIcon from '@mui/icons-material/PushPin';
import SettingsIcon from '@mui/icons-material/Settings';
import { article } from '../../api';
import { StorageManagerContext } from '../../api/storageManager';
import {
ARTICLE_TAB,
DEFAULT_ARTICLE_TAB,
@@ -92,6 +92,8 @@ function Placeholder({ label }) {
}
class ArticleTileTab extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -123,7 +125,7 @@ class ArticleTileTab extends Component {
this.setState({ busy: true });
try {
await article.delete(articleData.sku);
await this.context.article.delete(articleData.sku);
onArticleDeleted?.(articleData);
} catch (err) {
notifyError(err);

View File

@@ -1,5 +1,4 @@
import { Component } from 'react';
import { connect } from 'react-redux';
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
@@ -16,16 +15,11 @@ import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import { StorageManagerContext } from '../../api/storageManager';
import { requireObject } from '../../utils/apiShape';
import { notifyError } from '../../utils/notify';
import { parsePriceInput } from '../../utils/price';
import { COMMON_UNITS, DEFAULT_UNIT_SI, unitLabel } from '../../utils/units';
import {
deleteUnit,
loadUnits,
selectUnits,
selectUnitsStatus,
upsertUnit,
} from '../../store/unitSlice';
function emptyForm() {
return {
@@ -68,11 +62,15 @@ function formValid(form) {
);
}
class ArticleUnitEditorDialog extends Component {
export default class ArticleUnitEditorDialog extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
busy: false,
units: [],
unitsLoading: false,
form: emptyForm(),
selectedSi: null,
};
@@ -81,9 +79,18 @@ class ArticleUnitEditorDialog extends Component {
componentDidUpdate(prevProps) {
if (this.props.open && !prevProps.open) {
this.setState({ form: emptyForm(), selectedSi: null });
if (this.props.status !== 'succeeded') {
void this.props.loadUnits().catch((err) => notifyError(err));
}
void this.loadUnits();
}
}
async loadUnits() {
this.setState({ unitsLoading: true });
try {
const units = await this.context.unitPromise;
this.setState({ units, unitsLoading: false });
} catch (err) {
this.setState({ unitsLoading: false });
notifyError(err);
}
}
@@ -110,19 +117,23 @@ class ArticleUnitEditorDialog extends Component {
if (!formValid(form)) return;
this.setState({ busy: true });
try {
const unit = await this.props.upsertUnit({
const data = await this.context.unit.upsert({
si: String(form.si ?? '').trim(),
basesi: String(form.basesi ?? '').trim(),
basesifactor: parseFactor(form.basesifactor),
symbol: String(form.symbol ?? '').trim(),
name: String(form.name ?? '').trim(),
}).unwrap();
});
requireObject(data, 'PUT /api/unit');
requireObject(data.unit, 'unit');
const units = await this.context.updateUnit();
this.setState({
busy: false,
form: formFromUnit(unit),
selectedSi: unit.si,
units,
form: formFromUnit(data.unit),
selectedSi: data.unit.si,
});
this.props.onUnitUpserted?.(unit);
this.props.onUnitUpserted?.(data.unit);
} catch (err) {
this.setState({ busy: false });
notifyError(err);
@@ -134,8 +145,9 @@ class ArticleUnitEditorDialog extends Component {
if (!selectedSi || selectedSi === DEFAULT_UNIT_SI) return;
this.setState({ busy: true });
try {
await this.props.deleteUnit(selectedSi).unwrap();
this.setState({ busy: false, form: emptyForm(), selectedSi: null });
await this.context.unit.delete(selectedSi);
const units = await this.context.updateUnit();
this.setState({ busy: false, units, form: emptyForm(), selectedSi: null });
this.props.onUnitDeleted?.(selectedSi);
} catch (err) {
this.setState({ busy: false });
@@ -148,10 +160,10 @@ class ArticleUnitEditorDialog extends Component {
};
render() {
const { open, onClose, units, status } = this.props;
const { busy, form, selectedSi } = this.state;
const { open, onClose } = this.props;
const { busy, form, selectedSi, units, unitsLoading } = this.state;
const canSave = formValid(form) && !busy;
const loading = status === 'loading' && units.length === 0;
const loading = unitsLoading && units.length === 0;
return (
<Dialog open={open} onClose={busy ? undefined : onClose} maxWidth="md" fullWidth>
@@ -302,16 +314,3 @@ class ArticleUnitEditorDialog extends Component {
);
}
}
const mapStateToProps = (state) => ({
units: selectUnits(state),
status: selectUnitsStatus(state),
});
const mapDispatchToProps = (dispatch) => ({
loadUnits: () => dispatch(loadUnits()),
upsertUnit: (body) => dispatch(upsertUnit(body)),
deleteUnit: (si) => dispatch(deleteUnit(si)),
});
export default connect(mapStateToProps, mapDispatchToProps)(ArticleUnitEditorDialog);

View File

@@ -1,20 +1,15 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { PanelTileGrid } from '../panels';
import { requireDefined } from '../../utils/apiShape';
import { primaryPhoto } from '../../utils/articleDetail';
import { useStorageManager } from '../../api/storageManager';
import { parseCategoryTreeResponse } from '../../store/categorySlice';
import { PANEL_TILE_KEYS } from '../../theme';
import { notifyError } from '../../utils/notify';
import {
buildCategoryPath,
defaultCategoryPath,
} from '../../pages/artikel/categoryRoute';
import {
patchArticleInLists,
removeArticleBySku,
selectCategoryRootId,
loadCategoryArticles,
selectCurrentCategoryId,
} from '../../store/categorySlice';
import ArticleTileTab from './ArticleTileTab';
import ArticlePhotoTile from './ArticlePhotoTile';
@@ -32,48 +27,55 @@ export default function ArticleView({
onArticleUpdated,
}) {
const navigate = useNavigate();
const dispatch = useDispatch();
const categories = useSelector((state) => state.category.categories);
const rootCategoryId = useSelector(selectCategoryRootId);
const currentCategoryId = useSelector(selectCurrentCategoryId);
const store = useStorageManager();
const [categories, setCategories] = useState(null);
const [rootCategoryId, setRootCategoryId] = useState(null);
const syncCategoryListPhoto = (next) => {
const photo = primaryPhoto(next);
dispatch(
patchArticleInLists({
sku: next.sku,
fullHash: photo?.fullHash ?? null,
thumbW: photo?.thumbW ?? null,
thumbH: photo?.thumbH ?? null,
thumbHash: photo?.thumbHash ?? null,
}),
);
useEffect(() => {
let cancelled = false;
async function load() {
try {
const [treeData, companies, session] = await Promise.all([
store.categoryTreePromise,
store.companyPromise,
store.authMePromise,
]);
const companyId = requireDefined(session.user?.selectedCompanyId, 'selectedCompanyId');
const company = requireDefined(
companies.find((c) => c.id === companyId),
'company',
);
const parsed = parseCategoryTreeResponse(treeData, company.rootCategoryId);
if (!cancelled) {
setCategories(parsed.categories);
setRootCategoryId(parsed.rootCategoryId);
}
} catch (err) {
if (!cancelled) notifyError(err);
}
}
void load();
return () => {
cancelled = true;
};
}, [store]);
const refreshSearchCaches = () => {
void store.updateArticleIndex();
void store.updateCategoryTree();
};
const handleArticleUpdated = (next) => {
onArticleUpdated?.(next);
syncCategoryListPhoto(next);
dispatch(
patchArticleInLists({
sku: next.sku,
name: next.name,
categoryIds: next.categoryIds,
}),
);
// Reload articles to reflect the update
// The reducer handles removing from lists where article no longer belongs
if (currentCategoryId != null && String(currentCategoryId) !== String(rootCategoryId)) {
dispatch(loadCategoryArticles(currentCategoryId));
}
refreshSearchCaches();
};
const handleArticleDeleted = () => {
refreshSearchCaches();
const categoryId = requireDefined(
articleData.categoryIds?.[0],
'article.categoryIds[0]',
);
dispatch(removeArticleBySku(articleData.sku));
if (categories && rootCategoryId != null) {
navigate(buildCategoryPath(categories, rootCategoryId, categoryId));
return;
@@ -102,17 +104,17 @@ export default function ArticleView({
<ArticleViewBrand
article={articleData}
onArticleUpdated={onArticleUpdated}
onArticleUpdated={handleArticleUpdated}
/>
<ArticleViewEANs
article={articleData}
onArticleUpdated={onArticleUpdated}
onArticleUpdated={handleArticleUpdated}
/>
<ArticleViewPrice
article={articleData}
onArticleUpdated={onArticleUpdated}
onArticleUpdated={handleArticleUpdated}
/>
<ArticleTileTab

View File

@@ -1,13 +1,11 @@
import { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { EditablePanelTile } from '../../panels';
import { article } from '../../../api';
import { useStorageManager } from '../../../api/storageManager';
import { requireDefined, requireObject } from '../../../utils/apiShape';
import { loadBrands, selectBrands, selectBrandsStatus, upsertBrand } from '../../../store/brandSlice';
import { notifyError } from '../../../utils/notify';
import { PANEL_TILE_KEYS } from '../../../theme';
@@ -42,14 +40,22 @@ function brandDraftValid(draft) {
const SECOND_ROW_MIN_HEIGHT = 220;
export default function ArticleViewBrand({ article: articleData, onArticleUpdated }) {
const dispatch = useDispatch();
const brands = useSelector(selectBrands);
const brandsStatus = useSelector(selectBrandsStatus);
const store = useStorageManager();
const [brands, setBrands] = useState([]);
useEffect(() => {
if (brandsStatus !== 'idle') return;
void dispatch(loadBrands()).unwrap().catch((err) => notifyError(err));
}, [dispatch, brandsStatus]);
let cancelled = false;
store.brandPromise
.then((list) => {
if (!cancelled) setBrands(list);
})
.catch((err) => {
if (!cancelled) notifyError(err);
});
return () => {
cancelled = true;
};
}, [store]);
const handleSave = async (draft) => {
const name = String(draft.name ?? '').trim();
@@ -65,12 +71,16 @@ export default function ArticleViewBrand({ article: articleData, onArticleUpdate
if (existing) {
brandId = existing.id;
} else {
const created = await dispatch(upsertBrand({ name })).unwrap();
requireDefined(created.id, 'brand.id');
brandId = created.id;
const createdRes = await store.brand.upsert({ name });
requireObject(createdRes, 'PUT /api/brand');
requireObject(createdRes.brand, 'brand');
requireDefined(createdRes.brand.id, 'brand.id');
brandId = createdRes.brand.id;
const nextBrands = await store.updateBrand();
setBrands(nextBrands);
}
const res = await article.setBrandIdBySku(articleData.sku, brandId);
const res = await store.article.setBrand(articleData.sku, { brandId });
requireObject(res, 'PUT /api/article/:sku/brand');
requireObject(res.article, 'article');
onArticleUpdated?.(res.article);

View File

@@ -3,7 +3,7 @@ import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { EditablePanelTile } from '../../panels';
import { article } from '../../../api';
import { useStorageManager } from '../../../api/storageManager';
import { requireObject } from '../../../utils/apiShape';
import { articleEans } from '../../../utils/articleDetail';
import { PANEL_TILE_KEYS } from '../../../theme';
@@ -41,11 +41,13 @@ function eansDraftValid(draft) {
const SECOND_ROW_MIN_HEIGHT = 220;
export default function ArticleViewEANs({ article: articleData, onArticleUpdated }) {
const store = useStorageManager();
const handleSave = async (draft) => {
const eans = (draft.eans ?? [])
.map((ean) => String(ean).trim())
.filter((ean) => ean.length > 0);
const res = await article.setEansBySku(articleData.sku, eans);
const res = await store.article.setEans(articleData.sku, { eans });
requireObject(res, 'PUT /api/article/:sku/eans');
requireObject(res.article, 'article');
onArticleUpdated?.(res.article);

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { EditablePanelTile } from '../../panels';
import SingleLineTextEdit from '../../SingleLineTextEdit';
import { article } from '../../../api';
import { useStorageManager } from '../../../api/storageManager';
import { requireObject } from '../../../utils/apiShape';
import { PANEL_TILE_KEYS } from '../../../theme';
import { getCategoryBreadcrumbs } from '../../../store/categorySlice';
@@ -122,13 +122,15 @@ function CategoryPills({
}
export default function ArticleViewName({ article: articleData, categories, rootCategoryId, onArticleUpdated }) {
const store = useStorageManager();
const handleSave = async (draft) => {
const name = draft.name.trim();
const categoryIds = draft.categoryIds ?? [];
const [nameRes] = await Promise.all([
article.update(articleData.sku, { name }),
article.setCategories(articleData.sku, categoryIds),
store.article.update(articleData.sku, { name }),
store.article.setCategories(articleData.sku, { categoryIds }),
]);
requireObject(nameRes, 'PATCH /api/article/:sku');

View File

@@ -1,4 +1,3 @@
import { useDispatch, useSelector } from 'react-redux';
import { useEffect, useState } from 'react';
import Box from '@mui/material/Box';
import InputAdornment from '@mui/material/InputAdornment';
@@ -7,11 +6,8 @@ import Typography from '@mui/material/Typography';
import { EditablePanelTile } from '../../panels';
import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog';
import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog';
import { article } from '../../../api';
import { useStorageManager } from '../../../api/storageManager';
import { requireDefined, requireObject } from '../../../utils/apiShape';
import { loadUnits, selectUnits, selectUnitsStatus } from '../../../store/unitSlice';
import { selectCurrentCompany } from '../../../store/companySlice';
import { patchArticleInLists } from '../../../store/categorySlice';
import {
buildPosPricesPutBody,
companyTaxRateOptions,
@@ -219,12 +215,11 @@ function PriceAmountField({
/* ── main component ────────────────────────────────────────────────── */
export default function ArticleViewPrice({ article: articleData, onArticleUpdated }) {
const dispatch = useDispatch();
const store = useStorageManager();
const [tiersDialogOpen, setTiersDialogOpen] = useState(false);
const [unitEditorOpen, setUnitEditorOpen] = useState(false);
const units = useSelector(selectUnits);
const unitsStatus = useSelector(selectUnitsStatus);
const company = useSelector(selectCurrentCompany);
const [units, setUnits] = useState([]);
const [company, setCompany] = useState(null);
const basePrice = primaryPrice(articleData);
const taxRates = companyTaxRateOptions(company, basePrice?.taxRate);
const priceValue = priceDraftValue(articleData, company);
@@ -234,20 +229,32 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
: units;
useEffect(() => {
if (unitsStatus !== 'idle') return;
void dispatch(loadUnits()).unwrap().catch((err) => notifyError(err));
}, [dispatch, unitsStatus]);
const syncCategoryListPrice = (next) => {
const price = primaryPrice(next);
dispatch(
patchArticleInLists({
sku: next.sku,
net: price?.net ?? null,
taxRate: price?.taxRate ?? null,
}),
);
};
let cancelled = false;
async function load() {
try {
const [unitList, companies, session] = await Promise.all([
store.unitPromise,
store.companyPromise,
store.authMePromise,
]);
const companyId = requireDefined(session.user?.selectedCompanyId, 'selectedCompanyId');
const current = requireDefined(
companies.find((c) => c.id === companyId),
'company',
);
if (!cancelled) {
setUnits(unitList);
setCompany(current);
}
} catch (err) {
if (!cancelled) notifyError(err);
}
}
void load();
return () => {
cancelled = true;
};
}, [store]);
const handleSave = async (draft) => {
const saved = priceDraftValue(articleData, company);
@@ -269,12 +276,12 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
if (isNoPackUnit(unitSi, unitamount)) {
throw new Error('Einheit pc × 1 ist keine Packungseinheit');
}
const res = await article.update(articleData.sku, { unitamount, unitSi });
const res = await store.article.update(articleData.sku, { unitamount, unitSi });
requireObject(res, 'PATCH /api/article/:sku');
requireObject(res.article, 'article');
nextArticle = res.article;
} else {
const res = await article.update(articleData.sku, {
const res = await store.article.update(articleData.sku, {
unitamount: 1,
unitSi: 'pc',
});
@@ -299,11 +306,10 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
taxRate,
tiers: draft.tiers ?? [],
});
const res = await article.setPrices(articleData.sku, prices);
const res = await store.article.setPrices(articleData.sku, { prices });
requireObject(res, 'PUT /api/article/:sku/prices');
requireObject(res.article, 'article');
nextArticle = res.article;
syncCategoryListPrice(res.article);
}
onArticleUpdated?.(nextArticle);
@@ -421,12 +427,14 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
<ArticleUnitEditorDialog
open={unitEditorOpen}
onClose={() => setUnitEditorOpen(false)}
onUnitUpserted={(row) => {
onUnitUpserted={async (row) => {
setUnits(await store.unitPromise);
if (!draft.unitSi) {
onChange({ ...draft, unitSi: row.si });
}
}}
onUnitDeleted={(si) => {
onUnitDeleted={async (si) => {
setUnits(await store.unitPromise);
if (draft.unitSi === si) {
onChange({ ...draft, unitSi: '' });
}

View File

@@ -3,7 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import { article } from '../../api';
import { useStorageManager } from '../../api/storageManager';
import { ArticleView } from '../../components/article';
import BackendError from '../../components/BackendError';
import { requireObject } from '../../utils/apiShape';
@@ -16,6 +16,7 @@ import {
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);
@@ -51,7 +52,7 @@ export default function ArtikelSkuPage() {
setLoading(true);
setError(null);
try {
const res = await article.getBySku(sku);
const res = await store.article.get(sku);
requireObject(res, 'GET /api/article/:sku');
requireObject(res.article, 'article');
if (!cancelled) {
@@ -71,7 +72,7 @@ export default function ArtikelSkuPage() {
return () => {
cancelled = true;
};
}, [sku, reloadKey]);
}, [sku, reloadKey, store]);
useEffect(() => {
if (!data) return;