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 { Table, Skeleton, Card, Button, Flex, Typography, Modal } from 'antd'
import PlusIcon from '../../Icons/PlusIcon' import PlusIcon from '../../Icons/PlusIcon'
import ObjectProperty from './ObjectProperty' import ObjectProperty from './ObjectProperty'
import ObjectInfo from './ObjectInfo'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import BinIcon from '../../Icons/BinIcon' import BinIcon from '../../Icons/BinIcon'
import EyeIcon from '../../Icons/EyeIcon'
import EditIcon from '../../Icons/EditIcon'
const { Text, Link, Title } = Typography const { Text, Link, Title } = Typography
const DEFAULT_COLUMN_WIDTHS = { const DEFAULT_COLUMN_WIDTHS = {
@ -64,6 +67,7 @@ const ObjectChildTable = ({
value = [], value = [],
rollups = [], rollups = [],
onChange, onChange,
hiddenPropertyWidth = '300px',
minimal = false, minimal = false,
label = '', label = '',
...tableProps ...tableProps
@ -73,6 +77,9 @@ const ObjectChildTable = ({
const generatedRowKeysRef = useRef(new WeakMap()) const generatedRowKeysRef = useRef(new WeakMap())
const generatedRowKeyCountRef = useRef(0) const generatedRowKeyCountRef = useRef(0)
const [minimalModelOpen, setMinimalModelOpen] = useState(false) const [minimalModelOpen, setMinimalModelOpen] = useState(false)
const [detailModalOpen, setDetailModalOpen] = useState(false)
const [detailModalRecord, setDetailModalRecord] = useState(null)
const [detailModalIndex, setDetailModalIndex] = useState(null)
const getFallbackRowKey = (record) => { const getFallbackRowKey = (record) => {
if (!record || typeof record !== 'object') { if (!record || typeof record !== 'object') {
return `object-child-table-row-${String(record)}` return `object-child-table-row-${String(record)}`
@ -124,15 +131,14 @@ const ObjectChildTable = ({
}, [columns, properties]) }, [columns, properties])
const resolvedProperties = useMemo(() => { const resolvedProperties = useMemo(() => {
const explicit = orderedPropertyNames const baseProperties =
.map((name) => propertyMap.get(name)) columns && columns.length > 0
.filter(Boolean) ? orderedPropertyNames
.map((name) => propertyMap.get(name))
.filter(Boolean)
: properties
const remaining = properties.filter( return baseProperties.filter((property) => {
(property) => !orderedPropertyNames.includes(property.name)
)
return [...explicit, ...remaining].filter((property) => {
if (!property?.name) return false if (!property?.name) return false
if ( if (
visibleColumns && visibleColumns &&
@ -142,13 +148,87 @@ const ObjectChildTable = ({
} }
return true 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. // When used inside antd Form.Item without Form.List, `value` will be the controlled array.
const itemsSource = useMemo(() => { const itemsSource = useMemo(() => {
return value ?? [] return value ?? []
}, [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 tableColumns = useMemo(() => {
const propertyColumns = resolvedProperties.map((property) => ({ const propertyColumns = resolvedProperties.map((property) => ({
title: property.label || property.name, title: property.label || property.name,
@ -197,54 +277,44 @@ const ObjectChildTable = ({
} }
})) }))
const deleteColumn = const actionsColumn =
isEditing && canAddRemove hasHiddenProperties || (isEditing && canAddRemove)
? { ? {
title: '', title: '',
key: 'delete', key: 'actions',
width: 10, width: hasHiddenProperties && isEditing && canAddRemove ? 70 : 40,
fixed: 'right', fixed: 'right',
render: (_text, record, index) => { render: (_text, record, index) => {
if (record?.isSkeleton) { if (record?.isSkeleton) {
return null return null
} }
return ( return (
<Button <Flex gap={4}>
type='text' {hasHiddenProperties && (
danger <Button
size='small' type='text'
icon={<BinIcon />} size='small'
onClick={(e) => { icon={isEditing ? <EditIcon /> : <EyeIcon />}
e.stopPropagation() onClick={(e) => {
const currentItems = Array.isArray(itemsSource) e.stopPropagation()
? itemsSource handleOpenDetailModal(record, index)
: [] }}
/>
// Use record's unique identifier if available, otherwise use index )}
let newItems {isEditing && canAddRemove && (
if (typeof rowKey === 'string' && record[rowKey] != null) { <Button
// Use the unique key to find and remove the item type='text'
newItems = currentItems.filter( danger
(item) => item[rowKey] !== record[rowKey] size='small'
) icon={<BinIcon />}
} else if (typeof rowKey === 'function') { onClick={(e) => {
// If rowKey is a function, find the item by comparing resolved keys. e.stopPropagation()
// Ant Design deprecates index-based rowKey callbacks. handleRemoveItem(record, index)
const recordKey = getResolvedRecordKey(record) }}
newItems = currentItems.filter((item) => { />
const itemKey = getResolvedRecordKey(item) )}
return itemKey !== recordKey </Flex>
})
} else {
// Fallback to index-based removal
newItems = currentItems.filter((_, i) => i !== index)
}
if (typeof onChange === 'function') {
onChange(newItems)
}
}}
/>
) )
} }
} }
@ -253,17 +323,20 @@ const ObjectChildTable = ({
return [ return [
...propertyColumns, ...propertyColumns,
...additionalColumns, ...additionalColumns,
...(deleteColumn ? [deleteColumn] : []) ...(actionsColumn ? [actionsColumn] : [])
] ]
}, [ }, [
resolvedProperties, resolvedProperties,
additionalColumns, additionalColumns,
hasHiddenProperties,
isEditing, isEditing,
canAddRemove, canAddRemove,
itemsSource, itemsSource,
onChange, onChange,
rowKey, rowKey,
getResolvedRecordKey getResolvedRecordKey,
handleOpenDetailModal,
handleRemoveItem
]) ])
const skeletonData = useMemo(() => { const skeletonData = useMemo(() => {
@ -351,8 +424,8 @@ const ObjectChildTable = ({
}, [properties, rollups, objectData, value, name]) }, [properties, rollups, objectData, value, name])
const rollupColumns = useMemo(() => { const rollupColumns = useMemo(() => {
const propertyColumns = properties.map((property, index) => { const propertyColumns = resolvedProperties.map((property, index) => {
const nextProperty = properties[index + 1] const nextProperty = resolvedProperties[index + 1]
var nextRollup = null var nextRollup = null
if (nextProperty) { if (nextProperty) {
nextRollup = rollups?.find( nextRollup = rollups?.find(
@ -384,12 +457,12 @@ const ObjectChildTable = ({
} }
} }
}) })
const blankDeleteColumn = const blankActionsColumn =
isEditing && canAddRemove hasHiddenProperties || (isEditing && canAddRemove)
? { ? {
title: '', title: '',
key: 'delete', key: 'actions',
width: 40, width: hasHiddenProperties && isEditing && canAddRemove ? 70 : 40,
fixed: 'right', fixed: 'right',
render: () => { render: () => {
return <Flex></Flex> return <Flex></Flex>
@ -398,9 +471,15 @@ const ObjectChildTable = ({
: null : null
return [ return [
...propertyColumns, ...propertyColumns,
...(blankDeleteColumn ? [blankDeleteColumn] : []) ...(blankActionsColumn ? [blankActionsColumn] : [])
] ]
}, [properties, rollups, isEditing, canAddRemove]) }, [
resolvedProperties,
rollups,
isEditing,
canAddRemove,
hasHiddenProperties
])
const hasRollups = useMemo( const hasRollups = useMemo(
() => Array.isArray(rollups) && rollups.length > 0, () => Array.isArray(rollups) && rollups.length > 0,
@ -495,25 +574,56 @@ const ObjectChildTable = ({
</Flex> </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) { if (isEditing === true) {
return ( return (
<Card> <>
<Flex vertical gap={'middle'}> <Card>
<Flex justify={'space-between'}> <Flex vertical gap={'middle'}>
<Button>Actions</Button> <Flex justify={'space-between'}>
<Button>Actions</Button>
{canAddRemove && ( {canAddRemove && (
<Button <Button
type='primary' type='primary'
icon={<PlusIcon />} icon={<PlusIcon />}
onClick={handleAddItem} onClick={handleAddItem}
/> />
)} )}
</Flex>
{tableComponent}
</Flex> </Flex>
</Card>
{tableComponent} {detailModal}
</Flex> </>
</Card>
) )
} }
@ -541,11 +651,17 @@ const ObjectChildTable = ({
</Title> </Title>
{tableComponent} {tableComponent}
</Modal> </Modal>
{detailModal}
</> </>
) )
} }
return tableComponent return (
<>
{tableComponent}
{detailModal}
</>
)
} }
ObjectChildTable.propTypes = { ObjectChildTable.propTypes = {
@ -574,7 +690,8 @@ ObjectChildTable.propTypes = {
rollups: PropTypes.arrayOf(PropTypes.object), rollups: PropTypes.arrayOf(PropTypes.object),
objectData: PropTypes.object, objectData: PropTypes.object,
canAddRemove: PropTypes.bool, canAddRemove: PropTypes.bool,
minimal: PropTypes.bool minimal: PropTypes.bool,
hiddenPropertyWidth: PropTypes.string
} }
export default ObjectChildTable export default ObjectChildTable