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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user