This commit is contained in:
seb
2026-08-05 08:48:12 +02:00
parent a88a11224f
commit debd246012
21 changed files with 259 additions and 244 deletions

View File

@@ -1,8 +1,3 @@
import { createContext, useContext, useRef } from 'react';
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
export const StorageManagerContext = createContext(null);
function enc(value) { function enc(value) {
return encodeURIComponent(String(value)); return encodeURIComponent(String(value));
} }
@@ -23,38 +18,10 @@ function buildPath(path, query) {
} }
/** /**
* Stale-while-revalidate cache: readers keep the current promise; update() * Thin HTTP client for the dashback API (cookie auth, JSON / blob / empty).
* swaps only after a successful reload. Failed updates leave the cache intact.
*/ */
function createCache(label, load) { export class ApiClient {
let promise = load().catch((err) => {
console.error(`[storageMgr] ${label} failed`, err);
throw err;
});
async function update() {
try {
const data = await load();
promise = Promise.resolve(data);
return data;
} catch (err) {
console.error(`[storageMgr] update ${label} failed`, err);
throw err;
}
}
return {
update,
get promise() {
return promise;
},
};
}
class StorageManager {
#baseUrl; #baseUrl;
/** @type {EventSource | null} */
#events = null;
constructor(baseUrl = window.location.origin) { constructor(baseUrl = window.location.origin) {
this.#baseUrl = baseUrl; this.#baseUrl = baseUrl;
@@ -201,171 +168,11 @@ class StorageManager {
/** Absolute URL for <img src> (same-origin / cookie auth). */ /** Absolute URL for <img src> (same-origin / cookie auth). */
url: (fullHash) => buildUrl(this.#baseUrl, `/api/thumb/${enc(fullHash)}`), url: (fullHash) => buildUrl(this.#baseUrl, `/api/thumb/${enc(fullHash)}`),
}; };
this.#initCaches();
} }
#initCaches() { /** Absolute URL for a same-origin API path (e.g. SSE). */
const authMeCache = createCache('GET /auth/me', () => this.#loadAuthMe()); urlFor(path) {
const articleIndexCache = createCache('GET /articleIndex', () => this.#loadArticleIndex()); return buildUrl(this.#baseUrl, path);
const brandCache = createCache('GET /brand', () => this.#loadBrands());
const unitCache = createCache('GET /unit', () => this.#loadUnits());
const companyCache = createCache('GET /company', () => this.#loadCompanies());
const categoryTreeCache = createCache('GET /category/tree', () => this.#loadCategoryTree());
const posterminalCache = createCache('GET /posterminal', () => this.#loadPosterminals());
Object.defineProperties(this, {
authMePromise: { get: () => authMeCache.promise },
updateAuthMe: { value: authMeCache.update },
articleIndexPromise: { get: () => articleIndexCache.promise },
updateArticleIndex: { 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 },
/** Close and reopen GET /api/events (required after selected-company change). */
reconnectEvents: { value: () => this.#reconnectEvents() },
});
}
/**
* GET /api/events — SSE signals for the selected company.
* Stream is bound at connect time; reconnect after company switch.
*/
#reconnectEvents() {
this.#disconnectEvents();
const url = buildUrl(this.#baseUrl, '/api/events');
const es = new EventSource(url, { withCredentials: true });
es.addEventListener('category', () => {
void this.updateCategoryTree().catch((err) => {
console.error('[storageMgr] category event refresh failed', err);
});
});
es.addEventListener('articles', () => {
void this.updateArticleIndex().catch((err) => {
console.error('[storageMgr] articles event refresh failed', err);
});
});
es.onerror = () => {
console.error('[storageMgr] events stream error');
};
this.#events = es;
}
#disconnectEvents() {
if (!this.#events) return;
this.#events.close();
this.#events = null;
}
#syncEvents(user) {
if (user?.selectedCompanyId != null) {
this.#reconnectEvents();
} else {
this.#disconnectEvents();
}
}
async #loadAuthMe() {
try {
const data = await this.auth.me();
requireObject(data, 'GET /api/auth/me');
const session = {
user: data.user,
googleClientId: data.googleClientId,
};
this.#syncEvents(session.user);
return session;
} catch (err) {
if (err.status === 401 || err.status === 403) {
this.#syncEvents(null);
return {
user: null,
googleClientId: err.data?.googleClientId,
forbidden: err.status === 403,
message: err.message,
};
}
throw err;
}
}
async #loadArticleIndex() {
const data = await this.article.index();
requireObject(data, 'GET /api/articleIndex');
const articles = requireArray(data.articles, 'articleIndex.articles');
// Map backend rows to the recordset shape used by the result list.
// `categories` is a comma-joined string (formatCategoryList splits on ',').
return articles.map((a) => ({
id: a.id,
sku: a.sku,
name: a.name,
eans: a.eans ?? [],
categories: Array.isArray(a.categoryIds) ? a.categoryIds.join(',') : null,
}));
}
async #loadBrands() {
const data = await this.brand.list();
requireObject(data, 'GET /api/brand');
return requireArray(data.brands, 'brands');
}
async #loadUnits() {
const data = await this.unit.list();
requireObject(data, 'GET /api/unit');
return requireArray(data.units, 'units');
}
async #loadCompanies() {
const data = await this.company.list();
requireObject(data, 'GET /api/company');
return requireArray(data.companies, 'companies');
}
async #loadCategoryTree() {
const data = await this.category.tree();
requireObject(data, 'GET /api/category/tree');
requireArray(data.tree, 'category/tree.tree');
requireArray(data.articles, 'category/tree.articles');
requireDefined(data.languageId, 'category/tree.languageId');
return data;
}
async #loadPosterminals() {
const data = await this.posterminal.list();
requireObject(data, 'GET /api/posterminal');
return requireArray(data.posterminals, 'posterminals');
}
async doSearch({ target, filter, offset, maxCount, onUpdate }) {
const articles = await this.articleIndexPromise;
const query = target.trim().toLowerCase();
const matches = articles.filter(
(p) =>
p.name.toLowerCase().includes(query) ||
p.sku.toLowerCase().includes(query) ||
(p.eans ?? []).some((ean) => String(ean).includes(query)),
);
onUpdate({
categoryFacets: {},
totalCount: matches.length,
recordset: matches,
error: null,
});
} }
async #json(method, path, { body, query, contentType, raw } = {}) { async #json(method, path, { body, query, contentType, raw } = {}) {
@@ -441,31 +248,4 @@ class StorageManager {
} }
} }
/** export default ApiClient;
* Provider — mount once near the top of the tree.
*/
export function StorageManagerProvider({ children }) {
const mgrRef = useRef(null);
if (!mgrRef.current) {
mgrRef.current = new StorageManager();
}
return (
<StorageManagerContext.Provider value={mgrRef.current}>
{children}
</StorageManagerContext.Provider>
);
}
/**
* Hook for function components that need the store manager.
*/
export function useStorageManager() {
const ctx = useContext(StorageManagerContext);
if (!ctx) {
throw new Error('useStorageManager must be used within a <StorageManagerProvider>');
}
return ctx;
}
export default StorageManagerContext;

208
src/api/StorageManager.js Normal file
View File

@@ -0,0 +1,208 @@
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
import { ApiClient } from './ApiClient';
/**
* Stale-while-revalidate cache: readers keep the current promise; update()
* swaps only after a successful reload. Failed updates leave the cache intact.
*/
function createCache(label, load) {
let promise = load().catch((err) => {
console.error(`[storageMgr] ${label} failed`, err);
throw err;
});
async function update() {
try {
const data = await load();
promise = Promise.resolve(data);
return data;
} catch (err) {
console.error(`[storageMgr] update ${label} failed`, err);
throw err;
}
}
return {
update,
get promise() {
return promise;
},
};
}
/**
* ApiClient plus stale-while-revalidate caches and GET /api/events invalidation.
*/
export class StorageManager extends ApiClient {
/** @type {EventSource | null} */
#events = null;
constructor(baseUrl = window.location.origin) {
super(baseUrl);
this.#initCaches();
}
#initCaches() {
const authMeCache = createCache('GET /auth/me', () => this.#loadAuthMe());
const articleIndexCache = createCache('GET /articleIndex', () => this.#loadArticleIndex());
const brandCache = createCache('GET /brand', () => this.#loadBrands());
const unitCache = createCache('GET /unit', () => this.#loadUnits());
const companyCache = createCache('GET /company', () => this.#loadCompanies());
const categoryTreeCache = createCache('GET /category/tree', () => this.#loadCategoryTree());
const posterminalCache = createCache('GET /posterminal', () => this.#loadPosterminals());
Object.defineProperties(this, {
authMePromise: { get: () => authMeCache.promise },
updateAuthMe: { value: authMeCache.update },
articleIndexPromise: { get: () => articleIndexCache.promise },
updateArticleIndex: { 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 },
/** Close and reopen GET /api/events (required after selected-company change). */
reconnectEvents: { value: () => this.#reconnectEvents() },
});
}
/**
* GET /api/events — SSE signals for the selected company.
* Stream is bound at connect time; reconnect after company switch.
*/
#reconnectEvents() {
this.#disconnectEvents();
const es = new EventSource(this.urlFor('/api/events'), { withCredentials: true });
es.addEventListener('category', () => {
void this.updateCategoryTree().catch((err) => {
console.error('[storageMgr] category event refresh failed', err);
});
});
es.addEventListener('articles', () => {
void this.updateArticleIndex().catch((err) => {
console.error('[storageMgr] articles event refresh failed', err);
});
});
es.onerror = () => {
console.error('[storageMgr] events stream error');
};
this.#events = es;
}
#disconnectEvents() {
if (!this.#events) return;
this.#events.close();
this.#events = null;
}
#syncEvents(user) {
if (user?.selectedCompanyId != null) {
this.#reconnectEvents();
} else {
this.#disconnectEvents();
}
}
async #loadAuthMe() {
try {
const data = await this.auth.me();
requireObject(data, 'GET /api/auth/me');
const session = {
user: data.user,
googleClientId: data.googleClientId,
};
this.#syncEvents(session.user);
return session;
} catch (err) {
if (err.status === 401 || err.status === 403) {
this.#syncEvents(null);
return {
user: null,
googleClientId: err.data?.googleClientId,
forbidden: err.status === 403,
message: err.message,
};
}
throw err;
}
}
async #loadArticleIndex() {
const data = await this.article.index();
requireObject(data, 'GET /api/articleIndex');
const articles = requireArray(data.articles, 'articleIndex.articles');
// Map backend rows to the recordset shape used by the result list.
// `categories` is a comma-joined string (formatCategoryList splits on ',').
return articles.map((a) => ({
id: a.id,
sku: a.sku,
name: a.name,
eans: a.eans ?? [],
categories: Array.isArray(a.categoryIds) ? a.categoryIds.join(',') : null,
}));
}
async #loadBrands() {
const data = await this.brand.list();
requireObject(data, 'GET /api/brand');
return requireArray(data.brands, 'brands');
}
async #loadUnits() {
const data = await this.unit.list();
requireObject(data, 'GET /api/unit');
return requireArray(data.units, 'units');
}
async #loadCompanies() {
const data = await this.company.list();
requireObject(data, 'GET /api/company');
return requireArray(data.companies, 'companies');
}
async #loadCategoryTree() {
const data = await this.category.tree();
requireObject(data, 'GET /api/category/tree');
requireArray(data.tree, 'category/tree.tree');
requireArray(data.articles, 'category/tree.articles');
requireDefined(data.languageId, 'category/tree.languageId');
return data;
}
async #loadPosterminals() {
const data = await this.posterminal.list();
requireObject(data, 'GET /api/posterminal');
return requireArray(data.posterminals, 'posterminals');
}
async doSearch({ target, filter, offset, maxCount, onUpdate }) {
const articles = await this.articleIndexPromise;
const query = target.trim().toLowerCase();
const matches = articles.filter(
(p) =>
p.name.toLowerCase().includes(query) ||
p.sku.toLowerCase().includes(query) ||
(p.eans ?? []).some((ean) => String(ean).includes(query)),
);
onUpdate({
categoryFacets: {},
totalCount: matches.length,
recordset: matches,
error: null,
});
}
}
export default StorageManager;

View File

@@ -0,0 +1,27 @@
import { createContext, createElement, useContext, useRef } from 'react';
import { StorageManager } from './StorageManager';
export const StorageManagerContext = createContext(null);
/**
* Provider — mount once near the top of the tree.
*/
export function StorageManagerProvider({ children }) {
const mgrRef = useRef(null);
if (!mgrRef.current) {
mgrRef.current = new StorageManager();
}
return createElement(StorageManagerContext.Provider, { value: mgrRef.current }, children);
}
/**
* Hook for function components that need the store manager.
*/
export function useStorageManager() {
const ctx = useContext(StorageManagerContext);
if (!ctx) {
throw new Error('useStorageManager must be used within a <StorageManagerProvider>');
}
return ctx;
}

View File

@@ -2,7 +2,7 @@ import { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import { notifyError } from '../utils/notify'; import { notifyError } from '../utils/notify';
import { StorageManagerContext } from '../api/storageManager'; import { StorageManagerContext } from '../api/storageManagerContext';
import { bootstrapSession as bootstrapSessionThunk } from '../store/authSlice'; import { bootstrapSession as bootstrapSessionThunk } from '../store/authSlice';
let sessionBootstrapped = false; let sessionBootstrapped = false;

View File

@@ -25,7 +25,7 @@ import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import BackendError from './BackendError'; import BackendError from './BackendError';
import SingleLineTextEdit from './SingleLineTextEdit'; import SingleLineTextEdit from './SingleLineTextEdit';
import { StorageManagerContext } from '../api/storageManager'; import { StorageManagerContext } from '../api/storageManagerContext';
import { requireDefined, requireObject } from '../utils/apiShape'; import { requireDefined, requireObject } from '../utils/apiShape';
import { ISO639_OPTIONS, parseTaxRate } from '../utils/companyForm'; import { ISO639_OPTIONS, parseTaxRate } from '../utils/companyForm';
import { backendErrorFrom, isBackendFailure } from '../utils/backendError'; import { backendErrorFrom, isBackendFailure } from '../utils/backendError';

View File

@@ -22,7 +22,7 @@ import Typography from '@mui/material/Typography';
import BusinessIcon from '@mui/icons-material/Business'; import BusinessIcon from '@mui/icons-material/Business';
import BackendError from './BackendError'; import BackendError from './BackendError';
import { company } from '../api'; import { company } from '../api';
import { StorageManagerContext } from '../api/storageManager'; import { StorageManagerContext } from '../api/storageManagerContext';
import { import {
createCompany as createCompanyThunk, createCompany as createCompanyThunk,
logout as logoutThunk, logout as logoutThunk,

View File

@@ -6,7 +6,7 @@ import GoogleIcon from '@mui/icons-material/Google';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import { notifyErrorMessage } from '../utils/notify'; import { notifyErrorMessage } from '../utils/notify';
import { loginWithGoogle, selectAuthStatus, selectGoogleClientId } from '../store/authSlice'; import { loginWithGoogle, selectAuthStatus, selectGoogleClientId } from '../store/authSlice';
import { StorageManagerContext } from '../api/storageManager'; import { StorageManagerContext } from '../api/storageManagerContext';
import { import {
abortGoogleSignIn, abortGoogleSignIn,
FEDCM_BLOCKED_MESSAGE, FEDCM_BLOCKED_MESSAGE,

View File

@@ -9,7 +9,7 @@ import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import LogoutIcon from '@mui/icons-material/Logout'; import LogoutIcon from '@mui/icons-material/Logout';
import BusinessIcon from '@mui/icons-material/Business'; import BusinessIcon from '@mui/icons-material/Business';
import { StorageManagerContext } from '../api/storageManager'; import { StorageManagerContext } from '../api/storageManagerContext';
import { logout } from '../store/authSlice'; import { logout } from '../store/authSlice';
import CompanySettingsDialog from './CompanySettingsDialog'; import CompanySettingsDialog from './CompanySettingsDialog';
import { notifyError } from '../utils/notify'; import { notifyError } from '../utils/notify';

View File

@@ -1,6 +1,6 @@
import { Component } from 'react'; import { Component } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { StorageManagerContext } from '../../api/storageManager'; import { StorageManagerContext } from '../../api/storageManagerContext';
import ThumbHashImage from './ThumbHashImage'; import ThumbHashImage from './ThumbHashImage';
class ArticlePhoto extends Component { class ArticlePhoto extends Component {

View File

@@ -1,7 +1,7 @@
import { Component, createRef } from 'react'; import { Component, createRef } from 'react';
import ContentPasteIcon from '@mui/icons-material/ContentPaste'; import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import { StorageManagerContext } from '../../api/storageManager'; import { StorageManagerContext } from '../../api/storageManagerContext';
import { PanelTile } from '../panels'; import { PanelTile } from '../panels';
import { requireObject } from '../../utils/apiShape'; import { requireObject } from '../../utils/apiShape';
import { notifyError } from '../../utils/notify'; import { notifyError } from '../../utils/notify';

View File

@@ -11,7 +11,7 @@ import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined'; import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { StorageManagerContext } from '../../api/storageManager'; import { StorageManagerContext } from '../../api/storageManagerContext';
import { requireArray, requireObject } from '../../utils/apiShape'; import { requireArray, requireObject } from '../../utils/apiShape';
import { import {
articleWithPhotoOrder, articleWithPhotoOrder,

View File

@@ -14,7 +14,7 @@ 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/storageManager'; import { StorageManagerContext } from '../../api/storageManagerContext';
import { import {
ARTICLE_TAB, ARTICLE_TAB,
DEFAULT_ARTICLE_TAB, DEFAULT_ARTICLE_TAB,

View File

@@ -15,7 +15,7 @@ import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { StorageManagerContext } from '../../api/storageManager'; import { StorageManagerContext } from '../../api/storageManagerContext';
import { requireObject } from '../../utils/apiShape'; import { requireObject } from '../../utils/apiShape';
import { notifyError } from '../../utils/notify'; import { notifyError } from '../../utils/notify';
import { parsePriceInput } from '../../utils/price'; import { parsePriceInput } from '../../utils/price';

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { PanelTileGrid } from '../panels'; import { PanelTileGrid } from '../panels';
import { requireDefined } from '../../utils/apiShape'; import { requireDefined } from '../../utils/apiShape';
import { useStorageManager } from '../../api/storageManager'; import { useStorageManager } from '../../api/storageManagerContext';
import { parseCategoryTreeResponse } from '../../store/categorySlice'; import { parseCategoryTreeResponse } from '../../store/categorySlice';
import { PANEL_TILE_KEYS } from '../../theme'; import { PANEL_TILE_KEYS } from '../../theme';
import { notifyError } from '../../utils/notify'; import { notifyError } from '../../utils/notify';

View File

@@ -4,7 +4,7 @@ import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { EditablePanelTile } from '../../panels'; import { EditablePanelTile } from '../../panels';
import { useStorageManager } from '../../../api/storageManager'; import { useStorageManager } from '../../../api/storageManagerContext';
import { requireDefined, requireObject } from '../../../utils/apiShape'; import { requireDefined, requireObject } from '../../../utils/apiShape';
import { notifyError } from '../../../utils/notify'; import { notifyError } from '../../../utils/notify';
import { PANEL_TILE_KEYS } from '../../../theme'; import { PANEL_TILE_KEYS } from '../../../theme';

View File

@@ -3,7 +3,7 @@ import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { EditablePanelTile } from '../../panels'; import { EditablePanelTile } from '../../panels';
import { useStorageManager } from '../../../api/storageManager'; import { useStorageManager } from '../../../api/storageManagerContext';
import { requireObject } from '../../../utils/apiShape'; import { requireObject } from '../../../utils/apiShape';
import { articleEans } from '../../../utils/articleDetail'; import { articleEans } from '../../../utils/articleDetail';
import { PANEL_TILE_KEYS } from '../../../theme'; import { PANEL_TILE_KEYS } from '../../../theme';

View File

@@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { EditablePanelTile } from '../../panels'; import { EditablePanelTile } from '../../panels';
import SingleLineTextEdit from '../../SingleLineTextEdit'; import SingleLineTextEdit from '../../SingleLineTextEdit';
import { useStorageManager } from '../../../api/storageManager'; import { useStorageManager } from '../../../api/storageManagerContext';
import { requireObject } from '../../../utils/apiShape'; import { requireObject } from '../../../utils/apiShape';
import { PANEL_TILE_KEYS } from '../../../theme'; import { PANEL_TILE_KEYS } from '../../../theme';
import { getCategoryBreadcrumbs } from '../../../store/categorySlice'; import { getCategoryBreadcrumbs } from '../../../store/categorySlice';

View File

@@ -6,7 +6,7 @@ import Typography from '@mui/material/Typography';
import { EditablePanelTile } from '../../panels'; import { EditablePanelTile } from '../../panels';
import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog'; import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog';
import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog'; import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog';
import { useStorageManager } from '../../../api/storageManager'; import { useStorageManager } from '../../../api/storageManagerContext';
import { requireDefined, requireObject } from '../../../utils/apiShape'; import { requireDefined, requireObject } from '../../../utils/apiShape';
import { import {
buildPosPricesPutBody, buildPosPricesPutBody,

View File

@@ -4,7 +4,7 @@ import { ThemeProvider } from '@mui/material/styles';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { SnackbarProvider } from 'notistack'; import { SnackbarProvider } from 'notistack';
import AuthBootstrap from './components/AuthBootstrap'; import AuthBootstrap from './components/AuthBootstrap';
import { StorageManagerProvider } from './api/storageManager'; import { StorageManagerProvider } from './api/storageManagerContext';
import App from './App'; import App from './App';
import { store } from './store'; import { store } from './store';
import { getActiveTheme } from './theme'; import { getActiveTheme } from './theme';

View File

@@ -9,7 +9,7 @@ import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear'; import ClearIcon from '@mui/icons-material/Clear';
import { StorageManagerContext } from '../../api/storageManager'; import { StorageManagerContext } from '../../api/storageManagerContext';
import BackendError from '../../components/BackendError'; import BackendError from '../../components/BackendError';
import { formatPrice } from '../../utils/price'; import { formatPrice } from '../../utils/price';
import { import {

View File

@@ -3,7 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import { useStorageManager } from '../../api/storageManager'; import { useStorageManager } from '../../api/storageManagerContext';
import { ArticleView } from '../../components/article'; import { ArticleView } from '../../components/article';
import BackendError from '../../components/BackendError'; import BackendError from '../../components/BackendError';
import { requireObject } from '../../utils/apiShape'; import { requireObject } from '../../utils/apiShape';