From 678d5a0e90ed405ac5e25a645c40bb2d84c8d041 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 18 Aug 2025 00:58:52 +0100 Subject: [PATCH] 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. --- .../Dashboard/common/ActionHandler.jsx | 140 +++--- .../Dashboard/common/EditObjectForm.jsx | 437 ++++++++++-------- 2 files changed, 321 insertions(+), 256 deletions(-) diff --git a/src/components/Dashboard/common/ActionHandler.jsx b/src/components/Dashboard/common/ActionHandler.jsx index 1d22524..85aa40c 100644 --- a/src/components/Dashboard/common/ActionHandler.jsx +++ b/src/components/Dashboard/common/ActionHandler.jsx @@ -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, diff --git a/src/components/Dashboard/common/EditObjectForm.jsx b/src/components/Dashboard/common/EditObjectForm.jsx index 1d162f1..89a9e8d 100644 --- a/src/components/Dashboard/common/EditObjectForm.jsx +++ b/src/components/Dashboard/common/EditObjectForm.jsx @@ -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 ( - <> - setDeleteModalOpen(false)} - loading={deleteLoading} - objectType={type} - objectName={objectData?.name || objectData?.label || ''} - /> -
{ - setObjectData((prev) => ({ ...prev, ...values })) - }} - > - {contextHolder} - {children({ - loading: fetchLoading, - isEditing, - startEditing, - cancelEditing, - handleUpdate, - form, - formValid, - objectData, - setIsEditing, - setObjectData, - editLoading, - lock, - handleFetchObject, - handleDelete - })} -
- - ) -} + 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 ( + <> + 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 + })} +
+ + ) + } +) + +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