/** * ChatBindingSettings Component * ADR-024: Chat & Message Architecture Redesign * * Settings panel for configuring chat bindings: * - Default space/table for task binding * - Current row bindings (multiple rows allowed) * - Participant management */ import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { apiClient } from '@/shared/utils/apiClient'; import { cn } from '@/shared/utils/cn'; import { Settings, Link2, Unlink, Table2, FolderOpen, Hash, Plus, X, Check, ChevronDown, Search, Loader2 } from 'lucide-react'; interface Space { id: number; name: string; icon?: string; } interface Table { id: number; name: string; slug: string; icon?: string; display_column?: string; } interface Row { id: number; data: Record; table_id: number; } interface RowBinding { tableId: number; tableName: string; rowId: number; rowDisplay: string; } export interface ChatBindingConfig { defaultSpaceId?: number; defaultTableId?: number; bindings: RowBinding[]; } export interface ChatBindingSettingsProps { config: ChatBindingConfig; onConfigChange: (config: ChatBindingConfig) => void; currentSpaceId?: number; className?: string; } export function ChatBindingSettings({ config, onConfigChange, currentSpaceId, className }: ChatBindingSettingsProps) { const [isAddingBinding, setIsAddingBinding] = useState(false); const [selectedSpaceId, setSelectedSpaceId] = useState( config.defaultSpaceId || currentSpaceId || null ); const [selectedTableId, setSelectedTableId] = useState( config.defaultTableId || null ); const [searchQuery, setSearchQuery] = 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: Table[]; }>(`/tables?spaceId=${selectedSpaceId}`); return response.success ? response.data : []; }, enabled: !!selectedSpaceId }); // Fetch rows for selected table const { data: rows = [], isLoading: isLoadingRows } = useQuery({ queryKey: ['rows', selectedTableId, searchQuery], queryFn: async () => { if (!selectedTableId) return []; const params = new URLSearchParams(); if (searchQuery) params.append('search', searchQuery); params.append('limit', '20'); const response = await apiClient.get<{ success: boolean; data: { rows: Row[] }; }>(`/tables/${selectedTableId}/rows?${params}`); return response.success ? response.data.rows : []; }, enabled: !!selectedTableId && isAddingBinding }); // Get display value for a row const getRowDisplay = (row: Row, table?: Table): string => { const displayColumn = table?.display_column || 'name'; const data = row.data; return String(data[displayColumn] || data['title'] || data['name'] || `#${row.id}`); }; // Handle adding a binding const handleAddBinding = (row: Row) => { const table = tables.find(t => t.id === selectedTableId); if (!table) return; const newBinding: RowBinding = { tableId: table.id, tableName: table.name, rowId: row.id, rowDisplay: getRowDisplay(row, table) }; // Check if already bound const exists = config.bindings.some( b => b.tableId === newBinding.tableId && b.rowId === newBinding.rowId ); if (exists) return; onConfigChange({ ...config, bindings: [...config.bindings, newBinding] }); setIsAddingBinding(false); setSearchQuery(''); }; // Handle removing a binding const handleRemoveBinding = (binding: RowBinding) => { onConfigChange({ ...config, bindings: config.bindings.filter( b => !(b.tableId === binding.tableId && b.rowId === binding.rowId) ) }); }; // Handle default space change const handleDefaultSpaceChange = (spaceId: number) => { setSelectedSpaceId(spaceId); setSelectedTableId(null); onConfigChange({ ...config, defaultSpaceId: spaceId, defaultTableId: undefined }); }; // Handle default table change const handleDefaultTableChange = (tableId: number) => { setSelectedTableId(tableId); onConfigChange({ ...config, defaultTableId: tableId }); }; return (
{/* Default Binding Settings */}

Настройки привязки

{/* Default Space */}
{/* Default Table */} {selectedSpaceId && (
)}
{/* Current Bindings */}

Привязанные записи

{/* Bindings List */} {config.bindings.length === 0 ? (

Нет привязанных записей

) : (
{config.bindings.map((binding, index) => (
{binding.tableName} / {binding.rowDisplay}
))}
)}
{/* Add Binding Modal */} {isAddingBinding && (

Привязать запись

{/* Table Selector */}
{/* Search */} {selectedTableId && (
setSearchQuery(e.target.value)} placeholder="Поиск записей..." className="w-full pl-9 pr-3 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg" />
)} {/* Rows List */} {selectedTableId && (
{isLoadingRows ? (
) : rows.length === 0 ? (

Записи не найдены

) : ( rows.map(row => { const table = tables.find(t => t.id === selectedTableId); const isAlreadyBound = config.bindings.some( b => b.tableId === selectedTableId && b.rowId === row.id ); return ( ); }) )}
)}
)}
); }