|
|
|
@ -2,6 +2,9 @@
|
|
|
|
|
import type { Ref } from 'vue' |
|
|
|
|
import type { ListItem as AntListItem } from 'ant-design-vue' |
|
|
|
|
import jsep from 'jsep' |
|
|
|
|
import type { editor as MonacoEditor } from 'monaco-editor' |
|
|
|
|
import { KeyCode, Position, Range, languages, editor as monacoEditor } from 'monaco-editor' |
|
|
|
|
|
|
|
|
|
import { |
|
|
|
|
FormulaDataTypes, |
|
|
|
|
FormulaError, |
|
|
|
@ -14,18 +17,18 @@ import {
|
|
|
|
|
validateFormulaAndExtractTreeWithType, |
|
|
|
|
} from 'nocodb-sdk' |
|
|
|
|
import type { ColumnType, FormulaType } from 'nocodb-sdk' |
|
|
|
|
import formulaLanguage from '../../monaco/formula' |
|
|
|
|
|
|
|
|
|
const props = defineProps<{ |
|
|
|
|
value: any |
|
|
|
|
}>() |
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['update:value']) |
|
|
|
|
|
|
|
|
|
const uiTypesNotSupportedInFormulas = [UITypes.QrCode, UITypes.Barcode] |
|
|
|
|
|
|
|
|
|
const vModel = useVModel(props, 'value', emit) |
|
|
|
|
|
|
|
|
|
const { setAdditionalValidations, validateInfos, sqlUi, column, fromTableExplorer } = useColumnCreateStoreOrThrow() |
|
|
|
|
const { setAdditionalValidations, sqlUi, column, validateInfos, fromTableExplorer } = useColumnCreateStoreOrThrow() |
|
|
|
|
|
|
|
|
|
const { t } = useI18n() |
|
|
|
|
|
|
|
|
@ -53,6 +56,9 @@ const { getMeta } = useMetas()
|
|
|
|
|
|
|
|
|
|
const suggestionPreviewed = ref<Record<any, string> | undefined>() |
|
|
|
|
|
|
|
|
|
// If -1 Show Fields first, if 1 show Formulas first |
|
|
|
|
const priority = ref(0) |
|
|
|
|
|
|
|
|
|
const showFunctionList = ref<boolean>(true) |
|
|
|
|
|
|
|
|
|
const validators = { |
|
|
|
@ -89,8 +95,6 @@ const availableBinOps = ['+', '-', '*', '/', '>', '<', '==', '<=', '>=', '!=', '
|
|
|
|
|
|
|
|
|
|
const autocomplete = ref(false) |
|
|
|
|
|
|
|
|
|
const formulaRef = ref() |
|
|
|
|
|
|
|
|
|
const variableListRef = ref<(typeof AntListItem)[]>([]) |
|
|
|
|
|
|
|
|
|
const sugOptionsRef = ref<(typeof AntListItem)[]>([]) |
|
|
|
@ -169,31 +173,276 @@ const variableList = computed(() => {
|
|
|
|
|
return suggestion.value.filter((s) => s && s.type === 'column') |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
function isCurlyBracketBalanced() { |
|
|
|
|
// count number of opening curly brackets and closing curly brackets |
|
|
|
|
const cntCurlyBrackets = (formulaRef.value.$el.value.match(/\{|}/g) || []).reduce( |
|
|
|
|
(acc: Record<number, number>, cur: number) => { |
|
|
|
|
acc[cur] = (acc[cur] || 0) + 1 |
|
|
|
|
return acc |
|
|
|
|
const monacoRoot = ref<HTMLDivElement>() |
|
|
|
|
let editor: MonacoEditor.IStandaloneCodeEditor |
|
|
|
|
|
|
|
|
|
function getCurrentKeyword() { |
|
|
|
|
const model = editor.getModel() |
|
|
|
|
const position = editor.getPosition() |
|
|
|
|
if (!model || !position) { |
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const word = model.getWordAtPosition(position) |
|
|
|
|
|
|
|
|
|
return word?.word |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const handleInputDeb = useDebounceFn(function () { |
|
|
|
|
handleInput() |
|
|
|
|
}, 250) |
|
|
|
|
|
|
|
|
|
onMounted(async () => { |
|
|
|
|
if (monacoRoot.value) { |
|
|
|
|
const model = monacoEditor.createModel(vModel.value.formula_raw, 'formula') |
|
|
|
|
|
|
|
|
|
languages.register({ |
|
|
|
|
id: formulaLanguage.name, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
monacoEditor.defineTheme(formulaLanguage.name, formulaLanguage.theme) |
|
|
|
|
|
|
|
|
|
languages.setMonarchTokensProvider( |
|
|
|
|
formulaLanguage.name, |
|
|
|
|
formulaLanguage.generateLanguageDefinition(supportedColumns.value.map((c) => c.title!)), |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
languages.setLanguageConfiguration(formulaLanguage.name, formulaLanguage.languageConfiguration) |
|
|
|
|
|
|
|
|
|
monacoEditor.addKeybindingRules([ |
|
|
|
|
{ |
|
|
|
|
keybinding: KeyCode.DownArrow, |
|
|
|
|
when: 'editorTextFocus', |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
keybinding: KeyCode.UpArrow, |
|
|
|
|
when: 'editorTextFocus', |
|
|
|
|
}, |
|
|
|
|
]) |
|
|
|
|
|
|
|
|
|
editor = monacoEditor.create(monacoRoot.value, { |
|
|
|
|
model, |
|
|
|
|
'contextmenu': false, |
|
|
|
|
'theme': 'formula', |
|
|
|
|
'selectOnLineNumbers': false, |
|
|
|
|
'language': 'formula', |
|
|
|
|
'roundedSelection': false, |
|
|
|
|
'scrollBeyondLastLine': false, |
|
|
|
|
'lineNumbers': 'off', |
|
|
|
|
'glyphMargin': false, |
|
|
|
|
'folding': false, |
|
|
|
|
'wordWrap': 'on', |
|
|
|
|
'wrappingStrategy': 'advanced', |
|
|
|
|
// This seems to be a bug in the monoco. |
|
|
|
|
// https://github.com/microsoft/monaco-editor/issues/4535#issuecomment-2234042290 |
|
|
|
|
'bracketPairColorization.enabled': false, |
|
|
|
|
'padding': { |
|
|
|
|
top: 8, |
|
|
|
|
bottom: 8, |
|
|
|
|
}, |
|
|
|
|
'lineDecorationsWidth': 8, |
|
|
|
|
'lineNumbersMinChars': 0, |
|
|
|
|
'renderLineHighlight': 'none', |
|
|
|
|
'renderIndentGuides': false, |
|
|
|
|
'scrollbar': { |
|
|
|
|
horizontal: 'hidden', |
|
|
|
|
}, |
|
|
|
|
'tabSize': 2, |
|
|
|
|
'automaticLayout': false, |
|
|
|
|
'overviewRulerLanes': 0, |
|
|
|
|
'hideCursorInOverviewRuler': true, |
|
|
|
|
'overviewRulerBorder': false, |
|
|
|
|
'matchBrackets': 'never', |
|
|
|
|
'minimap': { |
|
|
|
|
enabled: false, |
|
|
|
|
}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
editor.layout({ |
|
|
|
|
width: 339, |
|
|
|
|
height: 120, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
editor.onDidChangeModelContent(async () => { |
|
|
|
|
vModel.value.formula_raw = editor.getValue() |
|
|
|
|
await handleInputDeb() |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
editor.onDidChangeCursorPosition(() => { |
|
|
|
|
const position = editor.getPosition() |
|
|
|
|
const model = editor.getModel() |
|
|
|
|
|
|
|
|
|
if (!position || !model) return |
|
|
|
|
|
|
|
|
|
const text = model.getValue() |
|
|
|
|
const offset = model.getOffsetAt(position) |
|
|
|
|
|
|
|
|
|
// IF cursor is inside string, don't show any suggestions |
|
|
|
|
if (isCursorInsideString(text, offset)) { |
|
|
|
|
autocomplete.value = false |
|
|
|
|
suggestion.value = [] |
|
|
|
|
} else { |
|
|
|
|
handleInput() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const findEnclosingFunction = (text: string, offset: number) => { |
|
|
|
|
const formulaRegex = /\b(?<!['"])(\w+)\s*\(/g // Regular expression to match function names |
|
|
|
|
const quoteRegex = /"/g // Regular expression to match quotes |
|
|
|
|
|
|
|
|
|
const functionStack = [] // Stack to keep track of functions |
|
|
|
|
let inQuote = false |
|
|
|
|
|
|
|
|
|
let match |
|
|
|
|
while ((match = formulaRegex.exec(text)) !== null) { |
|
|
|
|
if (match.index > offset) break |
|
|
|
|
|
|
|
|
|
if (!inQuote) { |
|
|
|
|
const functionData = { |
|
|
|
|
name: match[1], |
|
|
|
|
start: match.index, |
|
|
|
|
end: formulaRegex.lastIndex, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let parenBalance = 1 |
|
|
|
|
let childValueStart = -1 |
|
|
|
|
let childValueEnd = -1 |
|
|
|
|
for (let i = formulaRegex.lastIndex; i < text.length; i++) { |
|
|
|
|
if (text[i] === '(') { |
|
|
|
|
parenBalance++ |
|
|
|
|
} else if (text[i] === ')') { |
|
|
|
|
parenBalance-- |
|
|
|
|
if (parenBalance === 0) { |
|
|
|
|
functionData.end = i + 1 |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Child value handling |
|
|
|
|
if (childValueStart === -1 && ['(', ',', '{'].includes(text[i])) { |
|
|
|
|
childValueStart = i |
|
|
|
|
} else if (childValueStart !== -1 && ['(', ',', '{'].includes(text[i])) { |
|
|
|
|
childValueStart = i |
|
|
|
|
} else if (childValueStart !== -1 && ['}', ',', ')'].includes(text[i])) { |
|
|
|
|
childValueEnd = i |
|
|
|
|
childValueStart = -1 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (i >= offset) { |
|
|
|
|
// If we've reached the offset and parentheses are still open, consider the current position as the end of the function |
|
|
|
|
if (parenBalance > 0) { |
|
|
|
|
functionData.end = i + 1 |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Check for nested functions |
|
|
|
|
const nestedFunction = findEnclosingFunction( |
|
|
|
|
text.substring(functionData.start + match[1].length + 1, i), |
|
|
|
|
offset - functionData.start - match[1].length - 1, |
|
|
|
|
) |
|
|
|
|
if (nestedFunction) { |
|
|
|
|
return nestedFunction |
|
|
|
|
} else { |
|
|
|
|
functionStack.push(functionData) |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// If child value ended before offset, use child value end as function end |
|
|
|
|
if (childValueEnd !== -1 && childValueEnd < offset) { |
|
|
|
|
functionData.end = childValueEnd + 1 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
functionStack.push(functionData) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Check for quotes |
|
|
|
|
let quoteMatch |
|
|
|
|
while ((quoteMatch = quoteRegex.exec(text)) !== null && quoteMatch.index < match.index) { |
|
|
|
|
inQuote = !inQuote |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const enclosingFunctions = functionStack.filter((func) => func.start <= offset && func.end >= offset) |
|
|
|
|
return enclosingFunctions.length > 0 ? enclosingFunctions[enclosingFunctions.length - 1].name : null |
|
|
|
|
} |
|
|
|
|
const lastFunction = findEnclosingFunction(text, offset) |
|
|
|
|
|
|
|
|
|
suggestionPreviewed.value = |
|
|
|
|
(suggestionsList.value.find((s) => s.text === `${lastFunction}()`) as Record<any, string>) || undefined |
|
|
|
|
}) |
|
|
|
|
editor.focus() |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
useResizeObserver(monacoRoot, (entries) => { |
|
|
|
|
const entry = entries[0] |
|
|
|
|
const { height } = entry.contentRect |
|
|
|
|
editor.layout({ |
|
|
|
|
width: 339, |
|
|
|
|
height, |
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
function insertStringAtPosition(editor: MonacoEditor.IStandaloneCodeEditor, text: string, skipCursorMove = false) { |
|
|
|
|
const position = editor.getPosition() |
|
|
|
|
|
|
|
|
|
if (!position) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column) |
|
|
|
|
|
|
|
|
|
editor.executeEdits('', [ |
|
|
|
|
{ |
|
|
|
|
range, |
|
|
|
|
text, |
|
|
|
|
forceMoveMarkers: true, |
|
|
|
|
}, |
|
|
|
|
{}, |
|
|
|
|
) |
|
|
|
|
return (cntCurlyBrackets['{'] || 0) === (cntCurlyBrackets['}'] || 0) |
|
|
|
|
]) |
|
|
|
|
|
|
|
|
|
// Move the cursor to the end of the inserted text |
|
|
|
|
if (!skipCursorMove) { |
|
|
|
|
const newPosition = new Position(position.lineNumber, position.column + text.length - 1) |
|
|
|
|
editor.setPosition(newPosition) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
editor.focus() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function appendText(item: Record<string, any>) { |
|
|
|
|
const text = item.text |
|
|
|
|
const len = wordToComplete.value?.length || 0 |
|
|
|
|
|
|
|
|
|
if (!item?.text) return |
|
|
|
|
const text = item.text |
|
|
|
|
|
|
|
|
|
const position = editor.getPosition() |
|
|
|
|
|
|
|
|
|
if (!position) { |
|
|
|
|
return // No cursor position available |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const newColumn = Math.max(1, position.column - len) |
|
|
|
|
|
|
|
|
|
const range = new Range(position.lineNumber, newColumn, position.lineNumber, position.column) |
|
|
|
|
|
|
|
|
|
editor.executeEdits('', [ |
|
|
|
|
{ |
|
|
|
|
range, |
|
|
|
|
text: '', |
|
|
|
|
forceMoveMarkers: true, |
|
|
|
|
}, |
|
|
|
|
]) |
|
|
|
|
|
|
|
|
|
if (item.type === 'function') { |
|
|
|
|
vModel.value.formula_raw = insertAtCursor(formulaRef.value.$el, text, len, 1) |
|
|
|
|
insertStringAtPosition(editor, text) |
|
|
|
|
} else if (item.type === 'column') { |
|
|
|
|
vModel.value.formula_raw = insertAtCursor(formulaRef.value.$el, `{${text}}`, len + +!isCurlyBracketBalanced()) |
|
|
|
|
insertStringAtPosition(editor, `{${text}}`, true) |
|
|
|
|
} else { |
|
|
|
|
vModel.value.formula_raw = insertAtCursor(formulaRef.value.$el, text, len) |
|
|
|
|
insertStringAtPosition(editor, text, true) |
|
|
|
|
} |
|
|
|
|
autocomplete.value = false |
|
|
|
|
wordToComplete.value = '' |
|
|
|
|
|
|
|
|
|
if (item.type === 'function' || item.type === 'op') { |
|
|
|
|
// if function / operator is chosen, display columns only |
|
|
|
|
suggestion.value = suggestionsList.value.filter((f) => f.type === 'column') |
|
|
|
@ -203,27 +452,93 @@ function appendText(item: Record<string, any>) {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const handleInputDeb = useDebounceFn(function () { |
|
|
|
|
handleInput() |
|
|
|
|
}, 250) |
|
|
|
|
function isCursorBetweenParenthesis() { |
|
|
|
|
const cursorPosition = editor?.getPosition() |
|
|
|
|
|
|
|
|
|
if (!cursorPosition || !editor) return false |
|
|
|
|
|
|
|
|
|
const line = editor.getModel()?.getLineContent(cursorPosition.lineNumber) |
|
|
|
|
|
|
|
|
|
if (!line) { |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const cursorLine = line.substring(0, cursorPosition.column - 1) |
|
|
|
|
|
|
|
|
|
const openParenthesis = (cursorLine.match(/\(/g) || []).length |
|
|
|
|
|
|
|
|
|
const closeParenthesis = (cursorLine.match(/\)/g) || []).length |
|
|
|
|
|
|
|
|
|
return openParenthesis > closeParenthesis |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Function to check if cursor is inside Strings |
|
|
|
|
function isCursorInsideString(text: string, offset: number) { |
|
|
|
|
let inSingleQuoteString = false |
|
|
|
|
let inDoubleQuoteString = false |
|
|
|
|
let escapeNextChar = false |
|
|
|
|
|
|
|
|
|
for (let i = 0; i < offset; i++) { |
|
|
|
|
const char = text[i] |
|
|
|
|
|
|
|
|
|
if (escapeNextChar) { |
|
|
|
|
escapeNextChar = false |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (char === '\\') { |
|
|
|
|
escapeNextChar = true |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (char === "'" && !inDoubleQuoteString) { |
|
|
|
|
inSingleQuoteString = !inSingleQuoteString |
|
|
|
|
} else if (char === '"' && !inSingleQuoteString) { |
|
|
|
|
inDoubleQuoteString = !inDoubleQuoteString |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return inDoubleQuoteString || inSingleQuoteString |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function handleInput() { |
|
|
|
|
if (!editor) return |
|
|
|
|
|
|
|
|
|
const model = editor.getModel() |
|
|
|
|
const position = editor.getPosition() |
|
|
|
|
|
|
|
|
|
if (!model || !position) return |
|
|
|
|
|
|
|
|
|
const text = model.getValue() |
|
|
|
|
const offset = model.getOffsetAt(position) |
|
|
|
|
|
|
|
|
|
// IF cursor is inside string, don't show any suggestions |
|
|
|
|
if (isCursorInsideString(text, offset)) { |
|
|
|
|
autocomplete.value = false |
|
|
|
|
suggestion.value = [] |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!isCursorBetweenParenthesis()) priority.value = 1 |
|
|
|
|
|
|
|
|
|
selected.value = 0 |
|
|
|
|
suggestion.value = [] |
|
|
|
|
const query = getWordUntilCaret(formulaRef.value.$el) |
|
|
|
|
const query = getCurrentKeyword() ?? '' |
|
|
|
|
const parts = query.split(/\W+/) |
|
|
|
|
wordToComplete.value = parts.pop() || '' |
|
|
|
|
suggestion.value = acTree.value |
|
|
|
|
.complete(wordToComplete.value) |
|
|
|
|
?.sort((x: Record<string, any>, y: Record<string, any>) => sortOrder[x.type] - sortOrder[y.type]) |
|
|
|
|
|
|
|
|
|
if (suggestion.value.length > 0 && suggestion.value[0].type !== 'column') { |
|
|
|
|
suggestionPreviewed.value = suggestion.value[0] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!isCurlyBracketBalanced()) { |
|
|
|
|
suggestion.value = suggestion.value.filter((v) => v.type === 'column') |
|
|
|
|
showFunctionList.value = false |
|
|
|
|
if (isCursorBetweenParenthesis()) { |
|
|
|
|
// Show columns at top |
|
|
|
|
suggestion.value = suggestion.value.sort((a, b) => { |
|
|
|
|
if (a.type === 'column') return -1 |
|
|
|
|
if (b.type === 'column') return 1 |
|
|
|
|
return 0 |
|
|
|
|
}) |
|
|
|
|
selected.value = 0 |
|
|
|
|
priority.value = -1 |
|
|
|
|
} else if (!showFunctionList.value) { |
|
|
|
|
showFunctionList.value = true |
|
|
|
|
} |
|
|
|
@ -318,19 +633,19 @@ const suggestionPreviewPostion = ref({
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
|
|
until(() => formulaRef.value?.$el as Ref<HTMLTextAreaElement>) |
|
|
|
|
until(() => monacoRoot.value as HTMLDivElement) |
|
|
|
|
.toBeTruthy() |
|
|
|
|
.then(() => { |
|
|
|
|
setTimeout(() => { |
|
|
|
|
const textAreaPosition = formulaRef.value?.$el?.getBoundingClientRect() |
|
|
|
|
if (!textAreaPosition) return |
|
|
|
|
const monacoDivPosition = monacoRoot.value?.getBoundingClientRect() |
|
|
|
|
if (!monacoDivPosition) return |
|
|
|
|
|
|
|
|
|
suggestionPreviewPostion.value.top = `${textAreaPosition.top}px` |
|
|
|
|
suggestionPreviewPostion.value.top = `${monacoDivPosition.top}px` |
|
|
|
|
|
|
|
|
|
if (fromTableExplorer?.value || textAreaPosition.left > 352) { |
|
|
|
|
suggestionPreviewPostion.value.left = `${textAreaPosition.left - 344}px` |
|
|
|
|
if (fromTableExplorer?.value || monacoDivPosition.left > 352) { |
|
|
|
|
suggestionPreviewPostion.value.left = `${monacoDivPosition.left - 344}px` |
|
|
|
|
} else { |
|
|
|
|
suggestionPreviewPostion.value.left = `${textAreaPosition.right + 8}px` |
|
|
|
|
suggestionPreviewPostion.value.left = `${monacoDivPosition.right + 8}px` |
|
|
|
|
} |
|
|
|
|
}, 250) |
|
|
|
|
}) |
|
|
|
@ -423,7 +738,7 @@ watch(parsedTree, (value, oldValue) => {
|
|
|
|
|
> |
|
|
|
|
<div class="pr-3"> |
|
|
|
|
<div class="flex flex-row w-full justify-between pb-2 border-b-1"> |
|
|
|
|
<div class="flex items-center gap-x-1 font-semibold text-base text-gray-600"> |
|
|
|
|
<div class="flex items-center gap-x-1 font-semibold text-lg text-gray-600"> |
|
|
|
|
<component :is="iconMap.function" class="text-lg" /> |
|
|
|
|
{{ suggestionPreviewed.text }} |
|
|
|
|
</div> |
|
|
|
@ -433,17 +748,17 @@ watch(parsedTree, (value, oldValue) => {
|
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
<div class="flex flex-col max-h-120 nc-scrollbar-thin pr-2"> |
|
|
|
|
<div class="flex mt-3 text-sm">{{ suggestionPreviewed.description }}</div> |
|
|
|
|
<div class="flex mt-3 text-[13px] leading-6">{{ suggestionPreviewed.description }}</div> |
|
|
|
|
|
|
|
|
|
<div class="text-gray-500 uppercase text-xs mt-3 mb-2">Syntax</div> |
|
|
|
|
<div class="bg-white rounded-md py-1 px-2 border-1">{{ suggestionPreviewed.syntax }}</div> |
|
|
|
|
<div class="text-gray-500 uppercase text-xs mt-3 mb-2">Examples</div> |
|
|
|
|
<div class="text-gray-500 uppercase text-[11px] mt-3 mb-2">Syntax</div> |
|
|
|
|
<div class="bg-white rounded-md py-1 text-[13px] mono-font leading-6 px-2 border-1">{{ suggestionPreviewed.syntax }}</div> |
|
|
|
|
<div class="text-gray-500 uppercase text-[11px] mt-3 mb-2">Examples</div> |
|
|
|
|
<div |
|
|
|
|
v-for="(example, index) of suggestionPreviewed.examples" |
|
|
|
|
:key="example" |
|
|
|
|
class="bg-gray-100 py-1 px-2" |
|
|
|
|
class="bg-gray-100 mono-font text-[13px] leading-6 py-1 px-2" |
|
|
|
|
:class="{ |
|
|
|
|
'border-t-1 border-gray-200': index !== 0, |
|
|
|
|
'border-t-1 border-gray-200': index !== 0, |
|
|
|
|
'rounded-b-md': index === suggestionPreviewed.examples.length - 1 && suggestionPreviewed.examples.length !== 1, |
|
|
|
|
'rounded-t-md': index === 0 && suggestionPreviewed.examples.length !== 1, |
|
|
|
|
'rounded-md': suggestionPreviewed.examples.length === 1, |
|
|
|
@ -471,17 +786,18 @@ watch(parsedTree, (value, oldValue) => {
|
|
|
|
|
</template> |
|
|
|
|
<div class="px-0.5"> |
|
|
|
|
<a-form-item class="mt-4" v-bind="validateInfos.formula_raw"> |
|
|
|
|
<a-textarea |
|
|
|
|
ref="formulaRef" |
|
|
|
|
v-model:value="vModel.formula_raw" |
|
|
|
|
class="nc-formula-input !rounded-md" |
|
|
|
|
@keydown="handleKeydown" |
|
|
|
|
@change="handleInputDeb" |
|
|
|
|
/> |
|
|
|
|
<div |
|
|
|
|
ref="monacoRoot" |
|
|
|
|
:class="{ |
|
|
|
|
'!border-red-500 formula-error': validateInfos.formula_raw?.validateStatus === 'error', |
|
|
|
|
'!focus-within:border-brand-500 formula-success': validateInfos.formula_raw?.validateStatus !== 'error', |
|
|
|
|
}" |
|
|
|
|
class="formula-monaco" |
|
|
|
|
@keydown.stop="handleKeydown" |
|
|
|
|
></div> |
|
|
|
|
</a-form-item> |
|
|
|
|
|
|
|
|
|
<div class="h-[250px] overflow-auto nc-scrollbar-thin border-1 border-gray-200 rounded-lg mt-4"> |
|
|
|
|
<template v-if="suggestedFormulas && showFunctionList"> |
|
|
|
|
<div class="h-[250px] overflow-auto flex flex-col nc-scrollbar-thin border-1 border-gray-200 rounded-lg mt-4"> |
|
|
|
|
<div v-if="suggestedFormulas && showFunctionList" :style="{ order: priority === -1 ? 2 : 1 }"> |
|
|
|
|
<div class="border-b-1 bg-gray-50 px-3 py-1 uppercase text-gray-600 text-xs font-semibold sticky top-0 z-10"> |
|
|
|
|
Formulas |
|
|
|
|
</div> |
|
|
|
@ -520,9 +836,9 @@ watch(parsedTree, (value, oldValue) => {
|
|
|
|
|
</a-list-item> |
|
|
|
|
</template> |
|
|
|
|
</a-list> |
|
|
|
|
</template> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<template v-if="variableList"> |
|
|
|
|
<div v-if="variableList" :style="{ order: priority === 1 ? 2 : 1 }"> |
|
|
|
|
<div class="border-b-1 bg-gray-50 px-3 py-1 uppercase text-gray-600 text-xs font-semibold sticky top-0 z-10"> |
|
|
|
|
Fields |
|
|
|
|
</div> |
|
|
|
@ -549,7 +865,7 @@ watch(parsedTree, (value, oldValue) => {
|
|
|
|
|
<a-list-item-meta class="nc-variable-list-item"> |
|
|
|
|
<template #title> |
|
|
|
|
<div class="flex items-center gap-x-1 justify-between"> |
|
|
|
|
<div class="flex items-center gap-x-1 rounded-md bg-gray-200 px-1 h-5"> |
|
|
|
|
<div class="flex items-center gap-x-1 rounded-md px-1 h-5"> |
|
|
|
|
<component :is="item.icon" class="w-4 h-4 !text-gray-600" /> |
|
|
|
|
|
|
|
|
|
<span class="text-small leading-[18px] text-gray-800 font-weight-500">{{ item.text }}</span> |
|
|
|
@ -568,7 +884,7 @@ watch(parsedTree, (value, oldValue) => {
|
|
|
|
|
</a-list-item> |
|
|
|
|
</template> |
|
|
|
|
</a-list> |
|
|
|
|
</template> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</a-tab-pane> |
|
|
|
@ -675,6 +991,10 @@ watch(parsedTree, (value, oldValue) => {
|
|
|
|
|
@apply !pl-0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:deep(.ant-form-item-control-input) { |
|
|
|
|
@apply h-full; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:deep(.ant-tabs-content-holder) { |
|
|
|
|
@apply mt-4; |
|
|
|
|
} |
|
|
|
@ -690,4 +1010,26 @@ watch(parsedTree, (value, oldValue) => {
|
|
|
|
|
:deep(.ant-tabs-tab-btn) { |
|
|
|
|
@apply !mb-1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.formula-monaco { |
|
|
|
|
@apply rounded-md nc-scrollbar-md border-gray-200 border-1 overflow-y-auto overflow-x-hidden resize-y; |
|
|
|
|
min-height: 100px; |
|
|
|
|
height: 120px; |
|
|
|
|
max-height: 250px; |
|
|
|
|
|
|
|
|
|
&:focus-within:not(.formula-error) { |
|
|
|
|
box-shadow: 0 0 0 2px var(--ant-primary-color-outline); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
&:focus-within:not(.formula-success) { |
|
|
|
|
box-shadow: 0 0 0 2px var(--ant-error-color-outline); |
|
|
|
|
} |
|
|
|
|
.view-line { |
|
|
|
|
width: auto !important; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.mono-font { |
|
|
|
|
font-family: 'JetBrainsMono', monospace; |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|