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 { LoadingOutlined } from '@ant-design/icons' import BinIcon from '../../Icons/BinIcon' 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, 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 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 explicit = orderedPropertyNames .map((name) => propertyMap.get(name)) .filter(Boolean) const remaining = properties.filter( (property) => !orderedPropertyNames.includes(property.name) ) return [...explicit, ...remaining].filter((property) => { if (!property?.name) return false if ( visibleColumns && Object.prototype.hasOwnProperty.call(visibleColumns, property.name) ) { return visibleColumns[property.name] !== false } return true }) }, [orderedPropertyNames, propertyMap, properties, visibleColumns]) // When used inside antd Form.Item without Form.List, `value` will be the controlled array. const itemsSource = useMemo(() => { return value ?? [] }, [value]) 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 deleteColumn = isEditing && canAddRemove ? { title: '', key: 'delete', width: 10, fixed: 'right', render: (_text, record, index) => { if (record?.isSkeleton) { return null } return (