Tom Butcher 24c06c25d3 Refactor JavaScript Language Support and Enhance Autocomplete Functionality
- Consolidated JavaScript completion sources into a new module, improving organization and maintainability.
- Updated the `fcTemplateLang` to utilize the new `javascriptCompletionSources` for enhanced autocomplete capabilities.
- Removed redundant imports and streamlined the code in various components, including Invoices, Payments, TaxRecords, and Inventory sections, by replacing dropdown actions with a unified `ObjectActions` component.
- Improved the handling of object actions across multiple inventory and finance components, enhancing user experience and code consistency.
2026-08-20 19:35:09 +01:00

297 lines
7.5 KiB
JavaScript

import { createElement, useContext, useEffect, useRef } from 'react'
import { Dropdown, Button } from 'antd'
import { getModelByName } from '../../../database/ObjectModels'
import PropTypes from 'prop-types'
import { useNavigate, useLocation } from 'react-router-dom'
import { useActionsModal } from '../context/ActionsModalContext'
import { useActions } from '../context/ActionsContext'
import KeyboardShortcut from './KeyboardShortcut'
import { AuthContext } from '../context/AuthContext'
import {
actionVisibleOnPage,
buildActionUrl,
getActionPermissionTarget,
resolveAction,
stripPageActionParams
} from '../../../utils/modelActions'
import { hasActionPermission } from '../../../database/permissions'
function filterActionsByVisibility(actions, visibleActions) {
if (!visibleActions) return actions
return actions.filter((action) => {
if (action.type === 'divider') {
return true
}
const actionKey = action.key || action.name
const isVisible = visibleActions[actionKey] !== false
if (action.children && Array.isArray(action.children)) {
const filteredChildren = filterActionsByVisibility(
action.children,
visibleActions
)
action.children = filteredChildren
return (
isVisible &&
(filteredChildren.length > 0 || visibleActions[actionKey] === true)
)
}
return isVisible
})
}
function cleanDividers(items) {
if (!Array.isArray(items) || items.length === 0) return []
const cleaned = []
for (const item of items) {
if (!item) continue
if (item.type === 'divider') {
if (cleaned.length === 0) continue
if (cleaned[cleaned.length - 1]?.type === 'divider') continue
cleaned.push(item)
continue
}
if (item.children && Array.isArray(item.children)) {
const children = cleanDividers(item.children)
if (children.length === 0) continue
cleaned.push({ ...item, children })
continue
}
cleaned.push(item)
}
if (cleaned[cleaned.length - 1]?.type === 'divider') {
cleaned.pop()
}
return cleaned
}
function isSameActionUrl(action, actionUrl, currentUrl, pathname, search) {
if (action.type === 'modal' || action.type === 'alias') {
return actionUrl === currentUrl
}
return actionUrl === stripPageActionParams(pathname, search)
}
function actionDenied(userProfile, model, action, parentDenied = false) {
if (parentDenied) return true
const { model: permissionModel, actionName } = getActionPermissionTarget(
action,
model
)
return !hasActionPermission(userProfile, permissionModel, actionName)
}
function mapActionsToMenuItems(
actions,
currentUrlWithActions,
id,
objectData,
userProfile,
model,
pathname,
search,
parentDenied = false
) {
return cleanDividers(
actions.map((action) => {
if (action.type === 'divider') {
return { type: 'divider' }
}
const displayAction = resolveAction(action)
const actionUrl = buildActionUrl(model, action, id, pathname, search)
const denied = actionDenied(userProfile, model, action, parentDenied)
var disabled =
denied ||
isSameActionUrl(
action,
actionUrl,
currentUrlWithActions,
pathname,
search
)
var visible = true
if (action.disabled) {
if (typeof action.disabled === 'function') {
disabled =
denied ||
action.disabled({ ...objectData, _user: userProfile })
} else {
disabled = denied || action.disabled
}
}
if (action.visible) {
if (typeof action.visible === 'function') {
visible = action.visible(objectData)
} else {
visible = action.visible
}
}
if (visible != true) {
return null
}
const item = {
key: action.key || action.name,
label: displayAction.label,
danger: action?.danger || false,
icon: displayAction.icon
? createElement(displayAction.icon)
: undefined,
disabled
}
if (action.children && Array.isArray(action.children)) {
item.children = mapActionsToMenuItems(
action.children,
currentUrlWithActions,
id,
objectData,
userProfile,
model,
pathname,
search,
denied
)
}
return item
})
)
}
const ObjectActions = ({
type,
id,
objectData = {},
pageName = 'info',
onReload,
disabled = false,
buttonProps = {},
visibleActions = {},
...dropdownProps
}) => {
const model = getModelByName(type)
const actions = model.actions || []
const navigate = useNavigate()
const location = useLocation()
const { showActionsModal } = useActionsModal()
const { setOnModalOk } = useActions()
const { userProfile } = useContext(AuthContext)
const onReloadRef = useRef(onReload)
onReloadRef.current = onReload
useEffect(() => {
if (pageName !== 'list') return
setOnModalOk(() => () => onReloadRef.current?.())
return () => setOnModalOk(null)
}, [pageName, setOnModalOk])
const pageActions = actions.filter((action) =>
actionVisibleOnPage(action, pageName)
)
const visibilityFilteredActions = filterActionsByVisibility(
pageActions,
visibleActions
)
const filteredActions = cleanDividers(
visibilityFilteredActions.filter((action) => {
if (action.type === 'divider') return true
const actionUrl = buildActionUrl(
model,
action,
id,
location.pathname,
location.search
)
return !isSameActionUrl(
action,
actionUrl,
location.pathname + location.search,
location.pathname,
location.search
)
})
)
const currentUrlWithActions = location.pathname + location.search
const openActionsModal = () =>
showActionsModal(id, type, objectData, pageName)
const menu = {
items: mapActionsToMenuItems(
filteredActions,
currentUrlWithActions,
id,
objectData,
userProfile,
model,
location.pathname,
location.search
),
onClick: (info) => {
const findMenuAction = (acts, key, parentDenied = false) => {
for (const act of acts) {
if (act.type === 'divider') continue
const denied = actionDenied(userProfile, model, act, parentDenied)
if ((act.key || act.name) === key) {
return denied ? null : act
}
if (act.children) {
const found = findMenuAction(act.children, key, denied)
if (found) return found
}
}
return null
}
const action = findMenuAction(filteredActions, info.key)
if (!action) return
navigate(
buildActionUrl(model, action, id, location.pathname, location.search)
)
}
}
return (
<KeyboardShortcut shortcut='alt+a' onTrigger={openActionsModal}>
<Dropdown menu={menu} {...dropdownProps}>
<Button
{...buttonProps}
disabled={disabled || filteredActions.length === 0}
onClick={openActionsModal}
>
Actions
</Button>
</Dropdown>
</KeyboardShortcut>
)
}
ObjectActions.propTypes = {
type: PropTypes.string.isRequired,
objectData: PropTypes.object,
id: PropTypes.string,
pageName: PropTypes.string,
onReload: PropTypes.func,
disabled: PropTypes.bool,
buttonProps: PropTypes.object,
buttonLabel: PropTypes.string,
visibleActions: PropTypes.object
}
export default ObjectActions