Enhance PermissionsMatrix with session storage for expanded rows and scroll position

- Implemented session storage functionality to persist expanded row keys and scroll position in the PermissionsMatrix, improving user experience during navigation.
- Refactored state management to utilize hooks for handling expanded rows and scroll restoration.
- Added utility functions for reading and writing to session storage, ensuring data integrity and availability across user sessions.
- Updated event handling for scroll events to maintain the user's scroll position, enhancing usability in the permissions matrix.
This commit is contained in:
Tom Butcher 2026-08-20 19:42:25 +01:00
parent 24c06c25d3
commit 0a09905f9f

View File

@ -4,7 +4,9 @@ import {
useState,
createElement,
useContext,
useRef
useRef,
useEffect,
useLayoutEffect
} from 'react'
import PropTypes from 'prop-types'
import { Table, Flex, Tooltip, Button } from 'antd'
@ -21,6 +23,30 @@ import PermissionCheckbox from './PermissionCheckbox'
import { CaretRightOutlined } from '@ant-design/icons'
const DEFAULT_SCROLL_HEIGHT = 'calc(var(--unit-100vh) - 258px)'
const PERMISSIONS_MATRIX_SESSION_KEY = 'permissionsMatrix:ui'
const readPermissionsMatrixSession = () => {
try {
const parsed = JSON.parse(
sessionStorage.getItem(PERMISSIONS_MATRIX_SESSION_KEY)
)
if (!parsed || typeof parsed !== 'object') return {}
return parsed
} catch {
return {}
}
}
const writePermissionsMatrixSession = (patch) => {
try {
sessionStorage.setItem(
PERMISSIONS_MATRIX_SESSION_KEY,
JSON.stringify({ ...readPermissionsMatrixSession(), ...patch })
)
} catch {
// sessionStorage may be unavailable
}
}
const getColumnIndexProps = (columnIndex) => ({
'data-col': String(columnIndex),
@ -328,9 +354,26 @@ const PermissionsMatrix = ({
() => buildPermissionMatrixDataSource(models),
[models]
)
const [expandedRowKeys, setExpandedRowKeys] = useState(null)
const [expandedRowKeys, setExpandedRowKeys] = useState(() => {
const stored = readPermissionsMatrixSession().expandedRowKeys
return Array.isArray(stored) ? stored : []
})
const matrixRef = useRef(null)
const scrollPositionRef = useRef(null)
if (scrollPositionRef.current === null) {
const storedScroll = readPermissionsMatrixSession().scroll
scrollPositionRef.current = {
left: Number(storedScroll?.left) || 0,
top: Number(storedScroll?.top) || 0
}
}
const hoveredColumnIndexRef = useRef(null)
const persistExpandedRowKeys = useCallback((keys) => {
setExpandedRowKeys(keys)
writePermissionsMatrixSession({ expandedRowKeys: keys })
}, [])
const handleMatrixMouseOver = useCallback((event) => {
const cell = event.target.closest('.ant-table-cell')
if (!cell || !event.currentTarget.contains(cell)) return
@ -345,11 +388,44 @@ const PermissionsMatrix = ({
event.currentTarget.style.removeProperty('--permissions-hovered-col')
}, [])
const resolvedExpandedRowKeys = useMemo(
() => expandedRowKeys ?? dataSource.map((row) => row.key),
[dataSource, expandedRowKeys]
const resolvedExpandedRowKeys = useMemo(() => {
const sectionKeys = new Set(dataSource.map((row) => row.key))
return expandedRowKeys.filter((key) => sectionKeys.has(key))
}, [dataSource, expandedRowKeys])
const getTableBody = useCallback(
() => matrixRef.current?.querySelector('.ant-table-body') || null,
[]
)
const restoreTableScroll = useCallback(() => {
const body = getTableBody()
if (!body) return
body.scrollLeft = scrollPositionRef.current.left
body.scrollTop = scrollPositionRef.current.top
}, [getTableBody])
useLayoutEffect(() => {
restoreTableScroll()
}, [restoreTableScroll, resolvedExpandedRowKeys])
useEffect(() => {
const body = getTableBody()
if (!body) return
const handleScroll = (event) => {
const next = {
left: event.currentTarget.scrollLeft,
top: event.currentTarget.scrollTop
}
scrollPositionRef.current = next
writePermissionsMatrixSession({ scroll: next })
}
body.addEventListener('scroll', handleScroll, { passive: true })
return () => body.removeEventListener('scroll', handleScroll)
}, [getTableBody, resolvedExpandedRowKeys])
const isSectionExpanded = useCallback(
(record) => resolvedExpandedRowKeys.includes(record.key),
[resolvedExpandedRowKeys]
@ -358,14 +434,20 @@ const PermissionsMatrix = ({
const handleToggleSection = useCallback(
(record) => {
if (!record.isGroup) return
setExpandedRowKeys((current) => {
const keys = current ?? dataSource.map((row) => row.key)
return keys.includes(record.key)
? keys.filter((key) => key !== record.key)
: [...keys, record.key]
})
persistExpandedRowKeys(
expandedRowKeys.includes(record.key)
? expandedRowKeys.filter((key) => key !== record.key)
: [...expandedRowKeys, record.key]
)
},
[dataSource]
[expandedRowKeys, persistExpandedRowKeys]
)
const handleExpandedRowsChange = useCallback(
(keys) => {
persistExpandedRowKeys(keys)
},
[persistExpandedRowKeys]
)
var adjustedScrollHeight = scrollHeight
@ -627,6 +709,7 @@ const PermissionsMatrix = ({
return (
<div
ref={matrixRef}
className='permissions-matrix'
onMouseOver={handleMatrixMouseOver}
onMouseLeave={handleMatrixMouseLeave}
@ -645,7 +728,7 @@ const PermissionsMatrix = ({
}
expandable={{
expandedRowKeys: resolvedExpandedRowKeys,
onExpandedRowsChange: setExpandedRowKeys,
onExpandedRowsChange: handleExpandedRowsChange,
indentSize: 0,
expandIcon: () => null
}}