/** * TasksSourceConfig Component * ADR-024: Configure source table for Tasks tab * * Features: * - Select Project → Table * - Bind to existing row (for context) * - Uses useAllTables hook for hierarchical data */ import { useState, useEffect, useMemo } from 'react'; import { X, Table2, Check, Loader2, Link2, Unlink, ChevronRight, Search } from 'lucide-react'; import { cn } from '@/shared/utils/cn'; import { useAllTables } from '@/features/tables/hooks/useAllTables'; import { useQuery } from '@tanstack/react-query'; import { apiClient } from '@/shared/utils/apiClient'; import { TasksSourceConfig as Config } from './ContactsList.v3'; export interface BoundRowInfo { tableId: number; rowId: number; displayValue: string; tableName?: string; } export interface TasksSourceConfigProps { isOpen: boolean; onClose: () => void; currentConfig?: Config; onSave: (config: Config) => void; defaultSpaceId?: number; // Row binding boundRow?: BoundRowInfo; onBindRow?: (row: BoundRowInfo) => void; onUnbindRow?: () => void; } export function TasksSourceConfigModal({ isOpen, onClose, currentConfig, onSave, defaultSpaceId, boundRow, onBindRow, onUnbindRow }: TasksSourceConfigProps) { const { data: allTablesData, isLoading } = useAllTables(); // Selected project and table const [selectedProjectId, setSelectedProjectId] = useState(null); const [selectedTableId, setSelectedTableId] = useState( currentConfig?.tableId ? String(currentConfig.tableId) : null ); // Row binding state const [showRowBinding, setShowRowBinding] = useState(false); const [bindingTableId, setBindingTableId] = useState(null); const [rowSearchQuery, setRowSearchQuery] = useState(''); const [selectedRowId, setSelectedRowId] = useState(null); // Reset state when modal opens useEffect(() => { if (isOpen) { setSelectedTableId(currentConfig?.tableId ? String(currentConfig.tableId) : null); setShowRowBinding(false); setBindingTableId(null); setRowSearchQuery(''); setSelectedRowId(null); // Try to find the project of current table if (currentConfig?.tableId && allTablesData?.flat) { const currentTable = allTablesData.flat.find(t => t.id === String(currentConfig.tableId)); if (currentTable) { setSelectedProjectId(currentTable.projectId); } } } }, [isOpen, currentConfig, allTablesData]); // Fetch rows for binding const { data: bindingRows = [], isLoading: isLoadingRows } = useQuery({ queryKey: ['binding-rows', bindingTableId, rowSearchQuery], queryFn: async () => { if (!bindingTableId) return []; const params = new URLSearchParams({ limit: '50' }); if (rowSearchQuery) params.append('search', rowSearchQuery); const response = await apiClient.get<{ success: boolean; data: { rows: Array<{ id: number; data: Record }> }; }>(`/tables/${bindingTableId}/rows?${params}`); return response.success ? response.data.rows : []; }, enabled: !!bindingTableId && showRowBinding }); // Get binding table columns for display const bindingTableInfo = useMemo(() => { if (!bindingTableId) return null; return allTablesData?.flat.find(t => t.id === bindingTableId); }, [bindingTableId, allTablesData]); // Get tables for selected project const projectTables = useMemo(() => { if (!selectedProjectId || !allTablesData?.spacesWithTables) return []; for (const space of allTablesData.spacesWithTables) { const project = space.projects.find(p => p.id === selectedProjectId); if (project) { return project.tables || []; } } return []; }, [selectedProjectId, allTablesData]); // Handle project change const handleProjectChange = (projectId: string) => { const newProjectId = projectId ? Number(projectId) : null; setSelectedProjectId(newProjectId); setSelectedTableId(null); // Reset table when project changes }; // Handle table selection const handleTableChange = (tableId: string) => { setSelectedTableId(tableId || null); }; // Handle save const handleSave = () => { if (!selectedTableId) return; const table = allTablesData?.flat.find(t => t.id === selectedTableId); if (table) { onSave({ tableId: Number(table.id), tableName: table.displayName || table.name, tableIcon: table.icon, displayColumn: 'name' // Default, can be made configurable later }); onClose(); } }; if (!isOpen) return null; // Get first display column from row const getRowDisplayValue = (row: { id: number; data: Record }) => { const data = row.data; return String(data['name'] || data['title'] || data['Название'] || data['Name'] || `#${row.id}`); }; return (
{/* Header */}

{showRowBinding ? 'Привязать к записи' : 'Источник записей'}

{/* Current bound row */} {boundRow && !showRowBinding && (
Привязано к записи:
{boundRow.displayValue} #{boundRow.rowId}
{onUnbindRow && ( )}
)} {/* Content */}
{showRowBinding ? ( /* Row Binding View */
{/* Table selector for binding */}
{/* Search */} {bindingTableId && (
setRowSearchQuery(e.target.value)} placeholder="Поиск записей..." className="w-full pl-9 pr-3 py-2 rounded-lg bg-[var(--bg-tertiary)] border border-[var(--border-primary)] text-sm text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary-500)]/30" />
)} {/* Rows list */} {bindingTableId && (
{isLoadingRows ? (
) : bindingRows.length === 0 ? (
{rowSearchQuery ? 'Ничего не найдено' : 'Нет записей'}
) : ( bindingRows.map((row) => ( )) )}
)}
) : ( /* Main Config View */
{isLoading ? (
) : ( <> {/* Bind to Row button */} {onBindRow && ( )} {/* Divider */} {onBindRow && (
или
)} {/* Project selector with optgroups by Space */}
{/* Table selector */}
{/* Selected table preview */} {selectedTableId && (
{(() => { const table = allTablesData?.flat.find(t => t.id === selectedTableId); if (!table) return 'Таблица выбрана'; return `${table.icon || '📋'} ${table.displayName}`; })()}
)} )}
)}
{/* Footer */}
{showRowBinding ? ( ) : ( )}
); }