Tom Butcher 763bbf6f6e
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Refactor Context Providers and Enhance Error Handling
- Replaced NotificationProvider with ActionsProvider in App.jsx to improve context management.
- Introduced default actions in ActionsContext to streamline state handling for current objects and actions.
- Improved error logging in ApiServerContext by consolidating error response handling and ensuring robust error reporting.
2026-11-08 22:48:12 +00:00

312 lines
8.4 KiB
JavaScript

import {
createContext,
Suspense,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import PropTypes from 'prop-types'
import { Modal, Spin, Flex } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { useLocation, useNavigate } from 'react-router-dom'
import { getModelByName } from '../../../database/ObjectModels'
import {
buildActionUrl,
findAction,
getObjectIdFromSearch,
resolveAction,
stripModalActionParams
} from '../../../utils/modelActions'
import { AuthContext } from './AuthContext'
import { ElectronContext } from './ElectronContext'
import { useNavigationTabs } from './NavigationTabsContext'
const noop = () => {}
const defaultActions = {
currentObject: null,
currentObjectType: null,
setCurrentObject: noop,
setCurrentObjectType: noop,
callAction: noop,
clearAction: noop,
setOnModalOk: noop,
ctrlDown: false
}
const ActionsContext = createContext(defaultActions)
const ActionsProvider = ({ children }) => {
const navigate = useNavigate()
const location = useLocation()
const locationRef = useRef(location)
locationRef.current = location
const { userProfile } = useContext(AuthContext)
const [currentObject, setCurrentObject] = useState(null)
const [currentObjectType, setCurrentObjectType] = useState(null)
const [modalAction, setModalAction] = useState(null)
const [onModalOk, setOnModalOk] = useState(null)
const lastHandledAction = useRef(null)
const { isElectron } = useContext(ElectronContext)
const { addTab } = useNavigationTabs()
const searchParams = new URLSearchParams(location.search)
const actionName = searchParams.get('action')
const actionObjectType = searchParams.get('actionObjectType')
const clearAction = useCallback(() => {
const loc = locationRef.current
const nextUrl = stripModalActionParams(loc.pathname, loc.search)
if (nextUrl !== loc.pathname + loc.search) {
navigate(nextUrl, { replace: true })
}
setModalAction(null)
lastHandledAction.current = null
}, [navigate])
const handleModalOk = useCallback(() => {
onModalOk?.()
clearAction()
}, [clearAction, onModalOk])
const [ctrlDown, setCtrlDown] = useState(false)
useEffect(() => {
const isHoldKey = (key) => key === 'Meta' || key === 'Control'
const handleKeyDown = (event) => {
if (isHoldKey(event.key)) {
setCtrlDown(true)
}
}
const handleKeyUp = (event) => {
if (isHoldKey(event.key)) {
setCtrlDown(false)
}
}
const clearHold = () => setCtrlDown(false)
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp)
window.addEventListener('blur', clearHold)
document.addEventListener('visibilitychange', clearHold)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp)
window.removeEventListener('blur', clearHold)
document.removeEventListener('visibilitychange', clearHold)
}
}, [])
const handleNavigate = useCallback(
(url) => {
if (isElectron && ctrlDown) {
addTab(url)
return
}
if (!isElectron && ctrlDown) {
window.open(url, '_blank')
return
}
navigate(url, { replace: true })
},
[addTab, navigate, ctrlDown, isElectron]
)
const callAction = useCallback(
(action, objectData, objectType) => {
setCurrentObject(objectData)
setCurrentObjectType(objectType)
const model = getModelByName(objectType)
const loc = locationRef.current
const url = buildActionUrl(
model,
action,
objectData._id,
loc.pathname,
loc.search
)
handleNavigate(url)
},
[handleNavigate]
)
useEffect(() => {
if (!actionObjectType) return
const model = getModelByName(actionObjectType)
if (!model.pages?.length) return
const onPage = model.pages.some(
(page) => location.pathname === `${model.url}/${page.name}`
)
if (!onPage) return
if (actionName) {
const action = findAction(model, actionName)
if (action?.type === 'modal' || action?.type === 'alias') return
}
const params = new URLSearchParams(location.search)
params.delete('actionObjectType')
const query = params.toString()
const nextUrl = location.pathname + (query ? `?${query}` : '')
if (nextUrl !== location.pathname + location.search) {
navigate(nextUrl, { replace: true })
}
}, [
actionName,
actionObjectType,
location.pathname,
location.search,
navigate
])
useEffect(() => {
if (!actionName || !actionObjectType) {
setModalAction(null)
lastHandledAction.current = null
return
}
const actionKey = `${actionObjectType}:${actionName}:${location.pathname}${location.search}`
if (lastHandledAction.current === actionKey) return
const model = getModelByName(actionObjectType)
const action = findAction(model, actionName)
if (!action) return
if (action.type === 'modal' || action.type === 'alias') {
lastHandledAction.current = actionKey
setModalAction(action)
return
}
if (action.type === 'page') {
if (action.name === 'list') {
lastHandledAction.current = actionKey
if (location.pathname !== model.url) {
handleNavigate(model.url)
}
return
}
const pageName = action.pageName || action.name
const objectId =
getObjectIdFromSearch(actionObjectType, location.search) ||
currentObject?._id
if (!objectId) return
const expectedPath = `${model.url}/${pageName}`
if (location.pathname === expectedPath) {
lastHandledAction.current = actionKey
return
}
lastHandledAction.current = actionKey
const url = buildActionUrl(
model,
action,
objectId,
location.pathname,
location.search
)
handleNavigate(url)
}
}, [
actionName,
actionObjectType,
currentObject,
location.pathname,
location.search,
handleNavigate
])
const modalObjectData = currentObject
? { ...currentObject, _user: userProfile }
: { _user: userProfile }
const resolvedModalAction =
modalAction?.type === 'alias' ? resolveAction(modalAction) : modalAction
const [modelWidth, setModelWidth] = useState(520)
useEffect(() => {
if (resolvedModalAction?.modalWidth) {
setModelWidth(resolvedModalAction.modalWidth)
}
}, [resolvedModalAction?.modalWidth])
const [modalCentered, setModalCentered] = useState(false)
useEffect(() => {
if (!resolvedModalAction) return
setModalCentered(resolvedModalAction.modalCentered === true)
}, [resolvedModalAction])
const modalContentData =
modalAction?.type === 'alias'
? typeof modalAction.objectData === 'function'
? modalAction.objectData(modalObjectData)
: (modalAction.objectData ?? {})
: resolvedModalAction?.name === 'new'
? {}
: modalObjectData
const value = useMemo(
() => ({
currentObject,
currentObjectType,
setCurrentObject,
setCurrentObjectType,
callAction,
clearAction,
setOnModalOk,
ctrlDown
}),
[callAction, clearAction, currentObject, currentObjectType, ctrlDown]
)
return (
<ActionsContext.Provider value={value}>
<Modal
open={resolvedModalAction != null}
destroyOnHidden={true}
width={modelWidth}
onCancel={clearAction}
footer={null}
centered={modalCentered}
>
<Suspense
fallback={
<Flex
justify='center'
align='center'
style={{
height: modalCentered ? '120px' : '357px',
width: '100%'
}}
>
<Spin indicator={<LoadingOutlined spin />} />
</Flex>
}
>
{resolvedModalAction?.content
? resolvedModalAction.content(modalContentData, {
onOk: handleModalOk
})
: null}
</Suspense>
</Modal>
{children}
</ActionsContext.Provider>
)
}
ActionsProvider.propTypes = {
children: PropTypes.node.isRequired
}
const useActions = () => useContext(ActionsContext)
// eslint-disable-next-line react-refresh/only-export-components
export { ActionsProvider, ActionsContext, useActions }