Compare commits
14 Commits
12a4d46da8
...
8993caeac5
| Author | SHA1 | Date | |
|---|---|---|---|
| 8993caeac5 | |||
| 800e0d82a2 | |||
| d07e2be330 | |||
| 09bf3b81c7 | |||
| e2a96949b3 | |||
| 0eac407db4 | |||
| f2e7959a00 | |||
| 1b7fd77a99 | |||
| f90eb4ed37 | |||
| 45d9aabb87 | |||
| c7ddcd36e3 | |||
| b6cd1bac0b | |||
| 5cf4976635 | |||
| 475f06e5f0 |
@ -41,7 +41,8 @@ const DocumentTemplateDesign = () => {
|
|||||||
editLoading: false,
|
editLoading: false,
|
||||||
formValid: false,
|
formValid: false,
|
||||||
locked: false,
|
locked: false,
|
||||||
loading: false
|
loading: false,
|
||||||
|
objectData: {}
|
||||||
})
|
})
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
|||||||
@ -45,10 +45,18 @@ const IdDisplay = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex align={'end'} className='iddisplay'>
|
<Flex
|
||||||
|
align={'end'}
|
||||||
|
className='iddisplay'
|
||||||
|
style={{ minWidth: '0px', width: '100%' }}
|
||||||
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
const textElement = (
|
const textElement = (
|
||||||
<Text code ellipsis style={showCopy ? { marginRight: 6 } : undefined}>
|
<Text
|
||||||
|
code
|
||||||
|
ellipsis
|
||||||
|
style={showCopy ? { marginRight: 6, minWidth: '0px' } : undefined}
|
||||||
|
>
|
||||||
{displayId}
|
{displayId}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -35,18 +35,29 @@ function filterActionsByVisibility(actions, visibleActions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recursively map actions to AntD Dropdown items
|
// Recursively map actions to AntD Dropdown items
|
||||||
function mapActionsToMenuItems(actions, currentUrlWithActions, id) {
|
function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
|
||||||
return actions.map((action) => {
|
return actions.map((action) => {
|
||||||
if (action.type === 'divider') {
|
if (action.type === 'divider') {
|
||||||
return { type: 'divider' }
|
return { type: 'divider' }
|
||||||
}
|
}
|
||||||
const actionUrl = action.url ? action.url(id) : undefined
|
const actionUrl = action.url ? action.url(id) : undefined
|
||||||
|
|
||||||
|
var disabled = actionUrl && actionUrl === currentUrlWithActions
|
||||||
|
|
||||||
|
if (action.disabled) {
|
||||||
|
if (typeof action.disabled === 'function') {
|
||||||
|
disabled = action.disabled(objectData)
|
||||||
|
} else {
|
||||||
|
disabled = action.disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
key: action.key || action.name,
|
key: action.key || action.name,
|
||||||
label: action.label,
|
label: action.label,
|
||||||
danger: action?.danger || false,
|
danger: action?.danger || false,
|
||||||
icon: action.icon ? createElement(action.icon) : undefined,
|
icon: action.icon ? createElement(action.icon) : undefined,
|
||||||
disabled: actionUrl && actionUrl === currentUrlWithActions
|
disabled
|
||||||
}
|
}
|
||||||
if (action.children && Array.isArray(action.children)) {
|
if (action.children && Array.isArray(action.children)) {
|
||||||
item.children = mapActionsToMenuItems(
|
item.children = mapActionsToMenuItems(
|
||||||
@ -69,6 +80,7 @@ const stripActionParam = (pathname, search) => {
|
|||||||
const ObjectActions = ({
|
const ObjectActions = ({
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
|
objectData,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
buttonProps = {},
|
buttonProps = {},
|
||||||
visibleActions = {},
|
visibleActions = {},
|
||||||
@ -101,7 +113,12 @@ const ObjectActions = ({
|
|||||||
|
|
||||||
// Compose AntD Dropdown menu items
|
// Compose AntD Dropdown menu items
|
||||||
const menu = {
|
const menu = {
|
||||||
items: mapActionsToMenuItems(filteredActions, currentUrlWithActions, id),
|
items: mapActionsToMenuItems(
|
||||||
|
filteredActions,
|
||||||
|
currentUrlWithActions,
|
||||||
|
id,
|
||||||
|
objectData
|
||||||
|
),
|
||||||
onClick: (info) => {
|
onClick: (info) => {
|
||||||
// Find the action by key
|
// Find the action by key
|
||||||
const findAction = (acts, key) => {
|
const findAction = (acts, key) => {
|
||||||
@ -132,6 +149,7 @@ const ObjectActions = ({
|
|||||||
|
|
||||||
ObjectActions.propTypes = {
|
ObjectActions.propTypes = {
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
|
objectData: PropTypes.object.isRequired,
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
buttonProps: PropTypes.object,
|
buttonProps: PropTypes.object,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { getModelByName } from '../../../database/ObjectModels'
|
|||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import merge from 'lodash/merge'
|
import merge from 'lodash/merge'
|
||||||
|
import IdDisplay from './IdDisplay'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -55,7 +56,15 @@ const ObjectDisplay = ({ object, objectType }) => {
|
|||||||
<Flex gap={'small'} align='center'>
|
<Flex gap={'small'} align='center'>
|
||||||
<Icon />
|
<Icon />
|
||||||
{objectData?.color ? <Badge color={objectData?.color} /> : null}
|
{objectData?.color ? <Badge color={objectData?.color} /> : null}
|
||||||
<Text ellipsis>{objectData?.name ? objectData.name : null}</Text>
|
{objectData?.name ? <Text ellipsis>{objectData.name}</Text> : null}
|
||||||
|
{objectData?._id && !objectData?.name ? (
|
||||||
|
<IdDisplay
|
||||||
|
id={objectData?._id}
|
||||||
|
type={objectType}
|
||||||
|
longId={false}
|
||||||
|
showCopy={false}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,11 +59,10 @@ const ObjectForm = forwardRef(
|
|||||||
.validateFields({ validateOnly: true })
|
.validateFields({ validateOnly: true })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setFormValid(true)
|
setFormValid(true)
|
||||||
onStateChange({ formValid: true })
|
onStateChange({ formValid: true, objectData: form.getFieldsValue() })
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setFormValid(false)
|
onStateChange({ formValid: true, objectData: form.getFieldsValue() })
|
||||||
onStateChange({ formValid: true })
|
|
||||||
})
|
})
|
||||||
}, [form, formUpdateValues])
|
}, [form, formUpdateValues])
|
||||||
|
|
||||||
@ -146,6 +145,10 @@ const ObjectForm = forwardRef(
|
|||||||
updateLockEventHandler
|
updateLockEventHandler
|
||||||
])
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onStateChange({ objectData })
|
||||||
|
}, [objectData])
|
||||||
|
|
||||||
const startEditing = () => {
|
const startEditing = () => {
|
||||||
setIsEditing(true)
|
setIsEditing(true)
|
||||||
onStateChange({ isEditing: true })
|
onStateChange({ isEditing: true })
|
||||||
|
|||||||
@ -42,6 +42,9 @@ const ObjectInfo = ({
|
|||||||
items = items.filter((item) => item.required === required)
|
items = items.filter((item) => item.required === required)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showHyperlink) {
|
||||||
|
objectPropertyProps = { ...objectPropertyProps, showHyperlink }
|
||||||
|
}
|
||||||
// Filter items based on visibleProperties
|
// Filter items based on visibleProperties
|
||||||
// If a property key exists in visibleProperties and is false, hide it
|
// If a property key exists in visibleProperties and is false, hide it
|
||||||
items = items.filter((item) => {
|
items = items.filter((item) => {
|
||||||
@ -67,7 +70,6 @@ const ObjectInfo = ({
|
|||||||
<ObjectProperty
|
<ObjectProperty
|
||||||
{...item}
|
{...item}
|
||||||
{...objectPropertyProps}
|
{...objectPropertyProps}
|
||||||
showHyperlink={showHyperlink}
|
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
objectData={objectData}
|
objectData={objectData}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import ObjectTypeSelect from './ObjectTypeSelect'
|
|||||||
import ObjectTypeDisplay from './ObjectTypeDisplay'
|
import ObjectTypeDisplay from './ObjectTypeDisplay'
|
||||||
import CodeBlockEditor from './CodeBlockEditor'
|
import CodeBlockEditor from './CodeBlockEditor'
|
||||||
import StateDisplay from './StateDisplay'
|
import StateDisplay from './StateDisplay'
|
||||||
|
import AlertsDisplay from './AlertsDisplay'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -105,6 +106,10 @@ const ObjectProperty = ({
|
|||||||
suffix = suffix(objectData)
|
suffix = suffix(objectData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (masterFilter && typeof masterFilter == 'function' && objectData) {
|
||||||
|
masterFilter = masterFilter(objectData)
|
||||||
|
}
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
value = getPropertyValue(objectData, name)
|
value = getPropertyValue(objectData, name)
|
||||||
}
|
}
|
||||||
@ -116,7 +121,7 @@ const ObjectProperty = ({
|
|||||||
formItemName = name ? name.split('.') : undefined
|
formItemName = name ? name.split('.') : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
var textParams = {}
|
var textParams = { style: { whiteSpace: 'nowrap' } }
|
||||||
|
|
||||||
if (disabled == true) {
|
if (disabled == true) {
|
||||||
textParams = { ...textParams, delete: true, type: 'secondary' }
|
textParams = { ...textParams, delete: true, type: 'secondary' }
|
||||||
@ -317,7 +322,7 @@ const ObjectProperty = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case 'object': {
|
case 'object': {
|
||||||
if (value && value.name) {
|
if (value && value._id) {
|
||||||
return <ObjectDisplay object={value} objectType={objectType} />
|
return <ObjectDisplay object={value} objectType={objectType} />
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
@ -385,9 +390,9 @@ const ObjectProperty = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'mm': {
|
case 'alerts': {
|
||||||
if (value != null) {
|
if (value != null && value?.length != 0) {
|
||||||
return <Text {...textParams}>{`${value} mm`}</Text>
|
return <AlertsDisplay alerts={value} />
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Text type='secondary' {...textParams}>
|
<Text type='secondary' {...textParams}>
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
import { useEffect, useState, useContext, useCallback, useMemo } from 'react'
|
import {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useContext,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useRef
|
||||||
|
} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { TreeSelect, Space, Button, Input } from 'antd'
|
import { TreeSelect, Space, Button, Input } from 'antd'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
@ -72,6 +79,7 @@ const ObjectSelect = ({
|
|||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
title: (
|
title: (
|
||||||
|
<div style={{ paddingTop: '1px' }}>
|
||||||
<ObjectProperty
|
<ObjectProperty
|
||||||
key={object._id}
|
key={object._id}
|
||||||
type='object'
|
type='object'
|
||||||
@ -80,6 +88,7 @@ const ObjectSelect = ({
|
|||||||
objectData={object}
|
objectData={object}
|
||||||
isEditing={false}
|
isEditing={false}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
value: object._id,
|
value: object._id,
|
||||||
key: object._id,
|
key: object._id,
|
||||||
@ -199,7 +208,7 @@ const ObjectSelect = ({
|
|||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [objectPropertiesTree, properties, type, buildTreeData])
|
}, [objectPropertiesTree, properties, buildTreeData])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value && typeof value === 'object' && value !== null && !initialized) {
|
if (value && typeof value === 'object' && value !== null && !initialized) {
|
||||||
@ -236,6 +245,24 @@ const ObjectSelect = ({
|
|||||||
token
|
token
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const prevValuesRef = useRef({ type, masterFilter })
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const prevValues = prevValuesRef.current
|
||||||
|
|
||||||
|
// Deep comparison for objects, simple comparison for primitives
|
||||||
|
const hasChanged =
|
||||||
|
prevValues.type !== type ||
|
||||||
|
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
|
||||||
|
|
||||||
|
if (hasChanged) {
|
||||||
|
setObjectPropertiesTree({})
|
||||||
|
setTreeData([])
|
||||||
|
setInitialized(false)
|
||||||
|
prevValuesRef.current = { type, masterFilter }
|
||||||
|
}
|
||||||
|
}, [type, masterFilter])
|
||||||
|
|
||||||
// --- Error UI ---
|
// --- Error UI ---
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -53,7 +53,8 @@ const PrinterTemperaturePanel = ({
|
|||||||
ambiant: 0
|
ambiant: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const { subscribeToObjectEvent, connected } = useContext(ApiServerContext)
|
const { subscribeToObjectEvent, connected, sendObjectAction } =
|
||||||
|
useContext(ApiServerContext)
|
||||||
|
|
||||||
// Sync input values with actual temperature targets
|
// Sync input values with actual temperature targets
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -90,10 +91,19 @@ const PrinterTemperaturePanel = ({
|
|||||||
const [extruderTarget, setExtruderTarget] = useState(0)
|
const [extruderTarget, setExtruderTarget] = useState(0)
|
||||||
const [bedTarget, setBedTarget] = useState(0)
|
const [bedTarget, setBedTarget] = useState(0)
|
||||||
|
|
||||||
const handleSetTemperature = (data) => {
|
const handleSetTemperature = async (data) => {
|
||||||
if (id && connected == true) {
|
if (id && connected == true) {
|
||||||
console.log(data)
|
await sendObjectAction(
|
||||||
//sendObjectAction(id, 'printer', { type: 'setTemperature', data })
|
id,
|
||||||
|
'printer',
|
||||||
|
{
|
||||||
|
type: 'setTemperature',
|
||||||
|
data
|
||||||
|
},
|
||||||
|
(result) => {
|
||||||
|
console.log('setTemperatureResult', result)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,16 +179,20 @@ const PrinterTemperaturePanel = ({
|
|||||||
style={{ width: '120px' }}
|
style={{ width: '120px' }}
|
||||||
addonAfter='°C'
|
addonAfter='°C'
|
||||||
onChange={(value) => setExtruderTarget(value || 0)}
|
onChange={(value) => setExtruderTarget(value || 0)}
|
||||||
onPressEnter={handleSetTemperature({
|
onPressEnter={() =>
|
||||||
|
handleSetTemperature({
|
||||||
extruder: { target: extruderTarget }
|
extruder: { target: extruderTarget }
|
||||||
})}
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type='default'
|
type='default'
|
||||||
style={{ width: 40 }}
|
style={{ width: 40 }}
|
||||||
onClick={handleSetTemperature({
|
onClick={() =>
|
||||||
|
handleSetTemperature({
|
||||||
extruder: { target: extruderTarget }
|
extruder: { target: extruderTarget }
|
||||||
})}
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Set
|
Set
|
||||||
</Button>
|
</Button>
|
||||||
@ -226,16 +240,20 @@ const PrinterTemperaturePanel = ({
|
|||||||
style={{ width: '120px' }}
|
style={{ width: '120px' }}
|
||||||
addonAfter='°C'
|
addonAfter='°C'
|
||||||
onChange={(value) => setBedTarget(value || 0)}
|
onChange={(value) => setBedTarget(value || 0)}
|
||||||
onPressEnter={handleSetTemperature({
|
onPressEnter={() =>
|
||||||
|
handleSetTemperature({
|
||||||
bed: { target: bedTarget }
|
bed: { target: bedTarget }
|
||||||
})}
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type='default'
|
type='default'
|
||||||
style={{ width: 40 }}
|
style={{ width: 40 }}
|
||||||
onClick={handleSetTemperature({
|
onClick={() =>
|
||||||
|
handleSetTemperature({
|
||||||
bed: { target: bedTarget }
|
bed: { target: bedTarget }
|
||||||
})}
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Set
|
Set
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Descriptions, Typography, Space } from 'antd'
|
import { Descriptions, Typography, Flex } from 'antd'
|
||||||
import { getModelProperty } from '../../../database/ObjectModels'
|
import { getModelProperty } from '../../../database/ObjectModels'
|
||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
|
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
|
||||||
@ -51,7 +51,7 @@ const PropertyChanges = ({ type, value }) => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Descriptions.Item key={key} label={changeProperty.label}>
|
<Descriptions.Item key={key} label={changeProperty.label}>
|
||||||
<Space>
|
<Flex gap={'small'}>
|
||||||
{value?.old ? (
|
{value?.old ? (
|
||||||
<ObjectProperty
|
<ObjectProperty
|
||||||
{...changeProperty}
|
{...changeProperty}
|
||||||
@ -73,7 +73,7 @@ const PropertyChanges = ({ type, value }) => {
|
|||||||
objectData={value?.new}
|
objectData={value?.new}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Space>
|
</Flex>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
import { useState, useContext, useEffect, useRef, useCallback } from 'react'
|
import { useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Flex, Alert, Card, Spin, Splitter, Button, Modal, Input } from 'antd'
|
import { Flex, Alert, Card, Spin, Splitter, Button, Modal } from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import PlusIcon from '../../Icons/PlusIcon.jsx'
|
|
||||||
import MinusIcon from '../../Icons/MinusIcon.jsx'
|
|
||||||
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon.jsx'
|
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon.jsx'
|
||||||
import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx'
|
import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx'
|
||||||
import ObjectProperty from '../common/ObjectProperty.jsx'
|
import ObjectProperty from '../common/ObjectProperty.jsx'
|
||||||
import { ApiServerContext } from '../context/ApiServerContext.jsx'
|
import TemplatePreview from './TemplatePreview.jsx'
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
|
|
||||||
|
|
||||||
const TemplateEditor = ({
|
const TemplateEditor = ({
|
||||||
objectData,
|
objectData,
|
||||||
@ -17,134 +14,36 @@ const TemplateEditor = ({
|
|||||||
isEditing,
|
isEditing,
|
||||||
style
|
style
|
||||||
}) => {
|
}) => {
|
||||||
const iframeRef = useRef(null)
|
|
||||||
const { fetchTemplatePreview } = useContext(ApiServerContext)
|
|
||||||
const [testObjectOpen, setTestObjectOpen] = useState(false)
|
const [testObjectOpen, setTestObjectOpen] = useState(false)
|
||||||
const [previewMessage, setPreviewMessage] = useState('No issues found.')
|
const [previewMessage, setPreviewMessage] = useState('No issues found.')
|
||||||
const [previewError, setPreviewError] = useState(false)
|
const [previewError, setPreviewError] = useState(false)
|
||||||
const [previewContent, setPreviewContent] = useState('')
|
|
||||||
const [reloadLoading, setReloadLoading] = useState(false)
|
|
||||||
const [previewScale, setPreviewScale] = useState(1)
|
|
||||||
|
|
||||||
const updatePreviewContent = (html) => {
|
const handlePreviewMessage = (message, isError) => {
|
||||||
if (iframeRef.current) {
|
setPreviewMessage(message)
|
||||||
// Save current scroll position
|
setPreviewError(isError)
|
||||||
const scrollY = iframeRef.current.contentWindow.scrollY
|
|
||||||
const scrollX = iframeRef.current.contentWindow.scrollX
|
|
||||||
|
|
||||||
// Update srcDoc
|
|
||||||
setPreviewContent(html)
|
|
||||||
|
|
||||||
// Restore scroll position after iframe loads new content
|
|
||||||
const handleLoad = () => {
|
|
||||||
iframeRef.current.contentWindow.scrollTo(scrollX, scrollY)
|
|
||||||
iframeRef.current.removeEventListener('load', handleLoad)
|
|
||||||
}
|
}
|
||||||
iframeRef.current.addEventListener('load', handleLoad)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reloadPreview = useCallback(
|
|
||||||
(content, testObject = {}, scale = 1) => {
|
|
||||||
fetchTemplatePreview(
|
|
||||||
objectData._id,
|
|
||||||
content,
|
|
||||||
testObject,
|
|
||||||
scale,
|
|
||||||
(result) => {
|
|
||||||
setReloadLoading(false)
|
|
||||||
if (result?.error) {
|
|
||||||
setPreviewError(true)
|
|
||||||
setPreviewMessage(result.error)
|
|
||||||
} else {
|
|
||||||
setPreviewError(false)
|
|
||||||
updatePreviewContent(result.html)
|
|
||||||
setPreviewMessage('No issues found.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[fetchTemplatePreview, objectData?._id]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Move useEffect to component level and use state to track objectData changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (objectData) {
|
|
||||||
console.log('PreviewScale', previewScale)
|
|
||||||
reloadPreview(objectData.content, objectData.testObject, previewScale)
|
|
||||||
}
|
|
||||||
}, [objectData, previewScale, reloadPreview])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Splitter className={'farmcontrol-splitter'}>
|
<Splitter className={'farmcontrol-splitter'}>
|
||||||
{collapseState.preview == true && (
|
{collapseState.preview == true && (
|
||||||
<Splitter.Panel style={{ height: '100%' }}>
|
<Splitter.Panel style={{ height: '100%' }}>
|
||||||
<Spin
|
<Card
|
||||||
spinning={loading || reloadLoading}
|
spinning={loading}
|
||||||
indicator={<LoadingOutlined />}
|
style={style}
|
||||||
|
styles={{ body: { height: '100%' } }}
|
||||||
>
|
>
|
||||||
<Card style={style} styles={{ body: { height: '100%' } }}>
|
<TemplatePreview
|
||||||
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
|
objectData={objectData?.testObject}
|
||||||
<Flex gap={'small'}>
|
documentTemplate={objectData}
|
||||||
{objectData?.objectType ? (
|
loading={loading}
|
||||||
<ObjectProperty
|
isEditing={isEditing}
|
||||||
objectType={objectData?.objectType}
|
style={style}
|
||||||
name={'testObject'}
|
onTestObjectOpen={() => setTestObjectOpen(true)}
|
||||||
isEditing={true}
|
onPreviewMessage={handlePreviewMessage}
|
||||||
objectData={objectData}
|
showTestObject={true}
|
||||||
disabled={!isEditing || objectData?.global}
|
|
||||||
type={'object'}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<div style={{ flexGrow: 1 }}>
|
|
||||||
<Input disabled={true} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
icon={<InfoCircleIcon />}
|
|
||||||
disabled={objectData?.global}
|
|
||||||
onClick={() => {
|
|
||||||
setTestObjectOpen(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
icon={<PlusIcon />}
|
|
||||||
onClick={() => {
|
|
||||||
setPreviewScale((prev) => prev + 0.05)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
icon={<MinusIcon />}
|
|
||||||
onClick={() => {
|
|
||||||
setPreviewScale((prev) => prev - 0.05)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
readOnly={true}
|
|
||||||
style={{ width: '65px' }}
|
|
||||||
onClick={() => {
|
|
||||||
setPreviewScale(1)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{previewScale.toFixed(2)}x
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<iframe
|
|
||||||
ref={iframeRef}
|
|
||||||
srcDoc={previewContent}
|
|
||||||
frameBorder='0'
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
flexGrow: 1,
|
|
||||||
border: '1px solid #85858541',
|
|
||||||
overflow: 'auto'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
</Card>
|
||||||
</Spin>
|
|
||||||
</Splitter.Panel>
|
</Splitter.Panel>
|
||||||
)}
|
)}
|
||||||
{collapseState.editor == true && (
|
{collapseState.editor == true && (
|
||||||
|
|||||||
158
src/components/Dashboard/common/TemplatePreview.jsx
Normal file
158
src/components/Dashboard/common/TemplatePreview.jsx
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { useState, useContext, useEffect, useRef, useCallback } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Flex, Button, Input } from 'antd'
|
||||||
|
import PlusIcon from '../../Icons/PlusIcon.jsx'
|
||||||
|
import MinusIcon from '../../Icons/MinusIcon.jsx'
|
||||||
|
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
|
||||||
|
import ObjectProperty from '../common/ObjectProperty.jsx'
|
||||||
|
import { ApiServerContext } from '../context/ApiServerContext.jsx'
|
||||||
|
|
||||||
|
const TemplatePreview = ({
|
||||||
|
objectData,
|
||||||
|
documentTemplate,
|
||||||
|
loading,
|
||||||
|
isEditing,
|
||||||
|
onTestObjectOpen,
|
||||||
|
onPreviewMessage,
|
||||||
|
showTestObject = false
|
||||||
|
}) => {
|
||||||
|
const iframeRef = useRef(null)
|
||||||
|
const { fetchTemplatePreview } = useContext(ApiServerContext)
|
||||||
|
const [previewContent, setPreviewContent] = useState('')
|
||||||
|
const [reloadLoading, setReloadLoading] = useState(false)
|
||||||
|
const [previewScale, setPreviewScale] = useState(1)
|
||||||
|
|
||||||
|
const updatePreviewContent = (html) => {
|
||||||
|
if (iframeRef.current) {
|
||||||
|
// Save current scroll position
|
||||||
|
const scrollY = iframeRef.current.contentWindow.scrollY
|
||||||
|
const scrollX = iframeRef.current.contentWindow.scrollX
|
||||||
|
|
||||||
|
// Update srcDoc
|
||||||
|
setPreviewContent(html)
|
||||||
|
|
||||||
|
// Restore scroll position after iframe loads new content
|
||||||
|
const handleLoad = () => {
|
||||||
|
iframeRef.current.contentWindow.scrollTo(scrollX, scrollY)
|
||||||
|
iframeRef.current.removeEventListener('load', handleLoad)
|
||||||
|
}
|
||||||
|
iframeRef.current.addEventListener('load', handleLoad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reloadPreview = useCallback(
|
||||||
|
(content, testObject = {}, scale = 1) => {
|
||||||
|
if (!objectData?._id) {
|
||||||
|
onPreviewMessage('No object data available for preview.', true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setReloadLoading(true)
|
||||||
|
fetchTemplatePreview(
|
||||||
|
documentTemplate._id,
|
||||||
|
content,
|
||||||
|
testObject,
|
||||||
|
scale,
|
||||||
|
(result) => {
|
||||||
|
setReloadLoading(false)
|
||||||
|
if (result?.error) {
|
||||||
|
// Handle error through parent component
|
||||||
|
onPreviewMessage(result.error, true)
|
||||||
|
} else {
|
||||||
|
updatePreviewContent(result.html)
|
||||||
|
onPreviewMessage('No issues found.', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[fetchTemplatePreview, objectData?._id, onPreviewMessage]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Move useEffect to component level and use state to track objectData changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (objectData && documentTemplate?.content) {
|
||||||
|
console.log('PreviewScale', previewScale)
|
||||||
|
reloadPreview(documentTemplate.content, objectData, previewScale)
|
||||||
|
}
|
||||||
|
}, [objectData, documentTemplate, previewScale, reloadPreview])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
|
||||||
|
<Flex gap={'small'}>
|
||||||
|
{showTestObject == true ? (
|
||||||
|
<>
|
||||||
|
{documentTemplate?.objectType ? (
|
||||||
|
<ObjectProperty
|
||||||
|
objectType={documentTemplate?.objectType}
|
||||||
|
name={'testObject'}
|
||||||
|
isEditing={true}
|
||||||
|
objectData={objectData}
|
||||||
|
disabled={!isEditing || objectData?.global}
|
||||||
|
type={'object'}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ flexGrow: 1 }}>
|
||||||
|
<Input disabled={true} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
disabled={objectData?.global}
|
||||||
|
onClick={() => {
|
||||||
|
onTestObjectOpen()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
icon={<PlusIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewScale((prev) => prev + 0.05)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<MinusIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewScale((prev) => prev - 0.05)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
readOnly={true}
|
||||||
|
style={{ width: '65px' }}
|
||||||
|
loading={loading || reloadLoading}
|
||||||
|
disabled={loading || reloadLoading}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewScale(1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{previewScale.toFixed(2)}x
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
srcDoc={previewContent}
|
||||||
|
frameBorder='0'
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
flexGrow: 1,
|
||||||
|
border: '1px solid #85858541',
|
||||||
|
overflow: 'auto'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TemplatePreview.propTypes = {
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
objectData: PropTypes.object,
|
||||||
|
documentTemplate: PropTypes.object,
|
||||||
|
isEditing: PropTypes.bool,
|
||||||
|
style: PropTypes.object,
|
||||||
|
showTestObject: PropTypes.bool,
|
||||||
|
onTestObjectOpen: PropTypes.func.isRequired,
|
||||||
|
onPreviewMessage: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TemplatePreview
|
||||||
@ -12,7 +12,9 @@ const WizardView = ({
|
|||||||
title = 'Wizard View',
|
title = 'Wizard View',
|
||||||
onSubmit,
|
onSubmit,
|
||||||
formValid,
|
formValid,
|
||||||
loading
|
loading,
|
||||||
|
sideBar = null,
|
||||||
|
submitText = 'Done'
|
||||||
}) => {
|
}) => {
|
||||||
const [currentStep, setCurrentStep] = useState(0)
|
const [currentStep, setCurrentStep] = useState(0)
|
||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
@ -20,6 +22,9 @@ const WizardView = ({
|
|||||||
return (
|
return (
|
||||||
<Flex gap='middle'>
|
<Flex gap='middle'>
|
||||||
{!isMobile && showSteps == true ? (
|
{!isMobile && showSteps == true ? (
|
||||||
|
sideBar != null ? (
|
||||||
|
sideBar
|
||||||
|
) : (
|
||||||
<div style={{ minWidth: '160px' }}>
|
<div style={{ minWidth: '160px' }}>
|
||||||
<Steps
|
<Steps
|
||||||
current={currentStep}
|
current={currentStep}
|
||||||
@ -28,19 +33,22 @@ const WizardView = ({
|
|||||||
style={{ width: 'fit-content' }}
|
style={{ width: 'fit-content' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!isMobile && showSteps == true ? (
|
{!isMobile && showSteps == true ? (
|
||||||
<Divider type='vertical' style={{ height: 'unset' }} />
|
<Divider type='vertical' style={{ height: 'unset' }} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<Flex vertical justify='space-between' gap={'middle'}>
|
||||||
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
|
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
|
||||||
<Title level={2} style={{ margin: 0 }}>
|
<Title level={2} style={{ margin: 0 }}>
|
||||||
{title}
|
{title}
|
||||||
</Title>
|
</Title>
|
||||||
<div style={{ minHeight: '260px', marginBottom: 8 }}>
|
<div style={{ minHeight: '260px', marginBottom: 4 }}>
|
||||||
{steps[currentStep].content}
|
{steps[currentStep].content}
|
||||||
</div>
|
</div>
|
||||||
|
</Flex>
|
||||||
<NewObjectButtons
|
<NewObjectButtons
|
||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
totalSteps={steps.length}
|
totalSteps={steps.length}
|
||||||
@ -49,6 +57,7 @@ const WizardView = ({
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
formValid={formValid}
|
formValid={formValid}
|
||||||
submitLoading={loading}
|
submitLoading={loading}
|
||||||
|
submitText={submitText}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -61,7 +70,9 @@ WizardView.propTypes = {
|
|||||||
steps: PropTypes.array.isRequired,
|
steps: PropTypes.array.isRequired,
|
||||||
showSteps: PropTypes.bool,
|
showSteps: PropTypes.bool,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
loading: PropTypes.bool
|
loading: PropTypes.bool,
|
||||||
|
sideBar: PropTypes.node,
|
||||||
|
submitText: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardView
|
export default WizardView
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { Job } from './models/Job'
|
|||||||
import { Product } from './models/Product'
|
import { Product } from './models/Product'
|
||||||
import { Part } from './models/Part.js'
|
import { Part } from './models/Part.js'
|
||||||
import { Vendor } from './models/Vendor'
|
import { Vendor } from './models/Vendor'
|
||||||
|
import { File } from './models/File'
|
||||||
import { SubJob } from './models/SubJob'
|
import { SubJob } from './models/SubJob'
|
||||||
import { Initial } from './models/Initial'
|
import { Initial } from './models/Initial'
|
||||||
import { FilamentStock } from './models/FilamentStock'
|
import { FilamentStock } from './models/FilamentStock'
|
||||||
@ -21,6 +22,7 @@ import { Note } from './models/Note'
|
|||||||
import { DocumentSize } from './models/DocumentSize.js'
|
import { DocumentSize } from './models/DocumentSize.js'
|
||||||
import { DocumentTemplate } from './models/DocumentTemplate.js'
|
import { DocumentTemplate } from './models/DocumentTemplate.js'
|
||||||
import { DocumentPrinter } from './models/DocumentPrinter.js'
|
import { DocumentPrinter } from './models/DocumentPrinter.js'
|
||||||
|
import { DocumentJob } from './models/DocumentJob.js'
|
||||||
import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
|
import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
|
||||||
|
|
||||||
export const objectModels = [
|
export const objectModels = [
|
||||||
@ -33,6 +35,7 @@ export const objectModels = [
|
|||||||
Product,
|
Product,
|
||||||
Part,
|
Part,
|
||||||
Vendor,
|
Vendor,
|
||||||
|
File,
|
||||||
SubJob,
|
SubJob,
|
||||||
Initial,
|
Initial,
|
||||||
FilamentStock,
|
FilamentStock,
|
||||||
@ -46,7 +49,8 @@ export const objectModels = [
|
|||||||
Note,
|
Note,
|
||||||
DocumentSize,
|
DocumentSize,
|
||||||
DocumentTemplate,
|
DocumentTemplate,
|
||||||
DocumentPrinter
|
DocumentPrinter,
|
||||||
|
DocumentJob
|
||||||
]
|
]
|
||||||
|
|
||||||
// Re-export individual models for direct access
|
// Re-export individual models for direct access
|
||||||
@ -60,6 +64,7 @@ export {
|
|||||||
Product,
|
Product,
|
||||||
Part,
|
Part,
|
||||||
Vendor,
|
Vendor,
|
||||||
|
File,
|
||||||
SubJob,
|
SubJob,
|
||||||
Initial,
|
Initial,
|
||||||
FilamentStock,
|
FilamentStock,
|
||||||
@ -73,7 +78,8 @@ export {
|
|||||||
Note,
|
Note,
|
||||||
DocumentSize,
|
DocumentSize,
|
||||||
DocumentTemplate,
|
DocumentTemplate,
|
||||||
DocumentPrinter
|
DocumentPrinter,
|
||||||
|
DocumentJob
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getModelByName(name, ignoreCase = false) {
|
export function getModelByName(name, ignoreCase = false) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import JobIcon from '../../components/Icons/JobIcon'
|
import JobIcon from '../../components/Icons/JobIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
|
||||||
export const Job = {
|
export const Job = {
|
||||||
name: 'job',
|
name: 'job',
|
||||||
@ -16,6 +17,19 @@ export const Job = {
|
|||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}`
|
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}`
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'deploy',
|
||||||
|
label: 'Deploy',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/production/jobs/info?jobId=${_id}?action=deploy`,
|
||||||
|
disabled: (objectData) => {
|
||||||
|
console.log('Should be disabled', objectData?.state?.type != 'draft')
|
||||||
|
return objectData?.state?.type != 'draft'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'reload',
|
name: 'reload',
|
||||||
label: 'Reload',
|
label: 'Reload',
|
||||||
|
|||||||
@ -76,12 +76,24 @@ export const Printer = {
|
|||||||
showName: false,
|
showName: false,
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'active',
|
||||||
|
label: 'Active',
|
||||||
|
type: 'bool',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'online',
|
||||||
|
label: 'Online',
|
||||||
|
type: 'bool',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'vendor',
|
name: 'vendor',
|
||||||
label: 'Vendor',
|
label: 'Vendor',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
objectType: 'vendor',
|
objectType: 'vendor',
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'vendor._id',
|
name: 'vendor._id',
|
||||||
@ -149,7 +161,7 @@ export const Printer = {
|
|||||||
label: 'Filament Stock',
|
label: 'Filament Stock',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
objectType: 'filamentStock',
|
objectType: 'filamentStock',
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'currentFilamentStock._id',
|
name: 'currentFilamentStock._id',
|
||||||
@ -158,6 +170,42 @@ export const Printer = {
|
|||||||
objectType: 'filamentStock',
|
objectType: 'filamentStock',
|
||||||
showHyperlink: true,
|
showHyperlink: true,
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'currentJob',
|
||||||
|
label: 'Current Job',
|
||||||
|
type: 'object',
|
||||||
|
objectType: 'job',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'currentJob._id',
|
||||||
|
label: 'Current Job ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: 'job',
|
||||||
|
showHyperlink: true,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'currentSubJob',
|
||||||
|
label: 'Current Sub Job',
|
||||||
|
type: 'object',
|
||||||
|
objectType: 'subJob',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'currentSubJob._id',
|
||||||
|
label: 'Current Sub Job ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: 'subJob',
|
||||||
|
showHyperlink: true,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'alerts',
|
||||||
|
label: 'Alerts',
|
||||||
|
type: 'alerts',
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user