Enhance error handling in code editors and improve styling
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Added error line highlighting functionality in CodeBlockEditor and ObjectProperty components to visually indicate errors in code.
- Introduced a new EJSErrorFormatter component in TemplateEditor for better error message display.
- Updated App.css with new styles for error lines and various color classes for improved visual feedback.
- Enhanced ThemeContext to support dynamic text color based on theme mode.
This commit is contained in:
Tom Butcher 2026-08-19 22:19:00 +01:00
parent 6d0e9a7abc
commit 84adf58542
5 changed files with 169 additions and 7 deletions

View File

@ -1302,9 +1302,45 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
overflow: auto; overflow: auto;
} }
.cm-errorLine {
background-color: color-mix(in srgb, var(--color-error) 15%, transparent);
}
.cm-gutters .cm-errorLine {
color: var(--color-error);
}
.diff-label-square { .diff-label-square {
width: 7px; width: 7px;
height: 7px; height: 7px;
background-color: #85858541; background-color: #85858541;
border-radius: 2px; border-radius: 2px;
} }
.ͼo {
color: var(--color-text);
}
.ͼu {
color: var(--color-warning);
}
.ͼq {
color: var(--color-error);
}
.ͼ13 {
color: var(--color-success);
}
.ͼp {
color: var(--color-purple);
}
.ͼv {
color: var(--color-cyan);
}
.ͼt {
color: var(--color-magenta);
}

View File

@ -1,6 +1,8 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import CodeMirror from '@uiw/react-codemirror' import CodeMirror from '@uiw/react-codemirror'
import { EditorView, Decoration } from '@codemirror/view'
import { StateField } from '@codemirror/state'
import { javascript } from '@codemirror/lang-javascript' import { javascript } from '@codemirror/lang-javascript'
import { python } from '@codemirror/lang-python' import { python } from '@codemirror/lang-python'
import { json } from '@codemirror/lang-json' import { json } from '@codemirror/lang-json'
@ -80,6 +82,40 @@ export function getCodeLanguageExtension(
} }
} }
function buildErrorLineDecorations(state, errorLineSet) {
if (!errorLineSet.size) {
return Decoration.none
}
const decorations = []
for (let i = 1; i <= state.doc.lines; i++) {
if (errorLineSet.has(i)) {
const line = state.doc.line(i)
decorations.push(
Decoration.line({ class: 'cm-errorLine' }).range(line.from)
)
}
}
return Decoration.set(decorations, true)
}
function createErrorLineExtension(errorLines) {
const errorLineSet = new Set(errorLines)
return StateField.define({
create(state) {
return buildErrorLineDecorations(state, errorLineSet)
},
update(decorations, tr) {
if (tr.docChanged) {
return buildErrorLineDecorations(tr.state, errorLineSet)
}
return decorations.map(tr.changes)
},
provide: (f) => EditorView.decorations.from(f)
})
}
export default function CodeBlockEditor({ export default function CodeBlockEditor({
code = '', code = '',
value, value,
@ -92,7 +128,8 @@ export default function CodeBlockEditor({
showLineNumbers = true, showLineNumbers = true,
disabled = false, disabled = false,
minimal = false, minimal = false,
autoCompleteObject = null autoCompleteObject = null,
errorLines = []
}) { }) {
const { isDarkMode } = useThemeContext() const { isDarkMode } = useThemeContext()
const [codeMirrorOpen, setCodeMirrorOpen] = useState(false) const [codeMirrorOpen, setCodeMirrorOpen] = useState(false)
@ -104,6 +141,13 @@ export default function CodeBlockEditor({
[language, autoCompleteObject] [language, autoCompleteObject]
) )
const errorLineExtension = useMemo(() => {
if (!errorLines?.length) {
return []
}
return [createErrorLineExtension(errorLines)]
}, [errorLines])
const handleOnChange = (value) => { const handleOnChange = (value) => {
if (typeof code == 'object' && language == 'json') { if (typeof code == 'object' && language == 'json') {
onChange(JSON.parse(value)) onChange(JSON.parse(value))
@ -124,7 +168,7 @@ export default function CodeBlockEditor({
value={editorCode} value={editorCode}
height={height} height={height}
theme={isDarkMode ? oneDark : 'light'} theme={isDarkMode ? oneDark : 'light'}
extensions={[languageExtension]} extensions={[languageExtension, ...errorLineExtension]}
readOnly={readOnly || disabled} readOnly={readOnly || disabled}
onChange={handleOnChange} onChange={handleOnChange}
basicSetup={{ basicSetup={{
@ -208,5 +252,6 @@ CodeBlockEditor.propTypes = {
showLineNumbers: PropTypes.bool, showLineNumbers: PropTypes.bool,
disabled: PropTypes.bool, disabled: PropTypes.bool,
minimal: PropTypes.bool, minimal: PropTypes.bool,
autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]) autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
errorLines: PropTypes.arrayOf(PropTypes.number)
} }

View File

@ -113,6 +113,7 @@ const ObjectProperty = ({
height = 'auto', height = 'auto',
minimal = false, minimal = false,
autoCompleteObject = null, autoCompleteObject = null,
errorLines = [],
previewOpen = false, previewOpen = false,
showPreview = true, showPreview = true,
useFormItem = true, useFormItem = true,
@ -402,6 +403,7 @@ const ObjectProperty = ({
readOnly={true} readOnly={true}
minimal={minimal} minimal={minimal}
autoCompleteObject={autoCompleteObject} autoCompleteObject={autoCompleteObject}
errorLines={errorLines}
/> />
) )
} else { } else {
@ -910,6 +912,7 @@ const ObjectProperty = ({
height={height} height={height}
minimal={minimal} minimal={minimal}
autoCompleteObject={autoCompleteObject} autoCompleteObject={autoCompleteObject}
errorLines={errorLines}
{...inputProps} {...inputProps}
/> />
) )
@ -1042,6 +1045,7 @@ ObjectProperty.propTypes = {
name: PropTypes.string, name: PropTypes.string,
language: PropTypes.string, language: PropTypes.string,
autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
errorLines: PropTypes.arrayOf(PropTypes.number),
prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
min: PropTypes.number, min: PropTypes.number,

View File

@ -9,7 +9,8 @@ import {
Button, Button,
Modal, Modal,
Segmented, Segmented,
Popover Popover,
Typography
} from 'antd' } from 'antd'
import { LoadingOutlined, CaretDownOutlined } from '@ant-design/icons' import { LoadingOutlined, CaretDownOutlined } from '@ant-design/icons'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon.jsx' import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon.jsx'
@ -19,7 +20,79 @@ import ObjectProperty from '../common/ObjectProperty.jsx'
import TemplatePreview from './TemplatePreview.jsx' import TemplatePreview from './TemplatePreview.jsx'
import DataTree from './DataTree.jsx' import DataTree from './DataTree.jsx'
import { ApiServerContext } from '../context/ApiServerContext.jsx' import { ApiServerContext } from '../context/ApiServerContext.jsx'
//import { useMediaQuery } from 'react-responsive'
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 = ({ const TemplateEditor = ({
objectData, objectData,
@ -35,6 +108,7 @@ const TemplateEditor = ({
const [testObjectViewMode, setTestObjectViewMode] = useState('Tree') const [testObjectViewMode, setTestObjectViewMode] = useState('Tree')
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 [errorLines, setErrorLines] = useState([])
const [formatLoading, setFormatLoading] = useState(false) const [formatLoading, setFormatLoading] = useState(false)
//const isMobile = useMediaQuery({ maxWidth: 768 }) //const isMobile = useMediaQuery({ maxWidth: 768 })
@ -44,11 +118,11 @@ const TemplateEditor = ({
<Flex gap={'small'} align={'center'} justify={'space-between'}> <Flex gap={'small'} align={'center'} justify={'space-between'}>
Compile error. Compile error.
<Popover <Popover
content={message} content={<EJSErrorFormatter message={message} />}
trigger={['hover', 'click']} trigger={['hover', 'click']}
placement='bottomRight' placement='bottomRight'
arrow={false} arrow={false}
overlayStyle={{ width: '560px' }} overlayStyle={{ minWidth: '560px' }}
> >
<Button <Button
type={'text'} type={'text'}
@ -71,6 +145,7 @@ const TemplateEditor = ({
} }
setPreviewError(isError) setPreviewError(isError)
setErrorLines(isError ? parseEjsErrorLines(message) : [])
}, []) }, [])
const handleFormatCode = useCallback(async () => { const handleFormatCode = useCallback(async () => {
@ -178,6 +253,7 @@ const TemplateEditor = ({
objectData={objectData} objectData={objectData}
isEditing={isEditing} isEditing={isEditing}
autoCompleteObject={objectData?.testObject} autoCompleteObject={objectData?.testObject}
errorLines={errorLines}
/> />
</div> </div>
</Flex> </Flex>

View File

@ -156,6 +156,7 @@ export const ThemeProvider = ({ children }) => {
'--layout-modal-bg', '--layout-modal-bg',
isDarkMode ? '#1f1f1f' : '#ffffff' isDarkMode ? '#1f1f1f' : '#ffffff'
) )
root.style.setProperty('--color-text', isDarkMode ? '#ffffff' : '#000000')
}, [isDarkMode, primaryColorOverride]) }, [isDarkMode, primaryColorOverride])
const themeConfig = { const themeConfig = {