Files
dashfront/src/utils/categoryUtils.js
2026-09-23 17:24:33 +02:00

180 lines
5.0 KiB
JavaScript

/**
* Pure category tree helper functions.
* These operate on plain category maps and have no side-effects.
*/
export const ROOT_CATEGORY_NAME = 'Home';
/**
* @param {object} item
* @returns {object}
*/
export function normalizeCategoryNode(item) {
return {
...item,
id: item.id,
parentId: item.parentId,
name: item.name == null ? null : String(item.name).trim() || null,
posterminalIds: Array.isArray(item.posterminalIds) ? item.posterminalIds : [],
};
}
/**
* Parse the raw GET /api/category/tree response body into normalized state.
* @param {object} data
* @param {number|string|null} companyRootCategoryId
*/
export function parseCategoryTreeResponse(data, companyRootCategoryId) {
const list = Array.isArray(data.tree) ? data.tree : [];
const rootCategoryId = data.rootCategoryId != null ? data.rootCategoryId : companyRootCategoryId;
const categories = Object.fromEntries(
list.map((item) => [String(item.id), normalizeCategoryNode(item)]),
);
return {
rootCategoryId,
categories,
languageId: data.languageId,
};
}
/**
* Display name for a category node (root → "Home").
*/
export function categoryDisplayName(name, categoryId, rootCategoryId) {
if (rootCategoryId != null && String(categoryId) === String(rootCategoryId)) {
return ROOT_CATEGORY_NAME;
}
return name?.trim() || '';
}
/**
* URL slug for a category node (root → "Home").
*/
export function categorySlug(node, rootCategoryId) {
if (rootCategoryId != null && String(node.id) === String(rootCategoryId)) {
return ROOT_CATEGORY_NAME;
}
return node.slug || null;
}
/**
* Direct children of `parentId`, sorted by `sort` then `id`.
*/
export function getCategoryChildren(categories, parentId) {
if (!categories) return [];
const parentKey = String(parentId);
return Object.values(categories)
.filter((item) => item.parentId != null && String(item.parentId) === parentKey)
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0) || a.id - b.id);
}
/**
* Breadcrumb trail from root to `currentCategoryId`.
*/
export function getCategoryBreadcrumbs(categories, currentCategoryId, rootCategoryId) {
if (!categories || currentCategoryId == null) return [];
const crumbs = [];
const seen = new Set();
let id = currentCategoryId;
while (id != null && !seen.has(String(id))) {
seen.add(String(id));
const node = categories[String(id)];
if (!node) break;
crumbs.unshift({
id: node.id,
name: categoryDisplayName(node.name, node.id, rootCategoryId),
slug: categorySlug(node, rootCategoryId),
});
const parentId = node.parentId;
if (parentId == null || parentId === 0) break;
if (String(parentId) === String(id)) break;
id = parentId;
}
return crumbs;
}
/**
* Map of category.id → category node from a raw tree response.
* @param {object} data
* @param {number|string|null} companyRootCategoryId
*/
export function normalizeCategoryMap(data, companyRootCategoryId) {
return parseCategoryTreeResponse(data, companyRootCategoryId).categories;
}
/**
* Returns an array containing the category id and all of its recursive descendant ids.
* Useful for preventing cyclic moves.
* @param {object} categories - id -> node map
* @param {number|string} categoryId - Target category id
* @returns {number[]} Array of category ids
*/
export function getCategoryDescendantIds(categories, categoryId) {
const result = [];
if (!categories || categoryId == null) return result;
const numId = Number(categoryId);
result.push(numId);
const queue = [numId];
while (queue.length > 0) {
const currentId = queue.shift();
const children = getCategoryChildren(categories, currentId);
for (const child of children) {
result.push(Number(child.id));
queue.push(Number(child.id));
}
}
return result;
}
/**
* Normalizes a category slug path by stripping leading/trailing slashes.
* @param {string|null|undefined} slugPath
* @returns {string}
*/
export function cleanCategorySlugPath(slugPath) {
return (slugPath ?? '').replace(/^\/+|\/+$/g, '');
}
/**
* Resolves a numeric/string category ID from a URL slug path using the category tree.
* @param {object} categories - id -> node map
* @param {number|string|null} rootCategoryId
* @param {string|null|undefined} slugPath
* @returns {number|string|null}
*/
export function resolveCategoryIdFromSlugPath(categories, rootCategoryId, slugPath) {
if (!categories || rootCategoryId == null) return null;
let segments = (slugPath ?? '')
.split('/')
.filter(Boolean)
.map((segment) => decodeURIComponent(segment));
if (segments[0] === ROOT_CATEGORY_NAME) {
segments = segments.slice(1);
}
if (segments.length === 0) {
return rootCategoryId;
}
let currentId = rootCategoryId;
for (const slug of segments) {
const children = getCategoryChildren(categories, currentId);
const match = children.find(
(child) => categorySlug(child, rootCategoryId) === slug,
);
if (!match) return null;
currentId = match.id;
}
return currentId;
}