Compare commits
10 Commits
1e7697f1d5
...
97f0c816ad
| Author | SHA1 | Date | |
|---|---|---|---|
| 97f0c816ad | |||
| 8198f6a607 | |||
| 2190da21ba | |||
| 87a6198173 | |||
| df85d99aaf | |||
| 0594cf82cd | |||
| 986e5fcff5 | |||
| 11f75cab04 | |||
| 2eed3a518c | |||
| b288ce327f |
@ -314,3 +314,7 @@ body {
|
|||||||
.ant-select-selection-item .ant-tag {
|
.ant-select-selection-item .ant-tag {
|
||||||
margin-left: 1px !important;
|
margin-left: 1px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-badge.ant-badge-status {
|
||||||
|
line-height: 18.5px;
|
||||||
|
}
|
||||||
|
|||||||
@ -68,12 +68,14 @@ const NewDocumentJob = ({ onOk, defaultValues = {} }) => {
|
|||||||
}
|
}
|
||||||
onSubmit={async () => {
|
onSubmit={async () => {
|
||||||
const newDocumentJob = await handleSubmit()
|
const newDocumentJob = await handleSubmit()
|
||||||
if (newDocumentJob.sendToFile == true) {
|
await sendObjectAction(
|
||||||
sendObjectAction(newDocumentJob._id, 'documentJob', {
|
newDocumentJob.documentPrinter._id,
|
||||||
type: 'print',
|
'documentPrinter',
|
||||||
|
{
|
||||||
|
type: 'deploy',
|
||||||
data: newDocumentJob
|
data: newDocumentJob
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
if (onOk) {
|
if (onOk) {
|
||||||
onOk()
|
onOk()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
|||||||
import HostOTP from './HostOtp.jsx'
|
import HostOTP from './HostOtp.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
|
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
|
||||||
|
import DocumentPrinterIcon from '../../../Icons/DocumentPrinterIcon.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('HostInfo')
|
const log = loglevel.getLogger('HostInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -34,6 +35,7 @@ const HostInfo = () => {
|
|||||||
const [collapseState, updateCollapseState] = useCollapseState('HostInfo', {
|
const [collapseState, updateCollapseState] = useCollapseState('HostInfo', {
|
||||||
info: true,
|
info: true,
|
||||||
printers: true,
|
printers: true,
|
||||||
|
documentPrinters: true,
|
||||||
notes: true,
|
notes: true,
|
||||||
auditLogs: true
|
auditLogs: true
|
||||||
})
|
})
|
||||||
@ -95,6 +97,7 @@ const HostInfo = () => {
|
|||||||
items={[
|
items={[
|
||||||
{ key: 'info', label: 'Host Information' },
|
{ key: 'info', label: 'Host Information' },
|
||||||
{ key: 'printers', label: 'Printers' },
|
{ key: 'printers', label: 'Printers' },
|
||||||
|
{ key: 'documentPrinters', label: 'Document Printers' },
|
||||||
{ key: 'notes', label: 'Notes' },
|
{ key: 'notes', label: 'Notes' },
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
]}
|
]}
|
||||||
@ -149,7 +152,6 @@ const HostInfo = () => {
|
|||||||
style={{ height: '100%' }}
|
style={{ height: '100%' }}
|
||||||
ref={objectFormRef}
|
ref={objectFormRef}
|
||||||
onStateChange={(state) => {
|
onStateChange={(state) => {
|
||||||
console.log('Got edit form state change', state)
|
|
||||||
setEditFormState((prev) => ({ ...prev, ...state }))
|
setEditFormState((prev) => ({ ...prev, ...state }))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -188,6 +190,28 @@ const HostInfo = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Document Printers'
|
||||||
|
icon={<DocumentPrinterIcon />}
|
||||||
|
active={collapseState.documentPrinters}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('documentPrinters', expanded)
|
||||||
|
}
|
||||||
|
collapseKey='documentPrinters'
|
||||||
|
>
|
||||||
|
{objectFormState.loading ? (
|
||||||
|
<InfoCollapsePlaceholder />
|
||||||
|
) : (
|
||||||
|
<ObjectTable
|
||||||
|
type='documentPrinter'
|
||||||
|
masterFilter={{ 'host._id': hostId }}
|
||||||
|
visibleColumns={{
|
||||||
|
host: false,
|
||||||
|
'host._id': false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Notes'
|
title='Notes'
|
||||||
|
|||||||
56
src/components/Dashboard/common/CustomSelect.jsx
Normal file
56
src/components/Dashboard/common/CustomSelect.jsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { Select } from 'antd'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
const CustomSelect = ({
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
|
options = [],
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const prevOptionsRef = useRef(options)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if options have changed
|
||||||
|
const optionsChanged =
|
||||||
|
JSON.stringify(prevOptionsRef.current) !== JSON.stringify(options)
|
||||||
|
|
||||||
|
if (optionsChanged && value !== undefined && value !== null) {
|
||||||
|
// Check if current value exists in new options
|
||||||
|
const valueExists =
|
||||||
|
Array.isArray(options) &&
|
||||||
|
options.some((option) => option.value === value)
|
||||||
|
|
||||||
|
// If value doesn't exist in new options, clear it
|
||||||
|
if (!valueExists && onChange) {
|
||||||
|
onChange(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the ref with current options
|
||||||
|
prevOptionsRef.current = options
|
||||||
|
}, [options, value, onChange])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
options={Array.isArray(options) ? options : []}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSelect.propTypes = {
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
options: PropTypes.array,
|
||||||
|
value: PropTypes.any,
|
||||||
|
onChange: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomSelect
|
||||||
@ -30,13 +30,15 @@ import { getModelByName } from '../../../database/ObjectModels'
|
|||||||
const ObjectForm = forwardRef(
|
const ObjectForm = forwardRef(
|
||||||
({ id, type, style, children, onEdit, onStateChange }, ref) => {
|
({ id, type, style, children, onEdit, onStateChange }, ref) => {
|
||||||
const [objectData, setObjectData] = useState(null)
|
const [objectData, setObjectData] = useState(null)
|
||||||
const [serverObjectData, setServerObjectData] = useState(null)
|
const serverObjectData = useRef(null)
|
||||||
|
const onStateChangeRef = useRef(onStateChange)
|
||||||
const [fetchLoading, setFetchLoading] = useState(true)
|
const [fetchLoading, setFetchLoading] = useState(true)
|
||||||
const [editLoading, setEditLoading] = useState(false)
|
const [editLoading, setEditLoading] = useState(false)
|
||||||
const [lock, setLock] = useState({})
|
const [lock, setLock] = useState({})
|
||||||
const [initialized, setInitialized] = useState(false)
|
const [initialized, setInitialized] = useState(false)
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [formValid, setFormValid] = useState(false)
|
const [formValid, setFormValid] = useState(false)
|
||||||
|
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
const formUpdateValues = Form.useWatch([], form)
|
const formUpdateValues = Form.useWatch([], form)
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
@ -141,26 +143,32 @@ const ObjectForm = forwardRef(
|
|||||||
// Validate form on change (debounced to avoid heavy work on every keystroke)
|
// Validate form on change (debounced to avoid heavy work on every keystroke)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
|
const currentFormValues = form.getFieldsValue()
|
||||||
|
const mergedObjectData = {
|
||||||
|
...serverObjectData.current,
|
||||||
|
...currentFormValues
|
||||||
|
}
|
||||||
|
|
||||||
form
|
form
|
||||||
.validateFields({ validateOnly: true })
|
.validateFields({ validateOnly: true })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setFormValid(true)
|
setFormValid(true)
|
||||||
onStateChange({
|
onStateChangeRef.current({
|
||||||
formValid: true,
|
formValid: true,
|
||||||
objectData: { ...serverObjectData, ...form.getFieldsValue() }
|
objectData: mergedObjectData
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setFormValid(false)
|
setFormValid(false)
|
||||||
onStateChange({
|
onStateChangeRef.current({
|
||||||
formValid: false,
|
formValid: false,
|
||||||
objectData: { ...serverObjectData, ...form.getFieldsValue() }
|
objectData: mergedObjectData
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, 150)
|
}, 150)
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId)
|
return () => clearTimeout(timeoutId)
|
||||||
}, [form, formUpdateValues, onStateChange, serverObjectData])
|
}, [form, formUpdateValues])
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -185,13 +193,13 @@ const ObjectForm = forwardRef(
|
|||||||
const handleFetchObject = useCallback(async () => {
|
const handleFetchObject = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setFetchLoading(true)
|
setFetchLoading(true)
|
||||||
onStateChange({ loading: true })
|
onStateChangeRef.current({ loading: true })
|
||||||
const data = await fetchObject(id, type)
|
const data = await fetchObject(id, type)
|
||||||
const lockEvent = await fetchObjectLock(id, type)
|
const lockEvent = await fetchObjectLock(id, type)
|
||||||
setLock(lockEvent)
|
setLock(lockEvent)
|
||||||
onStateChange({ lock: lockEvent })
|
onStateChangeRef.current({ lock: lockEvent })
|
||||||
setObjectData(data)
|
setObjectData(data)
|
||||||
setServerObjectData(data)
|
serverObjectData.current = data
|
||||||
|
|
||||||
// Calculate and set computed values on initial load
|
// Calculate and set computed values on initial load
|
||||||
const computedValues = calculateComputedValues(data, model)
|
const computedValues = calculateComputedValues(data, model)
|
||||||
@ -199,7 +207,7 @@ const ObjectForm = forwardRef(
|
|||||||
|
|
||||||
form.setFieldsValue(initialFormData)
|
form.setFieldsValue(initialFormData)
|
||||||
setFetchLoading(false)
|
setFetchLoading(false)
|
||||||
onStateChange({ loading: false })
|
onStateChangeRef.current({ loading: false })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
messageApi.error('Failed to fetch object info')
|
messageApi.error('Failed to fetch object info')
|
||||||
@ -218,7 +226,7 @@ const ObjectForm = forwardRef(
|
|||||||
// Update event handler
|
// Update event handler
|
||||||
const updateLockEventHandler = useCallback((value) => {
|
const updateLockEventHandler = useCallback((value) => {
|
||||||
setLock((prev) => {
|
setLock((prev) => {
|
||||||
onStateChange({ lock: { ...prev, ...value } })
|
onStateChangeRef.current({ lock: { ...prev, ...value } })
|
||||||
return { ...prev, ...value }
|
return { ...prev, ...value }
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
@ -260,29 +268,31 @@ const ObjectForm = forwardRef(
|
|||||||
// Debounce objectData updates sent to parent to limit re-renders
|
// Debounce objectData updates sent to parent to limit re-renders
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
onStateChange({ objectData })
|
onStateChangeRef.current({ objectData })
|
||||||
}, 150)
|
}, 150)
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId)
|
return () => clearTimeout(timeoutId)
|
||||||
}, [objectData, onStateChange])
|
}, [objectData])
|
||||||
|
|
||||||
const startEditing = () => {
|
const startEditing = () => {
|
||||||
setIsEditing(true)
|
setIsEditing(true)
|
||||||
onStateChange({ isEditing: true })
|
onStateChangeRef.current({ isEditing: true })
|
||||||
lockObject(id, type)
|
lockObject(id, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelEditing = () => {
|
const cancelEditing = () => {
|
||||||
if (serverObjectData) {
|
if (serverObjectData.current) {
|
||||||
// Recalculate computed values when canceling
|
// Recalculate computed values when canceling
|
||||||
const computedValues = calculateComputedValues(serverObjectData, model)
|
const computedValues = calculateComputedValues(
|
||||||
const resetFormData = { ...serverObjectData, ...computedValues }
|
serverObjectData.current,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
const resetFormData = { ...serverObjectData.current, ...computedValues }
|
||||||
|
|
||||||
form.setFieldsValue(resetFormData)
|
form.setFieldsValue(resetFormData)
|
||||||
setObjectData(resetFormData)
|
setObjectData(resetFormData)
|
||||||
}
|
}
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
onStateChange({ isEditing: false })
|
onStateChangeRef.current({ isEditing: false })
|
||||||
unlockObject(id, type)
|
unlockObject(id, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,11 +300,11 @@ const ObjectForm = forwardRef(
|
|||||||
try {
|
try {
|
||||||
const value = await form.validateFields()
|
const value = await form.validateFields()
|
||||||
setEditLoading(true)
|
setEditLoading(true)
|
||||||
onStateChange({ editLoading: true })
|
onStateChangeRef.current({ editLoading: true })
|
||||||
await updateObject(id, type, value)
|
await updateObject(id, type, value)
|
||||||
setObjectData({ ...objectData, ...value })
|
setObjectData({ ...objectData, ...value })
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
onStateChange({ isEditing: false })
|
onStateChangeRef.current({ isEditing: false })
|
||||||
messageApi.success('Information updated successfully')
|
messageApi.success('Information updated successfully')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@ -309,7 +319,7 @@ const ObjectForm = forwardRef(
|
|||||||
} finally {
|
} finally {
|
||||||
handleFetchObject()
|
handleFetchObject()
|
||||||
setEditLoading(false)
|
setEditLoading(false)
|
||||||
onStateChange({ editLoading: false })
|
onStateChangeRef.current({ editLoading: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { Spin, Descriptions, Flex } from 'antd'
|
import { Spin, Descriptions, Flex } from 'antd'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
import { getModelProperties } from '../../../database/ObjectModels'
|
import { getModelProperties } from '../../../database/ObjectModels'
|
||||||
|
import merge from 'lodash/merge'
|
||||||
|
|
||||||
const ObjectInfo = ({
|
const ObjectInfo = ({
|
||||||
loading = false,
|
loading = false,
|
||||||
@ -27,6 +29,12 @@ const ObjectInfo = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const allItems = getModelProperties(type)
|
const allItems = getModelProperties(type)
|
||||||
|
|
||||||
|
const [combinedObjectData, setCombinedObjectData] = useState(objectData)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCombinedObjectData((prev) => merge({}, prev, objectData))
|
||||||
|
}, [objectData])
|
||||||
|
|
||||||
// If properties array is empty, show all properties
|
// If properties array is empty, show all properties
|
||||||
// Otherwise, filter and order by the properties array
|
// Otherwise, filter and order by the properties array
|
||||||
let items
|
let items
|
||||||
@ -82,7 +90,8 @@ const ObjectInfo = ({
|
|||||||
{...item}
|
{...item}
|
||||||
{...objectPropertyProps}
|
{...objectPropertyProps}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
objectData={objectData}
|
objectData={combinedObjectData}
|
||||||
|
showSince={true}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
span: item?.span || undefined
|
span: item?.span || undefined
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import ObjectDisplay from './ObjectDisplay'
|
|||||||
import ObjectTypeSelect from './ObjectTypeSelect'
|
import ObjectTypeSelect from './ObjectTypeSelect'
|
||||||
import ObjectTypeDisplay from './ObjectTypeDisplay'
|
import ObjectTypeDisplay from './ObjectTypeDisplay'
|
||||||
import CodeBlockEditor from './CodeBlockEditor'
|
import CodeBlockEditor from './CodeBlockEditor'
|
||||||
|
import CustomSelect from './CustomSelect'
|
||||||
import StateDisplay from './StateDisplay'
|
import StateDisplay from './StateDisplay'
|
||||||
import AlertsDisplay from './AlertsDisplay'
|
import AlertsDisplay from './AlertsDisplay'
|
||||||
import FileUpload from './FileUpload'
|
import FileUpload from './FileUpload'
|
||||||
@ -84,6 +85,7 @@ const ObjectProperty = ({
|
|||||||
options = [],
|
options = [],
|
||||||
roundNumber = false,
|
roundNumber = false,
|
||||||
showHyperlink,
|
showHyperlink,
|
||||||
|
showSince,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
if (value && typeof value == 'function' && objectData) {
|
if (value && typeof value == 'function' && objectData) {
|
||||||
@ -117,6 +119,9 @@ const ObjectProperty = ({
|
|||||||
if (masterFilter && typeof masterFilter == 'function' && objectData) {
|
if (masterFilter && typeof masterFilter == 'function' && objectData) {
|
||||||
masterFilter = masterFilter(objectData)
|
masterFilter = masterFilter(objectData)
|
||||||
}
|
}
|
||||||
|
if (options && typeof options == 'function' && objectData) {
|
||||||
|
options = options(objectData)
|
||||||
|
}
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
value = getPropertyValue(objectData, name)
|
value = getPropertyValue(objectData, name)
|
||||||
@ -169,8 +174,9 @@ const ObjectProperty = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case 'select': {
|
case 'select': {
|
||||||
const selectValue = options.find((option) => option.value === value)
|
if (options && Array.isArray(options)) {
|
||||||
if (selectValue) {
|
const selectValue =
|
||||||
|
options.find((option) => option.value === value) || 'n/a'
|
||||||
return <Text {...textParams}>{selectValue.label}</Text>
|
return <Text {...textParams}>{selectValue.label}</Text>
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
@ -206,7 +212,9 @@ const ObjectProperty = ({
|
|||||||
}
|
}
|
||||||
case 'dateTime': {
|
case 'dateTime': {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
return <TimeDisplay dateTime={value} {...rest} />
|
return (
|
||||||
|
<TimeDisplay dateTime={value} showSince={showSince} {...rest} />
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Text type='secondary' {...textParams}>
|
<Text type='secondary' {...textParams}>
|
||||||
@ -249,7 +257,7 @@ const ObjectProperty = ({
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
var roundedValue = value
|
var roundedValue = value
|
||||||
if (roundNumber != false) {
|
if (roundNumber != false && typeof value === 'number') {
|
||||||
roundedValue = value.toFixed(roundNumber)
|
roundedValue = value.toFixed(roundNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,7 +516,10 @@ const ObjectProperty = ({
|
|||||||
|
|
||||||
// Editable mode: wrap in Form.Item
|
// Editable mode: wrap in Form.Item
|
||||||
// Merge required rule if needed
|
// Merge required rule if needed
|
||||||
let mergedFormItemProps = { ...formItemProps, style: { flexGrow: 1 } }
|
let mergedFormItemProps = {
|
||||||
|
...formItemProps,
|
||||||
|
style: { flexGrow: 1, width: '100%' }
|
||||||
|
}
|
||||||
if (required && disabled == false) {
|
if (required && disabled == false) {
|
||||||
let rules
|
let rules
|
||||||
if (mergedFormItemProps.rules) {
|
if (mergedFormItemProps.rules) {
|
||||||
@ -575,11 +586,10 @@ const ObjectProperty = ({
|
|||||||
case 'select':
|
case 'select':
|
||||||
return (
|
return (
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
<Select
|
<CustomSelect
|
||||||
defaultValue={value}
|
|
||||||
placeholder={'Select a ' + label.toLowerCase() + '...'}
|
placeholder={'Select a ' + label.toLowerCase() + '...'}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
options={options}
|
options={Array.isArray(options) ? options : []}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
@ -804,7 +814,8 @@ ObjectProperty.propTypes = {
|
|||||||
previewOpen: PropTypes.bool,
|
previewOpen: PropTypes.bool,
|
||||||
showPreview: PropTypes.bool,
|
showPreview: PropTypes.bool,
|
||||||
showHyperlink: PropTypes.bool,
|
showHyperlink: PropTypes.bool,
|
||||||
options: PropTypes.array
|
options: PropTypes.array,
|
||||||
|
showSince: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectProperty
|
export default ObjectProperty
|
||||||
|
|||||||
@ -1,9 +1,18 @@
|
|||||||
// PrinterSelect.js
|
// PrinterSelect.js
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Progress, Flex, Space } from 'antd'
|
import { Progress, Flex, Space, Modal, Button, Typography } from 'antd'
|
||||||
import StateTag from './StateTag'
|
import StateTag from './StateTag'
|
||||||
|
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
const StateDisplay = ({ state, showProgress = true, showState = true }) => {
|
const { Text } = Typography
|
||||||
|
|
||||||
|
const StateDisplay = ({
|
||||||
|
state,
|
||||||
|
showProgress = true,
|
||||||
|
showState = true,
|
||||||
|
showMessage = true
|
||||||
|
}) => {
|
||||||
const loadingProgressTypes = [
|
const loadingProgressTypes = [
|
||||||
'loading',
|
'loading',
|
||||||
'processing',
|
'processing',
|
||||||
@ -15,8 +24,9 @@ const StateDisplay = ({ state, showProgress = true, showState = true }) => {
|
|||||||
type: 'unknown',
|
type: 'unknown',
|
||||||
progress: 0
|
progress: 0
|
||||||
}
|
}
|
||||||
|
const [showMessageModal, setShowMessageModal] = useState(false)
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Flex gap='small' align={'center'}>
|
<Flex gap='small' align={'center'}>
|
||||||
{showState && (
|
{showState && (
|
||||||
<Space>
|
<Space>
|
||||||
@ -34,14 +44,34 @@ const StateDisplay = ({ state, showProgress = true, showState = true }) => {
|
|||||||
style={{ width: '150px', marginBottom: '2px' }}
|
style={{ width: '150px', marginBottom: '2px' }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{showMessage && currentState?.message && (
|
||||||
|
<Button
|
||||||
|
type='text'
|
||||||
|
size='small'
|
||||||
|
style={{ padding: '4px' }}
|
||||||
|
onClick={() => setShowMessageModal(true)}
|
||||||
|
>
|
||||||
|
<InfoCircleIcon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<Modal
|
||||||
|
open={showMessageModal}
|
||||||
|
onCancel={() => setShowMessageModal(false)}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
<Text>{currentState.message}</Text>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
StateDisplay.propTypes = {
|
StateDisplay.propTypes = {
|
||||||
state: PropTypes.object,
|
state: PropTypes.object,
|
||||||
showProgress: PropTypes.bool,
|
showProgress: PropTypes.bool,
|
||||||
showState: PropTypes.bool
|
showState: PropTypes.bool,
|
||||||
|
showMessage: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export default StateDisplay
|
export default StateDisplay
|
||||||
|
|||||||
@ -32,6 +32,14 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
|
|||||||
status = 'warning'
|
status = 'warning'
|
||||||
text = 'Initializing'
|
text = 'Initializing'
|
||||||
break
|
break
|
||||||
|
case 'connecting':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Connecting'
|
||||||
|
break
|
||||||
|
case 'deploying':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Deploying'
|
||||||
|
break
|
||||||
case 'printing':
|
case 'printing':
|
||||||
status = 'processing'
|
status = 'processing'
|
||||||
text = 'Printing'
|
text = 'Printing'
|
||||||
|
|||||||
@ -337,8 +337,6 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
const callbacks = subscribedCallbacksRef.current
|
const callbacks = subscribedCallbacksRef.current
|
||||||
.get(objectType)
|
.get(objectType)
|
||||||
.filter((cb) => cb !== callback)
|
.filter((cb) => cb !== callback)
|
||||||
console.log('API: CALLBACKS', callbacks)
|
|
||||||
|
|
||||||
if (callbacks.length === 0) {
|
if (callbacks.length === 0) {
|
||||||
subscribedCallbacksRef.current.delete(objectType)
|
subscribedCallbacksRef.current.delete(objectType)
|
||||||
socketRef.current.emit('unsubscribeObjectTypeUpdate', {
|
socketRef.current.emit('unsubscribeObjectTypeUpdate', {
|
||||||
|
|||||||
@ -613,7 +613,7 @@ const AuthProvider = ({ children }) => {
|
|||||||
<Button
|
<Button
|
||||||
key='submit'
|
key='submit'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showAuthErrorModal(false)
|
setShowAuthErrorModal(false)
|
||||||
loginWithSSO()
|
loginWithSSO()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -14,7 +14,8 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
useRef,
|
useRef,
|
||||||
createElement
|
createElement,
|
||||||
|
useContext
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
@ -29,6 +30,8 @@ import {
|
|||||||
getModelByPrefix
|
getModelByPrefix
|
||||||
} from '../../../database/ObjectModels'
|
} from '../../../database/ObjectModels'
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||||
|
import { ApiServerContext } from './ApiServerContext'
|
||||||
|
import { AuthContext } from './AuthContext'
|
||||||
|
|
||||||
const SpotlightContext = createContext()
|
const SpotlightContext = createContext()
|
||||||
|
|
||||||
@ -41,6 +44,8 @@ const SpotlightProvider = ({ children }) => {
|
|||||||
const [listData, setListData] = useState([])
|
const [listData, setListData] = useState([])
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const [inputPrefix, setInputPrefix] = useState({ prefix: '', mode: null })
|
const [inputPrefix, setInputPrefix] = useState({ prefix: '', mode: null })
|
||||||
|
const { fetchSpotlightData } = useContext(ApiServerContext)
|
||||||
|
const { token } = useContext(AuthContext)
|
||||||
|
|
||||||
// Refs for throttling/debouncing
|
// Refs for throttling/debouncing
|
||||||
const lastFetchTime = useRef(0)
|
const lastFetchTime = useRef(0)
|
||||||
@ -116,7 +121,7 @@ const SpotlightProvider = ({ children }) => {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setListData([])
|
setListData([])
|
||||||
|
|
||||||
let response
|
let data
|
||||||
|
|
||||||
// Check if we have a prefix with ? mode (filter mode)
|
// Check if we have a prefix with ? mode (filter mode)
|
||||||
if (inputPrefix && inputPrefix.mode === '?') {
|
if (inputPrefix && inputPrefix.mode === '?') {
|
||||||
@ -128,37 +133,34 @@ const SpotlightProvider = ({ children }) => {
|
|||||||
// Parse the query parameters
|
// Parse the query parameters
|
||||||
const params = new URLSearchParams(queryParams)
|
const params = new URLSearchParams(queryParams)
|
||||||
|
|
||||||
response = await axios.get(`${config.backendUrl}/spotlight/${prefix}`, {
|
// For ? mode, use axios directly with query params
|
||||||
|
const response = await axios.get(
|
||||||
|
`${config.backendUrl}/spotlight/${prefix}`,
|
||||||
|
{
|
||||||
params: params,
|
params: params,
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json',
|
||||||
},
|
Authorization: `Bearer ${token}`
|
||||||
withCredentials: true
|
}
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// For other modes (:, ^), use the original behavior
|
|
||||||
response = await axios.get(
|
|
||||||
`${config.backendUrl}/spotlight/${encodeURIComponent(searchQuery.trim())}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
withCredentials: true
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
data = response.data
|
||||||
|
} else {
|
||||||
|
// For other modes (:, ^), use fetchSpotlightData from ApiServerContext
|
||||||
|
data = await fetchSpotlightData(searchQuery.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
// If the query contains a prefix mode character, and the response is an object, wrap it in an array
|
// If the query contains a prefix mode character, and the response is an object, wrap it in an array
|
||||||
if (
|
if (
|
||||||
/[:?^]/.test(searchQuery) &&
|
/[:?^]/.test(searchQuery) &&
|
||||||
response.data &&
|
data &&
|
||||||
!Array.isArray(response.data) &&
|
!Array.isArray(data) &&
|
||||||
typeof response.data === 'object'
|
typeof data === 'object'
|
||||||
) {
|
) {
|
||||||
setListData([response.data])
|
setListData([data])
|
||||||
} else {
|
} else {
|
||||||
setListData(response.data)
|
setListData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there's a pending query after this fetch completes
|
// Check if there's a pending query after this fetch completes
|
||||||
@ -464,11 +466,22 @@ const SpotlightProvider = ({ children }) => {
|
|||||||
align='center'
|
align='center'
|
||||||
justify='space-between'
|
justify='space-between'
|
||||||
>
|
>
|
||||||
<Flex gap={'small'} align='center'>
|
<Flex
|
||||||
|
gap={'small'}
|
||||||
|
align='center'
|
||||||
|
style={{ flexGrow: 1 }}
|
||||||
|
>
|
||||||
{Icon ? <Icon style={{ fontSize: '20px' }} /> : null}
|
{Icon ? <Icon style={{ fontSize: '20px' }} /> : null}
|
||||||
|
|
||||||
{item.name ? (
|
{item.name ? (
|
||||||
<Text ellipsis style={{ maxWidth: 170 }}>
|
<Text
|
||||||
|
ellipsis
|
||||||
|
style={{
|
||||||
|
maxWidth: 170,
|
||||||
|
flexGrow: 1,
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -90,7 +90,7 @@ export const ThemeProvider = ({ children }) => {
|
|||||||
colorWarning: '#FF9F0A',
|
colorWarning: '#FF9F0A',
|
||||||
colorInfo: '#0A84FF',
|
colorInfo: '#0A84FF',
|
||||||
colorLink: '#5AC8F5',
|
colorLink: '#5AC8F5',
|
||||||
borderRadius: '10px'
|
borderRadius: '12px'
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Layout: {
|
Layout: {
|
||||||
|
|||||||
@ -36,9 +36,9 @@ export const DocumentJob = {
|
|||||||
`/dashboard/management/documentjobs/info?documentJobId=${_id}&action=edit`
|
`/dashboard/management/documentjobs/info?documentJobId=${_id}&action=edit`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['name', '_id', 'width', 'height', 'createdAt', 'updatedAt'],
|
columns: ['name', '_id', 'state', 'createdAt', 'updatedAt'],
|
||||||
filters: ['name', '_id', 'width', 'height'],
|
filters: ['name', '_id', 'state'],
|
||||||
sorters: ['name', 'width', 'height', 'createdAt', 'updatedAt'],
|
sorters: ['name', 'state', 'createdAt', 'updatedAt'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
|
|||||||
@ -38,13 +38,15 @@ export const DocumentPrinter = {
|
|||||||
columns: [
|
columns: [
|
||||||
'name',
|
'name',
|
||||||
'_id',
|
'_id',
|
||||||
'documentSize',
|
'state',
|
||||||
'documentSize._id',
|
'host',
|
||||||
'createdAt',
|
'host._id',
|
||||||
|
'tags',
|
||||||
|
'connectedAt',
|
||||||
'updatedAt'
|
'updatedAt'
|
||||||
],
|
],
|
||||||
filters: ['name', '_id'],
|
filters: ['name', '_id'],
|
||||||
sorters: ['name', 'documentSize', 'createdAt', 'updatedAt'],
|
sorters: ['name', 'documentSize', 'connectedAt', 'updatedAt'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
@ -82,10 +84,10 @@ export const DocumentPrinter = {
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'active',
|
name: 'connectedAt',
|
||||||
label: 'Active',
|
label: 'Connected At',
|
||||||
type: 'bool',
|
type: 'dateTime',
|
||||||
required: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'online',
|
name: 'online',
|
||||||
@ -93,6 +95,12 @@ export const DocumentPrinter = {
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'active',
|
||||||
|
label: 'Active',
|
||||||
|
type: 'bool',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'host',
|
name: 'host',
|
||||||
label: 'Host',
|
label: 'Host',
|
||||||
@ -109,16 +117,6 @@ export const DocumentPrinter = {
|
|||||||
showCopy: true,
|
showCopy: true,
|
||||||
showHyperlink: true
|
showHyperlink: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'connection.mode',
|
|
||||||
label: 'Mode',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: 'Network', value: 'network' },
|
|
||||||
{ label: 'Serial', value: 'serial' }
|
|
||||||
],
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'connection.interface',
|
name: 'connection.interface',
|
||||||
label: 'Interface',
|
label: 'Interface',
|
||||||
@ -130,12 +128,43 @@ export const DocumentPrinter = {
|
|||||||
],
|
],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'connection.protocol',
|
||||||
|
label: 'Protocol',
|
||||||
|
type: 'select',
|
||||||
|
options: (objectData) => {
|
||||||
|
if (objectData?.connection?.interface == 'cups') {
|
||||||
|
return [
|
||||||
|
{ label: 'IPP', value: 'ipp' },
|
||||||
|
{ label: 'HTTP', value: 'http' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ label: 'TCP', value: 'tcp' },
|
||||||
|
{ label: 'Serial', value: 'serial' },
|
||||||
|
{ label: 'System', value: 'system' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
required: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'connection.host',
|
name: 'connection.host',
|
||||||
label: 'Connection String',
|
label: 'Host Name',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'connection.port',
|
||||||
|
label: 'Port',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
disabled: (objectData) => {
|
||||||
|
return (
|
||||||
|
objectData?.connection?.protocol == 'system' ||
|
||||||
|
objectData?.connection?.protocol == 'serial'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'currentDocumentSize',
|
name: 'currentDocumentSize',
|
||||||
label: 'Current Document Size',
|
label: 'Current Document Size',
|
||||||
|
|||||||
@ -35,9 +35,24 @@ export const DocumentSize = {
|
|||||||
`/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=edit`
|
`/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=edit`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['name', '_id', 'width', 'height', 'createdAt', 'updatedAt'],
|
columns: [
|
||||||
filters: ['name', '_id', 'width', 'height'],
|
'name',
|
||||||
sorters: ['name', 'width', 'height', 'createdAt', 'updatedAt'],
|
'_id',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'infiniteHeight',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt'
|
||||||
|
],
|
||||||
|
filters: ['name', '_id', 'width', 'height', 'infiniteHeight'],
|
||||||
|
sorters: [
|
||||||
|
'name',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'infiniteHeight',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt'
|
||||||
|
],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
@ -80,7 +95,17 @@ export const DocumentSize = {
|
|||||||
required: true,
|
required: true,
|
||||||
columnWidth: 150,
|
columnWidth: 150,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
suffix: 'mm'
|
suffix: 'mm',
|
||||||
|
disabled: (objectData) => {
|
||||||
|
return objectData.infiniteHeight
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'infiniteHeight',
|
||||||
|
label: 'Infinite Height',
|
||||||
|
required: true,
|
||||||
|
columnWidth: 150,
|
||||||
|
type: 'bool'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,6 +96,7 @@ export const File = {
|
|||||||
label: 'Size',
|
label: 'Size',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
roundNumber: 2,
|
||||||
required: true,
|
required: true,
|
||||||
suffix: (objectData) => {
|
suffix: (objectData) => {
|
||||||
const size = objectData?.size || 0
|
const size = objectData?.size || 0
|
||||||
|
|||||||
@ -167,7 +167,16 @@ export const Printer = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
|
columns: [
|
||||||
|
'name',
|
||||||
|
'_id',
|
||||||
|
'state',
|
||||||
|
'host',
|
||||||
|
'host._id',
|
||||||
|
'tags',
|
||||||
|
'connectedAt',
|
||||||
|
'updatedAt'
|
||||||
|
],
|
||||||
filters: ['name', '_id', 'state', 'tags'],
|
filters: ['name', '_id', 'state', 'tags'],
|
||||||
sorters: ['name', 'state', 'connectedAt'],
|
sorters: ['name', 'state', 'connectedAt'],
|
||||||
group: ['tags'],
|
group: ['tags'],
|
||||||
@ -180,8 +189,8 @@ export const Printer = {
|
|||||||
showCopy: true
|
showCopy: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'connectedAt',
|
name: 'createdAt',
|
||||||
label: 'Connected At',
|
label: 'Created At',
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
@ -193,6 +202,12 @@ export const Printer = {
|
|||||||
columnWidth: 200,
|
columnWidth: 200,
|
||||||
columnFixed: 'left'
|
columnFixed: 'left'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
label: 'Updated At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'state',
|
name: 'state',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
@ -202,10 +217,10 @@ export const Printer = {
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'active',
|
name: 'connectedAt',
|
||||||
label: 'Active',
|
label: 'Connected At',
|
||||||
type: 'bool',
|
type: 'dateTime',
|
||||||
required: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'online',
|
name: 'online',
|
||||||
@ -213,6 +228,12 @@ export const Printer = {
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'active',
|
||||||
|
label: 'Active',
|
||||||
|
type: 'bool',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'vendor',
|
name: 'vendor',
|
||||||
label: 'Vendor',
|
label: 'Vendor',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user