Add Permission Settings Management Component
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced a new PermissionSettings management component, including a list view and modal for creating new permission settings.
- Implemented a permissions matrix for managing user permissions with checkboxes for allow, deny, and inherit states.
- Added associated styles in App.css for the permissions matrix table and checkbox components.
- Created new icons and integrated them into the sidebar and relevant components for better visual representation.
- Updated routing and sidebar configuration to include the new Permission Settings section in the management dashboard.
This commit is contained in:
Tom Butcher 2026-08-20 12:28:20 +01:00
parent 6a4074674d
commit d928cf45b0
17 changed files with 1063 additions and 9 deletions

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.841213,0,0,0.841213,4.798602,1)">
<path d="M36.588,72.726C34.194,73.377 31.611,73.703 28.844,73.703C13.719,73.703 3.719,63.672 3.719,47.781L3.719,19.625C3.719,15.344 6.688,12.281 10.828,12.281C11.781,12.281 12.703,12.578 13.391,13.078L13.391,11.063C13.391,6.969 16.281,4.109 20.422,4.109C21.328,4.109 22.25,4.375 22.969,4.875C23.938,1.875 26.453,0 29.734,0C33.109,0 35.578,1.828 36.5,4.922C37.328,4.406 38.297,4.109 39.328,4.109C43.781,4.109 46.812,7.062 46.812,11.5L46.812,33.875C46.812,33.892 46.813,33.908 46.815,33.924C44.424,34.416 42.172,35.289 40.125,36.477L40.125,12.188C40.125,11.25 39.453,10.578 38.547,10.578C37.641,10.578 37.016,11.25 37.016,12.188L37.016,34.062C37.016,35.656 35.688,36.922 34.109,36.922C32.5,36.922 31.203,35.656 31.203,34.062L31.203,8.078C31.203,7.141 30.547,6.469 29.656,6.469C28.734,6.469 28.078,7.141 28.078,8.078L28.078,35.047C28.078,36.688 26.766,37.906 25.172,37.906C23.609,37.906 22.266,36.688 22.266,35.047L22.266,12.188C22.266,11.25 21.609,10.578 20.688,10.578C19.781,10.578 19.172,11.25 19.172,12.188L19.172,36.734C19.172,38.359 17.828,39.594 16.234,39.594C14.688,39.594 13.344,38.359 13.344,36.734L13.344,20.438C13.344,19.5 12.688,18.844 11.781,18.844C10.859,18.844 10.234,19.5 10.234,20.438L10.234,47.172C10.234,59.594 17.219,66.797 28.641,66.797C29.698,66.797 30.716,66.745 31.696,66.64C32.963,68.945 34.624,71.005 36.588,72.726ZM47.27,33.835L48.328,31.234C49.422,28.875 51.656,27.609 53.984,27.609C58.031,27.609 60.953,30.641 60.953,34.594C60.953,34.923 60.935,35.258 60.9,35.6C58.562,34.495 56.004,33.783 53.31,33.549C53.272,33.545 53.234,33.542 53.195,33.54C52.586,33.49 51.97,33.464 51.347,33.464C49.955,33.464 48.592,33.592 47.27,33.835Z"/>
</g>
<g transform="matrix(0.468542,0,0,0.468542,31.985381,32)">
<path d="M31.328,68.297L37,68.297C39.25,68.297 40.953,66.922 41.469,64.75L42.953,58.516C43.781,58.234 44.578,57.906 45.281,57.578L50.734,60.922C52.594,62.078 54.828,61.891 56.391,60.328L60.375,56.359C61.938,54.797 62.109,52.469 60.922,50.609L57.609,45.219C57.953,44.5 58.266,43.75 58.5,42.984L64.797,41.484C66.969,40.984 68.328,39.266 68.328,37.016L68.328,31.469C68.328,29.25 66.969,27.531 64.797,27.016L58.578,25.531C58.297,24.688 57.969,23.906 57.656,23.25L61,17.734C62.156,15.797 62,13.641 60.406,12.062L56.344,8.047C54.781,6.641 52.75,6.344 50.891,7.391L45.281,10.859C44.594,10.5 43.812,10.188 42.969,9.891L41.469,3.562C40.938,1.391 39.25,0 37,0L31.328,0C29.078,0 27.406,1.391 26.859,3.578L25.359,9.828C24.531,10.109 23.719,10.438 23,10.812L17.438,7.391C15.562,6.344 13.484,6.547 11.953,8.078L7.922,12.062C6.328,13.641 6.156,15.797 7.344,17.734L10.656,23.25C10.359,23.906 10.031,24.688 9.766,25.531L3.547,27.016C1.359,27.531 0,29.25 0,31.469L0,37.016C0,39.266 1.359,40.984 3.547,41.484L9.828,42.984C10.078,43.75 10.375,44.5 10.703,45.219L7.406,50.609C6.219,52.469 6.406,54.797 7.969,56.359L11.938,60.328C13.484,61.891 15.75,62.078 17.609,60.922L23.031,57.578C23.766,57.906 24.531,58.234 25.359,58.516L26.859,64.75C27.375,66.922 29.078,68.297 31.328,68.297ZM34.172,44.922C28.219,44.922 23.406,40.109 23.406,34.156C23.406,28.203 28.219,23.391 34.172,23.391C40.109,23.391 44.922,28.203 44.922,34.156C44.922,40.109 40.109,44.922 34.172,44.922Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1328,3 +1328,89 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
background-color: #85858541;
border-radius: 2px;
}
.permissions-matrix-table .ant-table-tbody .ant-table-cell {
vertical-align: middle;
}
.permissions-matrix-table .ant-table-thead > tr > th {
vertical-align: bottom;
padding-top: 12px;
padding-bottom: 12px;
}
.permissions-matrix-table .ant-table-thead .ant-table-column-title {
display: flex;
align-items: flex-end;
justify-content: center;
height: 100%;
}
.permissions-matrix-header {
writing-mode: vertical-rl;
transform: rotate(180deg);
white-space: nowrap;
line-height: 1.2;
display: inline-block;
}
.permission-checkbox {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
margin: 0;
border: none;
background: transparent;
cursor: pointer;
vertical-align: middle;
flex-shrink: 0;
}
.permission-checkbox:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.permission-checkbox:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: 50%;
}
.permission-checkbox-face {
position: absolute;
inset: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.permission-checkbox-inherit {
border: 1.5px solid;
background: transparent;
box-sizing: border-box;
}
.permission-checkbox[data-state='inherit'] .permission-checkbox-inherit,
.permission-checkbox[data-state='allow'] .permission-checkbox-allow,
.permission-checkbox[data-state='deny'] .permission-checkbox-deny {
opacity: 1;
}
.permission-checkbox-face .anticon {
display: flex;
line-height: 0;
}
.permission-checkbox-face .anticon svg path {
stroke: currentColor;
stroke-width: 8px;
}

View File

@ -0,0 +1,125 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
import NewPermissionSettings from './PermissionSettings/NewPermissionSettings'
import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import SortSidebarButton from '../common/SortSidebarButton'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const PermissionSettingsList = () => {
const [newPermissionSettingsOpen, setNewPermissionSettingsOpen] =
useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('permissionSettings')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('permissionSettings')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('PermissionSettings')
const [showSortSidebar, setShowSortSidebar] =
useSortSidebarVisibility('PermissionSettings')
const actionItems = {
items: [
{
label: 'New Permission Settings',
key: 'newPermissionSettings',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newPermissionSettings') {
setNewPermissionSettingsOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'} style={{ minHeight: 0 }}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='permissionSettings'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='permissionSettings' />
</Space>
<Space>
<SortSidebarButton
active={showSortSidebar}
onClick={() => setShowSortSidebar(!showSortSidebar)}
/>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
showEndingDivider={true}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='permissionSettings'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
showSortSidebar={showSortSidebar}
expandHeight={true}
saveFilterInSession={true}
saveFilterInUrl={true}
useFilterInSession={true}
useFilterInUrl={true}
saveSortInSession={true}
saveSortInUrl={true}
useSortInSession={true}
useSortInUrl={true}
/>
</Flex>
<Modal
open={newPermissionSettingsOpen}
onCancel={() => setNewPermissionSettingsOpen(false)}
footer={null}
destroyOnHidden={true}
width={700}
>
<NewPermissionSettings
onOk={() => {
setNewPermissionSettingsOpen(false)
tableRef.current?.reload()
}}
reset={!newPermissionSettingsOpen}
/>
</Modal>
</>
)
}
export default PermissionSettingsList

View File

@ -0,0 +1,77 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewPermissionSettings = ({ onOk, defaultValues }) => {
return (
<NewObjectForm
type={'permissionSettings'}
defaultValues={{ permissions: {}, ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='permissionSettings'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
labelWidth={100}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='permissionSettings'
column={1}
bordered={false}
visibleProperties={{
_id: false,
_reference: false,
createdAt: false,
updatedAt: false,
permissions: false
}}
objectPropertyProps={{ scrollHeight: '320px' }}
labelWidth={130}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Permission Settings'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewPermissionSettings.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewPermissionSettings

View File

@ -0,0 +1,256 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Card, Flex, Space } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import useCollapseState from '../../hooks/useCollapseState'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm'
import EditButtons from '../../common/EditButtons'
import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx'
import ActivityIndicator from '../../common/ActivityIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import {
getModelByName,
getModelProperty
} from '../../../../database/ObjectModels.js'
import ScrollBox from '../../common/ScrollBox.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import PermissionSettingsIcon from '../../../Icons/PermissionSettingsIcon.jsx'
const PermissionSettingsInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const permissionSettingsId = new URLSearchParams(location.search).get(
'permissionSettingsId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'PermissionSettingsInfo',
{
info: true,
notes: true,
matrix: true,
auditLogs: false
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
lock: null,
loading: false,
editDisabled: false,
objectData: {}
})
const actions = {
edit: () => {
objectFormRef?.current?.startEditing?.()
return false
},
cancelEdit: () => {
objectFormRef?.current?.cancelEditing?.()
return true
},
finishEdit: () => {
objectFormRef?.current?.handleUpdate?.()
return true
},
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='small'>
<Space size='small'>
<ObjectActions
type='permissionSettings'
id={permissionSettingsId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Permission Settings Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='permissionSettings'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='permissionSettings'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<ActivityIndicator
activities={objectFormState.activities}
showStartingDivider={true}
/>
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={
objectFormState.beingEditedByOther ||
objectFormState.loading ||
objectFormState.editDisabled
}
loading={objectFormState.editLoading}
/>
<ObjectTableNavigationButtons
disabled={objectFormState.loading || objectFormState.isEditing}
_id={permissionSettingsId}
objectType='permissionSettings'
showEndingDivider={true}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={permissionSettingsId}
type='permissionSettings'
style={{ height: '100%' }}
ref={objectFormRef}
setCurrentObject={true}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Permission Settings Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='permissionSettings'
objectData={objectData}
visibleProperties={{
permissions: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Permission Settings Matrix'
icon={<PermissionSettingsIcon />}
active={collapseState.matrix}
onToggle={(expanded) =>
updateCollapseState('matrix', expanded)
}
collapseKey='matrix'
>
<Card>
<ObjectProperty
{...getModelProperty(
'permissionSettings',
'permissions'
)}
isEditing={isEditing}
objectData={objectData}
loading={loading}
/>
</Card>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel
_id={permissionSettingsId}
type='permissionSettings'
/>
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{
parent:
getModelByName('permissionSettings').prefix +
':' +
permissionSettingsId
}}
visibleColumns={{ _id: false, parent: false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default PermissionSettingsInfo

View File

@ -23,8 +23,12 @@ import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js'
import {
getModelProperty,
getModelByName
} from '../../../../database/ObjectModels.js'
import { useMediaQuery } from 'react-responsive'
import PersonIcon from '../../../Icons/PersonIcon.jsx'
const UserInfo = () => {
const location = useLocation()
@ -184,7 +188,8 @@ const UserInfo = () => {
type='user'
objectData={objectData}
visibleProperties={{
profileImage: false
profileImage: false,
permissions: false
}}
/>
</Flex>
@ -192,6 +197,24 @@ const UserInfo = () => {
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='User Permissions'
icon={<PersonIcon />}
active={collapseState.permissions}
onToggle={(expanded) =>
updateCollapseState('permissions', expanded)
}
collapseKey='permissions'
>
<Card>
<ObjectProperty
{...getModelProperty('user', 'permissions')}
isEditing={objectFormState.isEditing}
objectData={objectFormState.objectData}
loading={objectFormState.loading}
/>
</Card>
</InfoCollapse>
<InfoCollapse
title='App Passwords'
icon={<AppPasswordIcon />}
@ -206,7 +229,9 @@ const UserInfo = () => {
) : (
<ObjectTable
type='appPassword'
masterFilter={{ user: getModelByName('user').prefix + ':' + userId }}
masterFilter={{
user: getModelByName('user').prefix + ':' + userId
}}
visibleColumns={{ user: false }}
ref={appPasswordsTableRef}
/>
@ -236,10 +261,7 @@ const UserInfo = () => {
<ObjectTable
type='auditLog'
masterFilter={{
parent:
getModelByName('user').prefix +
':' +
userId
parent: getModelByName('user').prefix + ':' + userId
}}
visibleColumns={{ _id: false, parent: false }}
/>

View File

@ -15,6 +15,7 @@ const breadcrumbNameMap = {
finance: 'Finance',
sales: 'Sales',
productcategories: 'Product Categories',
permissionSettings: 'Permission Settings',
overview: 'Overview',
info: 'Info',
design: 'Design',

View File

@ -52,6 +52,7 @@ import AddressDisplay from './AddressDisplay'
import ArrayDisplay from './ArrayDisplay'
import ArrayInput from './ArrayInput'
import ElipsisText from './ElipsisText'
import PermissionsMatrix from './PermissionsMatrix'
import { round } from '../utils/Utils'
const { Text } = Typography
@ -133,6 +134,8 @@ const ObjectProperty = ({
columns = [],
style = {},
hiddenPropertyWidth = '100px',
inherit = true,
scrollHeight,
...rest
}) => {
if (value && typeof value == 'function' && objectData) {
@ -616,6 +619,16 @@ const ObjectProperty = ({
case 'data': {
return <DataTree data={value} />
}
case 'permissionsMatrix': {
return (
<PermissionsMatrix
value={value}
disabled={true}
inherit={inherit}
scrollHeight={scrollHeight}
/>
)
}
case 'file': {
if (value == null || value?.length == 0 || value == undefined) {
return (
@ -1006,6 +1019,15 @@ const ObjectProperty = ({
/>
)
}
case 'permissionsMatrix':
return (
<PermissionsMatrix
inherit={inherit}
scrollHeight={scrollHeight}
disabled={disabled}
{...inputProps}
/>
)
default:
return <Input placeholder={label} {...inputProps} />
}
@ -1073,7 +1095,9 @@ ObjectProperty.propTypes = {
showCard: PropTypes.bool,
showDownload: PropTypes.bool,
style: PropTypes.object,
hiddenPropertyWidth: PropTypes.string
hiddenPropertyWidth: PropTypes.string,
inherit: PropTypes.bool,
scrollHeight: PropTypes.string
}
export default ObjectProperty

View File

@ -0,0 +1,60 @@
import PropTypes from 'prop-types'
import { theme } from 'antd'
import CheckIcon from '../../Icons/CheckIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
const ICON_STYLE = {
fontSize: 7,
color: '#ffffff'
}
const PermissionCheckbox = ({
checked = false,
indeterminate = false,
disabled = false,
onChange
}) => {
const { token } = theme.useToken()
const state = indeterminate ? 'inherit' : checked ? 'allow' : 'deny'
return (
<button
type='button'
className='permission-checkbox'
data-state={state}
disabled={disabled}
aria-checked={indeterminate ? 'mixed' : checked}
role='checkbox'
onClick={(event) => {
if (disabled || typeof onChange !== 'function') return
onChange(event)
}}
>
<span
className='permission-checkbox-face permission-checkbox-inherit'
style={{ borderColor: token.colorBorder }}
/>
<span
className='permission-checkbox-face permission-checkbox-allow'
style={{ backgroundColor: token.colorSuccess }}
>
<CheckIcon style={ICON_STYLE} />
</span>
<span
className='permission-checkbox-face permission-checkbox-deny'
style={{ backgroundColor: token.colorError }}
>
<XMarkIcon style={ICON_STYLE} />
</span>
</button>
)
}
PermissionCheckbox.propTypes = {
checked: PropTypes.bool,
indeterminate: PropTypes.bool,
disabled: PropTypes.bool,
onChange: PropTypes.func
}
export default PermissionCheckbox

View File

@ -0,0 +1,178 @@
import { useMemo, useCallback, createElement } from 'react'
import PropTypes from 'prop-types'
import { Table, Flex, Tooltip } from 'antd'
import { useMediaQuery } from 'react-responsive'
import { ElectronContext } from '../context/ElectronContext'
import { useContext } from 'react'
import {
getPermissionMatrixModels,
getPermissionMatrixActions,
modelHasPermissionAction
} from '../../../database/ObjectModels'
import PermissionCheckbox from './PermissionCheckbox'
const DEFAULT_SCROLL_HEIGHT = 'calc(var(--unit-100vh) - 258px)'
const getPermissionState = (permissions, modelName, actionName) => {
const value = permissions?.[modelName]?.[actionName]
if (value === true) return true
if (value === false) return false
return null
}
const setPermissionState = (permissions, modelName, actionName, next) => {
const current =
permissions && typeof permissions === 'object' ? permissions : {}
const modelPermissions = {
...(current[modelName] && typeof current[modelName] === 'object'
? current[modelName]
: {})
}
if (next === null) {
delete modelPermissions[actionName]
} else {
modelPermissions[actionName] = next
}
const nextPermissions = { ...current }
if (Object.keys(modelPermissions).length === 0) {
delete nextPermissions[modelName]
} else {
nextPermissions[modelName] = modelPermissions
}
return nextPermissions
}
const cyclePermissionState = (current, inherit) => {
if (!inherit) {
return current === true ? false : true
}
if (current === true) return false
if (current === false) return null
return true
}
const PermissionsMatrix = ({
value,
onChange,
disabled = false,
inherit = true,
scrollHeight = DEFAULT_SCROLL_HEIGHT,
size = 'middle'
}) => {
const { isElectron } = useContext(ElectronContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
const models = useMemo(() => getPermissionMatrixModels(), [])
const actions = useMemo(() => getPermissionMatrixActions(), [])
const permissions = value && typeof value === 'object' ? value : {}
var adjustedScrollHeight = scrollHeight
if (isMobile) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 298px)'
}
if (isElectron) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 238px)'
}
if (isMobile && isElectron) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 282px)'
}
const handleCycle = useCallback(
(modelName, actionName) => {
if (disabled || typeof onChange !== 'function') return
const current = getPermissionState(permissions, modelName, actionName)
const next = cyclePermissionState(current, inherit)
onChange(setPermissionState(permissions, modelName, actionName, next))
},
[disabled, inherit, onChange, permissions]
)
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
)
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>
)
}
}))
return [modelColumn, ...actionColumns]
}, [actions, disabled, handleCycle, inherit, isMobile, permissions])
const dataSource = useMemo(
() =>
models.map((model) => ({
key: model.name,
label: model.label,
icon: model.icon,
model
})),
[models]
)
return (
<Table
className='dashboard-table permissions-matrix-table'
dataSource={dataSource}
columns={columns}
pagination={false}
rowKey='key'
bordered={true}
size={size}
scroll={{ x: 'max-content', y: adjustedScrollHeight }}
/>
)
}
PermissionsMatrix.propTypes = {
value: PropTypes.object,
onChange: PropTypes.func,
disabled: PropTypes.bool,
inherit: PropTypes.bool,
scrollHeight: PropTypes.string,
size: PropTypes.string
}
export default PermissionsMatrix

View File

@ -0,0 +1,8 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/permissionsettingsicon.svg?react'
const PermissionSettingsIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
)
export default PermissionSettingsIcon

View File

@ -38,6 +38,7 @@ import SettingsIcon from './SettingsIcon'
import AuditLogIcon from './AuditLogIcon'
import DeveloperIcon from './DeveloperIcon'
import PersonIcon from './PersonIcon'
import PermissionSettingsIcon from './PermissionSettingsIcon'
import HostIcon from './HostIcon'
import DocumentPrinterIcon from './DocumentPrinterIcon'
import DocumentTemplateIcon from './DocumentTemplateIcon'
@ -95,6 +96,7 @@ const sidebarIconMap = {
auditLog: <AuditLogIcon />,
developer: <DeveloperIcon />,
person: <PersonIcon />,
permissionSettings: <PermissionSettingsIcon />,
host: <HostIcon />,
documentPrinter: <DocumentPrinterIcon />,
documentTemplate: <DocumentTemplateIcon />,

View File

@ -31,6 +31,7 @@ import { OrderItem } from './models/OrderItem'
import { Shipment } from './models/Shipment'
import { AuditLog } from './models/AuditLog'
import { User } from './models/User'
import { PermissionSettings } from './models/PermissionSettings.js'
import { AppPassword } from './models/AppPassword.js'
import { NoteType } from './models/NoteType'
import { Note } from './models/Note'
@ -83,6 +84,7 @@ export const objectModels = [
Shipment,
AuditLog,
User,
PermissionSettings,
AppPassword,
NoteType,
Note,
@ -136,6 +138,7 @@ export {
Shipment,
AuditLog,
User,
PermissionSettings,
AppPassword,
NoteType,
Note,
@ -227,6 +230,35 @@ export function searchModelsByLabel(label) {
)
}
const HIDDEN_PERMISSION_ACTIONS = new Set(['cancelEdit', 'finishEdit'])
export function getPermissionMatrixModels() {
return objectModels.filter((model) => model?.name && model.name !== 'unknown')
}
export function getPermissionMatrixActions() {
const seen = new Map()
getPermissionMatrixModels().forEach((model) => {
;(model.actions || []).forEach((action) => {
if (!action?.name || action.type === 'divider') return
if (HIDDEN_PERMISSION_ACTIONS.has(action.name)) return
if (!seen.has(action.name)) {
seen.set(action.name, {
name: action.name,
label: action.label || action.name
})
}
})
})
return [...seen.values()]
}
export function modelHasPermissionAction(model, actionName) {
return (model?.actions || []).some(
(action) => action?.name === actionName && action.type !== 'divider'
)
}
// Utility function to get nested object values
export const getPropertyValue = (obj, path) => {
if (!obj || !path) return undefined

View File

@ -0,0 +1,133 @@
import { createElement, lazy } from 'react'
const PermissionSettingsInfo = lazy(
() =>
import('../../components/Dashboard/Management/PermissionSettings/PermissionSettingsInfo')
)
import PermissionSettingsIcon from '../../components/Icons/PermissionSettingsIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const PermissionSettings = {
name: 'permissionSettings',
label: 'Permission Settings',
labelPlural: 'Permission Settings',
url: '/dashboard/management/permissionsettings',
prefix: 'PMS',
endpoint: 'permissionsettings',
icon: PermissionSettingsIcon,
actions: [
{
name: 'info',
type: 'page',
pageName: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon
},
{
name: 'edit',
type: 'page',
pageName: 'info',
label: 'Edit',
row: true,
icon: EditIcon,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
type: 'page',
pageName: 'info',
icon: XMarkIcon,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'finishEdit',
label: 'Save Edits',
type: 'page',
pageName: 'info',
icon: CheckIcon,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{ type: 'divider' },
{
name: 'delete',
type: 'page',
pageName: 'info',
label: 'Delete',
icon: BinIcon,
danger: true
}
],
pages: [
{
name: 'info',
content: () => createElement(PermissionSettingsInfo)
}
],
columns: ['_reference', 'name', 'createdAt', 'updatedAt'],
filters: ['name', 'createdAt', 'updatedAt', '_reference'],
sorters: ['name', 'createdAt', '_id', 'updatedAt'],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'permissionSettings',
showCopy: true,
columnWidth: 140
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
objectType: 'permissionSettings',
showCopy: true,
readOnly: true,
columnWidth: 180
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'name',
label: 'Name',
columnFixed: 'left',
required: true,
type: 'text',
columnWidth: 200
},
{
name: 'permissions',
label: 'Permissions',
type: 'permissionsMatrix',
inherit: true,
span: 2,
columnWidth: 200
}
]
}

View File

@ -87,13 +87,22 @@ export const User = {
content: () => createElement(UserInfo)
}
],
columns: ['_reference', 'name', 'username', 'email', 'role', 'createdAt', 'updatedAt'],
columns: [
'_reference',
'name',
'username',
'email',
'permissionSettings',
'createdAt',
'updatedAt'
],
filters: [
'name',
'username',
'firstName',
'lastName',
'email',
'permissionSettings',
'createdAt',
'updatedAt',
'_reference'
@ -169,6 +178,23 @@ export const User = {
columnWidth: 300,
type: 'email'
},
{
name: 'permissionSettings',
label: 'Permission Settings',
type: 'objectList',
objectType: 'permissionSettings',
span: 2,
columnWidth: 250
},
{
name: 'permissions',
label: 'Permissions',
type: 'permissionsMatrix',
inherit: false,
readOnly: true,
span: 2,
columnWidth: 200
},
{
name: 'profileImage',
label: 'Profile Image',

View File

@ -125,6 +125,12 @@ const managementSidebarItems = [
label: 'Users',
path: '/dashboard/management/users'
},
{
key: 'permissionSettings',
iconKey: 'permissionSettings',
label: 'Permission Settings',
path: '/dashboard/management/permissionsettings'
},
{
key: 'appPasswords',
iconKey: 'appPassword',

View File

@ -43,6 +43,9 @@ const NoteInfo = lazy(
() => import('../components/Dashboard/Management/Notes/NoteInfo.jsx')
)
const Users = lazy(() => import('../components/Dashboard/Management/Users.jsx'))
const PermissionSettings = lazy(
() => import('../components/Dashboard/Management/PermissionSettings.jsx')
)
const AppPasswords = lazy(
() => import('../components/Dashboard/Management/AppPasswords.jsx')
)
@ -92,6 +95,11 @@ const ManagementRoutes = [
<Route key='vendors' path='management/vendors' element={<Vendors />} />,
<Route key='hosts' path='management/hosts' element={<Hosts />} />,
<Route key='users' path='management/users' element={<Users />} />,
<Route
key='permissionsettings'
path='management/permissionsettings'
element={<PermissionSettings />}
/>,
<Route key='files' path='management/files' element={<Files />} />,
<Route key='materials' path='management/materials' element={<Materials />} />,
<Route key='couriers' path='management/couriers' element={<Couriers />} />,