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 (