Enhance ObjectChildTable component by adding detail modal functionality for property editing and viewing. Introduce new icons for edit and view actions, improve property resolution logic, and streamline item removal handling. Update table column rendering to accommodate hidden properties, enhancing user interaction and data management.

This commit is contained in:
Tom Butcher 2026-07-20 02:06:04 +01:00
parent 9442c9105b
commit ca0128d986

View File

@ -3,8 +3,11 @@ 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 = {
@ -64,6 +67,7 @@ const ObjectChildTable = ({
value = [],
rollups = [],
onChange,
hiddenPropertyWidth = '300px',
minimal = false,
label = '',
...tableProps
@ -73,6 +77,9 @@ const ObjectChildTable = ({
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)}`
@ -124,15 +131,14 @@ const ObjectChildTable = ({
}, [columns, properties])
const resolvedProperties = useMemo(() => {
const explicit = orderedPropertyNames
const baseProperties =
columns && columns.length > 0
? orderedPropertyNames
.map((name) => propertyMap.get(name))
.filter(Boolean)
: properties
const remaining = properties.filter(
(property) => !orderedPropertyNames.includes(property.name)
)
return [...explicit, ...remaining].filter((property) => {
return baseProperties.filter((property) => {
if (!property?.name) return false
if (
visibleColumns &&
@ -142,13 +148,87 @@ const ObjectChildTable = ({
}
return true
})
}, [orderedPropertyNames, propertyMap, properties, visibleColumns])
}, [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,
@ -197,18 +277,32 @@ const ObjectChildTable = ({
}
}))
const deleteColumn =
isEditing && canAddRemove
const actionsColumn =
hasHiddenProperties || (isEditing && canAddRemove)
? {
title: '',
key: 'delete',
width: 10,
key: 'actions',
width: hasHiddenProperties && isEditing && canAddRemove ? 70 : 40,
fixed: 'right',
render: (_text, record, index) => {
if (record?.isSkeleton) {
return null
}
return (
<Flex gap={4}>
{hasHiddenProperties && (
<Button
type='text'
size='small'
icon={isEditing ? <EditIcon /> : <EyeIcon />}
onClick={(e) => {
e.stopPropagation()
handleOpenDetailModal(record, index)
}}
/>
)}
{isEditing && canAddRemove && (
<Button
type='text'
danger
@ -216,35 +310,11 @@ const ObjectChildTable = ({
icon={<BinIcon />}
onClick={(e) => {
e.stopPropagation()
const currentItems = Array.isArray(itemsSource)
? itemsSource
: []
// Use record's unique identifier if available, otherwise use index
let newItems
if (typeof rowKey === 'string' && record[rowKey] != null) {
// Use the unique key to find and remove the item
newItems = currentItems.filter(
(item) => item[rowKey] !== record[rowKey]
)
} else if (typeof rowKey === 'function') {
// If rowKey is a function, find the item by comparing resolved keys.
// Ant Design deprecates index-based rowKey callbacks.
const recordKey = getResolvedRecordKey(record)
newItems = currentItems.filter((item) => {
const itemKey = getResolvedRecordKey(item)
return itemKey !== recordKey
})
} else {
// Fallback to index-based removal
newItems = currentItems.filter((_, i) => i !== index)
}
if (typeof onChange === 'function') {
onChange(newItems)
}
handleRemoveItem(record, index)
}}
/>
)}
</Flex>
)
}
}
@ -253,17 +323,20 @@ const ObjectChildTable = ({
return [
...propertyColumns,
...additionalColumns,
...(deleteColumn ? [deleteColumn] : [])
...(actionsColumn ? [actionsColumn] : [])
]
}, [
resolvedProperties,
additionalColumns,
hasHiddenProperties,
isEditing,
canAddRemove,
itemsSource,
onChange,
rowKey,
getResolvedRecordKey
getResolvedRecordKey,
handleOpenDetailModal,
handleRemoveItem
])
const skeletonData = useMemo(() => {
@ -351,8 +424,8 @@ const ObjectChildTable = ({
}, [properties, rollups, objectData, value, name])
const rollupColumns = useMemo(() => {
const propertyColumns = properties.map((property, index) => {
const nextProperty = properties[index + 1]
const propertyColumns = resolvedProperties.map((property, index) => {
const nextProperty = resolvedProperties[index + 1]
var nextRollup = null
if (nextProperty) {
nextRollup = rollups?.find(
@ -384,12 +457,12 @@ const ObjectChildTable = ({
}
}
})
const blankDeleteColumn =
isEditing && canAddRemove
const blankActionsColumn =
hasHiddenProperties || (isEditing && canAddRemove)
? {
title: '',
key: 'delete',
width: 40,
key: 'actions',
width: hasHiddenProperties && isEditing && canAddRemove ? 70 : 40,
fixed: 'right',
render: () => {
return <Flex></Flex>
@ -398,9 +471,15 @@ const ObjectChildTable = ({
: null
return [
...propertyColumns,
...(blankDeleteColumn ? [blankDeleteColumn] : [])
...(blankActionsColumn ? [blankActionsColumn] : [])
]
}, [properties, rollups, isEditing, canAddRemove])
}, [
resolvedProperties,
rollups,
isEditing,
canAddRemove,
hasHiddenProperties
])
const hasRollups = useMemo(
() => Array.isArray(rollups) && rollups.length > 0,
@ -495,8 +574,37 @@ const ObjectChildTable = ({
</Flex>
)
const detailModal = hasHiddenProperties ? (
<Modal
open={detailModalOpen}
onCancel={() => setDetailModalOpen(false)}
footer={null}
width='900px'
>
{label ? (
<Title
level={2}
style={{ marginTop: 0, lineHeight: '0.7', marginBottom: 20 }}
>
{label}
</Title>
) : null}
<ObjectInfo
propertyDefinitions={properties}
objectData={detailModalRecord}
parentData={objectData}
labelWidth={hiddenPropertyWidth}
isEditing={isEditing}
onPropertyChange={isEditing ? handleDetailPropertyChange : undefined}
column={1}
showLabels={true}
/>
</Modal>
) : null
if (isEditing === true) {
return (
<>
<Card>
<Flex vertical gap={'middle'}>
<Flex justify={'space-between'}>
@ -514,6 +622,8 @@ const ObjectChildTable = ({
{tableComponent}
</Flex>
</Card>
{detailModal}
</>
)
}
@ -541,11 +651,17 @@ const ObjectChildTable = ({
</Title>
{tableComponent}
</Modal>
{detailModal}
</>
)
}
return tableComponent
return (
<>
{tableComponent}
{detailModal}
</>
)
}
ObjectChildTable.propTypes = {
@ -574,7 +690,8 @@ ObjectChildTable.propTypes = {
rollups: PropTypes.arrayOf(PropTypes.object),
objectData: PropTypes.object,
canAddRemove: PropTypes.bool,
minimal: PropTypes.bool
minimal: PropTypes.bool,
hiddenPropertyWidth: PropTypes.string
}
export default ObjectChildTable