Compare commits

..

2 Commits

Author SHA1 Message Date
0b501a11c6 Enhance ObjectList and ObjectProperty components with hyperlink support
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Updated ObjectList to accept a new prop, showHyperlink, allowing for conditional rendering of hyperlinks for object items.
- Modified ObjectProperty to pass the showHyperlink prop to ObjectList and ObjectSelect components, enhancing their functionality.
- Improved error handling in ApiServerContext by adding detailed logging for error responses.
- Updated various database models to include showHyperlink attribute for object lists, improving user navigation and interaction.
- Enhanced CSS styles for the permissions matrix table to improve visual clarity and user experience.
2026-08-20 17:26:36 +01:00
f07b0d5eef Enhance PermissionsMatrix with sidebar integration and state management
- Added utility functions for normalizing sidebar paths and collecting metadata from sidebar items.
- Implemented logic to match models with sidebar sections, improving the organization of permissions.
- Enhanced the data source construction for the PermissionsMatrix, allowing for better grouping and display of models.
- Introduced state management for expanded rows and hovered columns to improve user interaction.
- Refactored existing functionality to accommodate new features while maintaining performance and clarity.
2026-08-20 17:20:22 +01:00
13 changed files with 496 additions and 63 deletions

View File

@ -1337,6 +1337,64 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
vertical-align: middle; vertical-align: middle;
} }
.permissions-matrix-table .permissions-matrix-group-row > .ant-table-cell {
font-weight: 600;
background-color: color-mix(
in srgb,
var(--color-text) 6%,
var(--layout-header-bg)
);
}
.permissions-matrix-table
.permissions-matrix-group-row
> .ant-table-cell.ant-table-cell-row-hover {
background: color-mix(
in srgb,
var(--color-text) 10%,
var(--layout-header-bg)
);
}
.permissions-matrix-table .ant-table-row-expand-icon,
.permissions-matrix-table .ant-table-row-indent {
display: none;
}
.permissions-matrix-expand-button.ant-btn {
width: 22px;
min-width: 22px;
height: 22px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
color: inherit;
}
.permissions-matrix-expand-button.ant-btn:hover,
.permissions-matrix-expand-button.ant-btn:focus,
.permissions-matrix-expand-button.ant-btn:focus-visible {
color: inherit;
}
.permissions-matrix-expand-spacer {
display: inline-block;
width: 18px;
height: 22px;
flex-shrink: 0;
}
.permissions-matrix-expand-icon {
font-size: 12px !important;
color: inherit;
transition: transform 0.2s ease;
}
.permissions-matrix-expand-icon-open {
transform: rotate(90deg);
}
.permissions-matrix-table .ant-table-thead > tr > th { .permissions-matrix-table .ant-table-thead > tr > th {
vertical-align: bottom; vertical-align: bottom;
padding-top: 12px; padding-top: 12px;

View File

@ -4,7 +4,7 @@ import { Space, Typography } from 'antd'
const { Text } = Typography const { Text } = Typography
const ObjectList = ({ value, objectType, style }) => { const ObjectList = ({ value, objectType, style, showHyperlink = false }) => {
if (!value || !Array.isArray(value) || value.length === 0) { if (!value || !Array.isArray(value) || value.length === 0) {
return <Text type='secondary'>n/a</Text> return <Text type='secondary'>n/a</Text>
} }
@ -12,7 +12,12 @@ const ObjectList = ({ value, objectType, style }) => {
return ( return (
<Space size={'small'} wrap style={style}> <Space size={'small'} wrap style={style}>
{value.map((item) => ( {value.map((item) => (
<ObjectDisplay object={item} objectType={objectType} key={item._id} /> <ObjectDisplay
object={item}
objectType={objectType}
key={item._id}
showHyperlink={showHyperlink}
/>
))} ))}
</Space> </Space>
) )
@ -21,7 +26,8 @@ const ObjectList = ({ value, objectType, style }) => {
ObjectList.propTypes = { ObjectList.propTypes = {
value: PropTypes.array, value: PropTypes.array,
objectType: PropTypes.string, objectType: PropTypes.string,
style: PropTypes.object style: PropTypes.object,
showHyperlink: PropTypes.bool
} }
export default ObjectList export default ObjectList

View File

@ -475,7 +475,13 @@ const ObjectProperty = ({
} }
} }
case 'objectList': { case 'objectList': {
return <ObjectList value={value} objectType={objectType} /> return (
<ObjectList
value={value}
objectType={objectType}
showHyperlink={showHyperlink}
/>
)
} }
case 'objectChildren': { case 'objectChildren': {
return ( return (
@ -958,7 +964,14 @@ const ObjectProperty = ({
<ObjectTypeSelect masterFilter={masterFilter} {...inputProps} /> <ObjectTypeSelect masterFilter={masterFilter} {...inputProps} />
) )
case 'objectList': case 'objectList':
return <ObjectSelect type={objectType} multiple {...inputProps} /> return (
<ObjectSelect
type={objectType}
multiple
showHyperlink={showHyperlink}
{...inputProps}
/>
)
case 'tags': case 'tags':
return ( return (
<TagsInput <TagsInput

View File

@ -1,15 +1,23 @@
import { useMemo, useCallback, createElement } from 'react' import {
useMemo,
useCallback,
useState,
createElement,
useContext
} from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Table, Flex, Tooltip } from 'antd' import { Table, Flex, Tooltip, Button } from 'antd'
import { useMediaQuery } from 'react-responsive' import { useMediaQuery } from 'react-responsive'
import { ElectronContext } from '../context/ElectronContext' import { ElectronContext } from '../context/ElectronContext'
import { useContext } from 'react'
import { import {
getPermissionMatrixModels, getPermissionMatrixModels,
getPermissionMatrixActions, getPermissionMatrixActions,
modelHasPermissionAction modelHasPermissionAction
} from '../../../database/ObjectModels' } from '../../../database/ObjectModels'
import { getSidebar, getSidebarMenuSections } from '../../../database/Sidebars'
import { getSidebarIconNode } from '../../Icons/sidebarIconMap'
import PermissionCheckbox from './PermissionCheckbox' import PermissionCheckbox from './PermissionCheckbox'
import { CaretRightOutlined } from '@ant-design/icons'
const DEFAULT_SCROLL_HEIGHT = 'calc(var(--unit-100vh) - 258px)' const DEFAULT_SCROLL_HEIGHT = 'calc(var(--unit-100vh) - 258px)'
@ -124,6 +132,167 @@ const getCheckboxTitle = (state, inherit) => {
return 'Deny' return 'Deny'
} }
const normalizePath = (path) => {
if (!path || typeof path !== 'string') return ''
return path.split('?')[0].replace(/\/+$/, '')
}
const collectSidebarMeta = (items, acc = { paths: [], iconKeys: [] }) => {
items.forEach((item) => {
if (item?.type === 'divider') return
if (item.path) acc.paths.push(normalizePath(item.path))
if (item.iconKey) acc.iconKeys.push(item.iconKey)
if (item.children?.length) collectSidebarMeta(item.children, acc)
})
return acc
}
const getModelPaths = (model) => {
const paths = []
const addPath = (url) => {
if (typeof url === 'string') {
paths.push(normalizePath(url))
return
}
if (typeof url === 'function') {
try {
const sample = url('')
if (typeof sample === 'string') {
paths.push(normalizePath(sample.split('?')[0]))
}
} catch {
// ignore uncallable url builders
}
}
}
addPath(model?.url)
;(model?.actions || []).forEach((action) => addPath(action?.url))
return [...new Set(paths.filter(Boolean))]
}
const pathMatches = (modelPath, sidebarPath) =>
modelPath === sidebarPath || modelPath.startsWith(`${sidebarPath}/`)
const getModelSectionOrder = (model, section) => {
const modelPaths = getModelPaths(model)
const pathIndex = section.paths.findIndex((sidebarPath) =>
modelPaths.some((modelPath) => pathMatches(modelPath, sidebarPath))
)
if (pathIndex !== -1) return pathIndex
const iconIndex = section.iconKeys.findIndex(
(iconKey) => iconKey === model.name
)
if (iconIndex !== -1) return iconIndex
return Number.MAX_SAFE_INTEGER
}
const modelMatchesSection = (model, section) => {
const modelPaths = getModelPaths(model)
if (
modelPaths.some((modelPath) =>
section.paths.some((sidebarPath) => pathMatches(modelPath, sidebarPath))
)
) {
return true
}
if (section.iconKeys.includes(model.name)) return true
const prefix = `/dashboard/${section.key}`
return modelPaths.some((modelPath) => pathMatches(modelPath, prefix))
}
const getRecordModels = (record) => {
if (record?.isGroup) {
return (record.children || []).map((child) => child.model).filter(Boolean)
}
return record?.model ? [record.model] : []
}
const getModelsRowPermissionStates = (permissions, models, actions, inherit) =>
models.flatMap((model) =>
getRowPermissionStates(permissions, model, actions, inherit)
)
const setModelsRowPermissionState = (permissions, models, actions, next) => {
let nextPermissions = permissions
models.forEach((model) => {
nextPermissions = setRowPermissionState(
nextPermissions,
model,
actions,
next
)
})
return nextPermissions
}
const buildPermissionMatrixDataSource = (models) => {
const assigned = new Set()
const sections = getSidebarMenuSections({ includeDev: false }).map(
(section) => {
const sidebar = getSidebar(section.key)
const meta = collectSidebarMeta(section.items || [])
Object.keys(sidebar?.routeAliases || {}).forEach((path) => {
meta.paths.push(normalizePath(path))
})
return {
key: section.key,
label: section.label,
iconKey: section.iconKey,
paths: meta.paths,
iconKeys: meta.iconKeys
}
}
)
const dataSource = []
sections.forEach((section) => {
const sectionModels = models
.filter(
(model) =>
!assigned.has(model.name) && modelMatchesSection(model, section)
)
.sort(
(a, b) =>
getModelSectionOrder(a, section) - getModelSectionOrder(b, section)
)
sectionModels.forEach((model) => assigned.add(model.name))
if (sectionModels.length === 0) return
dataSource.push({
key: `section-${section.key}`,
isGroup: true,
label: section.label,
iconNode: getSidebarIconNode(section.iconKey),
children: sectionModels.map((model) => ({
key: model.name,
label: model.label,
icon: model.icon,
model
}))
})
})
const leftover = models.filter((model) => !assigned.has(model.name))
if (leftover.length > 0) {
dataSource.push({
key: 'section-other',
isGroup: true,
label: 'Other',
iconNode: null,
children: leftover.map((model) => ({
key: model.name,
label: model.label,
icon: model.icon,
model
}))
})
}
return dataSource
}
const PermissionsMatrix = ({ const PermissionsMatrix = ({
value, value,
onChange, onChange,
@ -136,7 +305,50 @@ const PermissionsMatrix = ({
const isMobile = useMediaQuery({ maxWidth: 768 }) const isMobile = useMediaQuery({ maxWidth: 768 })
const models = useMemo(() => getPermissionMatrixModels(), []) const models = useMemo(() => getPermissionMatrixModels(), [])
const actions = useMemo(() => getPermissionMatrixActions(), []) const actions = useMemo(() => getPermissionMatrixActions(), [])
const permissions = value && typeof value === 'object' ? value : {} const permissions = useMemo(
() => (value && typeof value === 'object' ? value : {}),
[value]
)
const dataSource = useMemo(
() => buildPermissionMatrixDataSource(models),
[models]
)
const [expandedRowKeys, setExpandedRowKeys] = useState(null)
const [hoveredColumnKey, setHoveredColumnKey] = useState(null)
const resolvedExpandedRowKeys = useMemo(
() => expandedRowKeys ?? dataSource.map((row) => row.key),
[dataSource, expandedRowKeys]
)
const isSectionExpanded = useCallback(
(record) => resolvedExpandedRowKeys.includes(record.key),
[resolvedExpandedRowKeys]
)
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]
})
},
[dataSource]
)
const getColumnHoverProps = useCallback(
(columnKey, isHeader = false) => ({
onMouseEnter: () => setHoveredColumnKey(columnKey),
className:
!isHeader && hoveredColumnKey === columnKey
? 'ant-table-cell-row-hover'
: undefined
}),
[hoveredColumnKey]
)
var adjustedScrollHeight = scrollHeight var adjustedScrollHeight = scrollHeight
if (isMobile) { if (isMobile) {
@ -159,21 +371,53 @@ const PermissionsMatrix = ({
[disabled, inherit, onChange, permissions] [disabled, inherit, onChange, permissions]
) )
const handleCycleRow = useCallback( const handleCycleModels = useCallback(
(model) => { (recordModels) => {
if (disabled || typeof onChange !== 'function') return if (
disabled ||
typeof onChange !== 'function' ||
recordModels.length === 0
) {
return
}
const aggregate = getAggregatePermissionState( const aggregate = getAggregatePermissionState(
getRowPermissionStates(permissions, model, actions, inherit) getModelsRowPermissionStates(
permissions,
recordModels,
actions,
inherit
)
) )
const next = cyclePermissionState( const next = cyclePermissionState(
aggregate === 'mixed' ? null : aggregate, aggregate === 'mixed' ? null : aggregate,
inherit inherit
) )
onChange(setRowPermissionState(permissions, model, actions, next)) onChange(
setModelsRowPermissionState(permissions, recordModels, actions, next)
)
}, },
[actions, disabled, inherit, onChange, permissions] [actions, disabled, inherit, onChange, permissions]
) )
const handleCycleModelsAction = useCallback(
(recordModels, action) => {
if (disabled || typeof onChange !== 'function') return
const applicable = recordModels.filter((model) =>
modelHasPermissionAction(model, action.name)
)
if (applicable.length === 0) return
const aggregate = getAggregatePermissionState(
getColumnPermissionStates(permissions, applicable, action, inherit)
)
const next = cyclePermissionState(
aggregate === 'mixed' ? null : aggregate,
inherit
)
onChange(setColumnPermissionState(permissions, applicable, action, next))
},
[disabled, inherit, onChange, permissions]
)
const handleCycleColumn = useCallback( const handleCycleColumn = useCallback(
(action) => { (action) => {
if (disabled || typeof onChange !== 'function') return if (disabled || typeof onChange !== 'function') return
@ -218,10 +462,18 @@ const PermissionsMatrix = ({
key: 'model', key: 'model',
dataIndex: 'label', dataIndex: 'label',
fixed: isMobile ? undefined : 'left', fixed: isMobile ? undefined : 'left',
width: 248, width: 280,
onHeaderCell: () => getColumnHoverProps('model', true),
onCell: () => getColumnHoverProps('model'),
render: (label, record) => { render: (label, record) => {
const recordModels = getRecordModels(record)
const aggregate = getAggregatePermissionState( const aggregate = getAggregatePermissionState(
getRowPermissionStates(permissions, record.model, actions, inherit) getModelsRowPermissionStates(
permissions,
recordModels,
actions,
inherit
)
) )
return ( return (
@ -231,13 +483,52 @@ const PermissionsMatrix = ({
justify='space-between' justify='space-between'
style={{ width: '100%', paddingRight: '10px' }} style={{ width: '100%', paddingRight: '10px' }}
> >
<Flex align='center' gap='small' style={{ marginLeft: '6px' }}> <Flex
{record.icon ? createElement(record.icon) : null} align='center'
gap='small'
style={{
marginLeft: record.isGroup ? '0px' : '6px',
fontWeight: record.isGroup ? 600 : undefined
}}
>
{record.isGroup ? (
<Button
type='text'
size='small'
className='permissions-matrix-expand-button'
aria-expanded={isSectionExpanded(record)}
aria-label={
isSectionExpanded(record)
? 'Collapse section'
: 'Expand section'
}
icon={
<CaretRightOutlined
className={
isSectionExpanded(record)
? 'permissions-matrix-expand-icon permissions-matrix-expand-icon-open'
: 'permissions-matrix-expand-icon'
}
/>
}
onClick={(event) => {
event.stopPropagation()
handleToggleSection(record)
}}
/>
) : (
<span className='permissions-matrix-expand-spacer' />
)}
{record.iconNode
? record.iconNode
: record.icon
? createElement(record.icon)
: null}
{label} {label}
</Flex> </Flex>
<div style={{ paddingBottom: '1px' }}> <div style={{ paddingBottom: '1px' }}>
{renderCheckbox(aggregate, () => { {renderCheckbox(aggregate, () => {
handleCycleRow(record.model) handleCycleModels(recordModels)
})} })}
</div> </div>
</Flex> </Flex>
@ -252,7 +543,13 @@ const PermissionsMatrix = ({
return { return {
title: ( title: (
<Flex vertical align='center' gap={12} justify='flex-end'> <Flex
vertical
align='center'
gap={12}
justify='flex-end'
style={{ paddingTop: '8px' }}
>
<span className='permissions-matrix-header'>{action.label}</span> <span className='permissions-matrix-header'>{action.label}</span>
<div style={{ paddingBottom: '2px' }}> <div style={{ paddingBottom: '2px' }}>
{renderCheckbox(columnAggregate, () => { {renderCheckbox(columnAggregate, () => {
@ -264,18 +561,31 @@ const PermissionsMatrix = ({
key: action.name, key: action.name,
width: 48, width: 48,
align: 'center', align: 'center',
onHeaderCell: () => getColumnHoverProps(action.name, true),
onCell: () => getColumnHoverProps(action.name),
render: (_, record) => { render: (_, record) => {
if (!modelHasPermissionAction(record.model, action.name)) { const recordModels = getRecordModels(record)
return null const applicable = recordModels.filter((model) =>
} modelHasPermissionAction(model, action.name)
)
if (applicable.length === 0) return null
if (!record.isGroup && applicable.length === 1) {
const state = getPermissionState( const state = getPermissionState(
permissions, permissions,
record.model.name, applicable[0].name,
action.name action.name
) )
return renderCheckbox(state, () => { return renderCheckbox(state, () => {
handleCycle(record.model.name, action.name) handleCycle(applicable[0].name, action.name)
})
}
const state = getAggregatePermissionState(
getColumnPermissionStates(permissions, applicable, action, inherit)
)
return renderCheckbox(state, () => {
handleCycleModelsAction(applicable, action)
}) })
} }
} }
@ -284,28 +594,22 @@ const PermissionsMatrix = ({
return [modelColumn, ...actionColumns] return [modelColumn, ...actionColumns]
}, [ }, [
actions, actions,
getColumnHoverProps,
handleCycle, handleCycle,
handleCycleColumn, handleCycleColumn,
handleCycleRow, handleCycleModels,
handleCycleModelsAction,
handleToggleSection,
inherit, inherit,
isMobile, isMobile,
isSectionExpanded,
models, models,
permissions, permissions,
renderCheckbox renderCheckbox
]) ])
const dataSource = useMemo(
() =>
models.map((model) => ({
key: model.name,
label: model.label,
icon: model.icon,
model
})),
[models]
)
return ( return (
<div onMouseLeave={() => setHoveredColumnKey(null)}>
<Table <Table
className='dashboard-table permissions-matrix-table' className='dashboard-table permissions-matrix-table'
dataSource={dataSource} dataSource={dataSource}
@ -315,7 +619,17 @@ const PermissionsMatrix = ({
bordered={true} bordered={true}
size={size} size={size}
scroll={{ x: 'max-content', y: adjustedScrollHeight }} scroll={{ x: 'max-content', y: adjustedScrollHeight }}
rowClassName={(record) =>
record.isGroup ? 'permissions-matrix-group-row' : ''
}
expandable={{
expandedRowKeys: resolvedExpandedRowKeys,
onExpandedRowsChange: setExpandedRowKeys,
indentSize: 0,
expandIcon: () => null
}}
/> />
</div>
) )
} }

View File

@ -1015,6 +1015,16 @@ const ApiServerProvider = ({ children }) => {
const showError = useCallback( const showError = useCallback(
(error, callback = null) => { (error, callback = null) => {
console.error(error)
console.error(error.response)
console.error(error.response.data)
console.error(error.response.data.code)
console.error(error.response.data.error)
console.error(error.response.data.message)
console.error(error.response.data.status)
console.error(error.response.data.timestamp)
console.error(error.response.data.path)
console.error(error.response.data.method)
const code = error.response.data.code || 'UNKNOWN' const code = error.response.data.code || 'UNKNOWN'
if (code == 'UNAUTHORIZED') { if (code == 'UNAUTHORIZED') {
setUnauthenticated() setUnauthenticated()

View File

@ -208,7 +208,8 @@ export const DocumentTemplate = {
label: 'Document Printers', label: 'Document Printers',
required: false, required: false,
type: 'objectList', type: 'objectList',
objectType: 'documentPrinter' objectType: 'documentPrinter',
showHyperlink: true
}, },
{ {
name: 'content', name: 'content',

View File

@ -1,7 +1,8 @@
import { createElement, lazy } from 'react' import { createElement, lazy } from 'react'
const FilamentProfileInfo = lazy( const FilamentProfileInfo = lazy(
() => import('../../components/Dashboard/Production/FilamentProfiles/FilamentProfileInfo') () =>
import('../../components/Dashboard/Production/FilamentProfiles/FilamentProfileInfo')
) )
import FilamentProfileIcon from '../../components/Icons/FilamentProfileIcon' import FilamentProfileIcon from '../../components/Icons/FilamentProfileIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
@ -75,8 +76,15 @@ export const FilamentProfile = {
content: () => createElement(FilamentProfileInfo) content: () => createElement(FilamentProfileInfo)
} }
], ],
columns: ['_reference', 'name', 'filamentType', 'filament', 'createdAt', 'updatedAt'], columns: [
filters: ['_id','name','createdAt','updatedAt','_reference'], '_reference',
'name',
'filamentType',
'filament',
'createdAt',
'updatedAt'
],
filters: ['_id', 'name', 'createdAt', 'updatedAt', '_reference'],
sorters: ['name', 'createdAt', 'updatedAt'], sorters: ['name', 'createdAt', 'updatedAt'],
properties: [ properties: [
{ {
@ -1203,7 +1211,8 @@ export const FilamentProfile = {
type: 'objectList', type: 'objectList',
objectType: 'printerProfile', objectType: 'printerProfile',
span: 2, span: 2,
columnWidth: 220 columnWidth: 220,
showHyperlink: true
}, },
{ {
name: 'compatiblePrintersCondition', name: 'compatiblePrintersCondition',

View File

@ -270,7 +270,8 @@ export const Host = {
objectType: 'file', objectType: 'file',
required: false, required: false,
readOnly: true, readOnly: true,
columnWidth: 200 columnWidth: 200,
showHyperlink: true
} }
] ]
} }

View File

@ -245,7 +245,8 @@ export const Job = {
type: 'objectList', type: 'objectList',
objectType: 'printer', objectType: 'printer',
required: true, required: true,
columnWidth: 200 columnWidth: 200,
showHyperlink: true
} }
], ],
stats: [ stats: [

View File

@ -4,7 +4,8 @@ const ListingInfo = lazy(
() => import('../../components/Dashboard/Sales/Listings/ListingInfo') () => import('../../components/Dashboard/Sales/Listings/ListingInfo')
) )
const NewListingVarient = lazy( const NewListingVarient = lazy(
() => import('../../components/Dashboard/Sales/ListingVarients/NewListingVarient') () =>
import('../../components/Dashboard/Sales/ListingVarients/NewListingVarient')
) )
const PublishListing = lazy( const PublishListing = lazy(
() => import('../../components/Dashboard/Sales/Listings/PublishListing') () => import('../../components/Dashboard/Sales/Listings/PublishListing')
@ -76,7 +77,11 @@ export const Listing = {
label: 'New Listing Varient', label: 'New Listing Varient',
icon: PlusIcon, icon: PlusIcon,
content: (objectData, { onOk } = {}) => { content: (objectData, { onOk } = {}) => {
return createElement(NewListingVarient, { defaultValues: { listing: objectData }, onOk, reset: true }) return createElement(NewListingVarient, {
defaultValues: { listing: objectData },
onOk,
reset: true
})
} }
}, },
{ type: 'divider' }, { type: 'divider' },
@ -308,7 +313,7 @@ export const Listing = {
name: 'courierServices', name: 'courierServices',
label: 'Courier Services', label: 'Courier Services',
type: 'objectList', type: 'objectList',
multiple: true, showHyperlink: true,
objectType: 'courierService', objectType: 'courierService',
readOnly: false, readOnly: false,
required: true, required: true,

View File

@ -482,6 +482,7 @@ export const Printer = {
name: 'queue', name: 'queue',
label: 'Queue', label: 'Queue',
type: 'objectList', type: 'objectList',
showHyperlink: true,
objectType: 'subJob', objectType: 'subJob',
required: false, required: false,
readOnly: true, readOnly: true,

View File

@ -185,6 +185,7 @@ export const User = {
name: 'groups', name: 'groups',
label: 'Groups', label: 'Groups',
type: 'objectList', type: 'objectList',
showHyperlink: true,
objectType: 'userGroup', objectType: 'userGroup',
span: 2, span: 2,
columnWidth: 250 columnWidth: 250

View File

@ -75,8 +75,20 @@ export const UserGroup = {
content: () => createElement(UserGroupInfo) content: () => createElement(UserGroupInfo)
} }
], ],
columns: ['_reference', 'name', 'permissionSettings', 'createdAt', 'updatedAt'], columns: [
filters: ['name', 'permissionSettings', 'createdAt', 'updatedAt', '_reference'], '_reference',
'name',
'permissionSettings',
'createdAt',
'updatedAt'
],
filters: [
'name',
'permissionSettings',
'createdAt',
'updatedAt',
'_reference'
],
sorters: ['name', 'createdAt', '_id', 'updatedAt'], sorters: ['name', 'createdAt', '_id', 'updatedAt'],
properties: [ properties: [
{ {
@ -126,7 +138,8 @@ export const UserGroup = {
type: 'objectList', type: 'objectList',
objectType: 'permissionSetting', objectType: 'permissionSetting',
span: 2, span: 2,
columnWidth: 250 columnWidth: 250,
showHyperlink: true
}, },
{ {
name: 'permissions', name: 'permissions',