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:
parent
4201f2b4a3
commit
678d5a0e90
@ -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 PropTypes from 'prop-types'
|
||||
|
||||
const ActionHandler = ({
|
||||
children,
|
||||
actions = {},
|
||||
actionParam = 'action',
|
||||
clearAfterExecute = true,
|
||||
onAction,
|
||||
loading = true
|
||||
}) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const action = new URLSearchParams(location.search).get(actionParam)
|
||||
const ActionHandler = forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
actions = {},
|
||||
actionParam = 'action',
|
||||
clearAfterExecute = true,
|
||||
onAction,
|
||||
loading = true
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const action = new URLSearchParams(location.search).get(actionParam)
|
||||
|
||||
// Ref to track last executed action
|
||||
const lastExecutedAction = useRef(null)
|
||||
// Ref to track last executed action
|
||||
const lastExecutedAction = useRef(null)
|
||||
|
||||
// Method to add action as URL param
|
||||
const callAction = (actionName) => {
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
searchParams.set(actionParam, actionName)
|
||||
const newSearch = searchParams.toString()
|
||||
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
|
||||
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
|
||||
// Method to add action as URL param
|
||||
const callAction = (actionName) => {
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
searchParams.set(actionParam, actionName)
|
||||
const newSearch = searchParams.toString()
|
||||
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
|
||||
navigate(newPath, { replace: true })
|
||||
}
|
||||
}, [
|
||||
loading,
|
||||
action,
|
||||
actions,
|
||||
actionParam,
|
||||
clearAfterExecute,
|
||||
onAction,
|
||||
location.pathname,
|
||||
location.search,
|
||||
navigate
|
||||
])
|
||||
|
||||
// Return null as this is a utility component
|
||||
return <>{children({ callAction })}</>
|
||||
}
|
||||
// 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
|
||||
])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
callAction
|
||||
}))
|
||||
|
||||
// Return null as this is a utility component
|
||||
return children
|
||||
}
|
||||
)
|
||||
|
||||
ActionHandler.displayName = 'ActionHandler'
|
||||
|
||||
ActionHandler.propTypes = {
|
||||
children: PropTypes.func,
|
||||
|
||||
@ -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 { ApiServerContext } from '../context/ApiServerContext'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
@ -18,213 +25,259 @@ import merge from 'lodash/merge'
|
||||
* loading, isEditing, startEditing, cancelEditing, handleUpdate, form, formValid, objectData, setIsEditing, setObjectData
|
||||
* }) => ReactNode
|
||||
*/
|
||||
const EditObjectForm = ({ id, type, style, children }) => {
|
||||
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,
|
||||
subscribeToObject,
|
||||
subscribeToLock
|
||||
} = useContext(ApiServerContext)
|
||||
const { token } = useContext(AuthContext)
|
||||
// Validate form on change
|
||||
useEffect(() => {
|
||||
form
|
||||
.validateFields({ validateOnly: true })
|
||||
.then(() => setFormValid(true))
|
||||
.catch(() => setFormValid(false))
|
||||
}, [form, formUpdateValues])
|
||||
const EditObjectForm = 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 })
|
||||
})
|
||||
.catch(() => {
|
||||
setFormValid(false)
|
||||
onStateChange({ formValid: true })
|
||||
})
|
||||
}, [form, formUpdateValues])
|
||||
|
||||
// Cleanup on unmount
|
||||
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)
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (objectUnsubscribe) objectUnsubscribe()
|
||||
if (lockUnsubscribe) lockUnsubscribe()
|
||||
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) {
|
||||
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 = () => {
|
||||
setIsEditing(true)
|
||||
lockObject(id, type)
|
||||
}
|
||||
|
||||
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 })
|
||||
const cancelEditing = () => {
|
||||
if (serverObjectData) {
|
||||
form.setFieldsValue(serverObjectData)
|
||||
setObjectData(serverObjectData)
|
||||
}
|
||||
setIsEditing(false)
|
||||
messageApi.success('Information updated successfully')
|
||||
} catch (err) {
|
||||
if (err.errorFields) {
|
||||
return
|
||||
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) {
|
||||
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 = () => {
|
||||
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)
|
||||
const handleDelete = () => {
|
||||
setDeleteModalOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
setObjectData((prev) => ({ ...prev, ...values }))
|
||||
}}
|
||||
>
|
||||
{contextHolder}
|
||||
{children({
|
||||
loading: fetchLoading,
|
||||
isEditing,
|
||||
startEditing,
|
||||
cancelEditing,
|
||||
handleUpdate,
|
||||
form,
|
||||
formValid,
|
||||
objectData,
|
||||
setIsEditing,
|
||||
setObjectData,
|
||||
editLoading,
|
||||
lock,
|
||||
handleFetchObject,
|
||||
handleDelete
|
||||
})}
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
EditObjectForm.displayName = 'EditObjectForm'
|
||||
|
||||
EditObjectForm.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
children: PropTypes.func.isRequired,
|
||||
style: PropTypes.object
|
||||
style: PropTypes.object,
|
||||
onEdit: PropTypes.func,
|
||||
onStateChange: PropTypes.func
|
||||
}
|
||||
|
||||
export default EditObjectForm
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user