All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Wrapped the TemplatePreview component in a Spin component to provide a loading indicator while content is being loaded. - Enhanced the structure of the TemplatePreview component by ensuring proper style and props are passed, improving user experience during loading states.
349 lines
9.8 KiB
JavaScript
349 lines
9.8 KiB
JavaScript
import { useState, useCallback, useContext } from 'react'
|
|
import PropTypes from 'prop-types'
|
|
import {
|
|
Flex,
|
|
Alert,
|
|
Card,
|
|
Spin,
|
|
Splitter,
|
|
Button,
|
|
Modal,
|
|
Segmented,
|
|
Popover,
|
|
Typography
|
|
} from 'antd'
|
|
import { LoadingOutlined, CaretDownOutlined } from '@ant-design/icons'
|
|
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon.jsx'
|
|
import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx'
|
|
import MagicWandIcon from '../../Icons/MagicWandIcon.jsx'
|
|
import ObjectProperty from '../common/ObjectProperty.jsx'
|
|
import TemplatePreview from './TemplatePreview.jsx'
|
|
import DataTree from './DataTree.jsx'
|
|
import { ApiServerContext } from '../context/ApiServerContext.jsx'
|
|
|
|
const { Text } = Typography
|
|
|
|
const monoStyle = {
|
|
whiteSpace: 'pre',
|
|
display: 'block',
|
|
fontFamily: 'monospace'
|
|
}
|
|
|
|
const EJSErrorFormatter = ({ message }) => {
|
|
const renderLine = (line, index) => {
|
|
const errorLineMatch = line.match(/^(\s*)>>\s*(\d+)\|(.*)$/)
|
|
if (errorLineMatch) {
|
|
const [, indent, lineNum, content] = errorLineMatch
|
|
return (
|
|
<Text key={index} style={monoStyle}>
|
|
{indent}
|
|
<span style={{ color: 'var(--color-error)' }}>{'>>'}</span> {lineNum}|
|
|
{content || '\u00A0'}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
const contextLineMatch = line.match(/^(\s+)(\d+)\|(.*)$/)
|
|
if (contextLineMatch) {
|
|
const [, indent, lineNum, content] = contextLineMatch
|
|
return (
|
|
<Text key={index} style={monoStyle}>
|
|
{indent}
|
|
<span style={{ opacity: 0.5 }}>{lineNum}</span>|{content || '\u00A0'}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
if (/^ejs:\d+/.test(line)) {
|
|
return (
|
|
<Text key={index} style={{ ...monoStyle, opacity: 0.75 }}>
|
|
{line}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Text key={index} style={monoStyle}>
|
|
{line || '\u00A0'}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
return <Flex vertical>{message.split('\n').map(renderLine)}</Flex>
|
|
}
|
|
|
|
EJSErrorFormatter.propTypes = {
|
|
message: PropTypes.string.isRequired
|
|
}
|
|
|
|
const parseEjsErrorLines = (message) => {
|
|
if (!message || typeof message !== 'string') {
|
|
return []
|
|
}
|
|
|
|
const lines = new Set()
|
|
const headerMatch = message.match(/^ejs:(\d+)/m)
|
|
if (headerMatch) {
|
|
lines.add(Number(headerMatch[1]))
|
|
}
|
|
|
|
for (const match of message.matchAll(/^\s*>>\s*(\d+)\|/gm)) {
|
|
lines.add(Number(match[1]))
|
|
}
|
|
|
|
return [...lines]
|
|
}
|
|
|
|
const TemplateEditor = ({
|
|
objectData,
|
|
loading,
|
|
collapseState,
|
|
isEditing,
|
|
style,
|
|
form,
|
|
setObjectData
|
|
}) => {
|
|
const { fetchTemplateFormat } = useContext(ApiServerContext)
|
|
const [testObjectOpen, setTestObjectOpen] = useState(false)
|
|
const [testObjectViewMode, setTestObjectViewMode] = useState('Tree')
|
|
const [previewMessage, setPreviewMessage] = useState('No issues found.')
|
|
const [previewError, setPreviewError] = useState(false)
|
|
const [errorLines, setErrorLines] = useState([])
|
|
const [formatLoading, setFormatLoading] = useState(false)
|
|
//const isMobile = useMediaQuery({ maxWidth: 768 })
|
|
|
|
const handlePreviewMessage = useCallback((message, isError) => {
|
|
if (isError) {
|
|
setPreviewMessage(
|
|
<Flex gap={'small'} align={'center'} justify={'space-between'}>
|
|
Compile error.
|
|
<Popover
|
|
content={<EJSErrorFormatter message={message} />}
|
|
trigger={['hover', 'click']}
|
|
placement='bottomRight'
|
|
arrow={false}
|
|
overlayStyle={{ minWidth: '560px' }}
|
|
>
|
|
<Button
|
|
type={'text'}
|
|
size={'small'}
|
|
icon={
|
|
<CaretDownOutlined
|
|
style={{
|
|
marginLeft: '0.5px',
|
|
marginBottom: '2.5px',
|
|
fontSize: '12.5px'
|
|
}}
|
|
/>
|
|
}
|
|
/>
|
|
</Popover>
|
|
</Flex>
|
|
)
|
|
} else {
|
|
setPreviewMessage(message)
|
|
}
|
|
|
|
setPreviewError(isError)
|
|
setErrorLines(isError ? parseEjsErrorLines(message) : [])
|
|
}, [])
|
|
|
|
const handleFormatCode = useCallback(async () => {
|
|
const content = form?.getFieldValue?.('content') ?? objectData?.content
|
|
if (content == null || typeof content !== 'string') {
|
|
return
|
|
}
|
|
|
|
setFormatLoading(true)
|
|
try {
|
|
const result = await fetchTemplateFormat(content)
|
|
if (result?.error) {
|
|
handlePreviewMessage(result.error, true)
|
|
return
|
|
}
|
|
|
|
if (result?.content == null) {
|
|
return
|
|
}
|
|
|
|
form?.setFieldValue?.('content', result.content)
|
|
setObjectData?.((prev) => ({
|
|
...prev,
|
|
content: result.content
|
|
}))
|
|
} finally {
|
|
setFormatLoading(false)
|
|
}
|
|
}, [
|
|
form,
|
|
objectData?.content,
|
|
fetchTemplateFormat,
|
|
handlePreviewMessage,
|
|
setObjectData
|
|
])
|
|
|
|
return (
|
|
<>
|
|
<Splitter className={'farmcontrol-splitter'} vertical={true}>
|
|
{collapseState.preview == true && (
|
|
<Splitter.Panel style={{ height: '100%' }}>
|
|
<Spin spinning={loading} indicator={<LoadingOutlined />}>
|
|
<Card
|
|
spinning={loading}
|
|
style={style}
|
|
styles={{ body: { height: '100%', padding: '22px' } }}
|
|
>
|
|
<TemplatePreview
|
|
objectData={objectData?.testObject}
|
|
documentTemplate={objectData}
|
|
loading={loading}
|
|
isEditing={isEditing}
|
|
style={style}
|
|
onTestObjectOpen={() => setTestObjectOpen(true)}
|
|
onPreviewMessage={handlePreviewMessage}
|
|
showTestObject={true}
|
|
/>
|
|
</Card>
|
|
</Spin>
|
|
</Splitter.Panel>
|
|
)}
|
|
{collapseState.editor == true && (
|
|
<Splitter.Panel>
|
|
<Spin spinning={loading} indicator={<LoadingOutlined />}>
|
|
<Card
|
|
style={style}
|
|
styles={{ body: { height: '100%', padding: '22px' } }}
|
|
>
|
|
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
|
|
<Flex gap={'small'} align={'stretch'}>
|
|
<Alert
|
|
message={previewMessage}
|
|
showIcon
|
|
style={{
|
|
padding: '4px 5px 4px 8px',
|
|
flex: '1 1 auto',
|
|
minWidth: 0
|
|
}}
|
|
icon={
|
|
previewError ? (
|
|
<ExclamationOctagonIcon />
|
|
) : (
|
|
<CheckCircleIcon />
|
|
)
|
|
}
|
|
type={previewError ? 'error' : 'success'}
|
|
/>
|
|
{isEditing && (
|
|
<Button
|
|
icon={<MagicWandIcon />}
|
|
loading={formatLoading}
|
|
disabled={loading || formatLoading}
|
|
onClick={handleFormatCode}
|
|
/>
|
|
)}
|
|
</Flex>
|
|
|
|
<div
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
flex: '1 1 auto',
|
|
minHeight: 0
|
|
}}
|
|
>
|
|
<ObjectProperty
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
flex: '1 1 auto',
|
|
minHeight: 0
|
|
}}
|
|
useFormItem={false}
|
|
name={'content'}
|
|
height='100%'
|
|
type='codeBlock'
|
|
language={'fcTemplateLang'}
|
|
objectData={objectData}
|
|
onChange={(value) => {
|
|
form?.setFieldValue?.('content', value)
|
|
setObjectData?.((prev) => ({
|
|
...prev,
|
|
content: value
|
|
}))
|
|
}}
|
|
isEditing={isEditing}
|
|
autoCompleteObject={objectData?.testObject}
|
|
errorLines={errorLines}
|
|
/>
|
|
</div>
|
|
</Flex>
|
|
</Card>
|
|
</Spin>
|
|
</Splitter.Panel>
|
|
)}
|
|
</Splitter>
|
|
<Modal
|
|
open={testObjectOpen}
|
|
closeIcon={null}
|
|
width={800}
|
|
footer={
|
|
<Button
|
|
onClick={() => {
|
|
setTestObjectOpen(false)
|
|
}}
|
|
>
|
|
Close
|
|
</Button>
|
|
}
|
|
>
|
|
<Flex gap={'small'} vertical>
|
|
<div>
|
|
<Segmented
|
|
options={['Tree', 'Code']}
|
|
value={testObjectViewMode}
|
|
onChange={(value) => setTestObjectViewMode(value)}
|
|
size='small'
|
|
/>
|
|
</div>
|
|
<div
|
|
style={{
|
|
maxHeight: 'calc(var(--unit-100vh) - 280px)',
|
|
overflowY: 'scroll'
|
|
}}
|
|
>
|
|
{testObjectViewMode == 'Code' && (
|
|
<ObjectProperty
|
|
type={'codeBlock'}
|
|
name='testObject'
|
|
language='json'
|
|
objectData={objectData}
|
|
isEditing={true}
|
|
/>
|
|
)}
|
|
{testObjectViewMode == 'Tree' && (
|
|
<DataTree
|
|
data={objectData?.testObject}
|
|
defaultExpandAll={true}
|
|
showValueCopy={false}
|
|
showKeyCopy={true}
|
|
/>
|
|
)}
|
|
</div>
|
|
</Flex>
|
|
</Modal>
|
|
</>
|
|
)
|
|
}
|
|
|
|
TemplateEditor.propTypes = {
|
|
loading: PropTypes.bool,
|
|
objectData: PropTypes.object,
|
|
collapseState: PropTypes.object,
|
|
isEditing: PropTypes.bool,
|
|
style: PropTypes.object,
|
|
form: PropTypes.object,
|
|
setObjectData: PropTypes.func
|
|
}
|
|
|
|
export default TemplateEditor
|