290 lines
8.0 KiB
JavaScript

import {
useState,
useEffect,
useContext,
useCallback,
forwardRef,
useImperativeHandle
} from 'react'
import { Form, message } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import PropTypes from 'prop-types'
import DeleteObjectModal from './DeleteObjectModal'
import merge from 'lodash/merge'
/**
* ObjectForm is a reusable form component for editing any object type.
* It handles fetching, updating, locking, unlocking, and validation logic.
*
* Props:
* - id: string (required)
* - type: string (required)
* - formItems: array (for ObjectInfo/ObjectProperty items)
* - children: function({
* loading, isEditing, startEditing, cancelEditing, handleUpdate, form, formValid, objectData, setIsEditing, setObjectData
* }) => ReactNode
*/
const ObjectForm = forwardRef(
({ id, type, style, children, onEdit, onStateChange }, ref) => {
const [objectData, setObjectData] = useState(null)
const [serverObjectData, setServerObjectData] = useState(null)
const [fetchLoading, setFetchLoading] = useState(true)
const [editLoading, setEditLoading] = useState(false)
const [lock, setLock] = useState({})
const [initialized, setInitialized] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const [formValid, setFormValid] = useState(false)
const [form] = Form.useForm()
const formUpdateValues = Form.useWatch([], form)
const [messageApi, contextHolder] = message.useMessage()
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const [deleteLoading, setDeleteLoading] = useState(false)
const {
fetchObject,
updateObject,
deleteObject,
lockObject,
unlockObject,
fetchObjectLock,
showError,
connected,
subscribeToObjectUpdates,
subscribeToObjectLock
} = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
// Validate form on change
useEffect(() => {
form
.validateFields({ validateOnly: true })
.then(() => {
setFormValid(true)
onStateChange({ formValid: true, objectData: form.getFieldsValue() })
})
.catch(() => {
onStateChange({ formValid: true, objectData: form.getFieldsValue() })
})
}, [form, formUpdateValues])
// Cleanup on unmount
useEffect(() => {
return () => {
if (id) {
unlockObject(id, type)
}
}
}, [id, type, unlockObject])
const handleFetchObject = useCallback(async () => {
try {
setFetchLoading(true)
onStateChange({ loading: true })
const data = await fetchObject(id, type)
const lockEvent = await fetchObjectLock(id, type)
setLock(lockEvent)
onStateChange({ lock: lockEvent })
setObjectData(data)
setServerObjectData(data)
form.setFieldsValue(data)
setFetchLoading(false)
onStateChange({ loading: false })
} catch (err) {
console.error(err)
messageApi.error('Failed to fetch object info')
showError(
`Failed to fetch object information. Message: ${err.message}. Code: ${err.code}`,
fetchObject
)
}
}, [fetchObject, fetchObjectLock, id, type, form, messageApi, showError])
// Update event handler
const updateObjectEventHandler = useCallback((value) => {
setObjectData((prev) => merge({}, prev, value))
}, [])
// Update event handler
const updateLockEventHandler = useCallback((value) => {
setLock((prev) => {
onStateChange({ lock: { ...prev, ...value } })
return { ...prev, ...value }
})
}, [])
useEffect(() => {
if (initialized == false && id && token != null) {
setInitialized(true)
handleFetchObject()
}
}, [id, initialized, handleFetchObject, token])
useEffect(() => {
if (id && connected) {
const objectUpdatesUnsubscribe = subscribeToObjectUpdates(
id,
type,
updateObjectEventHandler
)
const lockUnsubscribe = subscribeToObjectLock(
id,
type,
updateLockEventHandler
)
return () => {
if (objectUpdatesUnsubscribe) objectUpdatesUnsubscribe()
if (lockUnsubscribe) lockUnsubscribe()
}
}
}, [
id,
type,
subscribeToObjectUpdates,
subscribeToObjectLock,
updateObjectEventHandler,
connected,
updateLockEventHandler
])
useEffect(() => {
onStateChange({ objectData })
}, [objectData])
const startEditing = () => {
setIsEditing(true)
onStateChange({ isEditing: true })
lockObject(id, type)
}
const cancelEditing = () => {
if (serverObjectData) {
form.setFieldsValue(serverObjectData)
setObjectData(serverObjectData)
}
setIsEditing(false)
onStateChange({ isEditing: false })
unlockObject(id, type)
}
const handleUpdate = async () => {
try {
const value = await form.validateFields()
setEditLoading(true)
onStateChange({ editLoading: true })
await updateObject(id, type, value)
setObjectData({ ...objectData, ...value })
setIsEditing(false)
onStateChange({ isEditing: false })
messageApi.success('Information updated successfully')
} catch (err) {
console.error(err)
if (err.errorFields) {
return
}
messageApi.error('Failed to update information')
showError(
`Failed to update information. Message: ${err.message}. Code: ${err.code}`,
() => handleUpdate()
)
} finally {
handleFetchObject()
setEditLoading(false)
onStateChange({ editLoading: false })
}
}
const handleDelete = () => {
setDeleteModalOpen(true)
}
const confirmDelete = async () => {
setDeleteLoading(true)
try {
await deleteObject(id, type)
setDeleteModalOpen(false)
messageApi.success('Deleted successfully')
// Optionally: trigger a callback to parent to remove this object from view
} catch (err) {
console.error(err)
messageApi.error('Failed to delete')
showError(
`Failed to delete. Message: ${err.message}. Code: ${err.code}`,
confirmDelete
)
} finally {
setDeleteLoading(false)
}
}
useImperativeHandle(ref, () => ({
startEditing,
cancelEditing,
handleUpdate,
handleDelete,
confirmDelete,
handleFetchObject,
editLoading,
fetchLoading,
isEditing,
objectData,
lock
}))
return (
<>
<DeleteObjectModal
open={deleteModalOpen}
onOk={confirmDelete}
onCancel={() => setDeleteModalOpen(false)}
loading={deleteLoading}
objectType={type}
objectName={objectData?.name || objectData?.label || ''}
/>
<Form
form={form}
layout='vertical'
style={style}
onValuesChange={(values) => {
if (onEdit != undefined) {
onEdit(values)
}
setObjectData((prev) => {
return { ...prev, ...values }
})
}}
>
{contextHolder}
{children({
loading: fetchLoading,
isEditing,
startEditing,
cancelEditing,
handleUpdate,
form,
formValid,
objectData,
setIsEditing,
setObjectData,
editLoading,
lock,
handleFetchObject,
handleDelete
})}
</Form>
</>
)
}
)
ObjectForm.displayName = 'ObjectForm'
ObjectForm.propTypes = {
id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
children: PropTypes.func.isRequired,
style: PropTypes.object,
onEdit: PropTypes.func,
onStateChange: PropTypes.func
}
export default ObjectForm