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

This commit is contained in:
2026-07-21 15:17:38 +03:00
parent 9e1612c4c3
commit 08b0979ca2
2 changed files with 101 additions and 16 deletions

View File

@@ -23,6 +23,7 @@ interface TreeGroup {
values: Record<number, string>;
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<number, string> = {}
): 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 (
<div
className={cn(
'group flex items-center gap-1 rounded-md px-2 py-1.5 text-sm cursor-pointer select-none transition-colors',
isSelected ? 'bg-accent text-accent-foreground' : 'hover:bg-muted'
)}
style={{ paddingLeft: `${group.level * 16 + 28}px` }}
onClick={() => onSelect(row)}
>
<FileText className="w-4 h-4 shrink-0 text-muted-foreground" />
<span className="flex-1 break-words whitespace-normal leading-tight">{group.label}</span>
<div
className="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity flex items-center"
onClick={(e) => e.stopPropagation()}
>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
title="Редактировать"
onClick={(e) => {
e.stopPropagation();
onEdit(row);
}}
>
<Pencil className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-destructive"
title="Удалить"
onClick={(e) => {
e.stopPropagation();
onDelete(row);
}}
>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
</div>
);
}
return (
<div>
@@ -262,7 +330,7 @@ export function DirectoryColumnTree({
searchQuery,
}: DirectoryColumnTreeProps) {
const groups = useMemo(
() => buildTree(rows, treeColumns),
() => buildTree(rows, treeColumns, treeDisplayColumns),
[rows, treeColumns]
);

View File

@@ -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) {