All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- 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.
312 lines
8.4 KiB
JavaScript
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 }
|