diff --git a/client/src/components/TableFieldWithSubdirectory.tsx b/client/src/components/TableFieldWithSubdirectory.tsx index 0804fb6..99ea39d 100644 --- a/client/src/components/TableFieldWithSubdirectory.tsx +++ b/client/src/components/TableFieldWithSubdirectory.tsx @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { apiRequest } from '@/lib/queryClient'; import TableFieldSelect from '@/components/TableFieldSelect'; import TableFieldMultiSelect from '@/components/TableFieldMultiSelect'; -import type { DataTableSubdirectory } from '@shared/schema'; +import type { DataTableRow, DataTableSubdirectory } from '@shared/schema'; interface TableFieldValue { parentId?: string | null; @@ -67,6 +67,23 @@ export default function TableFieldWithSubdirectory({ const showSubdirectory = !!opts.showSubdirectory; const subdirectoryId = opts.subdirectoryId as number | undefined; + const { data: tableData } = useQuery<{ + table: { + rows: DataTableRow[]; + columns: Array<{ name: string }>; + treeColumnIndex: number | null; + treeDisplayColumnIndex: number | null; + treeColumns: number[] | null; + treeDisplayColumns: number[] | null; + }; + }>({ + queryKey: ['/api/directories', tableId], + enabled: !!tableId, + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + retry: false, + }); + const { data: subdirectoriesData } = useQuery<{ subdirectories: DataTableSubdirectory[] }>({ queryKey: ['/api/directories', tableId, 'subdirectories'], queryFn: async () => { @@ -83,8 +100,25 @@ export default function TableFieldWithSubdirectory({ const { parentId, childId } = parseTableValue(value); + const parentRow = useMemo(() => { + if (!parentId || !tableData?.table?.rows) return null; + return tableData.table.rows.find((r) => r.id.toString() === parentId) || null; + }, [parentId, tableData]); + + const isParentLinked = useMemo(() => { + if (!subdirectory || !parentId) return false; + return subdirectory.parentRowIds?.includes(Number(parentId)) ?? false; + }, [subdirectory, parentId]); + + const childFilter = useMemo(() => { + if (!subdirectory || !parentRow) return undefined; + const col = String(subdirectory.childColumnIndex ?? 0); + const parentValue = parentRow.values?.[subdirectory.parentColumnIndex ?? 0] ?? ''; + if (!parentValue) return undefined; + return { [col]: parentValue }; + }, [subdirectory, parentRow]); + const handleParentChange = (newParentId: string) => { - // When parent changes, keep child only if it still belongs to new parent (simplified: reset child) onChange(serializeTableValue(newParentId || null, null)); }; @@ -92,12 +126,6 @@ export default function TableFieldWithSubdirectory({ onChange(serializeTableValue(parentId, newChildId || null)); }; - const childFilter = useMemo(() => { - if (!subdirectory || !parentId) return undefined; - const col = String(subdirectory.childColumnIndex ?? 0); - return { [col]: parentId }; - }, [subdirectory, parentId]); - if (!showSubdirectory || !subdirectory || allowMultiple) { const SelectComponent = allowMultiple ? TableFieldMultiSelect : TableFieldSelect; return ( @@ -117,6 +145,7 @@ export default function TableFieldWithSubdirectory({ ...opts, tableId: subdirectory.childTableId, displayColumn: subdirectory.childTable?.columns?.[subdirectory.childColumnIndex ?? 0]?.name || undefined, + filters: childFilter, }; return ( @@ -129,14 +158,16 @@ export default function TableFieldWithSubdirectory({ value={parentId || ''} onChange={handleParentChange} /> - + {isParentLinked && childFilter && ( + + )} ); } diff --git a/client/src/pages/TableEditor.tsx b/client/src/pages/TableEditor.tsx index a1fdff1..4e14b27 100644 --- a/client/src/pages/TableEditor.tsx +++ b/client/src/pages/TableEditor.tsx @@ -118,7 +118,7 @@ export default function TableEditor() { const [subdirectoryChildTableId, setSubdirectoryChildTableId] = useState('none'); const [subdirectoryParentColumnIndex, setSubdirectoryParentColumnIndex] = useState(0); const [subdirectoryChildColumnIndex, setSubdirectoryChildColumnIndex] = useState(0); - const [subdirectoryParentValues, setSubdirectoryParentValues] = useState>(new Set()); + const [subdirectoryParentRowIds, setSubdirectoryParentRowIds] = useState>(new Set()); const { filters, sort, setFilter, clearFilter, clearAll, setSort, clearSort, hasActiveFilters } = useColumnFilters(); @@ -213,7 +213,7 @@ export default function TableEditor() { }, [rowEditorOpen, rowEditorInitialValues]); const createSubdirectoryMutation = useMutation({ - mutationFn: async (payload: { childTableId: number; parentColumnIndex: number; childColumnIndex: number; parentValues: string[] }) => { + mutationFn: async (payload: { childTableId: number; parentColumnIndex: number; childColumnIndex: number; parentRowIds: number[] }) => { return apiRequest('POST', `/api/directories/${tableId}/subdirectories`, payload); }, onSuccess: () => { @@ -230,7 +230,7 @@ export default function TableEditor() { }); const updateSubdirectoryMutation = useMutation({ - mutationFn: async ({ linkId, payload }: { linkId: number; payload: { childTableId?: number; parentColumnIndex?: number; childColumnIndex?: number; parentValues?: string[] } }) => { + mutationFn: async ({ linkId, payload }: { linkId: number; payload: { childTableId?: number; parentColumnIndex?: number; childColumnIndex?: number; parentRowIds?: number[] } }) => { return apiRequest('PATCH', `/api/directories/${tableId}/subdirectories/${linkId}`, payload); }, onSuccess: () => { @@ -265,7 +265,7 @@ export default function TableEditor() { setSubdirectoryChildTableId('none'); setSubdirectoryParentColumnIndex(0); setSubdirectoryChildColumnIndex(0); - setSubdirectoryParentValues(new Set()); + setSubdirectoryParentRowIds(new Set()); setEditingSubdirectory(null); }; @@ -275,7 +275,7 @@ export default function TableEditor() { setSubdirectoryChildTableId(link.childTableId?.toString() || 'none'); setSubdirectoryParentColumnIndex(link.parentColumnIndex ?? 0); setSubdirectoryChildColumnIndex(link.childColumnIndex ?? 0); - setSubdirectoryParentValues(new Set(link.parentValues || [])); + setSubdirectoryParentRowIds(new Set(link.parentRowIds || [])); } else { resetSubdirectoryForm(); } @@ -292,7 +292,7 @@ export default function TableEditor() { childTableId: childId, parentColumnIndex: subdirectoryParentColumnIndex, childColumnIndex: subdirectoryChildColumnIndex, - parentValues: Array.from(subdirectoryParentValues), + parentRowIds: Array.from(subdirectoryParentRowIds), }; if (editingSubdirectory?.id) { updateSubdirectoryMutation.mutate({ linkId: editingSubdirectory.id, payload }); @@ -301,11 +301,11 @@ export default function TableEditor() { } }; - const toggleParentValue = (value: string) => { - setSubdirectoryParentValues((prev) => { + const toggleParentRowId = (rowId: number) => { + setSubdirectoryParentRowIds((prev) => { const next = new Set(prev); - if (next.has(value)) next.delete(value); - else next.add(value); + if (next.has(rowId)) next.delete(rowId); + else next.add(rowId); return next; }); }; @@ -1786,8 +1786,8 @@ export default function TableEditor() { {link.childTable?.name || `ID ${link.childTableId}`} по столбцу «{columns[link.parentColumnIndex ?? 0]?.name || '?'}» => «{link.childTable?.columns?.[link.childColumnIndex ?? 0]?.name || '?'}» - {link.parentValues && link.parentValues.length > 0 && ( - ({link.parentValues.length} значений) + {link.parentRowIds && link.parentRowIds.length > 0 && ( + ({link.parentRowIds.length} строк) )} @@ -1861,28 +1861,31 @@ export default function TableEditor() { - Значения родительского справочника + Строки родительского справочника {displayRows.length === 0 ? ( Нет строк в родительском справочнике ) : ( displayRows.map((row) => { - const value = row.values[subdirectoryParentColumnIndex] ?? ''; - if (!value) return null; + const displayCols = treeDisplayColumns.length > 0 ? treeDisplayColumns : [0]; + const label = displayCols + .map((ci) => String(row.values[ci] ?? '').trim()) + .filter(Boolean) + .join(' ') || `Строка ${row.id}`; return ( toggleParentValue(String(value))} + checked={subdirectoryParentRowIds.has(row.id)} + onCheckedChange={() => toggleParentRowId(row.id)} /> - {value} + {label} ); }) )} - Подсправочник будет доступен в форме только при выборе этих значений из основного справочника. + Подсправочник будет доступен в форме только при выборе этих строк из основного справочника. diff --git a/migrations/0062_data_table_links_parent_row_ids.sql b/migrations/0062_data_table_links_parent_row_ids.sql new file mode 100644 index 0000000..43b72db --- /dev/null +++ b/migrations/0062_data_table_links_parent_row_ids.sql @@ -0,0 +1,3 @@ +-- data_table_links: привязка подсправочника к строкам родителя +ALTER TABLE data_table_links DROP COLUMN IF EXISTS parent_values; +ALTER TABLE data_table_links ADD COLUMN IF NOT EXISTS parent_row_ids JSONB NOT NULL DEFAULT '[]'; diff --git a/server/routes/data-tables.routes.ts b/server/routes/data-tables.routes.ts index 88a0278..b554634 100644 --- a/server/routes/data-tables.routes.ts +++ b/server/routes/data-tables.routes.ts @@ -795,7 +795,7 @@ export function registerDataTableRoutes(app: import("express").Express): void { return res.status(403).json({ error: 'Нет прав для управления таблицей' }); } - const { childTableId, parentColumnIndex, childColumnIndex, parentValues } = req.body; + const { childTableId, parentColumnIndex, childColumnIndex, parentRowIds } = req.body; if (!childTableId || typeof childTableId !== 'number') { return res.status(400).json({ error: 'Необходимо указать подсправочник (childTableId)' }); } @@ -811,7 +811,7 @@ export function registerDataTableRoutes(app: import("express").Express): void { childTableId, parentColumnIndex: parentColumnIndex ?? 0, childColumnIndex: childColumnIndex ?? 0, - parentValues: Array.isArray(parentValues) ? parentValues : [], + parentRowIds: Array.isArray(parentRowIds) ? parentRowIds : [], }); res.status(201).json({ link }); @@ -845,12 +845,12 @@ export function registerDataTableRoutes(app: import("express").Express): void { return res.status(404).json({ error: 'Связь не найдена' }); } - const { childTableId, parentColumnIndex, childColumnIndex, parentValues } = req.body; + const { childTableId, parentColumnIndex, childColumnIndex, parentRowIds } = req.body; const updates: Partial = {}; if (childTableId !== undefined) updates.childTableId = childTableId; if (parentColumnIndex !== undefined) updates.parentColumnIndex = parentColumnIndex; if (childColumnIndex !== undefined) updates.childColumnIndex = childColumnIndex; - if (parentValues !== undefined) updates.parentValues = Array.isArray(parentValues) ? parentValues : []; + if (parentRowIds !== undefined) updates.parentRowIds = Array.isArray(parentRowIds) ? parentRowIds : []; const link = await storage.updateDataTableLink(linkId, req.organizationId!, updates); res.json({ link }); diff --git a/server/storage/data-tables-core.storage.ts b/server/storage/data-tables-core.storage.ts index faf7878..87c7e6c 100644 --- a/server/storage/data-tables-core.storage.ts +++ b/server/storage/data-tables-core.storage.ts @@ -69,7 +69,7 @@ export class DataTablesCoreStorage extends BotsStorage { childTableId: dataTableLinks.childTableId, parentColumnIndex: dataTableLinks.parentColumnIndex, childColumnIndex: dataTableLinks.childColumnIndex, - parentValues: dataTableLinks.parentValues, + parentRowIds: dataTableLinks.parentRowIds, createdAt: dataTableLinks.createdAt, updatedAt: dataTableLinks.updatedAt, childTable: { @@ -363,7 +363,7 @@ export class DataTablesCoreStorage extends BotsStorage { return link || undefined; } - async createDataTableLink(data: { organizationId: number; parentTableId: number; childTableId: number; parentColumnIndex?: number; childColumnIndex?: number; parentValues?: string[] }): Promise { + async createDataTableLink(data: { organizationId: number; parentTableId: number; childTableId: number; parentColumnIndex?: number; childColumnIndex?: number; parentRowIds?: number[] }): Promise { const [link] = await db .insert(dataTableLinks) .values({ @@ -372,14 +372,14 @@ export class DataTablesCoreStorage extends BotsStorage { childTableId: data.childTableId, parentColumnIndex: data.parentColumnIndex ?? 0, childColumnIndex: data.childColumnIndex ?? 0, - parentValues: data.parentValues ?? [], + parentRowIds: data.parentRowIds ?? [], }) .onConflictDoUpdate({ target: [dataTableLinks.parentTableId, dataTableLinks.childTableId], set: { parentColumnIndex: data.parentColumnIndex ?? 0, childColumnIndex: data.childColumnIndex ?? 0, - parentValues: data.parentValues ?? [], + parentRowIds: data.parentRowIds ?? [], updatedAt: new Date(), }, }) diff --git a/shared/schema.ts b/shared/schema.ts index 8ef1071..6b058fa 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -1859,7 +1859,7 @@ export const dataTableLinks = pgTable("data_table_links", { childTableId: integer("child_table_id").notNull().references(() => dataTables.id, { onDelete: "cascade" }), parentColumnIndex: integer("parent_column_index").notNull().default(0), childColumnIndex: integer("child_column_index").notNull().default(0), - parentValues: jsonb("parent_values").notNull().$type().default([]), + parentRowIds: jsonb("parent_row_ids").notNull().$type().default([]), createdAt: timestamp("created_at").defaultNow(), updatedAt: timestamp("updated_at").defaultNow(), }, (table) => ({
- Подсправочник будет доступен в форме только при выборе этих значений из основного справочника. + Подсправочник будет доступен в форме только при выборе этих строк из основного справочника.