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 ( } /> } > {resolvedModalAction?.content ? resolvedModalAction.content(modalContentData, { onOk: handleModalOk }) : null} {children} ) } ActionsProvider.propTypes = { children: PropTypes.node.isRequired } const useActions = () => useContext(ActionsContext) // eslint-disable-next-line react-refresh/only-export-components export { ActionsProvider, ActionsContext, useActions }