From 09861fc966c05ca19894048d5d368f39f25d057f 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 23:07:34 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=B8=D0=B2=D0=B0=D0=B5=D0=BC=D0=B0=D1=8F=20=D0=BA=D0=BE=D0=BB?= =?UTF-8?q?=D0=BE=D0=BD=D0=BA=D0=B0=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B5=20=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлено поле data_tables.tree_display_column_index. - Backend /api/directories/:id/rows/tree возвращает displayColumnIndex и формирует label для row-узлов на основе выбранной колонки. - В редакторе справочника добавлен селектор «Колонка для отображения в дереве». - DirectoryRowTree, DirectoryColumnTree, DirectoryTreeSelect, DirectoryTreeMultiSelect используют настроенную колонку. - TableFieldSelect / TableFieldMultiSelect используют treeDisplayColumnIndex справочника как fallback, если в поле формы не задан displayColumn. - Миграция 0057 + запись в _journal.json. --- .../src/components/TableFieldMultiSelect.tsx | 4 +-- client/src/components/TableFieldSelect.tsx | 4 +-- .../directories/DirectoryRowTree.tsx | 7 ++-- .../directories/DirectoryTreeMultiSelect.tsx | 24 ++++---------- .../directories/DirectoryTreeSelect.tsx | 24 ++++---------- client/src/pages/TableEditor.tsx | 32 ++++++++++++++++--- .../0057_add_tree_display_column_index.sql | 1 + migrations/meta/_journal.json | 7 ++++ server/routes/data-tables.routes.ts | 29 +++++++++++------ shared/schema.ts | 1 + 10 files changed, 76 insertions(+), 57 deletions(-) create mode 100644 migrations/0057_add_tree_display_column_index.sql diff --git a/client/src/components/TableFieldMultiSelect.tsx b/client/src/components/TableFieldMultiSelect.tsx index cad0151..4469e57 100644 --- a/client/src/components/TableFieldMultiSelect.tsx +++ b/client/src/components/TableFieldMultiSelect.tsx @@ -35,6 +35,7 @@ export default function TableFieldMultiSelect({ rows: DataTableRow[]; columns: Array<{ name: string }>; treeColumnIndex: number | null; + treeDisplayColumnIndex: number | null; }; }>({ queryKey: ['/api/directories', tableId], @@ -69,7 +70,7 @@ export default function TableFieldMultiSelect({ let displayColumnIndex = table?.columns?.findIndex((c) => c.name === displayColumn); if (displayColumnIndex === undefined || displayColumnIndex < 0) { - displayColumnIndex = 0; + displayColumnIndex = table?.treeDisplayColumnIndex ?? 0; } const selectedValues = useMemo(() => { @@ -166,7 +167,6 @@ export default function TableFieldMultiSelect({ {hasTree ? ( ; treeColumnIndex: number | null; + treeDisplayColumnIndex: number | null; }; }>({ queryKey: ['/api/directories', tableId], @@ -67,7 +68,7 @@ export default function TableFieldSelect({ let displayColumnIndex = table?.columns?.findIndex((c) => c.name === displayColumn); if (displayColumnIndex === undefined || displayColumnIndex < 0) { - displayColumnIndex = 0; + displayColumnIndex = table?.treeDisplayColumnIndex ?? 0; } const sortedRows = useMemo(() => { @@ -151,7 +152,6 @@ export default function TableFieldSelect({ {hasTree ? ( ): D function filterTree( tree: DirectoryRowTreeItem[], - query: string + query: string, + labelColumnIndex = 0 ): { tree: DirectoryRowTreeItem[]; expanded: Set } { if (!query.trim()) return { tree, expanded: new Set() }; const q = query.toLowerCase(); @@ -115,7 +116,7 @@ function filterTree( function walk(nodes: DirectoryRowTreeItem[]): DirectoryRowTreeItem[] { const visible: DirectoryRowTreeItem[] = []; for (const node of nodes) { - const label = String(node.values[0] ?? '').toLowerCase(); + const label = getRowLabel(node, labelColumnIndex).toLowerCase(); const children = walk(node.children); const childMatch = children.length > 0; if (label.includes(q) || childMatch) { @@ -298,7 +299,7 @@ export function DirectoryRowTree({ searchQuery, }: DirectoryRowTreeProps & { searchQuery?: string }) { const tree = useMemo(() => buildTree(rows), [rows]); - const filtered = useMemo(() => filterTree(tree, searchQuery || ''), [tree, searchQuery]); + const filtered = useMemo(() => filterTree(tree, searchQuery || '', labelColumnIndex), [tree, searchQuery, labelColumnIndex]); const effectiveExpanded = useMemo( () => (searchQuery?.trim() ? filtered.expanded : expanded), [searchQuery, filtered.expanded, expanded] diff --git a/client/src/components/directories/DirectoryTreeMultiSelect.tsx b/client/src/components/directories/DirectoryTreeMultiSelect.tsx index 65c56f3..27f3fec 100644 --- a/client/src/components/directories/DirectoryTreeMultiSelect.tsx +++ b/client/src/components/directories/DirectoryTreeMultiSelect.tsx @@ -6,7 +6,7 @@ import type { DataTableRow } from '@shared/schema'; export type DirectoryTreeNode = | { type: 'group'; id: string; label: string; level: number; children: DirectoryTreeNode[] } - | { type: 'row'; id: number; level: number; row: DataTableRow }; + | { type: 'row'; id: number; label: string; level: number; row: DataTableRow }; function assignLevels(nodes: DirectoryTreeNode[], level = 0): DirectoryTreeNode[] { return nodes.map((node) => { @@ -19,7 +19,6 @@ function assignLevels(nodes: DirectoryTreeNode[], level = 0): DirectoryTreeNode[ interface DirectoryTreeMultiSelectProps { nodes: DirectoryTreeNode[]; - displayColumnIndex: number; selected: Set; onToggle: (rowId: string) => void; searchTerm?: string; @@ -27,8 +26,7 @@ interface DirectoryTreeMultiSelectProps { function filterNodes( nodes: DirectoryTreeNode[], - query: string, - displayColumnIndex: number + query: string ): { nodes: DirectoryTreeNode[]; expanded: Set } { if (!query.trim()) return { nodes, expanded: new Set() }; const q = query.toLowerCase(); @@ -38,7 +36,7 @@ function filterNodes( const visible: DirectoryTreeNode[] = []; for (const node of items) { if (node.type === 'row') { - const label = String(node.row.values[displayColumnIndex] ?? node.row.values[0] ?? '').toLowerCase(); + const label = node.label.toLowerCase(); if (label.includes(q)) visible.push(node); } else { const children = walk(node.children); @@ -56,21 +54,14 @@ function filterNodes( return { nodes: walk(nodes), expanded }; } -function getRowLabel(row: DataTableRow, displayColumnIndex: number): string { - const values = Array.isArray(row.values) ? row.values : []; - return String(values[displayColumnIndex] ?? values[0] ?? '') || '(без названия)'; -} - function TreeNode({ node, - displayColumnIndex, selected, onToggle, expanded, onToggleExpand, }: { node: DirectoryTreeNode; - displayColumnIndex: number; selected: Set; onToggle: (rowId: string) => void; expanded: Set; @@ -87,7 +78,7 @@ function TreeNode({ - {getRowLabel(node.row, displayColumnIndex)} + {node.label} ); @@ -117,7 +108,6 @@ function TreeNode({ assignLevels(nodes), [nodes]); const filtered = useMemo( - () => filterNodes(nodesWithLevels, searchTerm, displayColumnIndex), - [nodesWithLevels, searchTerm, displayColumnIndex] + () => filterNodes(nodesWithLevels, searchTerm), + [nodesWithLevels, searchTerm] ); const [manualExpanded, setManualExpanded] = useState>(new Set()); @@ -168,7 +157,6 @@ export function DirectoryTreeMultiSelect({ { @@ -18,7 +18,6 @@ function assignLevels(nodes: DirectoryTreeNode[], level = 0): DirectoryTreeNode[ interface DirectoryTreeSelectProps { nodes: DirectoryTreeNode[]; - displayColumnIndex: number; value?: string | number | null; onChange: (rowId: string) => void; searchTerm?: string; @@ -26,8 +25,7 @@ interface DirectoryTreeSelectProps { function filterNodes( nodes: DirectoryTreeNode[], - query: string, - displayColumnIndex: number + query: string ): { nodes: DirectoryTreeNode[]; expanded: Set } { if (!query.trim()) return { nodes, expanded: new Set() }; const q = query.toLowerCase(); @@ -37,7 +35,7 @@ function filterNodes( const visible: DirectoryTreeNode[] = []; for (const node of items) { if (node.type === 'row') { - const label = String(node.row.values[displayColumnIndex] ?? node.row.values[0] ?? '').toLowerCase(); + const label = node.label.toLowerCase(); if (label.includes(q)) visible.push(node); } else { const children = walk(node.children); @@ -55,21 +53,14 @@ function filterNodes( return { nodes: walk(nodes), expanded }; } -function getRowLabel(row: DataTableRow, displayColumnIndex: number): string { - const values = Array.isArray(row.values) ? row.values : []; - return String(values[displayColumnIndex] ?? values[0] ?? '') || '(без названия)'; -} - function TreeNode({ node, - displayColumnIndex, value, onChange, expanded, onToggle, }: { node: DirectoryTreeNode; - displayColumnIndex: number; value?: string | number | null; onChange: (rowId: string) => void; expanded: Set; @@ -89,7 +80,7 @@ function TreeNode({ onClick={() => onChange(rowId)} > - {getRowLabel(node.row, displayColumnIndex)} + {node.label} ); } @@ -116,7 +107,6 @@ function TreeNode({ assignLevels(nodes), [nodes]); const filtered = useMemo( - () => filterNodes(nodesWithLevels, searchTerm, displayColumnIndex), - [nodesWithLevels, searchTerm, displayColumnIndex] + () => filterNodes(nodesWithLevels, searchTerm), + [nodesWithLevels, searchTerm] ); const [manualExpanded, setManualExpanded] = useState>(new Set()); @@ -176,7 +165,6 @@ export function DirectoryTreeSelect({ ([]); const [treeColumnIndex, setTreeColumnIndex] = useState(null); + const [treeDisplayColumnIndex, setTreeDisplayColumnIndex] = useState(null); const [importOpen, setImportOpen] = useState(false); const [importPreview, setImportPreview] = useState<{ added: { values: string[] }[]; @@ -175,6 +176,7 @@ export default function TableEditor() { setTableDescription(data.table.description || ''); setTableColumns(data.table.columns as TableColumn[]); setTreeColumnIndex(data.table.treeColumnIndex ?? null); + setTreeDisplayColumnIndex(data.table.treeDisplayColumnIndex ?? null); setNewRowValues(new Array(data.table.columns.length).fill('')); } }, [data]); @@ -186,7 +188,7 @@ export default function TableEditor() { }, [rowEditorOpen, rowEditorInitialValues]); const updateTableMutation = useMutation({ - mutationFn: async (updates: { name?: string; description?: string; columns?: TableColumn[]; treeColumnIndex?: number | null }) => { + mutationFn: async (updates: { name?: string; description?: string; columns?: TableColumn[]; treeColumnIndex?: number | null; treeDisplayColumnIndex?: number | null }) => { return apiRequest('PATCH', `/api/directories/${tableId}`, updates); }, onSuccess: () => { @@ -723,7 +725,7 @@ export default function TableEditor() { - {activeRow.values[0] || '—'} + {activeRow.values[treeDisplayColumnIndex ?? 0] || activeRow.values[0] || '—'} ) : null} @@ -1195,13 +1198,34 @@ export default function TableEditor() { Строки будут сгруппированы по уникальным значениям выбранной колонки.

+
+ + +

+ Значение этой колонки будет показано как название узла в дереве. +

+