diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css
index 40451f55..820b5764 100644
--- a/assets/stylesheets/App.css
+++ b/assets/stylesheets/App.css
@@ -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);
+}
diff --git a/src/components/Dashboard/common/CodeBlockEditor.jsx b/src/components/Dashboard/common/CodeBlockEditor.jsx
index d618e551..902ad6d0 100644
--- a/src/components/Dashboard/common/CodeBlockEditor.jsx
+++ b/src/components/Dashboard/common/CodeBlockEditor.jsx
@@ -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)
}
diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx
index fd3e3cc3..8ccb4548 100644
--- a/src/components/Dashboard/common/ObjectProperty.jsx
+++ b/src/components/Dashboard/common/ObjectProperty.jsx
@@ -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,
diff --git a/src/components/Dashboard/common/TemplateEditor.jsx b/src/components/Dashboard/common/TemplateEditor.jsx
index 93bafb06..f78a06ea 100644
--- a/src/components/Dashboard/common/TemplateEditor.jsx
+++ b/src/components/Dashboard/common/TemplateEditor.jsx
@@ -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 (
+
+ {indent}
+ {'>>'} {lineNum}|
+ {content || '\u00A0'}
+
+ )
+ }
+
+ const contextLineMatch = line.match(/^(\s+)(\d+)\|(.*)$/)
+ if (contextLineMatch) {
+ const [, indent, lineNum, content] = contextLineMatch
+ return (
+
+ {indent}
+ {lineNum}|{content || '\u00A0'}
+
+ )
+ }
+
+ if (/^ejs:\d+/.test(line)) {
+ return (
+
+ {line}
+
+ )
+ }
+
+ return (
+
+ {line || '\u00A0'}
+
+ )
+ }
+
+ return {message.split('\n').map(renderLine)}
+}
+
+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 = ({
Compile error.
}
trigger={['hover', 'click']}
placement='bottomRight'
arrow={false}
- overlayStyle={{ width: '560px' }}
+ overlayStyle={{ minWidth: '560px' }}
>
diff --git a/src/components/Dashboard/context/ThemeContext.jsx b/src/components/Dashboard/context/ThemeContext.jsx
index 96f26892..34f8ab90 100644
--- a/src/components/Dashboard/context/ThemeContext.jsx
+++ b/src/components/Dashboard/context/ThemeContext.jsx
@@ -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 = {