/** * ChatSettings Component * ADR-024: Chat & Message Architecture * * Settings panel for configuring chat bindings: * - Bind to Space (filter context) * - Bind to Table (default for row selection) * - Bind to Rows (multiple row bindings like Relations) */ import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { apiClient } from '@/shared/utils/apiClient'; import { cn } from '@/shared/utils/cn'; import { X, Loader2, Link2, Unlink, ChevronDown, Database, Table2, Hash, Plus, Trash2, Settings2 } from 'lucide-react'; interface Space { id: number; name: string; icon?: string; } interface TableInfo { id: number; name: string; slug?: string; icon?: string; } interface RowInfo { id: number; table_id: number; data: Record; } interface BoundRow { table_id: number; row_id: number; table_name?: string; row_title?: string; } export interface ChatSettingsProps { conversationId?: number; spaceId?: number | null; boundRows?: BoundRow[]; defaultTableId?: number | null; onSpaceChange: (spaceId: number | null) => void; onDefaultTableChange: (tableId: number | null) => void; onBindRow: (tableId: number, rowId: number) => void; onUnbindRow: (tableId: number, rowId: number) => void; onClose: () => void; className?: string; } export function ChatSettings({ conversationId, spaceId, boundRows = [], defaultTableId, onSpaceChange, onDefaultTableChange, onBindRow, onUnbindRow, onClose, className }: ChatSettingsProps) { const [selectedSpaceId, setSelectedSpaceId] = useState(spaceId ?? null); const [selectedTableId, setSelectedTableId] = useState(defaultTableId ?? null); const [showSpaceSelector, setShowSpaceSelector] = useState(false); const [showTableSelector, setShowTableSelector] = useState(false); const [showRowSelector, setShowRowSelector] = useState(false); const [rowSearchQuery, setRowSearchQuery] = useState(''); // Fetch spaces const { data: spaces = [], isLoading: isLoadingSpaces } = useQuery({ queryKey: ['spaces'], queryFn: async () => { const response = await apiClient.get<{ success: boolean; data: Space[] }>('/spaces'); return response.success ? response.data : []; } }); // Fetch tables for selected space const { data: tables = [], isLoading: isLoadingTables } = useQuery({ queryKey: ['tables', selectedSpaceId], queryFn: async () => { if (!selectedSpaceId) return []; const response = await apiClient.get<{ success: boolean; data: TableInfo[] }>( `/tables?spaceId=${selectedSpaceId}` ); return response.success ? response.data : []; }, enabled: !!selectedSpaceId }); // Fetch rows for row selector const { data: tableRows = [], isLoading: isLoadingRows } = useQuery({ queryKey: ['table-rows-for-binding', selectedTableId, rowSearchQuery], queryFn: async () => { if (!selectedTableId) return []; const params = new URLSearchParams(); if (rowSearchQuery) params.append('search', rowSearchQuery); params.append('limit', '20'); const response = await apiClient.get<{ success: boolean; data: { rows: RowInfo[] }; }>(`/tables/${selectedTableId}/rows?${params}`); return response.success ? response.data.rows : []; }, enabled: !!selectedTableId && showRowSelector }); const selectedSpace = spaces.find(s => s.id === selectedSpaceId); const selectedTable = tables.find(t => t.id === selectedTableId); const handleSpaceSelect = (space: Space | null) => { setSelectedSpaceId(space?.id ?? null); setSelectedTableId(null); // Reset table when space changes onSpaceChange(space?.id ?? null); setShowSpaceSelector(false); }; const handleTableSelect = (table: TableInfo | null) => { setSelectedTableId(table?.id ?? null); onDefaultTableChange(table?.id ?? null); setShowTableSelector(false); }; const handleRowBind = (row: RowInfo) => { if (selectedTableId) { onBindRow(selectedTableId, row.id); } setShowRowSelector(false); setRowSearchQuery(''); }; const getRowDisplayValue = (row: RowInfo) => { const data = row.data; return String(data['name'] || data['title'] || data['subject'] || `#${row.id}`); }; // Check if row is already bound const isRowBound = (tableId: number, rowId: number) => { return boundRows.some(br => br.table_id === tableId && br.row_id === rowId); }; return (
{/* Header */}
Настройки чата
{/* Space Binding */}
{showSpaceSelector && (
{isLoadingSpaces ? (
) : ( spaces.map(space => ( )) )}
)}
{/* Default Table */} {selectedSpaceId && (
{showTableSelector && (
{isLoadingTables ? (
) : ( tables.map(table => ( )) )}
)}
)} {/* Bound Rows */}
{selectedTableId && ( )}
{/* Row selector dropdown */} {showRowSelector && selectedTableId && (
setRowSearchQuery(e.target.value)} placeholder="Поиск..." className="w-full px-2 py-1.5 text-sm rounded border border-[var(--border-secondary)] bg-[var(--bg-secondary)] text-[var(--text-primary)] mb-2" />
{isLoadingRows ? (
) : tableRows.length === 0 ? (
Нет строк
) : ( tableRows.map(row => { const bound = isRowBound(row.table_id, row.id); return ( ); }) )}
)} {/* List of bound rows */} {boundRows.length === 0 ? (
Нет привязанных строк
) : (
{boundRows.map((br, idx) => (
{br.row_title || `#${br.row_id}`} {br.table_name && ( ({br.table_name}) )}
))}
)}
); }