feat: многоуровневое дерево справочника и настраиваемое отображение колонок

This commit is contained in:
2026-07-20 21:59:36 +03:00
parent 9fff937209
commit 7cf5ffff9a
7 changed files with 424 additions and 212 deletions

View File

@@ -323,13 +323,15 @@ export function registerDataTableRoutes(app: import("express").Express): void {
return res.status(403).json({ error: 'Нет прав для редактирования таблицы' });
}
const { name, description, columns, treeColumnIndex, treeDisplayColumnIndex } = req.body;
const { name, description, columns, treeColumnIndex, treeDisplayColumnIndex, treeColumns, treeDisplayColumns } = req.body;
const updates: Record<string, unknown> = {};
if (name !== undefined) updates.name = name;
if (description !== undefined) updates.description = description;
if (columns !== undefined) updates.columns = columns;
if (treeColumnIndex !== undefined) updates.treeColumnIndex = treeColumnIndex === null ? null : Number(treeColumnIndex);
if (treeDisplayColumnIndex !== undefined) updates.treeDisplayColumnIndex = treeDisplayColumnIndex === null ? null : Number(treeDisplayColumnIndex);
if (treeColumns !== undefined) updates.treeColumns = Array.isArray(treeColumns) ? treeColumns : null;
if (treeDisplayColumns !== undefined) updates.treeDisplayColumns = Array.isArray(treeDisplayColumns) ? treeDisplayColumns : null;
const updatedTable = await storage.updateDataTable(tableId, req.organizationId!, updates as Parameters<typeof storage.updateDataTable>[2]);
res.json({ table: updatedTable });
@@ -503,6 +505,7 @@ export function registerDataTableRoutes(app: import("express").Express): void {
let rows = await storage.getDataTableRows(tableId, req.organizationId!);
const treeColumnIndex = table.treeColumnIndex;
const treeDisplayColumnIndex = table.treeDisplayColumnIndex;
const search = String(req.query.search || '').toLowerCase();
const filtersStr = req.query.filters as string;
@@ -521,34 +524,47 @@ export function registerDataTableRoutes(app: import("express").Express): void {
} catch {}
}
const displayColumnIndex = table.treeDisplayColumnIndex ?? 0;
const treeColumns = table.treeColumns ?? (treeColumnIndex != null ? [treeColumnIndex] : []);
const treeDisplayColumns = table.treeDisplayColumns ?? (treeDisplayColumnIndex != null ? [treeDisplayColumnIndex] : [0]);
const displayColumnIndex = treeDisplayColumnIndex ?? treeDisplayColumns[0] ?? 0;
const getRowLabel = (row: typeof rows[number]): string => {
const values = Array.isArray(row.values) ? row.values : [];
return String(values[displayColumnIndex] ?? values[0] ?? '') || '(без названия)';
const parts = treeDisplayColumns
.map((ci) => String(values[ci] ?? '').trim())
.filter(Boolean);
return parts.length > 0 ? parts.join(' ') : '(без названия)';
};
type TreeNode =
| { type: 'group'; id: string; label: string; children: TreeNode[] }
| { type: 'row'; id: number; label: string; row: typeof rows[number] };
| { type: 'group'; id: string; label: string; level: number; children: TreeNode[] }
| { type: 'row'; id: number; label: string; level: number; row: typeof rows[number] };
const buildParentTree = (parentRows: typeof rows, parentId: number | null): TreeNode[] => {
const buildParentTree = (parentRows: typeof rows, parentId: number | null, level = 0): TreeNode[] => {
const items = parentRows
.filter((r) => (r.parentId ?? null) === parentId)
.sort((a, b) => a.position - b.position);
return items.map((row) => {
const children = buildParentTree(parentRows, row.id);
const children = buildParentTree(parentRows, row.id, level + 1);
const label = getRowLabel(row);
if (children.length > 0) {
return { type: 'group' as const, id: `row-group:${row.id}`, label, children: [...children, { type: 'row' as const, id: row.id, label, row }] };
return { type: 'group' as const, id: `row-group:${row.id}`, label, level, children: [...children, { type: 'row' as const, id: row.id, label, level: level + 1, row }] };
}
return { type: 'row' as const, id: row.id, label, row };
return { type: 'row' as const, id: row.id, label, level, row };
});
};
const buildColumnTree = (allRows: typeof rows, columnIndex: number): TreeNode[] => {
const buildMultiColumnTree = (allRows: typeof rows, columns: number[], level = 0): TreeNode[] => {
if (level >= columns.length) {
return allRows
.sort((a, b) => a.position - b.position)
.map((row) => ({ type: 'row' as const, id: row.id, label: getRowLabel(row), level, row }));
}
const columnIndex = columns[level];
const groups = new Map<string, typeof rows[number][]>();
const rootRows: typeof rows[number][] = [];
for (const row of allRows) {
const vals = Array.isArray(row.values) ? row.values : [];
const key = String(vals[columnIndex] ?? '').trim();
@@ -559,20 +575,22 @@ export function registerDataTableRoutes(app: import("express").Express): void {
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(row);
}
const nodes: TreeNode[] = [];
for (const [label, groupRows] of groups) {
const sorted = [...groupRows].sort((a, b) => a.position - b.position);
nodes.push({
type: 'group',
id: `group:${label}`,
id: `group:${level}:${label}`,
label,
children: sorted.map((row) => ({ type: 'row', id: row.id, label: getRowLabel(row), row })),
level,
children: buildMultiColumnTree(groupRows, columns, level + 1),
});
}
if (rootRows.length > 0) {
rootRows.sort((a, b) => a.position - b.position);
nodes.push(...rootRows.map((row) => ({ type: 'row' as const, id: row.id, label: getRowLabel(row), row })));
nodes.push(...buildMultiColumnTree(rootRows, columns, level + 1));
}
return nodes.sort((a, b) => {
const al = a.type === 'group' ? a.label : getRowLabel(a.row);
const bl = b.type === 'group' ? b.label : getRowLabel(b.row);
@@ -599,12 +617,19 @@ export function registerDataTableRoutes(app: import("express").Express): void {
return result;
};
const rawNodes = treeColumnIndex != null
? buildColumnTree(rows, treeColumnIndex)
const rawNodes = treeColumns.length > 0
? buildMultiColumnTree(rows, treeColumns)
: buildParentTree(rows, null);
const nodes = search ? filterNodes(rawNodes, search) : rawNodes;
res.json({ columnIndex: treeColumnIndex, displayColumnIndex, nodes });
res.json({
columnIndex: treeColumnIndex,
treeColumnIndex,
treeColumns,
treeDisplayColumns,
displayColumnIndex,
nodes,
});
} catch (error) {
console.error('Get table rows tree error:', error);
res.status(500).json({ error: 'Ошибка получения дерева строк' });