import { useMemo, useEffect, useRef, useState, useCallback } from 'react' import PropTypes from 'prop-types' import { Table, Skeleton, Card, Button, Flex, Typography, Modal } from 'antd' import PlusIcon from '../../Icons/PlusIcon' import ObjectProperty from './ObjectProperty' import ObjectInfo from './ObjectInfo' import { LoadingOutlined } from '@ant-design/icons' import BinIcon from '../../Icons/BinIcon' import EyeIcon from '../../Icons/EyeIcon' import EditIcon from '../../Icons/EditIcon' const { Text, Link, Title } = Typography const DEFAULT_COLUMN_WIDTHS = { text: 200, number: 120, dateTime: 200, state: 200, id: 180, bool: 120, tags: 200 } const getDefaultWidth = (type) => { return DEFAULT_COLUMN_WIDTHS[type] || 200 } const resolveChangeValue = (val, type) => { if (type === 'bool') return val if (val?.target && typeof val.target === 'object') { return val.target.value } return val } const createSkeletonRows = (rowCount, keyPrefix, keyName) => { return Array.from({ length: rowCount }).map((_, index) => { const skeletonKey = `${keyPrefix}-${index}` const row = { isSkeleton: true, _objectChildTableKey: skeletonKey } if (typeof keyName === 'string') { row[keyName] = skeletonKey } return row }) } const ObjectChildTable = ({ maxWidth = '100%', name, properties = [], columns = [], visibleColumns = {}, canAddRemove = true, objectData = null, scrollHeight = 240, size = 'small', loading = false, rowKey = '_id', skeletonRows = 5, additionalColumns = [], emptyText = 'No items', isEditing = false, value = [], rollups = [], onChange, hiddenPropertyWidth = '300px', minimal = false, label = '', ...tableProps }) => { const mainTableWrapperRef = useRef(null) const rollupTableWrapperRef = useRef(null) const generatedRowKeysRef = useRef(new WeakMap()) const generatedRowKeyCountRef = useRef(0) const [minimalModelOpen, setMinimalModelOpen] = useState(false) const [detailModalOpen, setDetailModalOpen] = useState(false) const [detailModalRecord, setDetailModalRecord] = useState(null) const [detailModalIndex, setDetailModalIndex] = useState(null) const getFallbackRowKey = (record) => { if (!record || typeof record !== 'object') { return `object-child-table-row-${String(record)}` } if (record._objectChildTableKey != null) { return record._objectChildTableKey } const existing = generatedRowKeysRef.current.get(record) if (existing) return existing const generated = `object-child-table-row-${generatedRowKeyCountRef.current}` generatedRowKeyCountRef.current += 1 generatedRowKeysRef.current.set(record, generated) return generated } const getResolvedRecordKey = useCallback( (record) => { if (typeof rowKey === 'function') { return rowKey(record) ?? getFallbackRowKey(record) } if (typeof rowKey === 'string' && rowKey.length > 0) { return record?.[rowKey] ?? getFallbackRowKey(record) } return getFallbackRowKey(record) }, [rowKey] ) const propertyMap = useMemo(() => { const map = new Map() properties.forEach((property) => { if (property?.name) { map.set(property.name, property) } }) return map }, [properties]) const orderedPropertyNames = useMemo(() => { if (columns && columns.length > 0) { return columns } return properties.map((property) => property.name).filter(Boolean) }, [columns, properties]) const resolvedProperties = useMemo(() => { const baseProperties = columns && columns.length > 0 ? orderedPropertyNames .map((name) => propertyMap.get(name)) .filter(Boolean) : properties return baseProperties.filter((property) => { if (!property?.name) return false if ( visibleColumns && Object.prototype.hasOwnProperty.call(visibleColumns, property.name) ) { return visibleColumns[property.name] !== false } return true }) }, [columns, orderedPropertyNames, propertyMap, properties, visibleColumns]) const hasHiddenProperties = useMemo(() => { return ( Array.isArray(columns) && columns.length > 0 && Array.isArray(properties) && columns.length < properties.length ) }, [columns, properties]) // When used inside antd Form.Item without Form.List, `value` will be the controlled array. const itemsSource = useMemo(() => { return value ?? [] }, [value]) const handleRemoveItem = useCallback( (record, index) => { const currentItems = Array.isArray(itemsSource) ? itemsSource : [] let newItems if (typeof rowKey === 'string' && record[rowKey] != null) { newItems = currentItems.filter( (item) => item[rowKey] !== record[rowKey] ) } else if (typeof rowKey === 'function') { const recordKey = getResolvedRecordKey(record) newItems = currentItems.filter((item) => { const itemKey = getResolvedRecordKey(item) return itemKey !== recordKey }) } else { newItems = currentItems.filter((_, i) => i !== index) } if (typeof onChange === 'function') { onChange(newItems) } }, [itemsSource, onChange, rowKey, getResolvedRecordKey] ) const handleOpenDetailModal = useCallback((record, index) => { setDetailModalRecord({ ...record }) setDetailModalIndex(index) setDetailModalOpen(true) }, []) const handleDetailPropertyChange = useCallback( (propName, newVal) => { const property = propertyMap.get(propName) const resolved = resolveChangeValue(newVal, property?.type) setDetailModalRecord((prev) => prev ? { ...prev, [propName]: resolved } : prev ) if (detailModalIndex == null) return const currentItems = Array.isArray(itemsSource) ? [...itemsSource] : [] const existingRowKey = currentItems[detailModalIndex] != null ? getResolvedRecordKey(currentItems[detailModalIndex]) : null const updatedItem = { ...currentItems[detailModalIndex], [propName]: resolved } if (existingRowKey) { generatedRowKeysRef.current.set(updatedItem, existingRowKey) } currentItems[detailModalIndex] = updatedItem if (typeof onChange === 'function') { onChange(currentItems) } }, [propertyMap, detailModalIndex, itemsSource, onChange, getResolvedRecordKey] ) const tableColumns = useMemo(() => { const propertyColumns = resolvedProperties.map((property) => ({ title: property.label || property.name, dataIndex: property.name, key: property.name, width: property.columnWidth || getDefaultWidth(property.type), render: (_text, record, index) => { if (record?.isSkeleton) { return ( ) } const handleCellChange = (newVal) => { const resolved = resolveChangeValue(newVal, property.type) const currentItems = Array.isArray(itemsSource) ? [...itemsSource] : [] const existingRowKey = getResolvedRecordKey(record) const updatedItem = { ...currentItems[index], [property.name]: resolved } // Preserve fallback row identity across immutable updates so the row // is not remounted while typing (which causes input focus loss). generatedRowKeysRef.current.set(updatedItem, existingRowKey) currentItems[index] = updatedItem if (typeof onChange === 'function') { onChange(currentItems) } } return ( ) } })) const actionsColumn = hasHiddenProperties || (isEditing && canAddRemove) ? { title: '', key: 'actions', width: hasHiddenProperties && isEditing && canAddRemove ? 70 : 40, fixed: 'right', render: (_text, record, index) => { if (record?.isSkeleton) { return null } return ( {hasHiddenProperties && (