From 782731674b163ced6fcda98391a5cbddb0a1c1ee 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: Sun, 12 Jul 2026 21:59:38 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D1=80=D0=B5=D0=B2=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=B4=D0=BD=D1=8B=D0=B5=20=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BE?= =?UTF-8?q?=D1=87=D0=BD=D0=B8=D0=BA=D0=B8:=20=D0=B2=D1=8B=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BA=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B4=D0=B5=D1=80=D0=B5=D0=B2=D0=B0=20(treeColu?= =?UTF-8?q?mnIndex)=20=D0=B8=20API=20/rows/tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/TableFieldMultiSelect.tsx | 26 +- client/src/components/TableFieldSelect.tsx | 28 ++- .../directories/DirectoryColumnTree.tsx | 222 ++++++++++++++++++ .../directories/DirectoryTreeMultiSelect.tsx | 167 ++++++------- .../directories/DirectoryTreeSelect.tsx | 170 +++++++------- client/src/pages/TableEditor.tsx | 139 +++++++---- ...6_add_tree_column_index_to_data_tables.sql | 2 + server/routes/data-tables.routes.ts | 107 ++++++++- shared/schema.ts | 1 + 9 files changed, 638 insertions(+), 224 deletions(-) create mode 100644 client/src/components/directories/DirectoryColumnTree.tsx create mode 100644 migrations/0056_add_tree_column_index_to_data_tables.sql diff --git a/client/src/components/TableFieldMultiSelect.tsx b/client/src/components/TableFieldMultiSelect.tsx index 1a26e96..cad0151 100644 --- a/client/src/components/TableFieldMultiSelect.tsx +++ b/client/src/components/TableFieldMultiSelect.tsx @@ -5,7 +5,8 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Button } from '@/components/ui/button'; import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'; import { Table, X } from 'lucide-react'; -import { DirectoryTreeMultiSelect } from '@/components/directories/DirectoryTreeMultiSelect'; +import { DirectoryTreeMultiSelect, type DirectoryTreeNode } from '@/components/directories/DirectoryTreeMultiSelect'; +import { apiRequest } from '@/lib/queryClient'; import type { DataTableRow } from '@shared/schema'; interface TableFieldMultiSelectProps { @@ -33,6 +34,7 @@ export default function TableFieldMultiSelect({ table: { rows: DataTableRow[]; columns: Array<{ name: string }>; + treeColumnIndex: number | null; }; }>({ queryKey: ['/api/directories', tableId], @@ -42,12 +44,28 @@ export default function TableFieldMultiSelect({ retry: false, }); + const { data: treeData } = useQuery<{ + columnIndex: number | null; + nodes: DirectoryTreeNode[]; + }>({ + queryKey: ['/api/directories', tableId, 'rows', 'tree'], + queryFn: async () => { + const res = await apiRequest('GET', `/api/directories/${tableId}/rows/tree`); + return res.json(); + }, + enabled: !!tableId && !!tableData, + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + retry: false, + }); + const [searchTerm, setSearchTerm] = useState(''); const [isOpen, setIsOpen] = useState(false); const inputRef = useRef(null); const table = tableData?.table; const rows = table?.rows || []; + const nodes = treeData?.nodes || []; let displayColumnIndex = table?.columns?.findIndex((c) => c.name === displayColumn); if (displayColumnIndex === undefined || displayColumnIndex < 0) { @@ -88,7 +106,7 @@ export default function TableFieldMultiSelect({ .filter(Boolean); }, [selectedValues, rows, displayColumnIndex]); - const hasHierarchy = rows.some((r) => r.parentId !== null && r.parentId !== undefined); + const hasTree = nodes.some((n) => n.type === 'group'); const toggle = (rowId: string) => { const next = new Set(selectedSet); @@ -145,9 +163,9 @@ export default function TableFieldMultiSelect({ }} >
- {hasHierarchy ? ( + {hasTree ? ( ; + treeColumnIndex: number | null; }; }>({ queryKey: ['/api/directories', tableId], @@ -40,12 +42,28 @@ export default function TableFieldSelect({ retry: false, }); + const { data: treeData } = useQuery<{ + columnIndex: number | null; + nodes: DirectoryTreeNode[]; + }>({ + queryKey: ['/api/directories', tableId, 'rows', 'tree'], + queryFn: async () => { + const res = await apiRequest('GET', `/api/directories/${tableId}/rows/tree`); + return res.json(); + }, + enabled: !!tableId && !!tableData, + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + retry: false, + }); + const [searchTerm, setSearchTerm] = useState(''); const [isOpen, setIsOpen] = useState(false); const inputRef = useRef(null); const table = tableData?.table; const rows = table?.rows || []; + const nodes = treeData?.nodes || []; let displayColumnIndex = table?.columns?.findIndex((c) => c.name === displayColumn); if (displayColumnIndex === undefined || displayColumnIndex < 0) { @@ -69,6 +87,8 @@ export default function TableFieldSelect({ }); }, [sortedRows, searchTerm, displayColumnIndex]); + const hasTree = nodes.some((n) => n.type === 'group'); + if (!tableId) { return
Таблица не настроена
; } @@ -96,8 +116,6 @@ export default function TableFieldSelect({ setIsOpen(false); }; - const hasHierarchy = rows.some((r) => r.parentId !== null && r.parentId !== undefined); - return ( @@ -130,9 +148,9 @@ export default function TableFieldSelect({ }} >
- {hasHierarchy ? ( + {hasTree ? ( void; + onEdit: (row: DataTableRow) => void; + onDelete: (row: DataTableRow) => void; + onAddChild: (parentValue: string | null) => void; + searchQuery?: string; +} + +interface ColumnTreeGroup { + label: string; + rows: DataTableRow[]; +} + +function groupRows(rows: DataTableRow[], columnIndex: number): { groups: ColumnTreeGroup[]; rootRows: DataTableRow[] } { + const groups = new Map(); + const rootRows: DataTableRow[] = []; + + for (const row of rows) { + const values = Array.isArray(row.values) ? row.values : []; + const key = String(values[columnIndex] ?? '').trim(); + if (!key) { + rootRows.push(row); + continue; + } + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(row); + } + + const sortedGroups = Array.from(groups.entries()) + .map(([label, groupRows]) => ({ + label, + rows: [...groupRows].sort((a, b) => a.position - b.position), + })) + .sort((a, b) => a.label.localeCompare(b.label, 'ru')); + + rootRows.sort((a, b) => a.position - b.position); + + return { groups: sortedGroups, rootRows }; +} + +function getRowLabel(row: DataTableRow, displayColumnIndex = 0): string { + const values = Array.isArray(row.values) ? row.values : []; + return String(values[displayColumnIndex] ?? values[0] ?? '') || '(без названия)'; +} + +function filterGroups( + groups: ColumnTreeGroup[], + rootRows: DataTableRow[], + query: string, + displayColumnIndex: number +): { groups: ColumnTreeGroup[]; rootRows: DataTableRow[]; expanded: Set } { + if (!query.trim()) return { groups, rootRows, expanded: new Set() }; + const q = query.toLowerCase(); + const expanded = new Set(); + + const filteredGroups = groups + .map((group) => ({ + ...group, + rows: group.rows.filter((row) => { + const label = getRowLabel(row, displayColumnIndex).toLowerCase(); + return label.includes(q); + }), + })) + .filter((group) => { + if (group.rows.length > 0) { + expanded.add(group.label); + return true; + } + if (group.label.toLowerCase().includes(q)) { + return true; + } + return false; + }); + + const filteredRootRows = rootRows.filter((row) => { + const label = getRowLabel(row, displayColumnIndex).toLowerCase(); + return label.includes(q); + }); + + return { groups: filteredGroups, rootRows: filteredRootRows, expanded }; +} + +export function DirectoryColumnTree({ + rows, + treeColumnIndex, + displayColumnIndex = 0, + selectedRowId, + onSelect, + onEdit, + onDelete, + onAddChild, + searchQuery, +}: DirectoryColumnTreeProps) { + const { groups, rootRows } = useMemo( + () => groupRows(rows, treeColumnIndex), + [rows, treeColumnIndex] + ); + + const filtered = useMemo( + () => filterGroups(groups, rootRows, searchQuery || '', displayColumnIndex), + [groups, rootRows, searchQuery, displayColumnIndex] + ); + + const [manualExpanded, setManualExpanded] = useState>(new Set()); + + const effectiveExpanded = useMemo( + () => (searchQuery?.trim() ? filtered.expanded : manualExpanded), + [searchQuery, filtered.expanded, manualExpanded] + ); + + const toggle = (label: string) => { + setManualExpanded((prev) => { + const next = new Set(prev); + if (next.has(label)) next.delete(label); + else next.add(label); + return next; + }); + }; + + const renderRow = (row: DataTableRow, level: number) => { + const isSelected = selectedRowId === row.id; + return ( +
onSelect(row)} + > + + {getRowLabel(row, displayColumnIndex)} +
e.stopPropagation()} + > + + +
+
+ ); + }; + + return ( +
+ {filtered.groups.map((group) => { + const isExpanded = effectiveExpanded.has(group.label); + return ( +
+
toggle(group.label)} + > + {isExpanded ? ( + + ) : ( + + )} + {group.label} + +
+ {isExpanded && ( +
+ {group.rows.map((row) => renderRow(row, 1))} +
+ )} +
+ ); + })} + {filtered.rootRows.length > 0 && ( +
+ {filtered.rootRows.map((row) => renderRow(row, 0))} +
+ )} +
+ ); +} diff --git a/client/src/components/directories/DirectoryTreeMultiSelect.tsx b/client/src/components/directories/DirectoryTreeMultiSelect.tsx index 5b42496..65c56f3 100644 --- a/client/src/components/directories/DirectoryTreeMultiSelect.tsx +++ b/client/src/components/directories/DirectoryTreeMultiSelect.tsx @@ -4,68 +4,56 @@ import { Checkbox } from '@/components/ui/checkbox'; import { cn } from '@/lib/utils'; import type { DataTableRow } from '@shared/schema'; -interface TreeItem extends DataTableRow { - level: number; - children: TreeItem[]; +export type DirectoryTreeNode = + | { type: 'group'; id: string; label: string; level: number; children: DirectoryTreeNode[] } + | { type: 'row'; id: number; level: number; row: DataTableRow }; + +function assignLevels(nodes: DirectoryTreeNode[], level = 0): DirectoryTreeNode[] { + return nodes.map((node) => { + if (node.type === 'row') { + return { ...node, level }; + } + return { ...node, level, children: assignLevels(node.children, level + 1) }; + }); } interface DirectoryTreeMultiSelectProps { - rows: DataTableRow[]; + nodes: DirectoryTreeNode[]; displayColumnIndex: number; selected: Set; onToggle: (rowId: string) => void; searchTerm?: string; } -function buildTree(rows: DataTableRow[]): TreeItem[] { - const map = new Map(); - const roots: TreeItem[] = []; - - const sorted = [...rows].sort((a, b) => { - if ((a.parentId ?? -1) !== (b.parentId ?? -1)) return 0; - return a.position - b.position; - }); - - for (const row of sorted) { - map.set(row.id, { ...row, level: 0, children: [] }); - } - - for (const row of sorted) { - const node = map.get(row.id)!; - if (row.parentId === null || row.parentId === undefined) { - roots.push(node); - } else { - const parent = map.get(row.parentId); - if (parent) { - node.level = parent.level + 1; - parent.children.push(node); - } else { - roots.push(node); - } - } - } - - return roots.sort((a, b) => a.position - b.position); -} - -function filterTree(tree: TreeItem[], query: string, displayColumnIndex: number): { tree: TreeItem[]; expanded: Set } { - if (!query.trim()) return { tree, expanded: new Set() }; +function filterNodes( + nodes: DirectoryTreeNode[], + query: string, + displayColumnIndex: number +): { nodes: DirectoryTreeNode[]; expanded: Set } { + if (!query.trim()) return { nodes, expanded: new Set() }; const q = query.toLowerCase(); - const expanded = new Set(); - function walk(nodes: TreeItem[]): TreeItem[] { - const visible: TreeItem[] = []; - for (const node of nodes) { - const label = String(node.values[displayColumnIndex] ?? node.values[0] ?? '').toLowerCase(); - const children = walk(node.children); - const childMatch = children.length > 0; - if (label.includes(q) || childMatch) { - visible.push({ ...node, children }); - if (childMatch) expanded.add(node.id); + const expanded = new Set(); + + function walk(items: DirectoryTreeNode[]): DirectoryTreeNode[] { + const visible: DirectoryTreeNode[] = []; + for (const node of items) { + if (node.type === 'row') { + const label = String(node.row.values[displayColumnIndex] ?? node.row.values[0] ?? '').toLowerCase(); + if (label.includes(q)) visible.push(node); + } else { + const children = walk(node.children); + if (children.length > 0) { + visible.push({ ...node, children }); + expanded.add(node.id); + } else if (node.label.toLowerCase().includes(q)) { + visible.push({ ...node, children: [] }); + } } } return visible; } - return { tree: walk(tree), expanded }; + + return { nodes: walk(nodes), expanded }; } function getRowLabel(row: DataTableRow, displayColumnIndex: number): string { @@ -81,49 +69,53 @@ function TreeNode({ expanded, onToggleExpand, }: { - node: TreeItem; + node: DirectoryTreeNode; displayColumnIndex: number; selected: Set; onToggle: (rowId: string) => void; - expanded: Set; - onToggleExpand: (id: number) => void; + expanded: Set; + onToggleExpand: (id: string) => void; }) { - const hasChildren = node.children.length > 0; - const rowId = node.id.toString(); + if (node.type === 'row') { + const rowId = node.id.toString(); + return ( +
{ e.preventDefault(); onToggle(rowId); }} + > + + + + {getRowLabel(node.row, displayColumnIndex)} + +
+ ); + } + const isExpanded = expanded.has(node.id); return (
{ e.preventDefault(); onToggle(rowId); }} + onMouseDown={(e) => { + e.preventDefault(); + onToggleExpand(node.id); + }} > - {hasChildren ? ( - { - e.stopPropagation(); - onToggleExpand(node.id); - }} - > - {expanded.has(node.id) ? ( - - ) : ( - - )} - + {isExpanded ? ( + ) : ( - + )} - - - {getRowLabel(node, displayColumnIndex)} + {node.label}
- {hasChildren && expanded.has(node.id) && ( + {isExpanded && (
{node.children.map((child) => ( buildTree(rows), [rows]); - const filtered = useMemo(() => filterTree(tree, searchTerm, displayColumnIndex), [tree, searchTerm, displayColumnIndex]); - const [manualExpanded, setManualExpanded] = useState>(new Set()); +export function DirectoryTreeMultiSelect({ + nodes, + displayColumnIndex, + selected, + onToggle, + searchTerm = '', +}: DirectoryTreeMultiSelectProps) { + const nodesWithLevels = useMemo(() => assignLevels(nodes), [nodes]); + const filtered = useMemo( + () => filterNodes(nodesWithLevels, searchTerm, displayColumnIndex), + [nodesWithLevels, searchTerm, displayColumnIndex] + ); + const [manualExpanded, setManualExpanded] = useState>(new Set()); const effectiveExpanded = useMemo( () => (searchTerm.trim() ? filtered.expanded : manualExpanded), [searchTerm, filtered.expanded, manualExpanded] ); const displayTree = useMemo( - () => (searchTerm.trim() ? filtered.tree : tree), - [searchTerm, filtered.tree, tree] + () => (searchTerm.trim() ? filtered.nodes : nodes), + [searchTerm, filtered.nodes, nodes] ); - const toggleExpand = (id: number) => { + const toggleExpand = (id: string) => { setManualExpanded((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); @@ -165,7 +166,7 @@ export function DirectoryTreeMultiSelect({ rows, displayColumnIndex, selected, o
{displayTree.map((node) => ( { + if (node.type === 'row') { + return { ...node, level }; + } + return { ...node, level, children: assignLevels(node.children, level + 1) }; + }); } interface DirectoryTreeSelectProps { - rows: DataTableRow[]; + nodes: DirectoryTreeNode[]; displayColumnIndex: number; value?: string | number | null; onChange: (rowId: string) => void; searchTerm?: string; } -function buildTree(rows: DataTableRow[]): TreeItem[] { - const map = new Map(); - const roots: TreeItem[] = []; - - const sorted = [...rows].sort((a, b) => { - if ((a.parentId ?? -1) !== (b.parentId ?? -1)) return 0; - return a.position - b.position; - }); - - for (const row of sorted) { - map.set(row.id, { ...row, level: 0, children: [] }); - } - - for (const row of sorted) { - const node = map.get(row.id)!; - if (row.parentId === null || row.parentId === undefined) { - roots.push(node); - } else { - const parent = map.get(row.parentId); - if (parent) { - node.level = parent.level + 1; - parent.children.push(node); - } else { - roots.push(node); - } - } - } - - return roots.sort((a, b) => a.position - b.position); -} - -function filterTree(tree: TreeItem[], query: string, displayColumnIndex: number): { tree: TreeItem[]; expanded: Set } { - if (!query.trim()) return { tree, expanded: new Set() }; +function filterNodes( + nodes: DirectoryTreeNode[], + query: string, + displayColumnIndex: number +): { nodes: DirectoryTreeNode[]; expanded: Set } { + if (!query.trim()) return { nodes, expanded: new Set() }; const q = query.toLowerCase(); - const expanded = new Set(); - function walk(nodes: TreeItem[]): TreeItem[] { - const visible: TreeItem[] = []; - for (const node of nodes) { - const label = String(node.values[displayColumnIndex] ?? node.values[0] ?? '').toLowerCase(); - const children = walk(node.children); - const childMatch = children.length > 0; - if (label.includes(q) || childMatch) { - visible.push({ ...node, children }); - if (childMatch) expanded.add(node.id); + const expanded = new Set(); + + function walk(items: DirectoryTreeNode[]): DirectoryTreeNode[] { + const visible: DirectoryTreeNode[] = []; + for (const node of items) { + if (node.type === 'row') { + const label = String(node.row.values[displayColumnIndex] ?? node.row.values[0] ?? '').toLowerCase(); + if (label.includes(q)) visible.push(node); + } else { + const children = walk(node.children); + if (children.length > 0) { + visible.push({ ...node, children }); + expanded.add(node.id); + } else if (node.label.toLowerCase().includes(q)) { + visible.push({ ...node, children: [] }); + } } } return visible; } - return { tree: walk(tree), expanded }; + + return { nodes: walk(nodes), expanded }; } function getRowLabel(row: DataTableRow, displayColumnIndex: number): string { @@ -80,52 +68,53 @@ function TreeNode({ expanded, onToggle, }: { - node: TreeItem; + node: DirectoryTreeNode; displayColumnIndex: number; value?: string | number | null; onChange: (rowId: string) => void; - expanded: Set; - onToggle: (id: number) => void; + expanded: Set; + onToggle: (id: string) => void; }) { - const hasChildren = node.children.length > 0; - const isSelected = value?.toString() === node.id.toString(); - - return ( -
+ if (node.type === 'row') { + const rowId = node.id.toString(); + const isSelected = value?.toString() === rowId; + return ( - {hasChildren && expanded.has(node.id) && ( + ); + } + + const isExpanded = expanded.has(node.id); + return ( +
+ + {isExpanded && (
{node.children.map((child) => ( buildTree(rows), [rows]); - const filtered = useMemo(() => filterTree(tree, searchTerm, displayColumnIndex), [tree, searchTerm, displayColumnIndex]); - const [manualExpanded, setManualExpanded] = useState>(new Set()); +export function DirectoryTreeSelect({ + nodes, + displayColumnIndex, + value, + onChange, + searchTerm = '', +}: DirectoryTreeSelectProps) { + const nodesWithLevels = useMemo(() => assignLevels(nodes), [nodes]); + const filtered = useMemo( + () => filterNodes(nodesWithLevels, searchTerm, displayColumnIndex), + [nodesWithLevels, searchTerm, displayColumnIndex] + ); + const [manualExpanded, setManualExpanded] = useState>(new Set()); const effectiveExpanded = useMemo( () => (searchTerm.trim() ? filtered.expanded : manualExpanded), [searchTerm, filtered.expanded, manualExpanded] ); const displayTree = useMemo( - () => (searchTerm.trim() ? filtered.tree : tree), - [searchTerm, filtered.tree, tree] + () => (searchTerm.trim() ? filtered.nodes : nodes), + [searchTerm, filtered.nodes, nodes] ); - const toggle = (id: number) => { + const toggle = (id: string) => { setManualExpanded((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); @@ -176,7 +174,7 @@ export function DirectoryTreeSelect({ rows, displayColumnIndex, value, onChange, {displayTree.map((node) => ( ([]); + const [treeColumnIndex, setTreeColumnIndex] = useState(null); const [importOpen, setImportOpen] = useState(false); const [importPreview, setImportPreview] = useState<{ added: { values: string[] }[]; @@ -103,6 +105,7 @@ export default function TableEditor() { const [rowEditorOpen, setRowEditorOpen] = useState(false); const [rowEditorInitialValues, setRowEditorInitialValues] = useState([]); const [rowEditorParentId, setRowEditorParentId] = useState(null); + const [rowEditorGroupValue, setRowEditorGroupValue] = useState(null); const [editorValues, setEditorValues] = useState([]); const [treeSearchQuery, setTreeSearchQuery] = useState(''); @@ -171,6 +174,7 @@ export default function TableEditor() { setTableName(data.table.name); setTableDescription(data.table.description || ''); setTableColumns(data.table.columns as TableColumn[]); + setTreeColumnIndex(data.table.treeColumnIndex ?? null); setNewRowValues(new Array(data.table.columns.length).fill('')); } }, [data]); @@ -182,7 +186,7 @@ export default function TableEditor() { }, [rowEditorOpen, rowEditorInitialValues]); const updateTableMutation = useMutation({ - mutationFn: async (updates: { name?: string; description?: string; columns?: TableColumn[] }) => { + mutationFn: async (updates: { name?: string; description?: string; columns?: TableColumn[]; treeColumnIndex?: number | null }) => { return apiRequest('PATCH', `/api/directories/${tableId}`, updates); }, onSuccess: () => { @@ -357,8 +361,9 @@ export default function TableEditor() { }); }; - const openRowEditor = (parentId: number | null = null, initialValues?: string[], rowId?: number) => { + const openRowEditor = (parentId: number | null = null, initialValues?: string[], rowId?: number, groupValue: string | null = null) => { setRowEditorParentId(parentId); + setRowEditorGroupValue(groupValue); setRowEditorInitialValues(initialValues ?? new Array(tableColumns.length).fill('')); if (rowId) { setEditingRowId(rowId); @@ -372,14 +377,19 @@ export default function TableEditor() { setRowEditorOpen(false); setRowEditorInitialValues([]); setRowEditorParentId(null); + setRowEditorGroupValue(null); setEditingRowId(null); }; const handleSaveRowEditor = (values: string[]) => { + const finalValues = [...values]; + if (treeColumnIndex !== null && rowEditorGroupValue !== null && editingRowId === null) { + finalValues[treeColumnIndex] = rowEditorGroupValue; + } if (editingRowId !== null) { - updateRowMutation.mutate({ rowId: editingRowId, values }); + updateRowMutation.mutate({ rowId: editingRowId, values: finalValues }); } else { - createRowMutation.mutate({ values, parentId: rowEditorParentId }); + createRowMutation.mutate({ values: finalValues, parentId: rowEditorParentId }); } }; @@ -393,6 +403,11 @@ export default function TableEditor() { openRowEditor(parentId, new Array(tableColumns.length).fill('')); }; + const handleColumnAddChild = (groupValue: string | null) => { + setSelectedRowId(null); + openRowEditor(null, new Array(tableColumns.length).fill(''), undefined, groupValue); + }; + const handleTreeDelete = (row: DataTableRow) => { if (window.confirm(`Удалить строку "${row.values[0] || row.id}"?`)) { deleteRowMutation.mutate(row.id); @@ -682,36 +697,48 @@ export default function TableEditor() { {viewMode === 'tree' ? ( - -
-
-
- - setTreeSearchQuery(e.target.value)} - /> -
- {canEdit && ( - - )} +
+
+
+ + setTreeSearchQuery(e.target.value)} + />
- {displayRows.length === 0 ? ( -
- {treeSearchQuery ? 'Ничего не найдено' : 'Таблица пуста'} -
- ) : ( + {canEdit && ( + + )} +
+ {displayRows.length === 0 ? ( +
+ {treeSearchQuery ? 'Ничего не найдено' : 'Таблица пуста'} +
+ ) : treeColumnIndex != null ? ( + + ) : ( + - )} -
- - {activeRow ? ( -
- - {activeRow.values[0] || '—'} -
- ) : null} -
- + + {activeRow ? ( +
+ + {activeRow.values[0] || '—'} +
+ ) : null} +
+ + )} +
) : (
@@ -1146,13 +1173,35 @@ export default function TableEditor() { +
+ + +

+ Строки будут сгруппированы по уникальным значениям выбранной колонки. +

+