u
This commit is contained in:
@@ -257,8 +257,8 @@ _none_
|
|||||||
- Params: `sku: str, body: { eans: str[] }`
|
- Params: `sku: str, body: { eans: str[] }`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Get article history by SKU — `GET /article/{sku}/history`
|
#### Get article history by SKU — `GET /article/{sku}/history`
|
||||||
- Params: `sku: str, query: limit?: int`
|
- Params: `sku: str, query: offset?: int, limit?: int, search?: str, seekMonths?: int`
|
||||||
- Returns: `200 { history: { id: int, date: str, text: str }[], truncated: bool }`
|
- Returns: `200 { history: { id: int, date: str, text: str }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
||||||
#### Add article image — `POST /article/{sku}/image`
|
#### Add article image — `POST /article/{sku}/image`
|
||||||
- Params: `sku: str, image/avif`
|
- Params: `sku: str, image/avif`
|
||||||
- Returns: `201 { image: { picId: int, sort: int, full: { w: int, h: int, size: int }, thumb: { w: int, h: int }, hash: str, thumbHash: str } }`
|
- Returns: `201 { image: { picId: int, sort: int, full: { w: int, h: int, size: int }, thumb: { w: int, h: int }, hash: str, thumbHash: str } }`
|
||||||
|
|||||||
570
package-lock.json
generated
570
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,8 +33,6 @@
|
|||||||
"@mui/material": "^9.5.0",
|
"@mui/material": "^9.5.0",
|
||||||
"@shopify/draggable": "^1.2.1",
|
"@shopify/draggable": "^1.2.1",
|
||||||
"@uiw/react-codemirror": "^4.25.12",
|
"@uiw/react-codemirror": "^4.25.12",
|
||||||
"ag-grid-community": "^36.2.0",
|
|
||||||
"ag-grid-react": "^36.2.0",
|
|
||||||
"dashgui": "file:../dashgui",
|
"dashgui": "file:../dashgui",
|
||||||
"flexsearch": "^0.8.212",
|
"flexsearch": "^0.8.212",
|
||||||
"jsbarcode": "^3.12.3",
|
"jsbarcode": "^3.12.3",
|
||||||
@@ -45,9 +43,8 @@
|
|||||||
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
|
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
|
||||||
"react-dom": "^19.3.1",
|
"react-dom": "^19.3.1",
|
||||||
"react-router-dom": "^7.18.4",
|
"react-router-dom": "^7.18.4",
|
||||||
"react-virtual-aggridautomat": "file:../react-virtual-aggridautomat",
|
|
||||||
"react-window": "^2.3.4",
|
"react-window": "^2.3.4",
|
||||||
"skelehash-wasm": "^0.1.0",
|
"skelehash-wasm": "file:../skelehash-wasm",
|
||||||
"sortablejs": "^1.15.7",
|
"sortablejs": "^1.15.7",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,11 +50,19 @@ export const article = {
|
|||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/article/:sku/history
|
* GET /api/article/:sku/history?offset=0&limit=50&search=...&seekMonths=-1
|
||||||
|
* @param {string} sku
|
||||||
|
* @param {{ offset?: number, limit?: number, search?: string, seekMonths?: number }} [params]
|
||||||
|
* → { history: ArticleHistoryEntry[], total: number, unfilteredTotal?: number, offset: number, limit: number }
|
||||||
*/
|
*/
|
||||||
getBySkuHistory(sku, { limit } = {}) {
|
getBySkuHistory(sku, { offset = 0, limit = 50, search, seekMonths } = {}) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (limit != null) params.set('limit', String(limit));
|
if (offset != null) params.set('offset', String(Math.max(0, Math.floor(Number(offset) || 0))));
|
||||||
|
if (limit != null) params.set('limit', String(Math.max(1, Math.floor(Number(limit) || 50))));
|
||||||
|
if (search && String(search).trim()) params.set('search', String(search).trim());
|
||||||
|
if (seekMonths != null && Number(seekMonths) < 0) {
|
||||||
|
params.set('seekMonths', String(Math.floor(Number(seekMonths))));
|
||||||
|
}
|
||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
return http(`/api/article/${encodeURIComponent(sku)}/history${qs ? `?${qs}` : ''}`);
|
return http(`/api/article/${encodeURIComponent(sku)}/history${qs ? `?${qs}` : ''}`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,26 +34,72 @@ import { requireArray, requireObject } from '../utils/apiShape';
|
|||||||
* @property {number|null} taxRate Tax rate in percent.
|
* @property {number|null} taxRate Tax rate in percent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { createChunkListAutomat } from './createChunkListAutomat';
|
||||||
|
import authAutomat from './authAutomat';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-SKU article change history (`GET /api/article/{sku}/history`).
|
* Registry of SKU-specific chunk-list automats for article history.
|
||||||
*
|
* @type {Map<string, import('react-automat').Automat>}
|
||||||
* The backend returns the full list; pagination happens client-side in
|
|
||||||
* `ArticleHistoryTab`. Keyed by SKU so several tabs can watch the same article.
|
|
||||||
*
|
|
||||||
* @type {KeyedAutomat<Array<ArticleHistoryEntry>>}
|
|
||||||
*/
|
*/
|
||||||
export const articleHistoryAutomat = new KeyedAutomat(
|
const skuHistoryAutomats = new Map();
|
||||||
null,
|
|
||||||
{},
|
/**
|
||||||
{
|
* Gets or creates the chunk-list automat for a specific article's history.
|
||||||
name: 'articleHistory',
|
*
|
||||||
loader: async (sku) => {
|
* @param {string|number} sku Article SKU.
|
||||||
const res = await articleApi.getBySkuHistory(String(sku));
|
* @returns {import('react-automat').Automat} Configured chunk-list automat for the SKU.
|
||||||
requireObject(res, 'GET /api/article/:sku/history');
|
*/
|
||||||
return requireArray(res.history, 'history');
|
export function getArticleHistoryAutomat(sku) {
|
||||||
|
const key = String(sku);
|
||||||
|
let inst = skuHistoryAutomats.get(key);
|
||||||
|
if (!inst) {
|
||||||
|
inst = createChunkListAutomat({
|
||||||
|
name: `articleHistory:${key}`,
|
||||||
|
chunkSize: 50,
|
||||||
|
maxLruChunks: 50,
|
||||||
|
fetchPage: (params) => articleApi.getBySkuHistory(key, params),
|
||||||
|
});
|
||||||
|
skuHistoryAutomats.set(key, inst);
|
||||||
|
}
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-SKU article change history manager (`GET /api/article/{sku}/history`).
|
||||||
|
*
|
||||||
|
* Backed by chunk-list automats (`createChunkListAutomat`) keyed by SKU with
|
||||||
|
* multi-filter memory caching, LRU eviction, and infinite-scrolling / FillSpace support.
|
||||||
|
*/
|
||||||
|
export const articleHistoryAutomat = {
|
||||||
|
get: getArticleHistoryAutomat,
|
||||||
|
key: getArticleHistoryAutomat,
|
||||||
|
has(sku) {
|
||||||
|
return skuHistoryAutomats.has(String(sku));
|
||||||
},
|
},
|
||||||
|
async load(sku) {
|
||||||
|
const inst = getArticleHistoryAutomat(sku);
|
||||||
|
return inst.getItems();
|
||||||
},
|
},
|
||||||
);
|
async reload(sku) {
|
||||||
|
const inst = skuHistoryAutomats.get(String(sku));
|
||||||
|
if (inst) {
|
||||||
|
await inst.actions.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
for (const inst of skuHistoryAutomats.values()) {
|
||||||
|
inst.actions.reset();
|
||||||
|
}
|
||||||
|
skuHistoryAutomats.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset on logout or company change
|
||||||
|
authAutomat.subscribe((auth) => {
|
||||||
|
if (auth.status === 'anon') {
|
||||||
|
articleHistoryAutomat.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Suppliers assigned to an article (`GET /api/article/{sku}/supplier`).
|
* Suppliers assigned to an article (`GET /api/article/{sku}/supplier`).
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export { createListAutomat, selectListItems, selectListStatus } from './createLi
|
|||||||
|
|
||||||
|
|
||||||
/** Per-SKU article detail views keyed by SKU. */
|
/** Per-SKU article detail views keyed by SKU. */
|
||||||
export { articleHistoryAutomat, articleSupplierAutomat } from './articleDetailAutomat';
|
export { articleHistoryAutomat, getArticleHistoryAutomat, articleSupplierAutomat } from './articleDetailAutomat';
|
||||||
|
|
||||||
/** Single shared GET /api/events connection. */
|
/** Single shared GET /api/events connection. */
|
||||||
export { default as sseAutomat, invalidateArticle } from './sseAutomat';
|
export { default as sseAutomat, invalidateArticle } from './sseAutomat';
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,3 @@
|
|||||||
@import 'ag-grid-community/styles/ag-grid.css';
|
|
||||||
@import 'ag-grid-community/styles/ag-theme-alpine.css';
|
|
||||||
|
|
||||||
/* App background. Provided as a CSS custom property by src/colors.js
|
/* App background. Provided as a CSS custom property by src/colors.js
|
||||||
(installCssVars), with a hard-coded fallback for the very first paint. */
|
(installCssVars), with a hard-coded fallback for the very first paint. */
|
||||||
html,
|
html,
|
||||||
|
|||||||
Reference in New Issue
Block a user