- 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.
297 lines
7.5 KiB
JavaScript
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
|