Added edit mode to tables.

This commit is contained in:
Tom Butcher 2025-12-27 13:50:30 +00:00
parent 38cafdb4a4
commit 50bc816e97

View File

@ -5,6 +5,7 @@ import {
useEffect, useEffect,
useState, useState,
useCallback, useCallback,
useMemo,
createElement createElement
} from 'react' } from 'react'
import { import {
@ -19,7 +20,8 @@ import {
Button, Button,
Input, Input,
Space, Space,
Tooltip Tooltip,
Form
} from 'antd' } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
@ -43,6 +45,46 @@ import { ElectronContext } from '../context/ElectronContext'
const logger = loglevel.getLogger('DasboardTable') const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel) logger.setLevel(config.logLevel)
const RowForm = ({ record, isEditing, onRegister, children }) => {
const [form] = Form.useForm()
useEffect(() => {
if (isEditing && record && !record.isSkeleton) {
form.setFieldsValue(record)
onRegister(record._id, form)
}
return () => {
if (record?._id) onRegister(record._id, null)
}
}, [isEditing, record, form, onRegister])
return (
<Form form={form} component={false}>
{children}
</Form>
)
}
RowForm.propTypes = {
record: PropTypes.object,
isEditing: PropTypes.bool,
onRegister: PropTypes.func,
children: PropTypes.node
}
const EditableRow = ({ record, isEditing, onRegister, ...props }) => {
return (
<RowForm record={record} isEditing={isEditing} onRegister={onRegister}>
<tr {...props} />
</RowForm>
)
}
EditableRow.propTypes = {
record: PropTypes.object,
isEditing: PropTypes.bool,
onRegister: PropTypes.func
}
const ObjectTable = forwardRef( const ObjectTable = forwardRef(
( (
{ {
@ -54,17 +96,26 @@ const ObjectTable = forwardRef(
cards = false, cards = false,
visibleColumns = {}, visibleColumns = {},
masterFilter = {}, masterFilter = {},
size = 'middle' size = 'middle',
onStateChange
}, },
ref ref
) => { ) => {
const { token } = useContext(AuthContext) const { token } = useContext(AuthContext)
const { isElectron } = useContext(ElectronContext) const { isElectron } = useContext(ElectronContext)
const onStateChangeRef = useRef(onStateChange)
useEffect(() => {
onStateChangeRef.current = onStateChange
}, [onStateChange])
const { const {
fetchObjects, fetchObjects,
connected, connected,
subscribeToObjectUpdates, subscribeToObjectUpdates,
subscribeToObjectTypeUpdates subscribeToObjectTypeUpdates,
updateMultipleObjects,
lockObject,
unlockObject
} = useContext(ApiServerContext) } = useContext(ApiServerContext)
const isMobile = useMediaQuery({ maxWidth: 768 }) const isMobile = useMediaQuery({ maxWidth: 768 })
const navigate = useNavigate() const navigate = useNavigate()
@ -98,6 +149,21 @@ const ObjectTable = forwardRef(
const [lazyLoading, setLazyLoading] = useState(false) const [lazyLoading, setLazyLoading] = useState(false)
const [tableData, setTableData] = useState([]) const [tableData, setTableData] = useState([])
const [isEditing, setIsEditing] = useState(false)
const [editLoading, setEditLoading] = useState(false)
const rowFormsRef = useRef({})
const registerForm = useCallback((id, form) => {
if (form) {
rowFormsRef.current[id] = form
} else {
delete rowFormsRef.current[id]
}
}, [])
useEffect(() => {
onStateChangeRef.current?.({ isEditing, editLoading })
}, [isEditing, editLoading])
const subscribedIdsRef = useRef([]) const subscribedIdsRef = useRef([])
// const [typeSubscribed, setTypeSubscribed] = useState(false) // const [typeSubscribed, setTypeSubscribed] = useState(false)
const unsubscribesRef = useRef([]) const unsubscribesRef = useRef([])
@ -300,6 +366,49 @@ const ObjectTable = forwardRef(
} }
}, [fetchData]) }, [fetchData])
const startEditing = useCallback(() => {
setIsEditing(true)
tableData.forEach((item) => {
if (!item.isSkeleton) {
console.log('Locking object:', item)
lockObject(item._id, type)
}
})
}, [tableData, lockObject, type])
const cancelEditing = useCallback(() => {
setIsEditing(false)
tableData.forEach((item) => {
if (!item.isSkeleton) {
unlockObject(item._id, type)
}
})
}, [tableData, unlockObject, type])
const handleUpdate = useCallback(async () => {
setEditLoading(true)
try {
const updates = await Promise.all(
Object.entries(rowFormsRef.current).map(async ([id, form]) => {
const values = await form.validateFields()
return { _id: id, ...values }
})
)
await updateMultipleObjects(type, updates)
setIsEditing(false)
reload()
tableData.forEach((item) => {
if (!item.isSkeleton) {
unlockObject(item._id, type)
}
})
} catch (err) {
console.error(err)
} finally {
setEditLoading(false)
}
}, [type, updateMultipleObjects, reload, tableData, unlockObject])
// Update event handler for real-time updates // Update event handler for real-time updates
const updateEventHandler = useCallback((id, updatedData) => { const updateEventHandler = useCallback((id, updatedData) => {
setPages((prevPages) => setPages((prevPages) =>
@ -316,6 +425,12 @@ const ObjectTable = forwardRef(
} }
}) })
) )
console.log('updatedData', updatedData)
if (rowFormsRef.current[id]) {
rowFormsRef.current[id].setFieldsValue(updatedData)
}
}, []) }, [])
// Store the latest updateEventHandler in a ref // Store the latest updateEventHandler in a ref
@ -349,6 +464,9 @@ const ObjectTable = forwardRef(
updateEventHandlerRef.current(itemId, updateData) updateEventHandlerRef.current(itemId, updateData)
} }
) )
console.log('unsubscribe', unsubscribe)
console.log('subscribedIdsRef', subscribedIdsRef.current)
console.log('unsubscribesRef', unsubscribesRef.current)
subscribedIdsRef.current.push(itemId) subscribedIdsRef.current.push(itemId)
if (unsubscribe) { if (unsubscribe) {
unsubscribesRef.current.push(unsubscribe) unsubscribesRef.current.push(unsubscribe)
@ -408,8 +526,8 @@ const ObjectTable = forwardRef(
}, [type, subscribeToObjectTypeUpdates, connected, newEventHandler]) }, [type, subscribeToObjectTypeUpdates, connected, newEventHandler])
const updateData = useCallback( const updateData = useCallback(
(_id, updatedData) => { (id, updatedData) => {
updateEventHandler({ _id, ...updatedData }) updateEventHandler(id, updatedData)
}, },
[updateEventHandler] [updateEventHandler]
) )
@ -445,7 +563,12 @@ const ObjectTable = forwardRef(
setData: (newData) => { setData: (newData) => {
setPages([{ pageNum: 1, items: newData }]) setPages([{ pageNum: 1, items: newData }])
}, },
updateData updateData,
startEditing,
cancelEditing,
handleUpdate,
isEditing,
editLoading
})) }))
useEffect(() => { useEffect(() => {
@ -614,7 +737,7 @@ const ObjectTable = forwardRef(
{...prop} {...prop}
longId={false} longId={false}
objectData={record} objectData={record}
isEditing={false} isEditing={isEditing}
/> />
) )
} }
@ -715,61 +838,71 @@ const ObjectTable = forwardRef(
loading={record.isSkeleton} loading={record.isSkeleton}
variant={'borderless'} variant={'borderless'}
> >
<Flex align={'center'} vertical gap={'middle'}> <RowForm
<Descriptions record={record}
column={1} isEditing={isEditing}
size='small' onRegister={registerForm}
bordered={true} >
style={{ width: '100%', tableLayout: 'fixed' }} <Flex align={'center'} vertical gap={'middle'}>
className='objectTableDescritions' <Descriptions
> column={1}
{(() => { size='small'
const descriptionItems = [] bordered={true}
style={{ width: '100%', tableLayout: 'fixed' }}
className='objectTableDescritions'
>
{(() => {
const descriptionItems = []
// Add columns in the order specified by model.columns (same logic as table) // Add columns in the order specified by model.columns (same logic as table)
model.columns.forEach((colName) => { model.columns.forEach((colName) => {
const prop = modelProperties.find( const prop = modelProperties.find(
(p) => p.name === colName (p) => p.name === colName
) )
if (prop) { if (prop) {
// Check if column should be visible based on visibleColumns prop // Check if column should be visible based on visibleColumns prop
if ( if (
Object.keys(visibleColumns).length > 0 && Object.keys(visibleColumns).length > 0 &&
visibleColumns[prop.name] === false visibleColumns[prop.name] === false
) { ) {
return // Skip this column if it's not visible return // Skip this column if it's not visible
}
descriptionItems.push(
<Descriptions.Item
label={prop.label}
key={prop.name}
colspan={2}
>
<ObjectProperty
{...prop}
longId={false}
objectData={record}
isEditing={isEditing}
name={prop.name}
/>
</Descriptions.Item>
)
} }
})
// Add actions if they exist (same as table)
if (rowActions.length > 0) {
descriptionItems.push( descriptionItems.push(
<Descriptions.Item <Descriptions.Item
label={prop.label} label={'Actions'}
key={prop.name} key={'actions'}
colspan={2}
> >
<ObjectProperty {renderActions(record)}
{...prop}
longId={false}
objectData={record}
isEditing={false}
/>
</Descriptions.Item> </Descriptions.Item>
) )
} }
})
// Add actions if they exist (same as table) return descriptionItems
if (rowActions.length > 0) { })()}
descriptionItems.push( </Descriptions>
<Descriptions.Item label={'Actions'} key={'actions'}> </Flex>
{renderActions(record)} </RowForm>
</Descriptions.Item>
)
}
return descriptionItems
})()}
</Descriptions>
</Flex>
</Card> </Card>
</Col> </Col>
))} ))}
@ -777,32 +910,52 @@ const ObjectTable = forwardRef(
) )
} }
return ( const components = useMemo(
<> () => ({
<Flex gap={'middle'} vertical> body: {
<Table row: EditableRow
ref={tableRef} }
dataSource={tableData} }),
columns={columnsWithSkeleton} []
className={cards ? 'dashboard-cards-header' : 'dashboard-table'}
pagination={false}
scroll={{ y: adjustedScrollHeight }}
rowKey='_id'
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
onScroll={handleScroll}
onChange={handleTableChange}
showSorterTooltip={false}
style={{ height: '100%' }}
size={size}
/>
{cards ? (
<Spin indicator={<LoadingOutlined />} spinning={loading}>
{renderCards()}
</Spin>
) : null}
</Flex>
</>
) )
const onRow = useCallback(
(record) => ({
record,
isEditing,
onRegister: registerForm
}),
[isEditing, registerForm]
)
const tableContent = (
<Flex gap={'middle'} vertical>
<Table
ref={tableRef}
dataSource={tableData}
columns={columnsWithSkeleton}
className={cards ? 'dashboard-cards-header' : 'dashboard-table'}
pagination={false}
scroll={{ y: adjustedScrollHeight }}
rowKey='_id'
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
onScroll={handleScroll}
onChange={handleTableChange}
showSorterTooltip={false}
style={{ height: '100%' }}
size={size}
components={components}
onRow={onRow}
/>
{cards ? (
<Spin indicator={<LoadingOutlined />} spinning={loading}>
{renderCards()}
</Spin>
) : null}
</Flex>
)
return tableContent
} }
) )
@ -818,7 +971,8 @@ ObjectTable.propTypes = {
cardRenderer: PropTypes.func, cardRenderer: PropTypes.func,
visibleColumns: PropTypes.object, visibleColumns: PropTypes.object,
masterFilter: PropTypes.object, masterFilter: PropTypes.object,
size: PropTypes.string size: PropTypes.string,
onStateChange: PropTypes.func
} }
export default ObjectTable export default ObjectTable