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' }} >