From daf2e1aab48a07c43689118d5aa5f29c3e54f9d8 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Thu, 20 Aug 2026 14:52:55 +0100 Subject: [PATCH] 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. --- .../Dashboard/common/PermissionsMatrix.jsx | 231 +++++++++++++++--- 1 file changed, 192 insertions(+), 39 deletions(-) diff --git a/src/components/Dashboard/common/PermissionsMatrix.jsx b/src/components/Dashboard/common/PermissionsMatrix.jsx index b80b966e..5337de58 100644 --- a/src/components/Dashboard/common/PermissionsMatrix.jsx +++ b/src/components/Dashboard/common/PermissionsMatrix.jsx @@ -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 ( + + + + + + ) + }, + [disabled, inherit] + ) + const columns = useMemo(() => { const modelColumn = { title: '', key: 'model', dataIndex: 'label', fixed: isMobile ? undefined : 'left', - width: 220, - render: (label, record) => ( - - {record.icon ? createElement(record.icon) : null} - {label} - - ) - } - - const actionColumns = actions.map((action) => ({ - title: {action.label}, - 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 ( - - - { - handleCycle(record.model.name, action.name) - }} - /> - - + + + {record.icon ? createElement(record.icon) : null} + {label} + +
+ {renderCheckbox(aggregate, () => { + handleCycleRow(record.model) + })} +
+
) } - })) + } + + const actionColumns = actions.map((action) => { + const columnAggregate = getAggregatePermissionState( + getColumnPermissionStates(permissions, models, action, inherit) + ) + + return { + title: ( + + {action.label} +
+ {renderCheckbox(columnAggregate, () => { + handleCycleColumn(action) + })} +
+
+ ), + 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( () =>