Enhance PermissionsMatrix functionality and UI

- Introduced new utility functions for normalizing and aggregating permission states, improving the management of user permissions.
- Added row and column cycling functionality to allow users to easily toggle permission states.
- Enhanced the rendering of checkboxes with tooltips for better user feedback on permission states.
- Updated the layout of the PermissionsMatrix to improve visual clarity and user interaction.
This commit is contained in:
Tom Butcher 2026-08-20 14:52:55 +01:00
parent e54106eb6d
commit daf2e1aab4

View File

@ -53,6 +53,77 @@ const cyclePermissionState = (current, inherit) => {
return true
}
const normalizePermissionState = (state, inherit) => {
if (!inherit && state === null) return false
return state
}
const getAggregatePermissionState = (states) => {
if (states.length === 0) return null
const first = states[0]
for (let i = 1; i < states.length; i++) {
if (states[i] !== first) return 'mixed'
}
return first
}
const getRowPermissionStates = (permissions, model, actions, inherit) =>
actions
.filter((action) => modelHasPermissionAction(model, action.name))
.map((action) =>
normalizePermissionState(
getPermissionState(permissions, model.name, action.name),
inherit
)
)
const getColumnPermissionStates = (permissions, models, action, inherit) =>
models
.filter((model) => modelHasPermissionAction(model, action.name))
.map((model) =>
normalizePermissionState(
getPermissionState(permissions, model.name, action.name),
inherit
)
)
const setRowPermissionState = (permissions, model, actions, next) => {
let nextPermissions = permissions
actions.forEach((action) => {
if (modelHasPermissionAction(model, action.name)) {
nextPermissions = setPermissionState(
nextPermissions,
model.name,
action.name,
next
)
}
})
return nextPermissions
}
const setColumnPermissionState = (permissions, models, action, next) => {
let nextPermissions = permissions
models.forEach((model) => {
if (modelHasPermissionAction(model, action.name)) {
nextPermissions = setPermissionState(
nextPermissions,
model.name,
action.name,
next
)
}
})
return nextPermissions
}
const getCheckboxTitle = (state, inherit) => {
if (state === 'mixed') return 'Mixed'
if (inherit && state === null) return 'Inherit'
if (state === true) return 'Allow'
return 'Deny'
}
const PermissionsMatrix = ({
value,
onChange,
@ -88,58 +159,140 @@ const PermissionsMatrix = ({
[disabled, inherit, onChange, permissions]
)
const handleCycleRow = useCallback(
(model) => {
if (disabled || typeof onChange !== 'function') return
const aggregate = getAggregatePermissionState(
getRowPermissionStates(permissions, model, actions, inherit)
)
const next = cyclePermissionState(
aggregate === 'mixed' ? null : aggregate,
inherit
)
onChange(setRowPermissionState(permissions, model, actions, next))
},
[actions, disabled, inherit, onChange, permissions]
)
const handleCycleColumn = useCallback(
(action) => {
if (disabled || typeof onChange !== 'function') return
const aggregate = getAggregatePermissionState(
getColumnPermissionStates(permissions, models, action, inherit)
)
const next = cyclePermissionState(
aggregate === 'mixed' ? null : aggregate,
inherit
)
onChange(setColumnPermissionState(permissions, models, action, next))
},
[disabled, inherit, models, onChange, permissions]
)
const renderCheckbox = useCallback(
(state, onCycle) => {
const mixed = state === 'mixed'
const checked = state === true
const indeterminate = inherit && state === null
return (
<Tooltip title={getCheckboxTitle(state, inherit)} arrow={false}>
<span>
<PermissionCheckbox
checked={checked}
indeterminate={indeterminate}
mixed={mixed}
disabled={disabled}
onChange={onCycle}
/>
</span>
</Tooltip>
)
},
[disabled, inherit]
)
const columns = useMemo(() => {
const modelColumn = {
title: '',
key: 'model',
dataIndex: 'label',
fixed: isMobile ? undefined : 'left',
width: 220,
render: (label, record) => (
<Flex align='center' gap='small'>
{record.icon ? createElement(record.icon) : null}
{label}
</Flex>
)
}
const actionColumns = actions.map((action) => ({
title: <span className='permissions-matrix-header'>{action.label}</span>,
key: action.name,
width: 48,
align: 'center',
render: (_, record) => {
if (!modelHasPermissionAction(record.model, action.name)) {
return null
}
const state = getPermissionState(
permissions,
record.model.name,
action.name
width: 248,
render: (label, record) => {
const aggregate = getAggregatePermissionState(
getRowPermissionStates(permissions, record.model, actions, inherit)
)
const checked = state === true
const indeterminate = inherit && state === null
const title = indeterminate ? 'Inherit' : checked ? 'Allow' : 'Deny'
return (
<Tooltip title={title} arrow={false}>
<span>
<PermissionCheckbox
checked={checked}
indeterminate={indeterminate}
disabled={disabled}
onChange={() => {
handleCycle(record.model.name, action.name)
}}
/>
</span>
</Tooltip>
<Flex
align='center'
gap='small'
justify='space-between'
style={{ width: '100%', paddingRight: '10px' }}
>
<Flex align='center' gap='small' style={{ marginLeft: '6px' }}>
{record.icon ? createElement(record.icon) : null}
{label}
</Flex>
<div style={{ paddingBottom: '1px' }}>
{renderCheckbox(aggregate, () => {
handleCycleRow(record.model)
})}
</div>
</Flex>
)
}
}))
}
const actionColumns = actions.map((action) => {
const columnAggregate = getAggregatePermissionState(
getColumnPermissionStates(permissions, models, action, inherit)
)
return {
title: (
<Flex vertical align='center' gap={12} justify='flex-end'>
<span className='permissions-matrix-header'>{action.label}</span>
<div style={{ paddingBottom: '2px' }}>
{renderCheckbox(columnAggregate, () => {
handleCycleColumn(action)
})}
</div>
</Flex>
),
key: action.name,
width: 48,
align: 'center',
render: (_, record) => {
if (!modelHasPermissionAction(record.model, action.name)) {
return null
}
const state = getPermissionState(
permissions,
record.model.name,
action.name
)
return renderCheckbox(state, () => {
handleCycle(record.model.name, action.name)
})
}
}
})
return [modelColumn, ...actionColumns]
}, [actions, disabled, handleCycle, inherit, isMobile, permissions])
}, [
actions,
handleCycle,
handleCycleColumn,
handleCycleRow,
inherit,
isMobile,
models,
permissions,
renderCheckbox
])
const dataSource = useMemo(
() =>