Refactor CodeMirror Language Support and Enhance Template Handling
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit

- Updated fcTemplateLang to utilize new helper functions for improved template type handling.
- Introduced typedScope for better autocompletion support in JavaScript language integration.
- Enhanced CodeBlockEditor and TemplateEditor components to support cursor change events and template type management.
- Added fetchTemplateIntellisense function in ApiServerContext for improved intellisense capabilities based on cursor position.
- Refactored related components to ensure consistent handling of template types and cursor changes, enhancing user experience.
This commit is contained in:
Tom Butcher 2026-09-12 23:30:00 +01:00
parent a9e4c8dad3
commit a590650d80
9 changed files with 571 additions and 60 deletions

View File

@ -9,7 +9,7 @@ import {
import { styleTags, tags as t } from '@lezer/highlight' import { styleTags, tags as t } from '@lezer/highlight'
import { parseMixed } from '@lezer/common' import { parseMixed } from '@lezer/common'
import { javascriptLanguage } from '@codemirror/lang-javascript' import { javascriptLanguage } from '@codemirror/lang-javascript'
import { javascriptCompletionSupport } from '../javascriptLang' import { javascriptCompletionSupport } from '../javascriptLang/index.js'
import { import {
xmlLanguage, xmlLanguage,
completeFromSchema, completeFromSchema,
@ -19,8 +19,9 @@ import { parser as ejsParser } from './ejs.parser.js'
import { import {
fcTemplateElements, fcTemplateElements,
fcTemplateAttributes, fcTemplateAttributes,
fcTemplateHelpers getFcTemplateHelpers
} from './schema.js' } from './schema.js'
import { normalizeTypedScope } from '../javascriptLang/typedScope.js'
/** /**
* FarmControl document template language: custom XML tags with EJS * FarmControl document template language: custom XML tags with EJS
@ -71,31 +72,22 @@ export const fcTemplateLanguage = LRLanguage.define({
} }
}) })
function buildJsScope(autoCompleteObject) { function buildJsScope(autoCompleteObject, templateType) {
let data = autoCompleteObject const typed = normalizeTypedScope(autoCompleteObject)
if (typeof data === 'string') { typed.properties = {
try { ...typed.properties,
data = JSON.parse(data) fc: getFcTemplateHelpers(templateType)
} catch {
data = {}
}
}
if (!data || typeof data !== 'object' || Array.isArray(data)) {
data = {}
}
return {
...data,
fc: fcTemplateHelpers
} }
return typed
} }
/** /**
* Language support for FarmControl EJS+XML document templates. * Language support for FarmControl EJS+XML document templates.
* @param {{ autoCompleteObject?: object|string }} [options] * @param {{ autoCompleteObject?: object|string, templateType?: string }} [options]
*/ */
export function fcTemplateLang(options = {}) { export function fcTemplateLang(options = {}) {
const { autoCompleteObject } = options const { autoCompleteObject, templateType = 'documentTemplate' } = options
const jsScope = buildJsScope(autoCompleteObject) const jsScope = buildJsScope(autoCompleteObject, templateType)
return new LanguageSupport(fcTemplateLanguage, [ return new LanguageSupport(fcTemplateLanguage, [
xmlLanguage.data.of({ xmlLanguage.data.of({

View File

@ -102,9 +102,49 @@ export const fcTemplateElements = allTags.map((name) => {
}) })
/** Runtime helpers available as `fc` inside templates. */ /** Runtime helpers available as `fc` inside templates. */
export const fcTemplateHelpers = { const fcSharedHelpers = {
listObjects: async () => [], listObjects: {
getObject: async () => ({}), $type: 'function',
formatDate: () => '', detail: '(objectType, filter?, populate?) => Promise<object[]>'
renderDocumentTemplate: async () => '' },
getObject: {
$type: 'function',
detail: '(objectType, id, populate?) => Promise<object>'
},
formatDate: {
$type: 'function',
detail: '(date, format) => string'
}
}
export const fcDocumentTemplateHelpers = {
$type: 'object',
name: 'fc',
properties: {
...fcSharedHelpers,
renderDocumentTemplate: {
$type: 'function',
detail: '(reference, object?) => Promise<string>'
}
}
}
export const fcEmailTemplateHelpers = {
$type: 'object',
name: 'fc',
properties: {
...fcSharedHelpers,
renderEmailTemplate: {
$type: 'function',
detail: '(reference, object?) => Promise<string>'
}
}
}
export const fcTemplateHelpers = fcDocumentTemplateHelpers
export function getFcTemplateHelpers(templateType) {
return templateType === 'emailTemplate'
? fcEmailTemplateHelpers
: fcDocumentTemplateHelpers
} }

View File

@ -4,27 +4,10 @@ import {
scopeCompletionSource scopeCompletionSource
} from '@codemirror/lang-javascript' } from '@codemirror/lang-javascript'
import { nodeJsScope } from './nodeScope.js' import { nodeJsScope } from './nodeScope.js'
import { typedScopeCompletionSource } from './typedScope.js'
function extraScope(autoCompleteObject) { export function nodeScopeCompletionSource() {
let data = autoCompleteObject return scopeCompletionSource(nodeJsScope)
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch {
data = {}
}
}
if (!data || typeof data !== 'object' || Array.isArray(data)) {
return {}
}
return data
}
export function nodeScopeCompletionSource(autoCompleteObject) {
return scopeCompletionSource({
...nodeJsScope,
...extraScope(autoCompleteObject)
})
} }
/** /**
@ -40,7 +23,10 @@ export function javascriptCompletionSupport(autoCompleteObject) {
return [ return [
javascript().support, javascript().support,
javascriptLanguage.data.of({ javascriptLanguage.data.of({
autocomplete: nodeScopeCompletionSource(autoCompleteObject) autocomplete: scopeCompletionSource(nodeJsScope)
}),
javascriptLanguage.data.of({
autocomplete: typedScopeCompletionSource(autoCompleteObject)
}) })
] ]
} }
@ -55,7 +41,10 @@ export function javascriptLang(options = {}) {
return [ return [
javascript(), javascript(),
javascriptLanguage.data.of({ javascriptLanguage.data.of({
autocomplete: nodeScopeCompletionSource(autoCompleteObject) autocomplete: scopeCompletionSource(nodeJsScope)
}),
javascriptLanguage.data.of({
autocomplete: typedScopeCompletionSource(autoCompleteObject)
}) })
] ]
} }

View File

@ -0,0 +1,319 @@
import { completionPath } from '@codemirror/lang-javascript'
const IDENTIFIER = /^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/
const ISO_DATE_RE =
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/
const OBJECT_ID_RE = /^[a-fA-F0-9]{24}$/
const MAX_STRING_LENGTH = 80
function fn(detail) {
return { $type: 'function', detail }
}
const STRING_MEMBERS = {
length: { $type: 'number' },
charAt: fn('(index) => string'),
includes: fn('(search) => boolean'),
indexOf: fn('(search) => number'),
padStart: fn('(length, fill?) => string'),
replace: fn('(pattern, replacement) => string'),
slice: fn('(start, end?) => string'),
split: fn('(separator) => string[]'),
startsWith: fn('(search) => boolean'),
endsWith: fn('(search) => boolean'),
substring: fn('(start, end?) => string'),
toLowerCase: fn('() => string'),
toUpperCase: fn('() => string'),
trim: fn('() => string')
}
const NUMBER_MEMBERS = {
toFixed: fn('(digits?) => string'),
toString: fn('() => string'),
valueOf: fn('() => number')
}
const DATE_MEMBERS = {
getDate: fn('() => number'),
getDay: fn('() => number'),
getFullYear: fn('() => number'),
getHours: fn('() => number'),
getMinutes: fn('() => number'),
getMonth: fn('() => number'),
getSeconds: fn('() => number'),
getTime: fn('() => number'),
toISOString: fn('() => string'),
toLocaleDateString: fn('() => string'),
toLocaleString: fn('() => string'),
toString: fn('() => string'),
valueOf: fn('() => number')
}
const ARRAY_MEMBERS = {
length: { $type: 'number' },
at: fn('(index) => any'),
concat: fn('(...items) => any[]'),
every: fn('(predicate) => boolean'),
filter: fn('(predicate) => any[]'),
find: fn('(predicate) => any'),
forEach: fn('(callback) => void'),
includes: fn('(value) => boolean'),
join: fn('(separator?) => string'),
map: fn('(callback) => any[]'),
reduce: fn('(callback, initial?) => any'),
slice: fn('(start, end?) => any[]'),
some: fn('(predicate) => boolean')
}
const BOOLEAN_MEMBERS = {
toString: fn('() => string'),
valueOf: fn('() => boolean')
}
export function isTypedNode(value) {
return (
value != null &&
typeof value === 'object' &&
!Array.isArray(value) &&
typeof value.$type === 'string'
)
}
function inferStringType(value, key) {
const truncated =
value.length > MAX_STRING_LENGTH ? value.slice(0, MAX_STRING_LENGTH) : value
if (ISO_DATE_RE.test(value)) {
return { $type: 'Date', value: truncated }
}
if (
(key === '_id' || key === 'id' || /Id$/.test(String(key || ''))) &&
OBJECT_ID_RE.test(value)
) {
return { $type: 'ObjectId', value: truncated }
}
return { $type: 'string', value: truncated }
}
export function inferTypedNode(value, key = null, depth = 0, seen = new WeakSet()) {
if (isTypedNode(value)) {
return value
}
if (value === undefined) {
return undefined
}
if (value === null) {
return { $type: 'null' }
}
const valueType = typeof value
if (valueType === 'string') {
return inferStringType(value, key)
}
if (valueType === 'number') {
return { $type: 'number', value }
}
if (valueType === 'boolean') {
return { $type: 'boolean', value }
}
if (valueType === 'function') {
return { $type: 'function', detail: 'fn' }
}
if (value instanceof Date) {
return { $type: 'Date', value: value.toISOString() }
}
if (valueType !== 'object') {
return { $type: 'any' }
}
if (seen.has(value) || depth > 6) {
return Array.isArray(value)
? { $type: 'array', element: { $type: 'any' } }
: { $type: 'object', properties: {} }
}
seen.add(value)
if (Array.isArray(value)) {
let element = null
for (const item of value.slice(0, 3)) {
const typed = inferTypedNode(item, null, depth + 1, seen)
if (typed && typed.$type !== 'null') {
element = typed
break
}
}
return { $type: 'array', element: element || { $type: 'any' } }
}
const properties = {}
for (const [nestedKey, nested] of Object.entries(value)) {
const typed = inferTypedNode(nested, nestedKey, depth + 1, seen)
if (typed) {
properties[nestedKey] = typed
}
}
return { $type: 'object', properties }
}
export function normalizeTypedScope(autoCompleteObject) {
let data = autoCompleteObject
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch {
data = {}
}
}
if (isTypedNode(data) && data.$type === 'object') {
return {
$type: 'object',
...(data.name ? { name: data.name } : {}),
properties: { ...(data.properties || {}) }
}
}
if (data != null && typeof data === 'object' && !Array.isArray(data)) {
const values = Object.values(data)
if (
values.length > 0 &&
values.every((value) => isTypedNode(value))
) {
return { $type: 'object', properties: { ...data } }
}
return inferTypedNode(data)
}
return { $type: 'object', properties: {} }
}
export function typeLabel(node) {
if (!isTypedNode(node)) {
return 'any'
}
switch (node.$type) {
case 'array':
return `${typeLabel(node.element)}[]`
case 'object':
return node.name || 'object'
case 'ObjectId':
return node.name ? `ObjectId<${node.name}>` : 'ObjectId'
case 'function':
return node.detail || 'function'
case 'null':
return 'null'
default:
return node.$type
}
}
function builtinMembers(node) {
switch (node?.$type) {
case 'string':
case 'ObjectId':
return STRING_MEMBERS
case 'number':
return NUMBER_MEMBERS
case 'Date':
return DATE_MEMBERS
case 'array':
return ARRAY_MEMBERS
case 'boolean':
return BOOLEAN_MEMBERS
default:
return null
}
}
function resolveTypedPath(root, path) {
let node = root
for (const step of path) {
if (!isTypedNode(node)) {
return null
}
if (node.$type === 'object') {
node = node.properties?.[step]
continue
}
if (node.$type === 'array' && step === 'length') {
node = { $type: 'number' }
continue
}
const members = builtinMembers(node)
if (members?.[step]) {
node = members[step]
continue
}
return null
}
return isTypedNode(node) ? node : null
}
function completionType(node, topLevel) {
if (node?.$type === 'function') {
return topLevel ? 'function' : 'method'
}
return topLevel ? 'variable' : 'property'
}
function enumerateTypedCompletions(node, topLevel) {
if (!isTypedNode(node)) {
return []
}
const options = []
const seen = new Set()
if (node.$type === 'object') {
for (const [name, child] of Object.entries(node.properties || {})) {
if (!IDENTIFIER.test(name) || seen.has(name)) {
continue
}
seen.add(name)
options.push({
label: name,
type: completionType(child, topLevel),
detail: typeLabel(child),
boost: 1
})
}
}
const members = builtinMembers(node)
if (members) {
for (const [name, child] of Object.entries(members)) {
if (!IDENTIFIER.test(name) || seen.has(name)) {
continue
}
seen.add(name)
options.push({
label: name,
type: completionType(child, false),
detail: typeLabel(child),
boost: 0
})
}
}
return options
}
/**
* Completes template locals from a typed intellisense tree, showing
* JS/mongoose types in the dropdown detail column.
*/
export function typedScopeCompletionSource(autoCompleteObject) {
const scope = normalizeTypedScope(autoCompleteObject)
const cache = new Map()
return (context) => {
const path = completionPath(context)
if (!path) {
return null
}
const target = path.path.length === 0 ? scope : resolveTypedPath(scope, path.path)
if (!target) {
return null
}
let options = cache.get(target)
if (!options) {
options = enumerateTypedCompletions(target, path.path.length === 0)
cache.set(target, options)
}
if (options.length === 0) {
return null
}
return {
from: context.pos - path.name.length,
options,
validFor: IDENTIFIER
}
}
}

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react' import { useMemo, useState, useRef, useEffect } 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, ViewPlugin } from '@codemirror/view' import { EditorView, Decoration, ViewPlugin } from '@codemirror/view'
@ -105,6 +105,37 @@ function buildErrorLineDecorations(state, errorLineSet) {
return Decoration.set(decorations, true) return Decoration.set(decorations, true)
} }
function createCursorChangeExtension(onCursorChangeRef) {
return ViewPlugin.fromClass(
class {
constructor(view) {
this.report(view)
}
update(update) {
if (update.selectionSet || update.docChanged) {
this.report(update.view)
}
}
report(view) {
const onCursorChange = onCursorChangeRef.current
if (typeof onCursorChange !== 'function') {
return
}
const head = view.state.selection.main.head
const line = view.state.doc.lineAt(head)
onCursorChange({
offset: head,
line: line.number,
column: head - line.from,
content: view.state.doc.toString()
})
}
}
)
}
function createErrorLineExtension(errorLines) { function createErrorLineExtension(errorLines) {
const errorLineSet = new Set(errorLines) const errorLineSet = new Set(errorLines)
@ -135,16 +166,23 @@ export default function CodeBlockEditor({
disabled = false, disabled = false,
minimal = false, minimal = false,
autoCompleteObject = null, autoCompleteObject = null,
errorLines = [] errorLines = [],
templateType,
onCursorChange = null
}) { }) {
const { isDarkMode } = useThemeContext() const { isDarkMode } = useThemeContext()
const [codeMirrorOpen, setCodeMirrorOpen] = useState(false) const [codeMirrorOpen, setCodeMirrorOpen] = useState(false)
const sourceCode = value !== undefined ? value : code const sourceCode = value !== undefined ? value : code
const editorCode = toEditorCode(sourceCode, language) const editorCode = toEditorCode(sourceCode, language)
const onCursorChangeRef = useRef(onCursorChange)
useEffect(() => {
onCursorChangeRef.current = onCursorChange
}, [onCursorChange])
const languageExtension = useMemo( const languageExtension = useMemo(
() => getCodeLanguageExtension(language, autoCompleteObject), () => getCodeLanguageExtension(language, autoCompleteObject, templateType),
[language, autoCompleteObject] [language, autoCompleteObject, templateType]
) )
const errorLineExtension = useMemo(() => { const errorLineExtension = useMemo(() => {
@ -154,14 +192,20 @@ export default function CodeBlockEditor({
return [createErrorLineExtension(errorLines)] return [createErrorLineExtension(errorLines)]
}, [errorLines]) }, [errorLines])
const cursorChangeExtension = useMemo(
() => createCursorChangeExtension(onCursorChangeRef),
[]
)
const editorExtensions = useMemo( const editorExtensions = useMemo(
() => [ () => [
languageExtension, languageExtension,
...errorLineExtension, ...errorLineExtension,
cursorChangeExtension,
codeBlockEditorLayoutTheme, codeBlockEditorLayoutTheme,
createScrollBoxSyncExtension() createScrollBoxSyncExtension()
], ],
[languageExtension, errorLineExtension] [languageExtension, errorLineExtension, cursorChangeExtension]
) )
const handleOnChange = (value) => { const handleOnChange = (value) => {
@ -282,5 +326,7 @@ CodeBlockEditor.propTypes = {
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) errorLines: PropTypes.arrayOf(PropTypes.number),
templateType: PropTypes.string,
onCursorChange: PropTypes.func
} }

View File

@ -117,6 +117,8 @@ const ObjectProperty = ({
minimal = false, minimal = false,
autoCompleteObject = null, autoCompleteObject = null,
errorLines = [], errorLines = [],
templateType,
onCursorChange,
previewOpen = false, previewOpen = false,
showPreview = true, showPreview = true,
useFormItem = true, useFormItem = true,
@ -436,6 +438,8 @@ const ObjectProperty = ({
minimal={minimal} minimal={minimal}
autoCompleteObject={autoCompleteObject} autoCompleteObject={autoCompleteObject}
errorLines={errorLines} errorLines={errorLines}
templateType={templateType}
onCursorChange={onCursorChange}
/> />
) )
} else { } else {
@ -987,6 +991,8 @@ const ObjectProperty = ({
minimal={minimal} minimal={minimal}
autoCompleteObject={autoCompleteObject} autoCompleteObject={autoCompleteObject}
errorLines={errorLines} errorLines={errorLines}
templateType={templateType}
onCursorChange={onCursorChange}
{...(onChange && !useFormItem ? { onChange } : {})} {...(onChange && !useFormItem ? { onChange } : {})}
{...inputProps} {...inputProps}
/> />
@ -1140,6 +1146,8 @@ ObjectProperty.propTypes = {
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), errorLines: PropTypes.arrayOf(PropTypes.number),
templateType: PropTypes.string,
onCursorChange: PropTypes.func,
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,

View File

@ -1,4 +1,4 @@
import { useState, useCallback, useContext } from 'react' import { useState, useCallback, useContext, useEffect, useRef } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { import {
Flex, Flex,
@ -108,7 +108,8 @@ const TemplateEditor = ({
form, form,
setObjectData setObjectData
}) => { }) => {
const { fetchTemplateFormat } = useContext(ApiServerContext) const { fetchTemplateFormat, fetchTemplateIntellisense } =
useContext(ApiServerContext)
const currentTemplate = template || objectData const currentTemplate = template || objectData
const [testObjectOpen, setTestObjectOpen] = useState(false) const [testObjectOpen, setTestObjectOpen] = useState(false)
const [testObjectViewMode, setTestObjectViewMode] = useState('Tree') const [testObjectViewMode, setTestObjectViewMode] = useState('Tree')
@ -116,6 +117,70 @@ const TemplateEditor = ({
const [previewError, setPreviewError] = useState(false) const [previewError, setPreviewError] = useState(false)
const [errorLines, setErrorLines] = useState([]) const [errorLines, setErrorLines] = useState([])
const [formatLoading, setFormatLoading] = useState(false) const [formatLoading, setFormatLoading] = useState(false)
const [intellisense, setIntellisense] = useState(null)
const intellisenseRequestIdRef = useRef(0)
const intellisenseTimerRef = useRef(null)
useEffect(() => {
setIntellisense(null)
}, [currentTemplate?._id])
useEffect(() => {
return () => {
if (intellisenseTimerRef.current) {
window.clearTimeout(intellisenseTimerRef.current)
}
}
}, [])
const handleCursorChange = useCallback(
(cursor) => {
const templateId = currentTemplate?._id
if (!templateId || !isEditing) {
return
}
const requestId = ++intellisenseRequestIdRef.current
if (intellisenseTimerRef.current) {
window.clearTimeout(intellisenseTimerRef.current)
}
intellisenseTimerRef.current = window.setTimeout(() => {
fetchTemplateIntellisense(
templateId,
cursor?.content ?? currentTemplate?.content,
currentTemplate?.testObject,
{
offset: cursor?.offset,
line: cursor?.line,
column: cursor?.column
},
templateType,
(result) => {
if (requestId !== intellisenseRequestIdRef.current) {
return
}
if (
result?.intellisense != null &&
typeof result.intellisense === 'object'
) {
if (Object.keys(result.intellisense).length > 0) {
setIntellisense(result.intellisense)
}
}
}
)
}, 200)
},
[
currentTemplate?._id,
currentTemplate?.content,
currentTemplate?.testObject,
fetchTemplateIntellisense,
isEditing,
templateType
]
)
//const isMobile = useMediaQuery({ maxWidth: 768 }) //const isMobile = useMediaQuery({ maxWidth: 768 })
@ -301,8 +366,12 @@ const TemplateEditor = ({
})) }))
}} }}
isEditing={isEditing} isEditing={isEditing}
autoCompleteObject={currentTemplate?.testObject} autoCompleteObject={
intellisense ?? currentTemplate?.testObject
}
errorLines={errorLines} errorLines={errorLines}
templateType={templateType}
onCursorChange={handleCursorChange}
/> />
</div> </div>
</Flex> </Flex>

View File

@ -25,7 +25,8 @@ export function toEditorCode(code, language = 'javascript') {
export function getCodeLanguageExtension( export function getCodeLanguageExtension(
language = 'javascript', language = 'javascript',
autoCompleteObject = null autoCompleteObject = null,
templateType
) { ) {
const lang = Array.isArray(language) ? language[0] : language const lang = Array.isArray(language) ? language[0] : language
@ -42,7 +43,7 @@ export function getCodeLanguageExtension(
return xml() return xml()
case 'fctemplatelang': case 'fctemplatelang':
case 'ejs': case 'ejs':
return fcTemplateLang({ autoCompleteObject }) return fcTemplateLang({ autoCompleteObject, templateType })
case 'html': case 'html':
return html() return html()
case 'css': case 'css':

View File

@ -2170,10 +2170,56 @@ const ApiServerProvider = ({ children }) => {
} catch (err) { } catch (err) {
const error = getTemplateErrorFromResponse(err) const error = getTemplateErrorFromResponse(err)
logger.error('Error fetching template preview:', error) logger.error('Error fetching template preview:', error)
const payload = { error }
if (typeof callback === 'function') { if (typeof callback === 'function') {
callback({ error }) callback(payload)
} }
return { error } return payload
}
}
const fetchTemplateIntellisense = async (
id,
content,
testObject,
cursor,
templateType = 'documentTemplate',
callback
) => {
logger.debug('Fetching template intellisense...')
try {
const response = await axios.post(
`${config.backendUrl}/${getObjectEndpoint(templateType)}/${id}/intellisense`,
{
content,
testObject,
cursor
},
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
if (typeof callback === 'function') {
callback(response.data)
}
return response.data
} catch (err) {
const error = getTemplateErrorFromResponse(err)
const intellisense = err?.response?.data?.intellisense
logger.error('Error fetching template intellisense:', error)
const payload = {
error,
...(intellisense != null && typeof intellisense === 'object'
? { intellisense }
: {})
}
if (typeof callback === 'function') {
callback(payload)
}
return payload
} }
} }
@ -2886,6 +2932,7 @@ const ApiServerProvider = ({ children }) => {
exportToExcel, exportToExcel,
exportToCsv, exportToCsv,
fetchTemplatePreview, fetchTemplatePreview,
fetchTemplateIntellisense,
fetchTemplateFormat, fetchTemplateFormat,
fetchTemplatePDF, fetchTemplatePDF,
fetchTemplateDownload, fetchTemplateDownload,