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 ( <> setDeleteModalOpen(false)} loading={deleteLoading} objectType={type} objectName={objectData?.name || objectData?.label || ''} />
{ 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 })}
) } ) 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