Refactor ActionHandler and EditObjectForm components to support forward refs and improve functionality

- Updated ActionHandler to use forwardRef, allowing parent components to access the callAction method.
- Enhanced EditObjectForm with forwardRef, enabling external control over editing state and form handling.
- Added onEdit and onStateChange props to EditObjectForm for better integration with parent components.
- Improved form validation and state management within EditObjectForm, ensuring a smoother user experience.
- Cleaned up code for better readability and maintainability.
This commit is contained in:
Tom Butcher 2025-08-18 00:58:52 +01:00
parent 4201f2b4a3
commit 678d5a0e90
2 changed files with 321 additions and 256 deletions

View File

@ -1,74 +1,86 @@
import React, { useEffect, useRef } from 'react' import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
const ActionHandler = ({ const ActionHandler = forwardRef(
children, (
actions = {}, {
actionParam = 'action', children,
clearAfterExecute = true, actions = {},
onAction, actionParam = 'action',
loading = true clearAfterExecute = true,
}) => { onAction,
const location = useLocation() loading = true
const navigate = useNavigate() },
const action = new URLSearchParams(location.search).get(actionParam) ref
) => {
const location = useLocation()
const navigate = useNavigate()
const action = new URLSearchParams(location.search).get(actionParam)
// Ref to track last executed action // Ref to track last executed action
const lastExecutedAction = useRef(null) const lastExecutedAction = useRef(null)
// Method to add action as URL param // Method to add action as URL param
const callAction = (actionName) => { const callAction = (actionName) => {
const searchParams = new URLSearchParams(location.search) const searchParams = new URLSearchParams(location.search)
searchParams.set(actionParam, actionName) searchParams.set(actionParam, actionName)
const newSearch = searchParams.toString() const newSearch = searchParams.toString()
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '') const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
navigate(newPath, { replace: true }) navigate(newPath, { replace: true })
}
// Execute action and clear from URL
useEffect(() => {
if (
!loading &&
action &&
actions[action] &&
lastExecutedAction.current !== action
) {
// Execute the action
const result = actions[action]()
// Mark this action as executed
lastExecutedAction.current = action
// Call optional callback
if (onAction) {
onAction(action, result)
}
// Clear action from URL if requested and result is true
if (clearAfterExecute && result == true) {
const searchParams = new URLSearchParams(location.search)
searchParams.delete(actionParam)
const newSearch = searchParams.toString()
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
navigate(newPath, { replace: true })
}
} else if (!action) {
// Reset lastExecutedAction if no action is present
lastExecutedAction.current = null
} }
}, [
loading,
action,
actions,
actionParam,
clearAfterExecute,
onAction,
location.pathname,
location.search,
navigate
])
// Return null as this is a utility component // Execute action and clear from URL
return <>{children({ callAction })}</> useEffect(() => {
} if (
!loading &&
action &&
actions[action] &&
lastExecutedAction.current !== action
) {
// Execute the action
const result = actions[action]()
// Mark this action as executed
lastExecutedAction.current = action
// Call optional callback
if (onAction) {
onAction(action, result)
}
// Clear action from URL if requested and result is true
if (clearAfterExecute && result == true) {
const searchParams = new URLSearchParams(location.search)
searchParams.delete(actionParam)
const newSearch = searchParams.toString()
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
navigate(newPath, { replace: true })
}
} else if (!action) {
// Reset lastExecutedAction if no action is present
lastExecutedAction.current = null
}
}, [
loading,
action,
actions,
actionParam,
clearAfterExecute,
onAction,
location.pathname,
location.search,
navigate
])
useImperativeHandle(ref, () => ({
callAction
}))
// Return null as this is a utility component
return children
}
)
ActionHandler.displayName = 'ActionHandler'
ActionHandler.propTypes = { ActionHandler.propTypes = {
children: PropTypes.func, children: PropTypes.func,

View File

@ -1,4 +1,11 @@
import React, { useState, useEffect, useContext, useCallback } from 'react' import React, {
useState,
useEffect,
useContext,
useCallback,
forwardRef,
useImperativeHandle
} from 'react'
import { Form, message } from 'antd' import { Form, message } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
@ -18,213 +25,259 @@ import merge from 'lodash/merge'
* loading, isEditing, startEditing, cancelEditing, handleUpdate, form, formValid, objectData, setIsEditing, setObjectData * loading, isEditing, startEditing, cancelEditing, handleUpdate, form, formValid, objectData, setIsEditing, setObjectData
* }) => ReactNode * }) => ReactNode
*/ */
const EditObjectForm = ({ id, type, style, children }) => { const EditObjectForm = forwardRef(
const [objectData, setObjectData] = useState(null) ({ id, type, style, children, onEdit, onStateChange }, ref) => {
const [serverObjectData, setServerObjectData] = useState(null) const [objectData, setObjectData] = useState(null)
const [fetchLoading, setFetchLoading] = useState(true) const [serverObjectData, setServerObjectData] = useState(null)
const [editLoading, setEditLoading] = useState(false) const [fetchLoading, setFetchLoading] = useState(true)
const [lock, setLock] = useState({}) const [editLoading, setEditLoading] = useState(false)
const [initialized, setInitialized] = useState(false) const [lock, setLock] = useState({})
const [isEditing, setIsEditing] = useState(false) const [initialized, setInitialized] = useState(false)
const [formValid, setFormValid] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [form] = Form.useForm() const [formValid, setFormValid] = useState(false)
const formUpdateValues = Form.useWatch([], form) const [form] = Form.useForm()
const [messageApi, contextHolder] = message.useMessage() const formUpdateValues = Form.useWatch([], form)
const [deleteModalOpen, setDeleteModalOpen] = useState(false) const [messageApi, contextHolder] = message.useMessage()
const [deleteLoading, setDeleteLoading] = useState(false) const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const { const [deleteLoading, setDeleteLoading] = useState(false)
fetchObject, const {
updateObject, fetchObject,
deleteObject, updateObject,
lockObject, deleteObject,
unlockObject, lockObject,
fetchObjectLock, unlockObject,
showError, fetchObjectLock,
connected, showError,
subscribeToObject, connected,
subscribeToLock subscribeToObjectUpdates,
} = useContext(ApiServerContext) subscribeToObjectLock
const { token } = useContext(AuthContext) } = useContext(ApiServerContext)
// Validate form on change const { token } = useContext(AuthContext)
useEffect(() => { // Validate form on change
form useEffect(() => {
.validateFields({ validateOnly: true }) form
.then(() => setFormValid(true)) .validateFields({ validateOnly: true })
.catch(() => setFormValid(false)) .then(() => {
}, [form, formUpdateValues]) setFormValid(true)
onStateChange({ formValid: true })
})
.catch(() => {
setFormValid(false)
onStateChange({ formValid: true })
})
}, [form, formUpdateValues])
// Cleanup on unmount // Cleanup on unmount
useEffect(() => { useEffect(() => {
return () => {
if (id) {
unlockObject(id, type)
}
}
}, [id, type, unlockObject])
const handleFetchObject = useCallback(async () => {
try {
setFetchLoading(true)
const data = await fetchObject(id, type)
const lockEvent = await fetchObjectLock(id, type)
setLock(lockEvent)
setObjectData(data)
setServerObjectData(data)
form.setFieldsValue(data)
setFetchLoading(false)
} catch (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) => ({ ...prev, ...value }))
}, [])
useEffect(() => {
if (!initialized && id && token != null) {
setInitialized(true)
handleFetchObject()
}
}, [id, initialized, handleFetchObject, token])
useEffect(() => {
if (id && connected) {
const objectUnsubscribe = subscribeToObject(
id,
type,
updateObjectEventHandler
)
const lockUnsubscribe = subscribeToLock(id, type, updateLockEventHandler)
return () => { return () => {
if (objectUnsubscribe) objectUnsubscribe() if (id) {
if (lockUnsubscribe) lockUnsubscribe() 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) {
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 && 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
])
const startEditing = () => {
setIsEditing(true)
onStateChange({ isEditing: true })
lockObject(id, type)
} }
}, [
id,
type,
subscribeToObject,
subscribeToLock,
updateObjectEventHandler,
connected,
updateLockEventHandler
])
const startEditing = () => { const cancelEditing = () => {
setIsEditing(true) if (serverObjectData) {
lockObject(id, type) form.setFieldsValue(serverObjectData)
} setObjectData(serverObjectData)
}
const cancelEditing = () => {
if (serverObjectData) {
form.setFieldsValue(serverObjectData)
setObjectData(serverObjectData)
}
setIsEditing(false)
unlockObject(id, type)
}
const handleUpdate = async () => {
try {
const value = await form.validateFields()
setEditLoading(true)
await updateObject(id, type, value)
setObjectData({ ...objectData, ...value })
setIsEditing(false) setIsEditing(false)
messageApi.success('Information updated successfully') onStateChange({ isEditing: false })
} catch (err) { unlockObject(id, type)
if (err.errorFields) { }
return
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) {
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 })
} }
messageApi.error('Failed to update information')
showError(
`Failed to update information. Message: ${err.message}. Code: ${err.code}`,
() => handleUpdate()
)
} finally {
handleFetchObject()
setEditLoading(false)
} }
}
const handleDelete = () => { const handleDelete = () => {
setDeleteModalOpen(true) 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) {
messageApi.error('Failed to delete')
showError(
`Failed to delete. Message: ${err.message}. Code: ${err.code}`,
confirmDelete
)
} finally {
setDeleteLoading(false)
} }
}
return ( const confirmDelete = async () => {
<> setDeleteLoading(true)
<DeleteObjectModal try {
open={deleteModalOpen} await deleteObject(id, type)
onOk={confirmDelete} setDeleteModalOpen(false)
onCancel={() => setDeleteModalOpen(false)} messageApi.success('Deleted successfully')
loading={deleteLoading} // Optionally: trigger a callback to parent to remove this object from view
objectType={type} } catch (err) {
objectName={objectData?.name || objectData?.label || ''} messageApi.error('Failed to delete')
/> showError(
<Form `Failed to delete. Message: ${err.message}. Code: ${err.code}`,
form={form} confirmDelete
layout='vertical' )
style={style} } finally {
onValuesChange={(values) => { setDeleteLoading(false)
setObjectData((prev) => ({ ...prev, ...values })) }
}} }
>
{contextHolder} useImperativeHandle(ref, () => ({
{children({ startEditing,
loading: fetchLoading, cancelEditing,
isEditing, handleUpdate,
startEditing, handleDelete,
cancelEditing, confirmDelete,
handleUpdate, handleFetchObject,
form, editLoading,
formValid, fetchLoading,
objectData, isEditing,
setIsEditing, objectData,
setObjectData, lock
editLoading, }))
lock,
handleFetchObject, return (
handleDelete <>
})} <DeleteObjectModal
</Form> 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>
</>
)
}
)
EditObjectForm.displayName = 'EditObjectForm'
EditObjectForm.propTypes = { EditObjectForm.propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
children: PropTypes.func.isRequired, children: PropTypes.func.isRequired,
style: PropTypes.object style: PropTypes.object,
onEdit: PropTypes.func,
onStateChange: PropTypes.func
} }
export default EditObjectForm export default EditObjectForm