no redux
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
|
||||
import { notifyError } from '../utils/notify';
|
||||
import { parseCategoryTreeResponse } from '../utils/categoryUtils';
|
||||
import { ApiClient } from './ApiClient';
|
||||
import artikelAutomat from '../automats/artikelAutomat';
|
||||
import companyAutomat from '../automats/companyAutomat';
|
||||
import brandAutomat from '../automats/brandAutomat';
|
||||
import unitAutomat from '../automats/unitAutomat';
|
||||
import authAutomat from '../automats/authAutomat';
|
||||
import categoryAutomat from '../automats/categoryAutomat';
|
||||
import categoryAutomat, { invalidateAllCategoryViews } from '../automats/categoryAutomat';
|
||||
import posterminalAutomat from '../automats/posterminalAutomat';
|
||||
|
||||
|
||||
@@ -175,24 +174,10 @@ export class StorageManager extends ApiClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed category map + root id from settled caches, or null if any piece missing.
|
||||
* @returns {{ categories: object, rootCategoryId: unknown } | null}
|
||||
* Parsed category map + root id from categoryAutomat, or null if not yet loaded.
|
||||
* @returns {{ categories: Record<string, object>, rootCategoryId: unknown } | null}
|
||||
*/
|
||||
getCategoryState() {
|
||||
const company = this.company;
|
||||
if (company == null) return null;
|
||||
|
||||
const treeData = this.categoryTree;
|
||||
if (treeData != null) {
|
||||
// Legacy: tree was loaded via the old cache mechanism
|
||||
const parsed = parseCategoryTreeResponse(treeData, company.rootCategoryId);
|
||||
return {
|
||||
categories: parsed.categories,
|
||||
rootCategoryId: parsed.rootCategoryId,
|
||||
};
|
||||
}
|
||||
|
||||
// New path: use categoryAutomat state directly
|
||||
const catState = categoryAutomat.state;
|
||||
if (catState.categories == null || catState.treeStatus !== 'succeeded') return null;
|
||||
return {
|
||||
@@ -413,12 +398,6 @@ export class StorageManager extends ApiClient {
|
||||
);
|
||||
const articleIndexCache = this.#articleIndexCache;
|
||||
|
||||
// category tree: deferred, reloaded via categoryAutomat
|
||||
const categoryTreeCache = this.#createDeferredCache(
|
||||
'GET /category/tree',
|
||||
() => this.#loadCategoryTree(),
|
||||
);
|
||||
|
||||
Object.defineProperties(this, {
|
||||
authMePromise: { get: () => authMeCache.promise },
|
||||
updateAuthMe: { value: authMeCache.update },
|
||||
@@ -499,11 +478,22 @@ export class StorageManager extends ApiClient {
|
||||
// Return a single-element array so .find() still works.
|
||||
companies: { get: () => companyAutomat.state.current ? [companyAutomat.state.current] : [] },
|
||||
|
||||
// ── Category tree bridge ─────────────────────────────────────────────────
|
||||
categoryTreePromise: { get: () => categoryTreeCache.promise },
|
||||
updateCategoryTree: { value: categoryTreeCache.update },
|
||||
/** Settled GET /category/tree body, or undefined. */
|
||||
categoryTree: { get: () => categoryTreeCache.value },
|
||||
// ── Category tree bridge (delegated to categoryAutomat) ──────────────────
|
||||
/** Category tree promise resolving when categoryAutomat tree is loaded. */
|
||||
categoryTreePromise: {
|
||||
get: () => categoryAutomat.state.treeStatus === 'succeeded'
|
||||
? Promise.resolve(categoryAutomat.state.categories)
|
||||
: new Promise((resolve, reject) => {
|
||||
const unsub = categoryAutomat.subscribe((s) => {
|
||||
if (s.treeStatus === 'succeeded') { unsub(); resolve(s.categories); }
|
||||
else if (s.treeStatus === 'failed') { unsub(); reject(new Error(s.error)); }
|
||||
});
|
||||
}),
|
||||
},
|
||||
/** Reload category tree via categoryAutomat. */
|
||||
updateCategoryTree: { value: () => categoryAutomat.actions.loadTree() },
|
||||
/** Current categories map from categoryAutomat, or null. */
|
||||
categoryTree: { get: () => categoryAutomat.state.categories },
|
||||
|
||||
// ── Deferred caches (auth-gated) ─────────────────────────────────────────
|
||||
deliveryPromise: { get: () => deliveryCache.promise },
|
||||
@@ -556,7 +546,6 @@ export class StorageManager extends ApiClient {
|
||||
// Unauthenticated or logged out
|
||||
this.#disconnectEvents();
|
||||
articleIndexCache.clear();
|
||||
categoryTreeCache.clear();
|
||||
deliveryCache.clear();
|
||||
customergroupCache.clear();
|
||||
entityCache.clear();
|
||||
@@ -568,7 +557,6 @@ export class StorageManager extends ApiClient {
|
||||
// Company established or switched
|
||||
if (prevCompanyId) {
|
||||
articleIndexCache.clear();
|
||||
categoryTreeCache.clear();
|
||||
deliveryCache.clear();
|
||||
customergroupCache.clear();
|
||||
entityCache.clear();
|
||||
@@ -678,11 +666,13 @@ export class StorageManager extends ApiClient {
|
||||
this.#disconnectEvents();
|
||||
const es = new EventSource(this.urlFor('/api/events'), { withCredentials: true });
|
||||
es.addEventListener('category', () => {
|
||||
void this.updateCategoryTree().catch((err) => {
|
||||
console.error('[storageMgr] category SSE refresh failed', err);
|
||||
void categoryAutomat.actions.loadTree().catch((err) => {
|
||||
console.error('[storageMgr] category tree SSE refresh failed', err);
|
||||
});
|
||||
invalidateAllCategoryViews();
|
||||
});
|
||||
es.addEventListener('articles', () => {
|
||||
invalidateAllCategoryViews();
|
||||
void this.updateArticleIndex().catch((err) => {
|
||||
console.error('[storageMgr] articles SSE refresh failed', err);
|
||||
});
|
||||
@@ -753,15 +743,6 @@ export class StorageManager extends ApiClient {
|
||||
}));
|
||||
}
|
||||
|
||||
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 #loadDeliveries() {
|
||||
const data = await this.delivery.list();
|
||||
requireObject(data, 'GET /api/delivery');
|
||||
|
||||
@@ -16,13 +16,8 @@ import {
|
||||
* @property {string|null} languageId
|
||||
* @property {Record<string,object>|null} categories id → node map
|
||||
* @property {number|string|null} rootCategoryId
|
||||
* @property {Array<object>} rootArticles
|
||||
* @property {number|string|null} currentCategoryId
|
||||
* @property {Array<object>} articles
|
||||
* @property {number} articlesTotal
|
||||
* @property {boolean} renameMode
|
||||
* @property {'idle'|'loading'|'succeeded'|'failed'} treeStatus
|
||||
* @property {'idle'|'loading'|'succeeded'|'failed'} articlesStatus
|
||||
* @property {string|null} error
|
||||
*/
|
||||
|
||||
@@ -32,25 +27,117 @@ const initialState = {
|
||||
languageId: null,
|
||||
categories: null,
|
||||
rootCategoryId: null,
|
||||
rootArticles: [],
|
||||
currentCategoryId: null,
|
||||
articles: [],
|
||||
articlesTotal: 0,
|
||||
renameMode: false,
|
||||
treeStatus: 'idle',
|
||||
articlesStatus: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
let categoryAutomat;
|
||||
|
||||
// ─── Pure helpers ──────────────────────────────────────────────────────────────
|
||||
// Registry of active per-route category view automats
|
||||
const categoryViewInstances = new Set();
|
||||
// Reverse index for O(1) parentId lookup to target route automat
|
||||
const categoryIdToRouteName = new Map();
|
||||
|
||||
function articlesForCategory(state, categoryId) {
|
||||
if (String(categoryId) === String(state.rootCategoryId)) {
|
||||
return state.rootArticles;
|
||||
/**
|
||||
* Loader for category view automat: fetches articles for categoryId.
|
||||
* Promise only resolves when all articles are loaded.
|
||||
*/
|
||||
async function fetchCategoryArticles(automat) {
|
||||
try {
|
||||
const res = await categoryApi.getArticles(automat.state.categoryId);
|
||||
const articles = Array.isArray(res.articles) ? res.articles : [];
|
||||
return { articles, loaded: true, loading: false, error: null };
|
||||
} catch (err) {
|
||||
const errorMsg = err?.message ?? String(err);
|
||||
automat.setState({ loading: false, loaded: false, error: errorMsg });
|
||||
throw err;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a per-route category view automat for the given slug and categoryId.
|
||||
* Returns null if categoryId is null.
|
||||
*
|
||||
* @param {{ slugPath?: string, categoryId: number|string|null }} param0
|
||||
* @returns {Automat|null}
|
||||
*/
|
||||
export function getOrCreateCategoryAutomat({ slugPath, categoryId }) {
|
||||
if (categoryId == null) return null;
|
||||
|
||||
const cleanSlug = (slugPath ?? '').replace(/^\/+|\/+$/g, '');
|
||||
const name = cleanSlug === '' ? 'category-view:root' : `category-view:${cleanSlug}`;
|
||||
|
||||
const existing = Automat.get(name);
|
||||
if (existing) {
|
||||
if (String(existing.state?.categoryId) === String(categoryId)) {
|
||||
categoryViewInstances.add(existing);
|
||||
categoryIdToRouteName.set(String(categoryId), name);
|
||||
return existing;
|
||||
}
|
||||
// Stale category ID guard: ID changed, dispose old automat
|
||||
try {
|
||||
existing.dispose();
|
||||
} catch {}
|
||||
categoryViewInstances.delete(existing);
|
||||
}
|
||||
|
||||
const automat = new Automat(
|
||||
{
|
||||
slug: cleanSlug,
|
||||
categoryId,
|
||||
articles: [],
|
||||
loaded: false,
|
||||
loading: true,
|
||||
error: null,
|
||||
},
|
||||
(inst) => ({
|
||||
reload: () => inst.setDirty({ keep: true, now: true }),
|
||||
retry: () => inst.setDirty({ keep: false, now: true }),
|
||||
}),
|
||||
{
|
||||
name,
|
||||
loader: fetchCategoryArticles,
|
||||
}
|
||||
);
|
||||
automat.reload = () => automat.setDirty({ keep: true, now: true });
|
||||
automat.retry = () => automat.setDirty({ keep: false, now: true });
|
||||
|
||||
categoryViewInstances.add(automat);
|
||||
categoryIdToRouteName.set(String(categoryId), name);
|
||||
return automat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all active category view automats by triggering background reload.
|
||||
*/
|
||||
export function invalidateAllCategoryViews() {
|
||||
for (const automat of categoryViewInstances) {
|
||||
if (typeof automat.reload === 'function') {
|
||||
automat.reload().catch((err) => {
|
||||
console.error('[categoryAutomat] reload view failed', err);
|
||||
});
|
||||
} else if (typeof automat.actions?.reload === 'function') {
|
||||
automat.actions.reload().catch((err) => {
|
||||
console.error('[categoryAutomat] reload view failed', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose all category view automats and clear registries (e.g. on logout or company switch).
|
||||
*/
|
||||
export function disposeAllCategoryViewAutomats() {
|
||||
for (const automat of categoryViewInstances) {
|
||||
try {
|
||||
automat.dispose();
|
||||
} catch (e) {
|
||||
console.error('[categoryAutomat] dispose view failed', e);
|
||||
}
|
||||
}
|
||||
categoryViewInstances.clear();
|
||||
categoryIdToRouteName.clear();
|
||||
}
|
||||
|
||||
// ─── Actions ───────────────────────────────────────────────────────────────────
|
||||
@@ -73,11 +160,6 @@ const actions = {
|
||||
languageId: parsed.languageId,
|
||||
categories: parsed.categories,
|
||||
rootCategoryId: parsed.rootCategoryId,
|
||||
currentCategoryId: parsed.rootCategoryId,
|
||||
rootArticles: parsed.rootArticles,
|
||||
articles: parsed.rootArticles,
|
||||
articlesTotal: parsed.rootArticles.length,
|
||||
articlesStatus: 'succeeded',
|
||||
error: null,
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -89,62 +171,6 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load articles for a non-root category.
|
||||
* @param {number|string} categoryId
|
||||
*/
|
||||
async loadArticles(categoryId) {
|
||||
categoryAutomat.setState({ articlesStatus: 'loading', error: null });
|
||||
try {
|
||||
const res = await categoryApi.getArticles(categoryId);
|
||||
const articles = Array.isArray(res.articles) ? res.articles : [];
|
||||
categoryAutomat.setState({
|
||||
articlesStatus: 'succeeded',
|
||||
articles,
|
||||
articlesTotal: articles.length,
|
||||
});
|
||||
} catch (err) {
|
||||
categoryAutomat.setState({
|
||||
articlesStatus: 'failed',
|
||||
articles: [],
|
||||
articlesTotal: 0,
|
||||
error: err?.message ?? String(err),
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/** Navigate to a category by id. */
|
||||
navigateTo(categoryId) {
|
||||
const { rootCategoryId, rootArticles } = categoryAutomat.state;
|
||||
const isRoot = String(categoryId) === String(rootCategoryId);
|
||||
categoryAutomat.setState({
|
||||
currentCategoryId: categoryId,
|
||||
error: null,
|
||||
articles: isRoot ? rootArticles : [],
|
||||
articlesTotal: isRoot ? rootArticles.length : 0,
|
||||
articlesStatus: isRoot ? 'succeeded' : 'idle',
|
||||
});
|
||||
},
|
||||
|
||||
/** Navigate up to the parent category. */
|
||||
navigateBack() {
|
||||
const { categories, currentCategoryId, rootCategoryId, rootArticles } = categoryAutomat.state;
|
||||
const current = categories?.[String(currentCategoryId)];
|
||||
const parentId = current?.parentId;
|
||||
if (parentId == null || parentId === 0) return;
|
||||
if (String(parentId) === String(rootCategoryId) || categories?.[String(parentId)]) {
|
||||
const isRoot = String(parentId) === String(rootCategoryId);
|
||||
categoryAutomat.setState({
|
||||
currentCategoryId: parentId,
|
||||
error: null,
|
||||
articles: isRoot ? rootArticles : [],
|
||||
articlesTotal: isRoot ? rootArticles.length : 0,
|
||||
articlesStatus: isRoot ? 'succeeded' : 'idle',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/** Toggle rename/edit mode for the category grid. */
|
||||
setRenameMode(enabled) {
|
||||
categoryAutomat.setState({ renameMode: Boolean(enabled) });
|
||||
@@ -192,17 +218,17 @@ const actions = {
|
||||
async createArticle({ parentId, name }) {
|
||||
const res = await articleApi.create({ parentId, name });
|
||||
const created = res.article;
|
||||
const { currentCategoryId, rootCategoryId, articles, rootArticles } = categoryAutomat.state;
|
||||
const patch = {};
|
||||
if (String(created.parentId) === String(currentCategoryId)) {
|
||||
patch.articles = [...articles, created];
|
||||
patch.articlesTotal = patch.articles.length;
|
||||
patch.articlesStatus = 'succeeded';
|
||||
if (created && parentId != null) {
|
||||
const routeName = categoryIdToRouteName.get(String(parentId));
|
||||
if (routeName) {
|
||||
const routeAutomat = Automat.get(routeName);
|
||||
if (routeAutomat && Array.isArray(routeAutomat.state?.articles)) {
|
||||
routeAutomat.setState({
|
||||
articles: [...routeAutomat.state.articles, created],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (String(created.parentId) === String(rootCategoryId)) {
|
||||
patch.rootArticles = [...rootArticles, created];
|
||||
}
|
||||
if (Object.keys(patch).length) categoryAutomat.setState(patch);
|
||||
return created;
|
||||
},
|
||||
|
||||
@@ -218,19 +244,17 @@ const actions = {
|
||||
const res = await articleApi.create({ parentId, name });
|
||||
if (res.article) created.push(res.article);
|
||||
}
|
||||
const { currentCategoryId, rootCategoryId, articles, rootArticles } = categoryAutomat.state;
|
||||
const patch = {};
|
||||
const forCurrent = created.filter((a) => String(a.parentId) === String(currentCategoryId));
|
||||
if (forCurrent.length > 0) {
|
||||
patch.articles = [...articles, ...forCurrent];
|
||||
patch.articlesTotal = patch.articles.length;
|
||||
patch.articlesStatus = 'succeeded';
|
||||
if (created.length > 0 && parentId != null) {
|
||||
const routeName = categoryIdToRouteName.get(String(parentId));
|
||||
if (routeName) {
|
||||
const routeAutomat = Automat.get(routeName);
|
||||
if (routeAutomat && Array.isArray(routeAutomat.state?.articles)) {
|
||||
routeAutomat.setState({
|
||||
articles: [...routeAutomat.state.articles, ...created],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const forRoot = created.filter((a) => String(a.parentId) === String(rootCategoryId));
|
||||
if (forRoot.length > 0) {
|
||||
patch.rootArticles = [...rootArticles, ...forRoot];
|
||||
}
|
||||
if (Object.keys(patch).length) categoryAutomat.setState(patch);
|
||||
return created;
|
||||
},
|
||||
|
||||
@@ -247,14 +271,9 @@ const actions = {
|
||||
const res = await categoryApi.updateName(categoryId, trimmed);
|
||||
const updated = normalizeCategoryNode(res.category);
|
||||
const key = String(updated.id);
|
||||
const { categories, rootArticles } = categoryAutomat.state;
|
||||
const { categories } = categoryAutomat.state;
|
||||
categoryAutomat.setState({
|
||||
categories: categories ? { ...categories, [key]: { ...categories[key], ...updated } } : null,
|
||||
rootArticles: rootArticles.map((row) =>
|
||||
row?.categoryId != null && String(row.categoryId) === key
|
||||
? { ...row, name: updated.name }
|
||||
: row,
|
||||
),
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
@@ -264,7 +283,7 @@ const actions = {
|
||||
* @param {{ parentId?: number|string, categoryIds: number[] }} param0
|
||||
*/
|
||||
async reorderCategories({ parentId, categoryIds }) {
|
||||
const targetParentId = parentId != null ? parentId : categoryAutomat.state.currentCategoryId;
|
||||
const targetParentId = parentId != null ? parentId : categoryAutomat.state.rootCategoryId;
|
||||
const res = await categoryApi.reorder(targetParentId, categoryIds);
|
||||
const ordered = res.categoryIds;
|
||||
const parentKey = String(targetParentId);
|
||||
@@ -304,18 +323,11 @@ const actions = {
|
||||
*/
|
||||
async deleteCategory(categoryId) {
|
||||
await categoryApi.delete(categoryId);
|
||||
const { categories, currentCategoryId, rootCategoryId, rootArticles } = categoryAutomat.state;
|
||||
const { categories } = categoryAutomat.state;
|
||||
if (!categories) return;
|
||||
const next = { ...categories };
|
||||
delete next[String(categoryId)];
|
||||
const patch = { categories: next };
|
||||
if (String(currentCategoryId) === String(categoryId)) {
|
||||
patch.currentCategoryId = rootCategoryId;
|
||||
patch.articles = rootArticles;
|
||||
patch.articlesTotal = rootArticles.length;
|
||||
patch.articlesStatus = 'succeeded';
|
||||
}
|
||||
categoryAutomat.setState(patch);
|
||||
categoryAutomat.setState({ categories: next });
|
||||
return categoryId;
|
||||
},
|
||||
|
||||
@@ -372,55 +384,17 @@ const actions = {
|
||||
|
||||
// ─── Local state patches ───────────────────────────────────────────────────
|
||||
|
||||
/** Remove an article from both article lists (used after article deletion). */
|
||||
/** Remove an article from all live category view lists (used after article deletion). */
|
||||
removeArticleBySku(sku) {
|
||||
categoryAutomat.setState({
|
||||
articles: categoryAutomat.state.articles.filter((r) => r.sku !== sku),
|
||||
articlesTotal: categoryAutomat.state.articles.filter((r) => r.sku !== sku).length,
|
||||
rootArticles: categoryAutomat.state.rootArticles.filter((r) => r.sku !== sku),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Patch article fields and category membership in local lists.
|
||||
* @param {{ sku: string, categoryIds?: number[], [key: string]: any }} payload
|
||||
*/
|
||||
patchArticleInLists({ sku, categoryIds, ...fields }) {
|
||||
if (sku == null) return;
|
||||
const { articles, rootArticles, rootCategoryId, currentCategoryId } = categoryAutomat.state;
|
||||
|
||||
const patchList = (list) =>
|
||||
list.map((row) => (row.sku !== sku ? row : { ...row, ...fields }));
|
||||
|
||||
let nextArticles = patchList(articles);
|
||||
let nextRoot = patchList(rootArticles);
|
||||
let nextArticlesTotal = nextArticles.length;
|
||||
|
||||
if (categoryIds !== undefined) {
|
||||
const newIds = categoryIds ?? [];
|
||||
const articleObj = nextArticles.find((r) => r.sku === sku) ?? nextRoot.find((r) => r.sku === sku);
|
||||
|
||||
if (rootCategoryId != null) {
|
||||
const inRoot = newIds.some((id) => String(id) === String(rootCategoryId));
|
||||
const wasInRoot = nextRoot.some((r) => r.sku === sku);
|
||||
if (!inRoot) nextRoot = nextRoot.filter((r) => r.sku !== sku);
|
||||
else if (!wasInRoot && articleObj) nextRoot = [...nextRoot, articleObj];
|
||||
}
|
||||
|
||||
if (currentCategoryId != null) {
|
||||
const inCurrent = newIds.some((id) => String(id) === String(currentCategoryId));
|
||||
const wasInCurrent = nextArticles.some((r) => r.sku === sku);
|
||||
if (!inCurrent) { nextArticles = nextArticles.filter((r) => r.sku !== sku); }
|
||||
else if (!wasInCurrent && articleObj) { nextArticles = [...nextArticles, articleObj]; }
|
||||
nextArticlesTotal = nextArticles.length;
|
||||
if (!sku) return;
|
||||
for (const automat of categoryViewInstances) {
|
||||
const articles = automat.state?.articles;
|
||||
if (Array.isArray(articles) && articles.some((r) => r.sku === sku)) {
|
||||
automat.setState({
|
||||
articles: articles.filter((r) => r.sku !== sku),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
categoryAutomat.setState({
|
||||
articles: nextArticles,
|
||||
articlesTotal: nextArticlesTotal,
|
||||
rootArticles: nextRoot,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -445,6 +419,7 @@ const actions = {
|
||||
|
||||
/** Reset state to idle (logout / company switch). */
|
||||
reset() {
|
||||
disposeAllCategoryViewAutomats();
|
||||
categoryAutomat.setState(initialState);
|
||||
},
|
||||
};
|
||||
@@ -464,6 +439,7 @@ companyAutomat.subscribe((company) => {
|
||||
const rootCategoryId = company.current?.rootCategoryId ?? null;
|
||||
if (id && rootCategoryId && id !== _lastCompanyId) {
|
||||
_lastCompanyId = id;
|
||||
disposeAllCategoryViewAutomats();
|
||||
void categoryAutomat.actions.loadTree();
|
||||
}
|
||||
if (!id && _lastCompanyId) {
|
||||
|
||||
@@ -51,4 +51,8 @@ export {
|
||||
categoryDisplayName,
|
||||
categorySlug,
|
||||
normalizeCategoryMap,
|
||||
getOrCreateCategoryAutomat,
|
||||
disposeAllCategoryViewAutomats,
|
||||
invalidateAllCategoryViews,
|
||||
} from './categoryAutomat';
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
resolveCategoryIdFromSlugPath,
|
||||
ROOT_CATEGORY_NAME,
|
||||
} from './categoryRoute';
|
||||
import categoryAutomat from '../../automats/categoryAutomat';
|
||||
import categoryAutomat, { getOrCreateCategoryAutomat } from '../../automats/categoryAutomat';
|
||||
import posterminalAutomat from '../../automats/posterminalAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { setLastArticleSku } from '../../automats';
|
||||
@@ -37,29 +37,13 @@ import {
|
||||
|
||||
// ─── Helper selectors for this page ────────────────────────────────────────────
|
||||
function selectFromCategory(s) {
|
||||
const { categories, rootCategoryId, currentCategoryId, articles, articlesTotal, renameMode, treeStatus, articlesStatus, error } = s;
|
||||
const breadcrumbs = getCategoryBreadcrumbs(categories, currentCategoryId, rootCategoryId);
|
||||
const childCategories = getCategoryChildren(categories, currentCategoryId);
|
||||
const canGoBack =
|
||||
currentCategoryId != null &&
|
||||
rootCategoryId != null &&
|
||||
String(currentCategoryId) !== String(rootCategoryId) &&
|
||||
categories?.[String(currentCategoryId)]?.parentId != null &&
|
||||
categories[String(currentCategoryId)].parentId !== 0;
|
||||
|
||||
const { categories, rootCategoryId, renameMode, treeStatus, error } = s;
|
||||
return {
|
||||
categories,
|
||||
rootCategoryId,
|
||||
treeStatus,
|
||||
articlesStatus,
|
||||
error,
|
||||
currentCategoryId,
|
||||
breadcrumbs,
|
||||
childCategories,
|
||||
articles,
|
||||
articlesTotal,
|
||||
canGoBack,
|
||||
renameMode,
|
||||
treeStatus,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,9 +60,30 @@ const gridSx = {
|
||||
class ArtikelKategorienPage extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const catState = selectFromCategory(categoryAutomat.state);
|
||||
const { categories, rootCategoryId, treeStatus } = catState;
|
||||
|
||||
let resolvedCategoryId = null;
|
||||
if (treeStatus === 'succeeded' && categories && rootCategoryId != null) {
|
||||
resolvedCategoryId = resolveCategoryIdFromSlugPath(
|
||||
categories,
|
||||
rootCategoryId,
|
||||
props.categorySlugPath,
|
||||
);
|
||||
}
|
||||
|
||||
this.routeAutomat = resolvedCategoryId != null
|
||||
? getOrCreateCategoryAutomat({ slugPath: props.categorySlugPath, categoryId: resolvedCategoryId })
|
||||
: null;
|
||||
|
||||
this.state = {
|
||||
drafts: {},
|
||||
...selectFromCategory(categoryAutomat.state),
|
||||
...catState,
|
||||
resolvedCategoryId,
|
||||
articles: this.routeAutomat?.state?.articles ?? [],
|
||||
loaded: this.routeAutomat?.state?.loaded ?? false,
|
||||
loading: this.routeAutomat?.state?.loading ?? false,
|
||||
routeError: this.routeAutomat?.state?.error ?? null,
|
||||
...selectTerminals(posterminalAutomat.state),
|
||||
companyId: companyAutomat.state.current?.id ?? null,
|
||||
moveCategory: null,
|
||||
@@ -99,31 +104,28 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
this.ensureTreeLoaded();
|
||||
this.ensurePosterminalsLoaded();
|
||||
this.syncCategoryFromUrl();
|
||||
this.ensureArticlesLoaded();
|
||||
this.subscribeRouteAutomat();
|
||||
this.syncSortable();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevState) {
|
||||
// Note: PureComponent — comparisons use this.state not this.props
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.companyId !== prevState.companyId) {
|
||||
this.ensureTreeLoaded();
|
||||
}
|
||||
if (this.state.treeStatus === 'succeeded' && prevState.treeStatus !== 'succeeded') {
|
||||
this.syncCategoryFromUrl();
|
||||
this.ensureArticlesLoaded();
|
||||
}
|
||||
if (
|
||||
this.state.treeStatus === 'succeeded' &&
|
||||
this.props.categorySlugPath !== (this._prevSlugPath ?? this.props.categorySlugPath)
|
||||
this.props.categorySlugPath !== prevProps.categorySlugPath ||
|
||||
this.state.categories !== prevState.categories ||
|
||||
this.state.rootCategoryId !== prevState.rootCategoryId
|
||||
) {
|
||||
this.syncCategoryFromUrl();
|
||||
}
|
||||
this._prevSlugPath = this.props.categorySlugPath;
|
||||
|
||||
if (this.state.currentCategoryId !== prevState.currentCategoryId) {
|
||||
this.ensureArticlesLoaded();
|
||||
}
|
||||
if (this.state.renameMode !== prevState.renameMode) {
|
||||
if (
|
||||
this.state.renameMode !== prevState.renameMode ||
|
||||
(this.state.renameMode && this.state.resolvedCategoryId !== prevState.resolvedCategoryId)
|
||||
) {
|
||||
this.syncSortable();
|
||||
}
|
||||
}
|
||||
@@ -132,16 +134,33 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
this._unsubCat?.();
|
||||
this._unsubTerm?.();
|
||||
this._unsubCompany?.();
|
||||
this._unsubRoute?.();
|
||||
this.sortableModule?.destroy();
|
||||
}
|
||||
|
||||
subscribeRouteAutomat() {
|
||||
this._unsubRoute?.();
|
||||
this._unsubRoute = null;
|
||||
if (this.routeAutomat) {
|
||||
this._unsubRoute = this.routeAutomat.subscribe(this, (s) => ({
|
||||
articles: s.articles,
|
||||
loaded: s.loaded,
|
||||
loading: s.loading,
|
||||
routeError: s.error,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#ensureModules = () => {
|
||||
const el = this.gridRef.current;
|
||||
if (!el) return;
|
||||
if (!this.sortableModule) {
|
||||
this.sortableModule = new CategorySortable(el, {
|
||||
onReorder: ({ categoryIds }) =>
|
||||
categoryAutomat.actions.reorderCategories({ categoryIds }),
|
||||
categoryAutomat.actions.reorderCategories({
|
||||
parentId: this.state.resolvedCategoryId,
|
||||
categoryIds,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -168,19 +187,12 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
void posterminalAutomat.actions.load();
|
||||
};
|
||||
|
||||
ensureArticlesLoaded = () => {
|
||||
const { currentCategoryId, articlesStatus, rootCategoryId, treeStatus } = this.state;
|
||||
if (treeStatus !== 'succeeded' || !currentCategoryId || articlesStatus !== 'idle') return;
|
||||
if (String(currentCategoryId) === String(rootCategoryId)) return;
|
||||
void categoryAutomat.actions.loadArticles(currentCategoryId);
|
||||
};
|
||||
|
||||
syncCategoryFromUrl = () => {
|
||||
const {
|
||||
categories,
|
||||
rootCategoryId,
|
||||
currentCategoryId,
|
||||
treeStatus,
|
||||
resolvedCategoryId,
|
||||
} = this.state;
|
||||
const { categorySlugPath, navigate } = this.props;
|
||||
|
||||
@@ -204,8 +216,23 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
navigate(buildCategoryPath(categories, rootCategoryId, resolvedId), { replace: true });
|
||||
}
|
||||
|
||||
if (resolvedId !== currentCategoryId) {
|
||||
categoryAutomat.actions.navigateTo(resolvedId);
|
||||
const cleanSlug = (categorySlugPath ?? '').replace(/^\/+|\/+$/g, '');
|
||||
const currentRouteSlug = this.routeAutomat?.state?.slug;
|
||||
|
||||
if (resolvedId !== resolvedCategoryId || cleanSlug !== currentRouteSlug || !this.routeAutomat) {
|
||||
this._unsubRoute?.();
|
||||
this.routeAutomat = getOrCreateCategoryAutomat({
|
||||
slugPath: categorySlugPath,
|
||||
categoryId: resolvedId,
|
||||
});
|
||||
this.setState({
|
||||
resolvedCategoryId: resolvedId,
|
||||
articles: this.routeAutomat?.state?.articles ?? [],
|
||||
loaded: this.routeAutomat?.state?.loaded ?? false,
|
||||
loading: this.routeAutomat?.state?.loading ?? false,
|
||||
routeError: this.routeAutomat?.state?.error ?? null,
|
||||
});
|
||||
this.subscribeRouteAutomat();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -266,14 +293,20 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
};
|
||||
|
||||
handleRetryArticles = () => {
|
||||
const { currentCategoryId } = this.state;
|
||||
if (currentCategoryId) {
|
||||
void categoryAutomat.actions.loadArticles(currentCategoryId);
|
||||
if (this.routeAutomat) {
|
||||
if (typeof this.routeAutomat.retry === 'function') {
|
||||
void this.routeAutomat.retry();
|
||||
} else if (typeof this.routeAutomat.actions?.retry === 'function') {
|
||||
void this.routeAutomat.actions.retry();
|
||||
} else {
|
||||
void this.routeAutomat.setDirty({ keep: false, now: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleNavigateBack = () => {
|
||||
const { breadcrumbs } = this.state;
|
||||
const { categories, resolvedCategoryId, rootCategoryId } = this.state;
|
||||
const breadcrumbs = getCategoryBreadcrumbs(categories, resolvedCategoryId, rootCategoryId);
|
||||
if (breadcrumbs.length < 2) return;
|
||||
this.navigateToCategoryById(breadcrumbs[breadcrumbs.length - 2].id);
|
||||
};
|
||||
@@ -289,13 +322,12 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
categories,
|
||||
rootCategoryId,
|
||||
treeStatus,
|
||||
articlesStatus,
|
||||
error,
|
||||
canGoBack,
|
||||
breadcrumbs,
|
||||
currentCategoryId,
|
||||
childCategories,
|
||||
resolvedCategoryId,
|
||||
articles,
|
||||
loaded,
|
||||
loading,
|
||||
routeError,
|
||||
renameMode,
|
||||
terminals,
|
||||
drafts,
|
||||
@@ -335,7 +367,16 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
const showArticlesLoading = articlesStatus === 'loading';
|
||||
const breadcrumbs = getCategoryBreadcrumbs(categories, resolvedCategoryId, rootCategoryId);
|
||||
const childCategories = getCategoryChildren(categories, resolvedCategoryId);
|
||||
const canGoBack =
|
||||
resolvedCategoryId != null &&
|
||||
rootCategoryId != null &&
|
||||
String(resolvedCategoryId) !== String(rootCategoryId) &&
|
||||
categories?.[String(resolvedCategoryId)]?.parentId != null &&
|
||||
categories[String(resolvedCategoryId)].parentId !== 0;
|
||||
|
||||
const showArticlesLoading = loading;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -344,9 +385,9 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
onNavigate={this.navigateToCategoryById}
|
||||
/>
|
||||
|
||||
{error && articlesStatus === 'failed' && (
|
||||
{routeError && (
|
||||
<BackendError
|
||||
error={error}
|
||||
error={routeError}
|
||||
sx={{ mb: 2 }}
|
||||
onRetry={this.handleRetryArticles}
|
||||
/>
|
||||
@@ -356,12 +397,12 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
{canGoBack && (
|
||||
<BackTile
|
||||
onClick={this.handleNavigateBack}
|
||||
data-id={String(breadcrumbs[breadcrumbs.length - 2].id)}
|
||||
data-id={breadcrumbs.length >= 2 ? String(breadcrumbs[breadcrumbs.length - 2].id) : undefined}
|
||||
className="sortable-item not-sortable"
|
||||
/>
|
||||
)}
|
||||
|
||||
{childCategories.map((item, index) => {
|
||||
{childCategories.map((item) => {
|
||||
const label = categoryDisplayName(item.name, item.id, rootCategoryId);
|
||||
const draftName =
|
||||
drafts[item.id] !== undefined
|
||||
@@ -469,10 +510,10 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
/>
|
||||
))}
|
||||
|
||||
{renameMode && <NewCategoryTile parentId={currentCategoryId} />}
|
||||
{renameMode && articlesStatus === 'succeeded' && (
|
||||
{renameMode && <NewCategoryTile parentId={resolvedCategoryId} />}
|
||||
{renameMode && loaded && (
|
||||
<NewArticleTile
|
||||
parentId={currentCategoryId}
|
||||
parentId={resolvedCategoryId}
|
||||
onOpenArticle={this.handleArticleClick}
|
||||
sx={{ gridColumnStart: 1 }}
|
||||
/>
|
||||
@@ -505,3 +546,4 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
}
|
||||
|
||||
export default withCategoryRoute(ArtikelKategorienPage);
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ export function parseCategoryTreeResponse(data, companyRootCategoryId) {
|
||||
return {
|
||||
rootCategoryId,
|
||||
categories,
|
||||
rootArticles: Array.isArray(data.articles) ? data.articles : [],
|
||||
languageId: data.languageId,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user