From a590650d806135b7a21e22623c4429bc8d8d6886 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 12 Sep 2026 23:30:00 +0100 Subject: [PATCH] Refactor CodeMirror Language Support and Enhance Template Handling - 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. --- src/codemirror/fcTemplateLang/index.js | 32 +- src/codemirror/fcTemplateLang/schema.js | 50 ++- src/codemirror/javascriptLang/index.js | 33 +- src/codemirror/javascriptLang/typedScope.js | 319 ++++++++++++++++++ .../Dashboard/common/CodeBlockEditor.jsx | 58 +++- .../Dashboard/common/ObjectProperty.jsx | 8 + .../Dashboard/common/TemplateEditor.jsx | 75 +++- .../Dashboard/common/codeBlockEditorUtils.js | 5 +- .../Dashboard/context/ApiServerContext.jsx | 51 ++- 9 files changed, 571 insertions(+), 60 deletions(-) create mode 100644 src/codemirror/javascriptLang/typedScope.js diff --git a/src/codemirror/fcTemplateLang/index.js b/src/codemirror/fcTemplateLang/index.js index f9af37ed..6ffb6c4e 100644 --- a/src/codemirror/fcTemplateLang/index.js +++ b/src/codemirror/fcTemplateLang/index.js @@ -9,7 +9,7 @@ import { import { styleTags, tags as t } from '@lezer/highlight' import { parseMixed } from '@lezer/common' import { javascriptLanguage } from '@codemirror/lang-javascript' -import { javascriptCompletionSupport } from '../javascriptLang' +import { javascriptCompletionSupport } from '../javascriptLang/index.js' import { xmlLanguage, completeFromSchema, @@ -19,8 +19,9 @@ import { parser as ejsParser } from './ejs.parser.js' import { fcTemplateElements, fcTemplateAttributes, - fcTemplateHelpers + getFcTemplateHelpers } from './schema.js' +import { normalizeTypedScope } from '../javascriptLang/typedScope.js' /** * FarmControl document template language: custom XML tags with EJS @@ -71,31 +72,22 @@ export const fcTemplateLanguage = LRLanguage.define({ } }) -function buildJsScope(autoCompleteObject) { - let data = autoCompleteObject - if (typeof data === 'string') { - try { - data = JSON.parse(data) - } catch { - data = {} - } - } - if (!data || typeof data !== 'object' || Array.isArray(data)) { - data = {} - } - return { - ...data, - fc: fcTemplateHelpers +function buildJsScope(autoCompleteObject, templateType) { + const typed = normalizeTypedScope(autoCompleteObject) + typed.properties = { + ...typed.properties, + fc: getFcTemplateHelpers(templateType) } + return typed } /** * Language support for FarmControl EJS+XML document templates. - * @param {{ autoCompleteObject?: object|string }} [options] + * @param {{ autoCompleteObject?: object|string, templateType?: string }} [options] */ export function fcTemplateLang(options = {}) { - const { autoCompleteObject } = options - const jsScope = buildJsScope(autoCompleteObject) + const { autoCompleteObject, templateType = 'documentTemplate' } = options + const jsScope = buildJsScope(autoCompleteObject, templateType) return new LanguageSupport(fcTemplateLanguage, [ xmlLanguage.data.of({ diff --git a/src/codemirror/fcTemplateLang/schema.js b/src/codemirror/fcTemplateLang/schema.js index 5afb5be0..c7b9e192 100644 --- a/src/codemirror/fcTemplateLang/schema.js +++ b/src/codemirror/fcTemplateLang/schema.js @@ -102,9 +102,49 @@ export const fcTemplateElements = allTags.map((name) => { }) /** Runtime helpers available as `fc` inside templates. */ -export const fcTemplateHelpers = { - listObjects: async () => [], - getObject: async () => ({}), - formatDate: () => '', - renderDocumentTemplate: async () => '' +const fcSharedHelpers = { + listObjects: { + $type: 'function', + detail: '(objectType, filter?, populate?) => Promise' + }, + getObject: { + $type: 'function', + detail: '(objectType, id, populate?) => Promise' + }, + formatDate: { + $type: 'function', + detail: '(date, format) => string' + } +} + +export const fcDocumentTemplateHelpers = { + $type: 'object', + name: 'fc', + properties: { + ...fcSharedHelpers, + renderDocumentTemplate: { + $type: 'function', + detail: '(reference, object?) => Promise' + } + } +} + +export const fcEmailTemplateHelpers = { + $type: 'object', + name: 'fc', + properties: { + ...fcSharedHelpers, + renderEmailTemplate: { + $type: 'function', + detail: '(reference, object?) => Promise' + } + } +} + +export const fcTemplateHelpers = fcDocumentTemplateHelpers + +export function getFcTemplateHelpers(templateType) { + return templateType === 'emailTemplate' + ? fcEmailTemplateHelpers + : fcDocumentTemplateHelpers } diff --git a/src/codemirror/javascriptLang/index.js b/src/codemirror/javascriptLang/index.js index c7722a19..57daaa30 100644 --- a/src/codemirror/javascriptLang/index.js +++ b/src/codemirror/javascriptLang/index.js @@ -4,27 +4,10 @@ import { scopeCompletionSource } from '@codemirror/lang-javascript' import { nodeJsScope } from './nodeScope.js' +import { typedScopeCompletionSource } from './typedScope.js' -function extraScope(autoCompleteObject) { - let data = autoCompleteObject - 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) - }) +export function nodeScopeCompletionSource() { + return scopeCompletionSource(nodeJsScope) } /** @@ -40,7 +23,10 @@ export function javascriptCompletionSupport(autoCompleteObject) { return [ javascript().support, javascriptLanguage.data.of({ - autocomplete: nodeScopeCompletionSource(autoCompleteObject) + autocomplete: scopeCompletionSource(nodeJsScope) + }), + javascriptLanguage.data.of({ + autocomplete: typedScopeCompletionSource(autoCompleteObject) }) ] } @@ -55,7 +41,10 @@ export function javascriptLang(options = {}) { return [ javascript(), javascriptLanguage.data.of({ - autocomplete: nodeScopeCompletionSource(autoCompleteObject) + autocomplete: scopeCompletionSource(nodeJsScope) + }), + javascriptLanguage.data.of({ + autocomplete: typedScopeCompletionSource(autoCompleteObject) }) ] } diff --git a/src/codemirror/javascriptLang/typedScope.js b/src/codemirror/javascriptLang/typedScope.js new file mode 100644 index 00000000..0f01681c --- /dev/null +++ b/src/codemirror/javascriptLang/typedScope.js @@ -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 + } + } +} diff --git a/src/components/Dashboard/common/CodeBlockEditor.jsx b/src/components/Dashboard/common/CodeBlockEditor.jsx index 4fabdf94..ccd8671f 100644 --- a/src/components/Dashboard/common/CodeBlockEditor.jsx +++ b/src/components/Dashboard/common/CodeBlockEditor.jsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useMemo, useState, useRef, useEffect } from 'react' import PropTypes from 'prop-types' import CodeMirror from '@uiw/react-codemirror' import { EditorView, Decoration, ViewPlugin } from '@codemirror/view' @@ -105,6 +105,37 @@ function buildErrorLineDecorations(state, errorLineSet) { 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) { const errorLineSet = new Set(errorLines) @@ -135,16 +166,23 @@ export default function CodeBlockEditor({ disabled = false, minimal = false, autoCompleteObject = null, - errorLines = [] + errorLines = [], + templateType, + onCursorChange = null }) { const { isDarkMode } = useThemeContext() const [codeMirrorOpen, setCodeMirrorOpen] = useState(false) const sourceCode = value !== undefined ? value : code const editorCode = toEditorCode(sourceCode, language) + const onCursorChangeRef = useRef(onCursorChange) + + useEffect(() => { + onCursorChangeRef.current = onCursorChange + }, [onCursorChange]) const languageExtension = useMemo( - () => getCodeLanguageExtension(language, autoCompleteObject), - [language, autoCompleteObject] + () => getCodeLanguageExtension(language, autoCompleteObject, templateType), + [language, autoCompleteObject, templateType] ) const errorLineExtension = useMemo(() => { @@ -154,14 +192,20 @@ export default function CodeBlockEditor({ return [createErrorLineExtension(errorLines)] }, [errorLines]) + const cursorChangeExtension = useMemo( + () => createCursorChangeExtension(onCursorChangeRef), + [] + ) + const editorExtensions = useMemo( () => [ languageExtension, ...errorLineExtension, + cursorChangeExtension, codeBlockEditorLayoutTheme, createScrollBoxSyncExtension() ], - [languageExtension, errorLineExtension] + [languageExtension, errorLineExtension, cursorChangeExtension] ) const handleOnChange = (value) => { @@ -282,5 +326,7 @@ CodeBlockEditor.propTypes = { disabled: PropTypes.bool, minimal: PropTypes.bool, autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - errorLines: PropTypes.arrayOf(PropTypes.number) + errorLines: PropTypes.arrayOf(PropTypes.number), + templateType: PropTypes.string, + onCursorChange: PropTypes.func } diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx index 1be7947a..2fb7f5b2 100644 --- a/src/components/Dashboard/common/ObjectProperty.jsx +++ b/src/components/Dashboard/common/ObjectProperty.jsx @@ -117,6 +117,8 @@ const ObjectProperty = ({ minimal = false, autoCompleteObject = null, errorLines = [], + templateType, + onCursorChange, previewOpen = false, showPreview = true, useFormItem = true, @@ -436,6 +438,8 @@ const ObjectProperty = ({ minimal={minimal} autoCompleteObject={autoCompleteObject} errorLines={errorLines} + templateType={templateType} + onCursorChange={onCursorChange} /> ) } else { @@ -987,6 +991,8 @@ const ObjectProperty = ({ minimal={minimal} autoCompleteObject={autoCompleteObject} errorLines={errorLines} + templateType={templateType} + onCursorChange={onCursorChange} {...(onChange && !useFormItem ? { onChange } : {})} {...inputProps} /> @@ -1140,6 +1146,8 @@ ObjectProperty.propTypes = { language: PropTypes.string, autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), errorLines: PropTypes.arrayOf(PropTypes.number), + templateType: PropTypes.string, + onCursorChange: PropTypes.func, 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 250af5e0..f99e6760 100644 --- a/src/components/Dashboard/common/TemplateEditor.jsx +++ b/src/components/Dashboard/common/TemplateEditor.jsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useContext } from 'react' +import { useState, useCallback, useContext, useEffect, useRef } from 'react' import PropTypes from 'prop-types' import { Flex, @@ -108,7 +108,8 @@ const TemplateEditor = ({ form, setObjectData }) => { - const { fetchTemplateFormat } = useContext(ApiServerContext) + const { fetchTemplateFormat, fetchTemplateIntellisense } = + useContext(ApiServerContext) const currentTemplate = template || objectData const [testObjectOpen, setTestObjectOpen] = useState(false) const [testObjectViewMode, setTestObjectViewMode] = useState('Tree') @@ -116,6 +117,70 @@ const TemplateEditor = ({ const [previewError, setPreviewError] = useState(false) const [errorLines, setErrorLines] = useState([]) 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 }) @@ -301,8 +366,12 @@ const TemplateEditor = ({ })) }} isEditing={isEditing} - autoCompleteObject={currentTemplate?.testObject} + autoCompleteObject={ + intellisense ?? currentTemplate?.testObject + } errorLines={errorLines} + templateType={templateType} + onCursorChange={handleCursorChange} /> diff --git a/src/components/Dashboard/common/codeBlockEditorUtils.js b/src/components/Dashboard/common/codeBlockEditorUtils.js index 639c649e..a7bc5493 100644 --- a/src/components/Dashboard/common/codeBlockEditorUtils.js +++ b/src/components/Dashboard/common/codeBlockEditorUtils.js @@ -25,7 +25,8 @@ export function toEditorCode(code, language = 'javascript') { export function getCodeLanguageExtension( language = 'javascript', - autoCompleteObject = null + autoCompleteObject = null, + templateType ) { const lang = Array.isArray(language) ? language[0] : language @@ -42,7 +43,7 @@ export function getCodeLanguageExtension( return xml() case 'fctemplatelang': case 'ejs': - return fcTemplateLang({ autoCompleteObject }) + return fcTemplateLang({ autoCompleteObject, templateType }) case 'html': return html() case 'css': diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx index 94cda6a8..9796ae4f 100644 --- a/src/components/Dashboard/context/ApiServerContext.jsx +++ b/src/components/Dashboard/context/ApiServerContext.jsx @@ -2170,10 +2170,56 @@ const ApiServerProvider = ({ children }) => { } catch (err) { const error = getTemplateErrorFromResponse(err) logger.error('Error fetching template preview:', error) + const payload = { error } 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, exportToCsv, fetchTemplatePreview, + fetchTemplateIntellisense, fetchTemplateFormat, fetchTemplatePDF, fetchTemplateDownload,