import { Input, Flex, List, Typography, Modal, Tag } from 'antd' import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react' import PropTypes from 'prop-types' import { useLocation, useNavigate } from 'react-router-dom' import { getModelByName } from '../../../database/ObjectModels' const ActionsModalContext = createContext() // Remove the "action" query param from a URL so we don't navigate to the same URL again const stripActionParam = (pathname, search) => { const params = new URLSearchParams(search) params.delete('action') const query = params.toString() return pathname + (query ? `?${query}` : '') } // Flatten nested actions (including children) into a single list const flattenActions = (actions, parentLabel = '') => { if (!Array.isArray(actions)) return [] const flat = [] actions.forEach((action) => { if (!action || action.type === 'divider') { return } const hasUrl = typeof action.url === 'function' const hasChildren = Array.isArray(action.children) && action.children.length > 0 const currentLabel = action.label || action.name || '' const fullLabel = parentLabel ? `${parentLabel} / ${currentLabel}` : currentLabel // Only push actions that are actually runnable if (hasUrl) { flat.push({ ...action, key: action.key || action.name || fullLabel, fullLabel }) } if (hasChildren) { flat.push(...flattenActions(action.children, fullLabel)) } }) return flat } const ActionsModalProvider = ({ children }) => { const { Text } = Typography const navigate = useNavigate() const location = useLocation() const [visible, setVisible] = useState(false) const [query, setQuery] = useState('') const [context, setContext] = useState({ id: null, type: null, objectData: null }) const inputRef = useRef(null) const showActionsModal = (id, type, objectData = null) => { setContext({ id, type, objectData }) setQuery('') setVisible(true) } const hideActionsModal = () => { setVisible(false) setQuery('') } // Focus and select text in input when modal becomes visible useEffect(() => { // Use a small timeout to ensure the modal is fully rendered and visible setTimeout(() => { if (visible) { console.log('visible', visible) console.log('inputRef.current', inputRef.current) if (visible && inputRef.current) { console.log('focusing input') const input = inputRef.current.input if (input) { input.focus() input.select() // Select all text } } } }, 50) }, [visible]) const model = context.type ? getModelByName(context.type) : null const ModelIcon = model?.icon || null const modelLabel = model?.label || model?.name || '' const flattenedActions = useMemo( () => flattenActions(model?.actions || []), [model] ) const currentUrlWithoutActions = stripActionParam( location.pathname, location.search ) const getActionDisabled = (action) => { const { id, objectData } = context if (!action) return true let disabled = false const url = action.url ? action.url(id) : undefined // Match ObjectActions default disabling behaviour if (url && url === currentUrlWithoutActions) { disabled = true } if (typeof action.disabled !== 'undefined') { if (typeof action.disabled === 'function') { disabled = action.disabled(objectData) } else { disabled = action.disabled } } return disabled } const getVisibleDisabled = (action) => { const { objectData } = context if (!action) return true let visible = true if (typeof action.visible !== 'undefined') { if (typeof action.visible === 'function') { visible = action.visible(objectData) } else { visible = action.visible } } return visible } const normalizedQuery = query.trim().toLowerCase() const filteredActions = flattenedActions.filter((action) => { if (!normalizedQuery) return true const haystack = [ action.fullLabel || '', action.label || '', action.name || '', modelLabel ] .join(' ') .toLowerCase() return haystack.includes(normalizedQuery) }) const runAction = (action) => { if (!action || typeof action.url !== 'function') return if (getActionDisabled(action)) return const { id } = context const targetUrl = action.url(id) if (targetUrl && targetUrl !== '#') { navigate(targetUrl) hideActionsModal() } } const handleKeyDown = (e) => { if (!filteredActions.length) return // Enter triggers first visible action if (e.key === 'Enter') { e.preventDefault() const first = filteredActions[0] runAction(first) return } // Number keys 1-9 trigger corresponding actions if (/^[1-9]$/.test(e.key)) { e.preventDefault() const index = parseInt(e.key, 10) if (index < filteredActions.length) { const action = filteredActions[index] runAction(action) } } } return ( } placeholder='Search actions...' size='large' value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={handleKeyDown} /> {filteredActions.length > 0 && (
getVisibleDisabled(action) )} renderItem={(action, index) => { const Icon = action.icon const disabled = getActionDisabled(action) let shortcutText = '' if (index === 0) { shortcutText = 'ENTER' } else if (index <= 9) { shortcutText = index.toString() } return ( !disabled && runAction(action)} style={{ cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1 }} > {Icon ? : null} {action.fullLabel || action.label || action.name} {action.danger && Danger} {shortcutText && {shortcutText}} ) }} />
)}
{children}
) } ActionsModalProvider.propTypes = { children: PropTypes.node.isRequired } const useActionsModal = () => useContext(ActionsModalContext) // eslint-disable-next-line react-refresh/only-export-components export { ActionsModalProvider, ActionsModalContext, useActionsModal }