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>; values: Record<number, string>;
rows: DataTableRow[]; rows: DataTableRow[];
children: TreeGroup[]; children: TreeGroup[];
isLeaf?: boolean;
} }
function getRowLabel(row: DataTableRow, displayColumns: number[]): string { function getRowLabel(row: DataTableRow, displayColumns: number[]): string {
@@ -43,6 +44,7 @@ function getRowLabel(row: DataTableRow, displayColumns: number[]): string {
function buildTree( function buildTree(
rows: DataTableRow[], rows: DataTableRow[],
treeColumns: number[], treeColumns: number[],
displayColumns: number[],
level = 0, level = 0,
values: Record<number, string> = {} values: Record<number, string> = {}
): TreeGroup[] { ): TreeGroup[] {
@@ -68,19 +70,38 @@ function buildTree(
const result: TreeGroup[] = []; const result: TreeGroup[] = [];
for (const [label, groupRows] of groups) { for (const [label, groupRows] of groups) {
const groupValues = { ...values, [columnIndex]: label }; const groupValues = { ...values, [columnIndex]: label };
// Если у группы все строки имеют пустое значение в 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({ result.push({
id: `${level}:${label}`, id: `${level}:${label}`,
label, label,
level, level,
values: groupValues, values: groupValues,
rows: groupRows, rows: groupRows,
children: buildTree(groupRows, treeColumns, level + 1, groupValues), 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) { if (rootRows.length > 0) {
const rootGroup = buildTree(rootRows, treeColumns, level + 1, values); const rootGroup = buildTree(rootRows, treeColumns, displayColumns, level + 1, values);
result.push(...rootGroup); result.push(...rootGroup);
} }
@@ -149,7 +170,54 @@ function TreeNode({
onAddChild, onAddChild,
}: TreeNodeProps) { }: TreeNodeProps) {
const isExpanded = expanded.has(group.id); 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 ( return (
<div> <div>
@@ -262,7 +330,7 @@ export function DirectoryColumnTree({
searchQuery, searchQuery,
}: DirectoryColumnTreeProps) { }: DirectoryColumnTreeProps) {
const groups = useMemo( const groups = useMemo(
() => buildTree(rows, treeColumns), () => buildTree(rows, treeColumns, treeDisplayColumns),
[rows, treeColumns] [rows, treeColumns]
); );

View File

@@ -585,6 +585,22 @@ export function registerDataTableRoutes(app: import("express").Express): void {
const nodes: TreeNode[] = []; const nodes: TreeNode[] = [];
for (const [label, groupRows] of groups) { for (const [label, groupRows] of groups) {
// Если у группы все строки имеют пустое значение в 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({ nodes.push({
type: 'group', type: 'group',
id: `group:${level}:${label}`, id: `group:${level}:${label}`,
@@ -593,6 +609,7 @@ export function registerDataTableRoutes(app: import("express").Express): void {
children: buildMultiColumnTree(groupRows, columns, level + 1), children: buildMultiColumnTree(groupRows, columns, level + 1),
}); });
} }
}
if (rootRows.length > 0) { if (rootRows.length > 0) {
nodes.push(...buildMultiColumnTree(rootRows, columns, level + 1)); nodes.push(...buildMultiColumnTree(rootRows, columns, level + 1));