Refactor CodeMirror Language Support and Enhance Template Handling
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
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:
parent
a9e4c8dad3
commit
a590650d80
@ -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({
|
||||
|
||||
@ -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<object[]>'
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
319
src/codemirror/javascriptLang/typedScope.js
Normal file
319
src/codemirror/javascriptLang/typedScope.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user