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;
}
.cm-errorLine {
background-color: color-mix(in srgb, var(--color-error) 15%, transparent);
}
.cm-gutters .cm-errorLine {
color: var(--color-error);
}
.diff-label-square {
width: 7px;
height: 7px;
background-color: #85858541;
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 PropTypes from 'prop-types'
import CodeMirror from '@uiw/react-codemirror'
import { EditorView, Decoration } from '@codemirror/view'
import { StateField } from '@codemirror/state'
import { javascript } from '@codemirror/lang-javascript'
import { python } from '@codemirror/lang-python'
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({
code = '',
value,
@ -92,7 +128,8 @@ export default function CodeBlockEditor({
showLineNumbers = true,
disabled = false,
minimal = false,
autoCompleteObject = null
autoCompleteObject = null,
errorLines = []
}) {
const { isDarkMode } = useThemeContext()
const [codeMirrorOpen, setCodeMirrorOpen] = useState(false)
@ -104,6 +141,13 @@ export default function CodeBlockEditor({
[language, autoCompleteObject]
)
const errorLineExtension = useMemo(() => {
if (!errorLines?.length) {
return []
}
return [createErrorLineExtension(errorLines)]
}, [errorLines])
const handleOnChange = (value) => {
if (typeof code == 'object' && language == 'json') {
onChange(JSON.parse(value))
@ -124,7 +168,7 @@ export default function CodeBlockEditor({
value={editorCode}
height={height}
theme={isDarkMode ? oneDark : 'light'}
extensions={[languageExtension]}
extensions={[languageExtension, ...errorLineExtension]}
readOnly={readOnly || disabled}
onChange={handleOnChange}
basicSetup={{
@ -208,5 +252,6 @@ CodeBlockEditor.propTypes = {
showLineNumbers: PropTypes.bool,
disabled: 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',
minimal = false,
autoCompleteObject = null,
errorLines = [],
previewOpen = false,
showPreview = true,
useFormItem = true,
@ -402,6 +403,7 @@ const ObjectProperty = ({
readOnly={true}
minimal={minimal}
autoCompleteObject={autoCompleteObject}
errorLines={errorLines}
/>
)
} else {
@ -910,6 +912,7 @@ const ObjectProperty = ({
height={height}
minimal={minimal}
autoCompleteObject={autoCompleteObject}
errorLines={errorLines}
{...inputProps}
/>
)
@ -1042,6 +1045,7 @@ ObjectProperty.propTypes = {
name: PropTypes.string,
language: PropTypes.string,
autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
errorLines: PropTypes.arrayOf(PropTypes.number),
prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
min: PropTypes.number,

View File

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

View File

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