Enhance error handling in code editors and improve styling
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
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:
parent
6d0e9a7abc
commit
84adf58542
@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user