u
This commit is contained in:
@@ -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
208
src/api/StorageManager.js
Normal 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;
|
||||||
27
src/api/storageManagerContext.js
Normal file
27
src/api/storageManagerContext.js
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user