From 08b0979ca2fe9b5ebdd03b2b4ba606f1a7a99fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=D1=81=20=D0=A1=D1=83=D0=BB=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Tue, 21 Jul 2026 15:17:38 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=B0=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20=D0=B4=D0=B5=D1=82=D0=B5=D0=B9=20=D1=81=20?= =?UTF-8?q?=D0=BF=D1=83=D1=81=D1=82=D1=8B=D0=BC=20=D0=BE=D1=82=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=D1=81=D1=8F=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D0=B5=D1=87=D0=BD=D0=BE=D0=B9=20=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=D0=BE=D0=B9=20=D0=B2=20=D0=B4=D0=B5=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../directories/DirectoryColumnTree.tsx | 88 ++++++++++++++++--- server/routes/data-tables.routes.ts | 29 ++++-- 2 files changed, 101 insertions(+), 16 deletions(-) diff --git a/client/src/components/directories/DirectoryColumnTree.tsx b/client/src/components/directories/DirectoryColumnTree.tsx index bfd8706..81dc4ab 100644 --- a/client/src/components/directories/DirectoryColumnTree.tsx +++ b/client/src/components/directories/DirectoryColumnTree.tsx @@ -23,6 +23,7 @@ interface TreeGroup { values: Record; rows: DataTableRow[]; children: TreeGroup[]; + isLeaf?: boolean; } function getRowLabel(row: DataTableRow, displayColumns: number[]): string { @@ -43,6 +44,7 @@ function getRowLabel(row: DataTableRow, displayColumns: number[]): string { function buildTree( rows: DataTableRow[], treeColumns: number[], + displayColumns: number[], level = 0, values: Record = {} ): TreeGroup[] { @@ -68,19 +70,38 @@ function buildTree( const result: TreeGroup[] = []; for (const [label, groupRows] of groups) { const groupValues = { ...values, [columnIndex]: label }; - result.push({ - id: `${level}:${label}`, - label, - level, - values: groupValues, - rows: groupRows, - children: buildTree(groupRows, treeColumns, level + 1, groupValues), + // Если у группы все строки имеют пустое значение в displayColumns, + // группа становится конечной строкой (можно выбрать в задачах) + const allEmpty = groupRows.every((row) => { + const vals = Array.isArray(row.values) ? row.values : []; + return displayColumns.every((ci) => !String(vals[ci] ?? '').trim()); }); + + if (allEmpty && groupRows.length === 1) { + result.push({ + id: `${level}:${label}`, + label, + level, + values: groupValues, + rows: groupRows, + children: [], + isLeaf: true, + }); + } else { + result.push({ + id: `${level}:${label}`, + label, + level, + values: groupValues, + rows: groupRows, + children: buildTree(groupRows, treeColumns, displayColumns, level + 1, groupValues), + }); + } } // Строки без значения в этой колонке относятся к корню текущего уровня if (rootRows.length > 0) { - const rootGroup = buildTree(rootRows, treeColumns, level + 1, values); + const rootGroup = buildTree(rootRows, treeColumns, displayColumns, level + 1, values); result.push(...rootGroup); } @@ -149,7 +170,54 @@ function TreeNode({ onAddChild, }: TreeNodeProps) { const isExpanded = expanded.has(group.id); - const isLeaf = group.children.length === 0; + const isLeaf = group.isLeaf || group.children.length === 0; + + if (group.isLeaf && group.rows.length === 1) { + const row = group.rows[0]; + const isSelected = selectedRowId === row.id; + return ( +
onSelect(row)} + > + + {group.label} +
e.stopPropagation()} + > + + +
+
+ ); + } return (
@@ -262,7 +330,7 @@ export function DirectoryColumnTree({ searchQuery, }: DirectoryColumnTreeProps) { const groups = useMemo( - () => buildTree(rows, treeColumns), + () => buildTree(rows, treeColumns, treeDisplayColumns), [rows, treeColumns] ); diff --git a/server/routes/data-tables.routes.ts b/server/routes/data-tables.routes.ts index 6641d2c..061bfc1 100644 --- a/server/routes/data-tables.routes.ts +++ b/server/routes/data-tables.routes.ts @@ -585,13 +585,30 @@ export function registerDataTableRoutes(app: import("express").Express): void { const nodes: TreeNode[] = []; for (const [label, groupRows] of groups) { - nodes.push({ - type: 'group', - id: `group:${level}:${label}`, - label, - level, - children: buildMultiColumnTree(groupRows, columns, level + 1), + // Если у группы все строки имеют пустое значение в treeDisplayColumns, + // группа становится конечной строкой (можно выбрать в задачах) + const allEmpty = groupRows.every((row) => { + const vals = Array.isArray(row.values) ? row.values : []; + return treeDisplayColumns.every((ci) => !String(vals[ci] ?? '').trim()); }); + + if (allEmpty && groupRows.length === 1) { + nodes.push({ + type: 'row', + id: groupRows[0].id, + label, + level, + row: groupRows[0], + }); + } else { + nodes.push({ + type: 'group', + id: `group:${level}:${label}`, + label, + level, + children: buildMultiColumnTree(groupRows, columns, level + 1), + }); + } } if (rootRows.length > 0) {