import { useState, useMemo, useEffect, useCallback } from 'react'; import { useQueries } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { Settings2, Calendar, User, Search, X as XIcon, Paperclip, AlertCircle, CheckCircle2, GripVertical } from 'lucide-react'; import { type ColumnSort } from '@/hooks/useColumnFilters'; import { resolveFieldDisplayValue, type FieldDisplayContext } from '@/lib/fieldDisplay'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core'; import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable, arrayMove, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { useTableMaps } from '@/hooks/useTableMaps'; import { ColumnFilter } from '@/components/ui/ColumnFilter'; import { ActiveFilterBadges } from '@/components/ui/ActiveFilterBadges'; import { apiRequest, queryClient, OfflineQueuedError } from '@/lib/queryClient'; import { useToast } from '@/hooks/use-toast'; import { addFileToken, openFilePresigned } from '@/lib/fileUtils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import ContractNumberDisplay from '@/components/ContractNumberDisplay'; import RegistryCellEditor from './RegistryCellEditor'; interface TaskAssigneeInfo { userId: number; user: { firstName: string | null; lastName: string | null; middleName?: string | null; email: string }; } interface TaskData { id: number; title: string; description?: string; currentStatusId: number; assignedTo?: number; assignees?: TaskAssigneeInfo[]; createdBy: number; createdAt: string; updatedAt: string; completedAt?: string; isCompleted?: boolean; dueDate?: string; fieldValues?: { [fieldId: number]: unknown }; } interface FormFieldData { id: number; name: string; code: string; type: string; isRequired: boolean; placeholder?: string; options?: any; autoProlongation?: boolean; autoProlongationLabel?: string; ganttRole?: 'start' | 'end' | 'milestone' | null; } interface FormStatusData { id: number; name: string; color: string; isInitial: boolean; isFinal: boolean; } interface UserData { id: number; firstName: string; lastName: string; middleName?: string; email: string; fullName?: string; } interface TaskRegistryProps { tasks: TaskData[]; relatedTaskTitles?: Record; fields: FormFieldData[]; statuses: FormStatusData[]; users?: UserData[]; roles?: Array<{ id: number; name?: string }>; formId: number; onFilteredCountChange?: (count: number) => void; onTaskClick?: (task: TaskData) => void; // External filter state from TaskView (server-driven) filters: Record; sort: ColumnSort | null; setFilter: (column: string, value: string) => void; clearFilter: (column: string) => void; clearAll: () => void; setSort: (column: string) => void; clearSort: () => void; hasActiveFilters: boolean; // Global search (URL-synced, multi-term) searches: string[]; onAddSearch: (value: string) => void; onRemoveSearch: (index: number) => void; // Bulk selection isAdmin?: boolean; selectedTaskIds?: Set; onToggleSelected?: (taskId: number) => void; onToggleSelectAll?: () => void; onSelectRange?: (taskIds: number[]) => void; } // Отображает значение поля с учётом типа function displayFieldValue( field: FormFieldData | undefined, value: unknown, context: FieldDisplayContext ): string { return resolveFieldDisplayValue(value, (field || {}) as { type?: string; options?: unknown; [key: string]: unknown }, context); } interface FileEntry { name: string; url?: string; } function extractFileEntries(value: unknown): FileEntry[] { if (value === null || value === undefined || value === '') return []; if (Array.isArray(value)) { return value .filter((f): f is Record => typeof f === 'object' && f !== null) .map(f => ({ name: typeof f.name === 'string' ? f.name : '', url: typeof f.url === 'string' ? f.url : undefined })) .filter(f => f.name); } if (typeof value === 'object' && !Array.isArray(value)) { const obj = value as Record; if (typeof obj.name === 'string' && obj.name) { return [{ name: obj.name, url: typeof obj.url === 'string' ? obj.url : undefined }]; } } return []; } function pluralizeFiles(count: number): string { if (count % 10 === 1 && count % 100 !== 11) return `${count} файл`; if (count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20)) return `${count} файла`; return `${count} файлов`; } function FileFieldCell({ value }: { value: unknown }) { const files = extractFileEntries(value); if (files.length === 0) return —; return ( ); } interface SortableColumnItemProps { id: string; label: React.ReactNode; checked: boolean; onCheckedChange: () => void; } function SortableColumnItem({ id, label, checked, onCheckedChange }: SortableColumnItemProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; return (
); } // Типы полей, поддерживающие inline-редактирование в реестре const REGISTRY_INLINE_TYPES = new Set([ 'text', 'number', 'textarea', 'rich-text', 'date', 'datetime', 'select', 'radio-group', 'user', 'role', 'table', 'company', 'geo', 'contract-number', 'contact', ]); // Типы полей, которые переключаются по одиночному клику const REGISTRY_QUICK_TOGGLE_TYPES = new Set(['checkbox', 'toggle']); const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users = [], roles = [], formId, onFilteredCountChange, onTaskClick, filters, sort, setFilter, clearFilter, clearAll, setSort, clearSort, hasActiveFilters, searches, onAddSearch, onRemoveSearch, isAdmin, selectedTaskIds, onToggleSelected, onToggleSelectAll, onSelectRange }: TaskRegistryProps) => { const { toast } = useToast(); // Текущая редактируемая ячейка: { taskId, columnKey } const [editingCell, setEditingCell] = useState<{ taskId: number; columnKey: string } | null>(null); // Последняя выделенная задача — якорь для Shift+клика const [lastSelectedTaskId, setLastSelectedTaskId] = useState(null); // Функция для получения имени пользователя по ID const getUserName = (userId: number): string => { const user = users.find(u => u.id === userId); if (user) { return user.fullName || `${user.firstName} ${user.middleName || ''} ${user.lastName}`.trim() || user.email; } return `ID ${userId}`; }; // Карта названий задач для полей типа 'task' const taskTitleMap = useMemo(() => { const map = new Map(); for (const task of tasks) { if (task.id) map.set(task.id, task.title); } for (const [id, title] of Object.entries(relatedTaskTitles)) { const numId = Number(id); if (!isNaN(numId) && title) map.set(numId, title); } return map; }, [tasks, relatedTaskTitles]); // Карты справочников для полей типа 'table' const { tableMap } = useTableMaps(fields); // Общий контекст для resolveFieldDisplayValue const displayContext = useMemo( () => ({ users, roles, taskTitleMap, tableMap }), [users, roles, taskTitleMap, tableMap] ); // Загрузка юрлиц для полей типа 'contract-number' (для клиентского поиска) const contractNumberFields = useMemo(() => fields.filter(f => f.type === 'contract-number'), [fields]); const legalEntityFormIds = useMemo(() => { const ids = new Set(); for (const f of contractNumberFields) { const leFormId = (f.options as any)?.contractNumber?.legalEntityFormId; if (leFormId) ids.add(leFormId); } return Array.from(ids); }, [contractNumberFields]); const legalEntityQueries = useQueries({ queries: legalEntityFormIds.map(formId => ({ queryKey: ['/api/forms', formId, 'tasks'], queryFn: async () => { const res = await apiRequest('GET', `/api/forms/${formId}/tasks`); const data = await res.json() as { tasks: Array<{ id: number; title: string }> }; if (process.env.NODE_ENV === 'development') { console.log('[TaskRegistry] Loaded legal entities for form', formId, 'count:', data?.tasks?.length); } return data; }, enabled: !!formId, staleTime: Infinity, gcTime: 10 * 60 * 1000, })), }); const legalEntityMap = useMemo(() => { const map = new Map(); for (const query of legalEntityQueries) { if (query.data?.tasks) { for (const task of query.data.tasks) { map.set(task.id, task.title); } } } if (process.env.NODE_ENV === 'development') { console.log('[TaskRegistry] legalEntityMap size:', map.size, 'formIds:', legalEntityFormIds); } return map; }, [legalEntityQueries, legalEntityFormIds]); // Инициализация видимых колонок с учетом кастомных полей const storageKey = `registry-columns-${formId}`; const getDefaultColumns = () => { const defaultColumns: { [key: string]: boolean } = { title: true, status: true, assignedTo: true, createdAt: true, dueDate: true, description: false, updatedAt: false, createdBy: false, completedAt: false, }; fields.forEach(field => { defaultColumns[`field_${field.id}`] = false; }); return defaultColumns; }; const getDefaultColumnOrder = (): string[] => { const order = [ 'title', 'status', 'assignedTo', 'createdAt', 'dueDate', 'description', 'updatedAt', 'createdBy', 'completedAt', ]; fields.forEach(field => { order.push(`field_${field.id}`); }); return order; }; const loadColumns = () => { const defaults = getDefaultColumns(); try { const saved = localStorage.getItem(storageKey); if (saved) { const parsed = JSON.parse(saved) as { [key: string]: boolean }; return { ...defaults, ...parsed }; } } catch { /* ignore */ } return defaults; }; const loadColumnOrder = (): string[] => { try { const saved = localStorage.getItem(`${storageKey}-order`); if (saved) { const parsed = JSON.parse(saved) as string[]; const defaults = getDefaultColumnOrder(); // Добавляем новые поля в конец, сохраняя существующий порядок const merged = [...parsed]; defaults.forEach(key => { if (!merged.includes(key)) merged.push(key); }); return merged; } } catch { /* ignore */ } return getDefaultColumnOrder(); }; const [visibleColumns, setVisibleColumns] = useState<{ [key: string]: boolean }>(loadColumns); const [columnOrder, setColumnOrder] = useState(loadColumnOrder); // searches приходит как prop из TaskView (синхронизирован с URL) const [inputValue, setInputValue] = useState(''); const [showCompleted, setShowCompleted] = useState(() => { try { return JSON.parse(localStorage.getItem(`registry-completed-${formId}`) || 'false'); } catch { return false; } }); const formatDate = (dateString: string) => { if (!dateString) return ''; const date = new Date(dateString); if (isNaN(date.getTime())) return '—'; return date.toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); }; // Сохраняем настройки при любом изменении useEffect(() => { try { localStorage.setItem(storageKey, JSON.stringify(visibleColumns)); } catch { /* ignore */ } }, [visibleColumns, storageKey]); useEffect(() => { try { localStorage.setItem(`${storageKey}-order`, JSON.stringify(columnOrder)); } catch { /* ignore */ } }, [columnOrder, storageKey]); useEffect(() => { try { localStorage.setItem(`registry-completed-${formId}`, JSON.stringify(showCompleted)); } catch { /* ignore */ } }, [showCompleted, formId]); // При появлении новых полей добавляем их в состояние (не затирая сохранённые) useEffect(() => { setVisibleColumns(prev => { const updated = { ...prev }; fields.forEach(field => { const key = `field_${field.id}`; if (!(key in updated)) updated[key] = false; }); return updated; }); setColumnOrder(prev => { const updated = [...prev]; fields.forEach(field => { const key = `field_${field.id}`; if (!updated.includes(key)) updated.push(key); }); return updated; }); }, [fields]); // DND для настройки порядка колонок const columnSensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); const handleColumnDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; setColumnOrder(prev => { const oldIndex = prev.indexOf(active.id as string); const newIndex = prev.indexOf(over.id as string); if (oldIndex === -1 || newIndex === -1) return prev; return arrayMove(prev, oldIndex, newIndex); }); }; // Создаем карты для быстрого поиска const statusMap = useMemo(() => { const map = new Map(); statuses.forEach(status => map.set(status.id, status)); return map; }, [statuses]); // Проверяет, содержит ли задача заданный поисковый запрос const taskMatchesSearch = useCallback((task: TaskData, qLower: string): boolean => { if (task.title?.toLowerCase().includes(qLower)) return true; if (task.description?.toLowerCase().includes(qLower)) return true; const status = statusMap.get(task.currentStatusId); if (status?.name?.toLowerCase().includes(qLower)) return true; if (task.assignees && task.assignees.length > 0) { const assigneeNames = task.assignees.map(a => `${a.user.firstName || ''} ${a.user.middleName || ''} ${a.user.lastName || ''}`.trim() || a.user.email).join(' '); if (assigneeNames.toLowerCase().includes(qLower)) return true; } else if (task.assignedTo) { if (getUserName(task.assignedTo).toLowerCase().includes(qLower)) return true; } if (task.createdAt && formatDate(task.createdAt).includes(qLower)) return true; if (task.dueDate && formatDate(task.dueDate).includes(qLower)) return true; if (task.fieldValues) { for (const [fieldIdStr, val] of Object.entries(task.fieldValues)) { const fieldId = parseInt(fieldIdStr); const field = fields.find(f => f.id === fieldId); if (field?.type === 'contract-number') { let parsed: any; try { parsed = typeof val === 'string' ? JSON.parse(val) : val; } catch { parsed = null; } const num = parsed?.value ? String(parsed.value) : ''; const entityTitle = parsed?.legalEntityTaskId ? legalEntityMap.get(Number(parsed.legalEntityTaskId)) : undefined; const display = entityTitle ? `${num} — ${entityTitle}` : num; if (display.toLowerCase().includes(qLower)) return true; } else if (field?.type === 'task') { const display = displayFieldValue(field, val, displayContext); if (display.toLowerCase().includes(qLower)) return true; } else { const str = Array.isArray(val) ? (val as Record[]).map(f => f?.name ?? '').join(' ') : String(val ?? ''); if (str.toLowerCase().includes(qLower)) return true; } } } return false; }, [statusMap, fields, legalEntityMap, getUserName, displayContext]); // Клиентский поиск + скрытие завершённых поверх серверной фильтрации const filteredTasks = useMemo(() => { return tasks.filter(task => { const status = statusMap.get(task.currentStatusId); const isCompleted = task.isCompleted || status?.isFinal || false; if (!showCompleted && isCompleted) return false; // Проверяем зафиксированные поисковые термы (badges) for (const q of searches) { const qLower = q.toLowerCase().trim(); if (!qLower) continue; if (!taskMatchesSearch(task, qLower)) return false; } // Проверяем текущее значение input в реальном времени const inputLower = inputValue.toLowerCase().trim(); if (inputLower && !taskMatchesSearch(task, inputLower)) return false; return true; }); }, [tasks, searches, inputValue, showCompleted, statusMap, taskMatchesSearch]); const fetchTaskColumnSuggestions = useCallback((column: string) => async (search: string): Promise => { const params = new URLSearchParams({ column, search }); const context = Object.fromEntries(Object.entries(filters).filter(([k, v]) => k !== column && !!v)); if (Object.keys(context).length > 0) params.set('context', JSON.stringify(context)); const res = await apiRequest('GET', `/api/forms/${formId}/tasks/column-values?${params.toString()}`); const data = await res.json(); const values: string[] = data.values || []; // Если завершённые задачи скрыты, убираем финальные статусы из подсказок фильтра if (column === 'status' && !showCompleted) { const finalStatusNames = new Set(statuses.filter(s => s.isFinal).map(s => s.name)); return values.filter(value => !finalStatusNames.has(value)); } return values; }, [formId, filters, showCompleted, statuses]); const taskColumnLabels: Record = useMemo(() => { const base: Record = { title: 'Название', status: 'Статус', currentStatusId: 'Статус', assignedTo: 'Ответственный', dueDate: 'Срок', createdAt: 'Создана', updatedAt: 'Обновлена', createdBy: 'Автор', overdue: 'Просрочено', }; fields.forEach(f => { base[`field_${f.id}`] = f.name; }); return base; }, [fields]); const toggleColumnVisibility = (column: string) => { setVisibleColumns(prev => ({ ...prev, [column]: !prev[column as keyof typeof prev] })); }; const getStatusBadge = (statusId: number) => { const status = statusMap.get(statusId); if (!status) return Неизвестно; return ( {status.name} ); }; // Дебаг: логируем состояние поиска по юрлицам useEffect(() => { if (process.env.NODE_ENV === 'development' && searches.length > 0) { console.log('[TaskRegistry] Searches:', searches, 'filtered:', filteredTasks.length, 'legalEntityMap:', legalEntityMap.size, 'contractFields:', contractNumberFields.length); } }, [searches, filteredTasks.length, legalEntityMap.size, contractNumberFields.length]); // Уведомляем родительский компонент о количестве отфильтрованных задач useEffect(() => { onFilteredCountChange?.(filteredTasks.length); }, [filteredTasks.length, onFilteredCountChange]); const handleQuickToggle = async (taskId: number, fieldId: number, currentValue: unknown) => { const newVal = !(currentValue === true || currentValue === 'true'); try { await apiRequest('PATCH', `/api/tasks/${taskId}/field-values/${fieldId}`, { value: newVal }); queryClient.invalidateQueries({ queryKey: ['/api/forms', formId, 'tasks', 'with-fields'] }); } catch (err: unknown) { if (err instanceof OfflineQueuedError) { queryClient.invalidateQueries({ queryKey: ['/api/forms', formId, 'tasks', 'with-fields'] }); toast({ title: 'Изменения сохранены локально', description: 'Отправим при появлении сети' }); return; } const msg = err instanceof Error ? err.message : 'Ошибка сохранения'; toast({ title: 'Ошибка', description: msg, variant: 'destructive' }); } }; const handleRowClick = (task: TaskData, e: React.MouseEvent) => { if (!isAdmin || !onToggleSelected) { onTaskClick?.(task); return; } const isCtrl = e.ctrlKey || e.metaKey; const isShift = e.shiftKey; if (isShift && lastSelectedTaskId != null) { e.preventDefault(); const ids = filteredTasks.map(t => t.id); const start = ids.indexOf(lastSelectedTaskId); const end = ids.indexOf(task.id); if (start !== -1 && end !== -1) { const range = ids.slice(Math.min(start, end), Math.max(start, end) + 1); onSelectRange?.(range); } setLastSelectedTaskId(task.id); return; } if (isCtrl) { e.preventDefault(); onToggleSelected(task.id); setLastSelectedTaskId(task.id); return; } // Обычный клик по строке открывает задачу onTaskClick?.(task); }; const isEditing = (taskId: number, columnKey: string) => editingCell?.taskId === taskId && editingCell?.columnKey === columnKey; const getColumnLabel = (key: string): string => { if (key === 'title') return 'Название'; if (key === 'status') return 'Статус'; if (key === 'assignedTo') return 'Ответственный'; if (key === 'createdAt') return 'Создана'; if (key === 'dueDate') return 'Срок'; if (key === 'description') return 'Описание'; if (key === 'updatedAt') return 'Обновлена'; if (key === 'createdBy') return 'Автор'; if (key === 'completedAt') return 'Завершена'; if (key.startsWith('field_')) { const fieldId = parseInt(key.replace('field_', '')); const field = fields.find(f => f.id === fieldId); return field ? field.name : 'Неизвестное поле'; } return key; }; return (
{/* Панель управления + активные фильтры (один ряд) */}
Настройки реестра Отображение колонок и задач
{/* Показать завершённые */}
setShowCompleted(Boolean(v))} />
{columnOrder .filter(key => key in visibleColumns) .map(key => ( toggleColumnVisibility(key)} /> ))}
{/* Глобальный поиск по всем колонкам (многоступенчатый) */}
setInputValue(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); if (inputValue.trim()) { onAddSearch(inputValue.trim()); setInputValue(''); } } }} placeholder="Поиск..." className="h-5 pl-6 pr-5 text-xs rounded-full border-border/60 w-36 focus:w-52 transition-all duration-200" /> {inputValue && ( )}
{searches.map((term, i) => ( Поиск: {term} ))}
{hasActiveFilters && ( )}
{/* Таблица задач */}
{filteredTasks.length === 0 ? (
{tasks.length === 0 ? (

Нет задач

Создайте первую задачу чтобы начать работу

) : (

Не найдено задач

Попробуйте изменить условия поиска или фильтры

)}
) : (
{isAdmin && ( 0 && filteredTasks.every(t => selectedTaskIds?.has(t.id))} data-state={ filteredTasks.length > 0 && filteredTasks.some(t => selectedTaskIds?.has(t.id)) && !filteredTasks.every(t => selectedTaskIds?.has(t.id)) ? 'indeterminate' : (filteredTasks.length > 0 && filteredTasks.every(t => selectedTaskIds?.has(t.id)) ? 'checked' : 'unchecked') } onCheckedChange={() => onToggleSelectAll?.()} aria-label="Выделить все видимые задачи" /> )} {columnOrder.filter(key => visibleColumns[key]).map(key => { if (key === 'title') { return ( ); } if (key === 'status') { return ( ); } if (key === 'assignedTo') { return ( ); } if (key === 'createdAt') { return ( ); } if (key === 'dueDate') { return ( ); } if (key === 'description') { return Описание; } if (key === 'updatedAt') { return ( ); } if (key === 'createdBy') { return ( ); } if (key === 'completedAt') { return ( ); } if (key.startsWith('field_')) { const fieldId = parseInt(key.replace('field_', '')); const field = fields.find(f => f.id === fieldId); return field ? ( ) : null; } return null; })} {filteredTasks.map((task) => { const taskStatus = statusMap.get(task.currentStatusId); const isCompleted = task.isCompleted || taskStatus?.isFinal || false; const isOverdue = !isCompleted && !!task.dueDate && new Date(task.dueDate) < new Date(); return ( handleRowClick(task, e)} > {isAdmin && ( e.stopPropagation()}> onToggleSelected?.(task.id)} onClick={(e) => e.stopPropagation()} aria-label="Выделить задачу" /> )} {columnOrder.filter(key => visibleColumns[key]).map(key => { if (key === 'title') { return (
{task.description && (

{task.description}

)}
); } if (key === 'status') { return {getStatusBadge(task.currentStatusId)}; } if (key === 'assignedTo') { return ( {isEditing(task.id, 'assignedTo') ? ( setEditingCell(null)} onCancel={() => setEditingCell(null)} /> ) : (
setEditingCell({ taskId: task.id, columnKey: 'assignedTo' })} > {(task.assignees && task.assignees.length > 0) ? (
{task.assignees.map(a => `${a.user.firstName || ''} ${a.user.middleName || ''} ${a.user.lastName || ''}`.trim() || a.user.email).join(', ')}
) : task.assignedTo ? (
{getUserName(task.assignedTo)}
) : ( — )}
)}
); } if (key === 'createdAt') { return (
{formatDate(task.createdAt)}
); } if (key === 'dueDate') { return ( {task.dueDate ? ( (() => { const overdue = !isCompleted && new Date(task.dueDate) < new Date(); return (
{formatDate(task.dueDate)} {overdue && }
); })() ) : ( — )}
); } if (key === 'description') { return ( {task.description ? (

{task.description}

) : ( — )}
); } if (key === 'updatedAt') { return (
{formatDate(task.updatedAt)}
); } if (key === 'createdBy') { return (
{getUserName(task.createdBy)}
); } if (key === 'completedAt') { return task.completedAt ? (
{formatDate(task.completedAt)}
) : ( ); } if (key.startsWith('field_')) { const fieldId = parseInt(key.replace('field_', '')); const field = fields.find(f => f.id === fieldId); const rawValue = task.fieldValues?.[fieldId]; return field ? ( {isEditing(task.id, key) ? ( setEditingCell(null)} onCancel={() => setEditingCell(null)} /> ) : field.type === 'contract-number' ? ( ) : field.type === 'file' ? ( ) : REGISTRY_QUICK_TOGGLE_TYPES.has(field.type) ? ( ) : REGISTRY_INLINE_TYPES.has(field.type) ? (
setEditingCell({ taskId: task.id, columnKey: key })} > {displayFieldValue(field, rawValue, displayContext) || —}
) : ( {displayFieldValue(field, rawValue, displayContext) || —} )}
) : null; } return null; })}
); })}
)}
); }; export default TaskRegistry;