@ -2,34 +2,28 @@
import type { Ref } from 'vue'
import type { ListItem as AntListItem } from 'ant-design-vue'
import jsep from 'jsep'
import type { ColumnType , FormulaType , LinkToAnotherRecordType , TableType } from 'nocodb-sdk'
import {
FormulaError ,
UITypes ,
isLinksOrLTAR ,
isNumericCol ,
isSystemColumn ,
jsepCurlyHook ,
substituteColumnIdWithAliasInFormula ,
validateDateWithUnknownFormat ,
validateFormulaAndExtractTreeWithType ,
} from 'nocodb-sdk'
import type { ColumnType , FormulaType } from 'nocodb-sdk'
import {
MetaInj ,
NcAutocompleteTree ,
computed ,
formulaList ,
formulaTypes ,
formulas ,
getUIDTIcon ,
getWordUntilCaret ,
iconMap ,
inject ,
insertAtCursor ,
isDate ,
nextTick ,
onMounted ,
ref ,
storeToRefs ,
useBase ,
useColumnCreateStoreOrThrow ,
useDebounceFn ,
useI18n ,
@ -52,59 +46,40 @@ const { setAdditionalValidations, validateInfos, sqlUi, column } = useColumnCrea
const { t } = useI18n ( )
const baseStore = useBase ( )
const { tables } = storeToRefs ( baseStore )
const { predictFunction : _predictFunction } = useNocoEe ( )
enum JSEPNode {
COMPOUND = 'Compound' ,
IDENTIFIER = 'Identifier' ,
MEMBER _EXP = 'MemberExpression' ,
LITERAL = 'Literal' ,
THIS _EXP = 'ThisExpression' ,
CALL _EXP = 'CallExpression' ,
UNARY _EXP = 'UnaryExpression' ,
BINARY _EXP = 'BinaryExpression' ,
ARRAY _EXP = 'ArrayExpression' ,
}
const meta = inject ( MetaInj , ref ( ) )
const supportedColumns = computed (
( ) => meta ? . value ? . columns ? . filter ( ( col ) => ! uiTypesNotSupportedInFormulas . includes ( col . uidt as UITypes ) ) || [ ] ,
)
const { metas } = useMetas ( )
const refTables = computed ( ( ) => {
if ( ! tables . value || ! tables . value . length || ! meta . value || ! meta . value . columns ) {
return [ ]
}
const { getMeta } = useMetas ( )
const _refTables = meta . value . columns
. filter ( ( column ) => isLinksOrLTAR ( column ) && ! column . system && column . source _id === meta . value ? . source _id )
. map ( ( column ) => ( {
col : column . colOptions ,
column ,
... tables . value . find ( ( table ) => table . id === ( column . colOptions as LinkToAnotherRecordType ) . fk _related _model _id ) ,
} ) )
. filter ( ( table ) => ( table . col as LinkToAnotherRecordType ) ? . fk _related _model _id === table . id && ! table . mm )
return _refTables as Required < TableType & { column : ColumnType ; col : Required < LinkToAnotherRecordType > } > [ ]
} )
const suggestionPreviewed = ref < Record < any , string > | undefined > ( )
const validators = {
formula _raw : [
{
validator : ( _ : any , formula : any ) => {
return new Promise < void > ( ( resolve , reject ) => {
if ( ! formula ? . trim ( ) ) return reject ( new Error ( 'Required' ) )
const res = parseAndValidateFormula ( formula )
if ( res !== true ) {
return reject ( new Error ( res ) )
}
resolve ( )
return ( async ( ) => {
if ( ! formula ? . trim ( ) ) throw new Error ( 'Required' )
try {
await validateFormulaAndExtractTreeWithType ( {
column : column . value ,
formula ,
columns : supportedColumns . value ,
clientOrSqlUi : sqlUi . value ,
getMeta ,
} )
} catch ( e : any ) {
if ( e instanceof FormulaError && e . extra ? . key ) {
throw new Error ( t ( e . extra . key , e . extra ) )
}
throw new Error ( e . message )
}
} ) ( )
} ,
} ,
] ,
@ -120,6 +95,8 @@ const formulaRef = ref()
const sugListRef = ref ( )
const variableListRef = ref < ( typeof AntListItem ) [ ] > ( [ ] )
const sugOptionsRef = ref < ( typeof AntListItem ) [ ] > ( [ ] )
const wordToComplete = ref < string | undefined > ( '' )
@ -143,6 +120,7 @@ const suggestionsList = computed(() => {
description : formulas [ fn ] . description ,
syntax : formulas [ fn ] . syntax ,
examples : formulas [ fn ] . examples ,
docsUrl : formulas [ fn ] . docsUrl ,
} ) ) ,
... supportedColumns . value
. filter ( ( c ) => {
@ -176,521 +154,13 @@ const acTree = computed(() => {
return ref
} )
function parseAndValidateFormula ( formula : string ) {
try {
const parsedTree = jsep ( formula )
const metaErrors = validateAgainstMeta ( parsedTree )
if ( metaErrors . size ) {
return [ ... metaErrors ] . join ( ', ' )
}
return true
} catch ( e : any ) {
return e . message
}
}
function validateAgainstMeta ( parsedTree : any , errors = new Set ( ) , typeErrors = new Set ( ) ) {
if ( parsedTree . type === JSEPNode . CALL _EXP ) {
const calleeName = parsedTree . callee . name . toUpperCase ( )
/ / v a l i d a t e f u n c t i o n n a m e
if ( ! availableFunctions . includes ( calleeName ) ) {
errors . add ( t ( 'msg.formula.functionNotAvailable' , { function : calleeName } ) )
}
/ / v a l i d a t e a r g u m e n t s
const validation = formulas [ calleeName ] && formulas [ calleeName ] . validation
if ( validation && validation . args ) {
if ( validation . args . rqd !== undefined && validation . args . rqd !== parsedTree . arguments . length ) {
errors . add ( t ( 'msg.formula.requiredArgumentsFormula' , { requiredArguments : validation . args . rqd , calleeName } ) )
} else if ( validation . args . min !== undefined && validation . args . min > parsedTree . arguments . length ) {
errors . add ( t ( 'msg.formula.minRequiredArgumentsFormula' , { minRequiredArguments : validation . args . min , calleeName } ) )
} else if ( validation . args . max !== undefined && validation . args . max < parsedTree . arguments . length ) {
errors . add ( t ( 'msg.formula.maxRequiredArgumentsFormula' , { maxRequiredArguments : validation . args . max , calleeName } ) )
}
}
parsedTree . arguments . map ( ( arg : Record < string , any > ) => validateAgainstMeta ( arg , errors ) )
/ / v a l i d a t e d a t a t y p e
if ( parsedTree . callee . type === JSEPNode . IDENTIFIER ) {
const expectedType = formulas [ calleeName . toUpperCase ( ) ] . type
if ( expectedType === formulaTypes . NUMERIC ) {
if ( calleeName === 'WEEKDAY' ) {
/ / p a r s e d T r e e . a r g u m e n t s [ 0 ] = d a t e
validateAgainstType (
parsedTree . arguments [ 0 ] ,
formulaTypes . DATE ,
( v : any ) => {
if ( ! validateDateWithUnknownFormat ( v ) ) {
typeErrors . add ( t ( 'msg.formula.firstParamWeekDayHaveDate' ) )
}
} ,
typeErrors ,
)
/ / p a r s e d T r e e . a r g u m e n t s [ 1 ] = s t a r t D a y O f W e e k ( o p t i o n a l )
validateAgainstType (
parsedTree . arguments [ 1 ] ,
formulaTypes . STRING ,
( v : any ) => {
if (
typeof v !== 'string' ||
! [ 'sunday' , 'monday' , 'tuesday' , 'wednesday' , 'thursday' , 'friday' , 'saturday' ] . includes ( v . toLowerCase ( ) )
) {
typeErrors . add ( t ( 'msg.formula.secondParamWeekDayHaveDate' ) )
}
} ,
typeErrors ,
)
} else {
parsedTree . arguments . map ( ( arg : Record < string , any > ) => validateAgainstType ( arg , expectedType , null , typeErrors ) )
}
} else if ( expectedType === formulaTypes . DATE ) {
if ( calleeName === 'DATEADD' ) {
/ / p a r s e d T r e e . a r g u m e n t s [ 0 ] = d a t e
validateAgainstType (
parsedTree . arguments [ 0 ] ,
formulaTypes . DATE ,
( v : any ) => {
if ( ! validateDateWithUnknownFormat ( v ) ) {
typeErrors . add ( t ( 'msg.formula.firstParamDateAddHaveDate' ) )
}
} ,
typeErrors ,
)
/ / p a r s e d T r e e . a r g u m e n t s [ 1 ] = n u m e r i c
validateAgainstType (
parsedTree . arguments [ 1 ] ,
formulaTypes . NUMERIC ,
( v : any ) => {
if ( typeof v !== 'number' ) {
typeErrors . add ( t ( 'msg.formula.secondParamDateAddHaveNumber' ) )
}
} ,
typeErrors ,
)
/ / p a r s e d T r e e . a r g u m e n t s [ 2 ] = [ " d a y " | " w e e k " | " m o n t h " | " y e a r " ]
validateAgainstType (
parsedTree . arguments [ 2 ] ,
formulaTypes . STRING ,
( v : any ) => {
if ( ! [ 'day' , 'week' , 'month' , 'year' ] . includes ( v ) ) {
typeErrors . add ( typeErrors . add ( t ( 'msg.formula.thirdParamDateAddHaveDate' ) ) )
}
} ,
typeErrors ,
)
} else if ( calleeName === 'DATETIME_DIFF' ) {
/ / p a r s e d T r e e . a r g u m e n t s [ 0 ] = d a t e
validateAgainstType (
parsedTree . arguments [ 0 ] ,
formulaTypes . DATE ,
( v : any ) => {
if ( ! validateDateWithUnknownFormat ( v ) ) {
typeErrors . add ( t ( 'msg.formula.firstParamDateDiffHaveDate' ) )
}
} ,
typeErrors ,
)
/ / p a r s e d T r e e . a r g u m e n t s [ 1 ] = d a t e
validateAgainstType (
parsedTree . arguments [ 1 ] ,
formulaTypes . DATE ,
( v : any ) => {
if ( ! validateDateWithUnknownFormat ( v ) ) {
typeErrors . add ( t ( 'msg.formula.secondParamDateDiffHaveDate' ) )
}
} ,
typeErrors ,
)
/ / p a r s e d T r e e . a r g u m e n t s [ 2 ] = [ " m i l l i s e c o n d s " | " m s " | " s e c o n d s " | " s " | " m i n u t e s " | " m " | " h o u r s " | " h " | " d a y s " | " d " | " w e e k s " | " w " | " m o n t h s " | " M " | " q u a r t e r s " | " Q " | " y e a r s " | " y " ]
validateAgainstType (
parsedTree . arguments [ 2 ] ,
formulaTypes . STRING ,
( v : any ) => {
if (
! [
'milliseconds' ,
'ms' ,
'seconds' ,
's' ,
'minutes' ,
'm' ,
'hours' ,
'h' ,
'days' ,
'd' ,
'weeks' ,
'w' ,
'months' ,
'M' ,
'quarters' ,
'Q' ,
'years' ,
'y' ,
] . includes ( v )
) {
typeErrors . add ( t ( 'msg.formula.thirdParamDateDiffHaveDate' ) )
}
} ,
typeErrors ,
)
}
}
}
errors = new Set ( [ ... errors , ... typeErrors ] )
} else if ( parsedTree . type === JSEPNode . IDENTIFIER ) {
if ( supportedColumns . value . filter ( ( c ) => ! column || column . value ? . id !== c . id ) . every ( ( c ) => c . title !== parsedTree . name ) ) {
errors . add (
t ( 'msg.formula.columnNotAvailable' , {
columnName : parsedTree . name ,
} ) ,
)
}
/ / c h e c k c i r c u l a r r e f e r e n c e
/ / e . g . f o r m u l a 1 - > f o r m u l a 2 - > f o r m u l a 1 s h o u l d r e t u r n c i r c u l a r r e f e r e n c e e r r o r
/ / g e t a l l f o r m u l a c o l u m n s e x c l u d i n g i t s e l f
const formulaPaths = supportedColumns . value
. filter ( ( c ) => c . id !== column . value ? . id && c . uidt === UITypes . Formula )
. reduce ( ( res : Record < string , any > [ ] , c : Record < string , any > ) => {
/ / i n ` f o r m u l a ` , g e t a l l t h e ( u n i q u e ) t a r g e t n e i g h b o u r s
/ / i . e . a l l c o l u m n i d ( e . g . c l _ x x x x x x x x x x x x x x ) w i t h f o r m u l a t y p e
const neighbours = [
... new Set (
( c . colOptions . formula . match ( /cl_\w{14}/g ) || [ ] ) . filter (
( colId : string ) =>
supportedColumns . value . filter ( ( col : ColumnType ) => col . id === colId && col . uidt === UITypes . Formula ) . length ,
) ,
) ,
]
if ( neighbours . length > 0 ) {
/ / e . g . f o r m u l a c o l u m n 1 - > [ f o r m u l a c o l u m n 2 , f o r m u l a c o l u m n 3 ]
res . push ( { [ c . id ] : neighbours } )
}
return res
} , [ ] )
/ / i n c l u d e t a r g e t f o r m u l a c o l u m n ( i . e . t h e o n e t o b e s a v e d i f a p p l i c a b l e )
const targetFormulaCol = supportedColumns . value . find (
( c : ColumnType ) => c . title === parsedTree . name && c . uidt === UITypes . Formula ,
)
if ( targetFormulaCol && column . value ? . id ) {
formulaPaths . push ( {
[ column . value ? . id as string ] : [ targetFormulaCol . id ] ,
} )
}
const vertices = formulaPaths . length
if ( vertices > 0 ) {
/ / p e r f o r m k a h n ' s a l g o f o r c y c l e d e t e c t i o n
const adj = new Map ( )
const inDegrees = new Map ( )
/ / i n i t a d j a c e n c y l i s t & i n d e g r e e
for ( const [ _ , v ] of Object . entries ( formulaPaths ) ) {
const src = Object . keys ( v ) [ 0 ]
const neighbours = v [ src ]
inDegrees . set ( src , inDegrees . get ( src ) || 0 )
for ( const neighbour of neighbours ) {
adj . set ( src , ( adj . get ( src ) || new Set ( ) ) . add ( neighbour ) )
inDegrees . set ( neighbour , ( inDegrees . get ( neighbour ) || 0 ) + 1 )
}
}
const queue : string [ ] = [ ]
/ / p u t a l l v e r t i c e s w i t h i n - d e g r e e = 0 ( i . e . n o i n c o m i n g e d g e s ) t o q u e u e
inDegrees . forEach ( ( inDegree , col ) => {
if ( inDegree === 0 ) {
/ / i n - d e g r e e = 0 m e a n s w e s t a r t t r a v e r s i n g f r o m t h i s n o d e
queue . push ( col )
}
} )
/ / i n i t c o u n t o f v i s i t e d v e r t i c e s
let visited = 0
/ / B F S
while ( queue . length !== 0 ) {
/ / r e m o v e a v e r t e x f r o m t h e q u e u e
const src = queue . shift ( )
/ / i f t h i s n o d e h a s n e i g h b o u r s , i n c r e a s e v i s i t e d b y 1
const neighbours = adj . get ( src ) || new Set ( )
if ( neighbours . size > 0 ) {
visited += 1
}
/ / i t e r a t e e a c h n e i g h b o u r i n g n o d e s
neighbours . forEach ( ( neighbour : string ) => {
/ / d e c r e a s e i n - d e g r e e o f i t s n e i g h b o u r s b y 1
inDegrees . set ( neighbour , inDegrees . get ( neighbour ) - 1 )
/ / i f i n - d e g r e e b e c o m e s 0
if ( inDegrees . get ( neighbour ) === 0 ) {
/ / t h e n p u t t h e n e i g h b o r i n g n o d e t o t h e q u e u e
queue . push ( neighbour )
}
const suggestedFormulas = computed ( ( ) => {
return suggestion . value . filter ( ( s ) => s && s . type !== 'column' )
} )
}
/ / v e r t i c e s n o t s a m e a s v i s i t e d = c y c l e f o u n d
if ( vertices !== visited ) {
errors . add ( t ( 'msg.formula.cantSaveCircularReference' ) )
}
}
} else if ( parsedTree . type === JSEPNode . BINARY _EXP ) {
if ( ! availableBinOps . includes ( parsedTree . operator ) ) {
errors . add ( t ( 'msg.formula.operationNotAvailable' , { operation : parsedTree . operator } ) )
}
validateAgainstMeta ( parsedTree . left , errors )
validateAgainstMeta ( parsedTree . right , errors )
} else if ( parsedTree . type === JSEPNode . LITERAL || parsedTree . type === JSEPNode . UNARY _EXP ) {
/ / d o n o t h i n g
} else if ( parsedTree . type === JSEPNode . COMPOUND ) {
if ( parsedTree . body . length ) {
errors . add ( t ( 'msg.formula.cantSaveFieldFormulaInvalid' ) )
}
} else {
errors . add ( t ( 'msg.formula.cantSaveFieldFormulaInvalid' ) )
}
return errors
}
function validateAgainstType ( parsedTree : any , expectedType : string , func : any , typeErrors = new Set ( ) ) {
if ( parsedTree === false || typeof parsedTree === 'undefined' ) {
return typeErrors
}
if ( parsedTree . type === JSEPNode . LITERAL ) {
if ( typeof func === 'function' ) {
func ( parsedTree . value )
} else if ( expectedType === formulaTypes . NUMERIC ) {
if ( typeof parsedTree . value !== 'number' ) {
typeErrors . add ( t ( 'msg.formula.numericTypeIsExpected' ) )
}
} else if ( expectedType === formulaTypes . STRING ) {
if ( typeof parsedTree . value !== 'string' ) {
typeErrors . add ( t ( 'msg.formula.stringTypeIsExpected' ) )
}
}
} else if ( parsedTree . type === JSEPNode . IDENTIFIER ) {
const col = supportedColumns . value . find ( ( c ) => c . title === parsedTree . name )
if ( col === undefined ) {
return
}
if ( col . uidt === UITypes . Formula ) {
const foundType = getRootDataType ( jsep ( col . colOptions ? . formula _raw ) )
if ( foundType === 'N/A' ) {
typeErrors . add ( t ( 'msg.formula.notSupportedToReferenceColumn' , { columnName : col . title } ) )
} else if ( expectedType !== foundType ) {
typeErrors . add (
t ( 'msg.formula.typeIsExpectedButFound' , {
type : expectedType ,
found : foundType ,
} ) ,
)
}
} else {
switch ( col . uidt ) {
/ / s t r i n g
case UITypes . SingleLineText :
case UITypes . LongText :
case UITypes . MultiSelect :
case UITypes . SingleSelect :
case UITypes . PhoneNumber :
case UITypes . Email :
case UITypes . URL :
if ( expectedType !== formulaTypes . STRING ) {
typeErrors . add (
t ( 'msg.formula.columnWithTypeFoundButExpected' , {
columnName : parsedTree . name ,
columnType : formulaTypes . STRING ,
expectedType ,
} ) ,
)
}
break
/ / n u m e r i c
case UITypes . Year :
case UITypes . Number :
case UITypes . Decimal :
case UITypes . Rating :
case UITypes . Count :
case UITypes . AutoNumber :
case UITypes . Currency :
if ( expectedType !== formulaTypes . NUMERIC ) {
typeErrors . add (
t ( 'msg.formula.columnWithTypeFoundButExpected' , {
columnName : parsedTree . name ,
columnType : formulaTypes . NUMERIC ,
expectedType ,
} ) ,
)
}
break
/ / d a t e
case UITypes . Date :
case UITypes . DateTime :
case UITypes . CreateTime :
case UITypes . LastModifiedTime :
if ( expectedType !== formulaTypes . DATE ) {
typeErrors . add (
t ( 'msg.formula.columnWithTypeFoundButExpected' , {
columnName : parsedTree . name ,
columnType : formulaTypes . DATE ,
expectedType ,
} ) ,
)
}
break
case UITypes . Rollup : {
const rollupFunction = col . colOptions . rollup _function
if ( [ 'count' , 'avg' , 'sum' , 'countDistinct' , 'sumDistinct' , 'avgDistinct' ] . includes ( rollupFunction ) ) {
/ / t h e s e f u n c t i o n s p r o d u c e a n u m e r i c v a l u e , w h i c h c a n b e u s e d i n n u m e r i c f u n c t i o n s
if ( expectedType !== formulaTypes . NUMERIC ) {
typeErrors . add (
t ( 'msg.formula.columnWithTypeFoundButExpected' , {
columnName : parsedTree . name ,
columnType : formulaTypes . NUMERIC ,
expectedType ,
} ) ,
)
}
} else {
/ / t h e v a l u e i s b a s e d o n t h e f o r e i g n r o l l u p c o l u m n t y p e
const selectedTable = refTables . value . find ( ( t ) => t . column . id === col . colOptions . fk _relation _column _id )
const refTableColumns = metas . value [ selectedTable . id ] . columns . filter (
( c : ColumnType ) =>
vModel . value . fk _lookup _column _id === c . id ||
( ! isSystemColumn ( c ) && c . id !== vModel . value . id && c . uidt !== UITypes . Links ) ,
)
const childFieldColumn = refTableColumns . find (
( column : ColumnType ) => column . id === col . colOptions . fk _rollup _column _id ,
)
const abstractType = sqlUi . value . getAbstractType ( childFieldColumn )
if ( expectedType === formulaTypes . DATE && ! isDate ( childFieldColumn , sqlUi . value . getAbstractType ( childFieldColumn ) ) ) {
typeErrors . add (
t ( 'msg.formula.columnWithTypeFoundButExpected' , {
columnName : parsedTree . name ,
columnType : abstractType ,
expectedType ,
} ) ,
)
} else if ( expectedType === formulaTypes . NUMERIC && ! isNumericCol ( childFieldColumn ) ) {
typeErrors . add (
t ( 'msg.formula.columnWithTypeFoundButExpected' , {
columnName : parsedTree . name ,
columnType : abstractType ,
expectedType ,
} ) ,
)
}
}
break
}
/ / n o t s u p p o r t e d
case UITypes . ForeignKey :
case UITypes . Attachment :
case UITypes . ID :
case UITypes . Time :
case UITypes . Percent :
case UITypes . Duration :
case UITypes . Lookup :
case UITypes . Barcode :
case UITypes . Button :
case UITypes . Checkbox :
case UITypes . Collaborator :
case UITypes . QrCode :
default :
typeErrors . add ( t ( 'msg.formula.notSupportedToReferenceColumn' , { columnName : parsedTree . name } ) )
break
}
}
} else if ( parsedTree . type === JSEPNode . UNARY _EXP || parsedTree . type === JSEPNode . BINARY _EXP ) {
if ( expectedType !== formulaTypes . NUMERIC ) {
/ / p a r s e d T r e e . n a m e w o n ' t b e a v a i l a b l e h e r e
typeErrors . add (
t ( 'msg.formula.typeIsExpectedButFound' , {
type : formulaTypes . NUMERIC ,
found : expectedType ,
} ) ,
)
}
} else if ( parsedTree . type === JSEPNode . CALL _EXP ) {
const calleeName = parsedTree . callee . name . toUpperCase ( )
if ( formulas [ calleeName ] ? . type && expectedType !== formulas [ calleeName ] . type ) {
typeErrors . add (
t ( 'msg.formula.typeIsExpectedButFound' , {
type : expectedType ,
found : formulas [ calleeName ] . type ,
} ) ,
)
}
}
return typeErrors
}
function getRootDataType ( parsedTree : any ) : any {
/ / g i v e n a p a r s e t r e e , r e t u r n t h e d a t a t y p e o f i t
if ( parsedTree . type === JSEPNode . CALL _EXP ) {
return formulas [ parsedTree . callee . name . toUpperCase ( ) ] . type
} else if ( parsedTree . type === JSEPNode . IDENTIFIER ) {
const col = supportedColumns . value . find ( ( c ) => c . title === parsedTree . name ) as Record < string , any >
if ( col ? . uidt === UITypes . Formula ) {
return getRootDataType ( jsep ( col ? . formula _raw ) )
} else {
switch ( col ? . uidt ) {
/ / s t r i n g
case UITypes . SingleLineText :
case UITypes . LongText :
case UITypes . MultiSelect :
case UITypes . SingleSelect :
case UITypes . PhoneNumber :
case UITypes . Email :
case UITypes . URL :
return formulaTypes . STRING
/ / n u m e r i c
case UITypes . Year :
case UITypes . Number :
case UITypes . Decimal :
case UITypes . Rating :
case UITypes . Count :
case UITypes . AutoNumber :
return formulaTypes . NUMERIC
/ / d a t e
case UITypes . Date :
case UITypes . DateTime :
case UITypes . CreateTime :
case UITypes . LastModifiedTime :
return formulaTypes . DATE
/ / n o t s u p p o r t e d
case UITypes . ForeignKey :
case UITypes . Attachment :
case UITypes . ID :
case UITypes . Time :
case UITypes . Currency :
case UITypes . Percent :
case UITypes . Duration :
case UITypes . Rollup :
case UITypes . Lookup :
case UITypes . Barcode :
case UITypes . Button :
case UITypes . Checkbox :
case UITypes . Collaborator :
case UITypes . QrCode :
default :
return 'N/A'
}
}
} else if ( parsedTree . type === JSEPNode . BINARY _EXP || parsedTree . type === JSEPNode . UNARY _EXP ) {
return formulaTypes . NUMERIC
} else if ( parsedTree . type === JSEPNode . LITERAL ) {
return typeof parsedTree . value
} else {
return 'N/A'
}
}
const variableList = computed ( ( ) => {
return suggestion . value . filter ( ( s ) => s && s . type === 'column' )
} )
function isCurlyBracketBalanced ( ) {
/ / c o u n t n u m b e r o f o p e n i n g c u r l y b r a c k e t s a n d c l o s i n g c u r l y b r a c k e t s
@ -739,6 +209,11 @@ function handleInput() {
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' )
}
@ -746,14 +221,21 @@ function handleInput() {
}
function selectText ( ) {
if ( suggestion . value && selected . value > - 1 && selected . value < suggestion . value . length ) {
appendText ( suggestion . value [ selected . value ] )
if ( suggestion . value && selected . value > - 1 && selected . value < suggestionsList . value . length ) {
if ( selected . value < suggestedFormulas . value . length ) {
appendText ( suggestedFormulas . value [ selected . value ] )
} else {
appendText ( variableList . value [ selected . value + suggestedFormulas . value . length ] )
}
}
selected . value = 0
}
function suggestionListUp ( ) {
if ( suggestion . value ) {
selected . value = -- selected . value > - 1 ? selected . value : suggestion . value . length - 1
suggestionPreviewed . value = suggestedFormulas . value [ selected . value ]
scrollToSelectedOption ( )
}
}
@ -761,6 +243,8 @@ function suggestionListUp() {
function suggestionListDown ( ) {
if ( suggestion . value ) {
selected . value = ++ selected . value % suggestion . value . length
suggestionPreviewed . value = suggestedFormulas . value [ selected . value ]
scrollToSelectedOption ( )
}
}
@ -769,9 +253,9 @@ function scrollToSelectedOption() {
nextTick ( ( ) => {
if ( sugOptionsRef . value [ selected . value ] ) {
try {
sugList Ref . value . $el . scrollTo ( {
top : sugOptionsRef . value [ selected . value ] . $el . offsetTop ,
behavior : 'smooth ',
sugOptions Ref . value [ selected . value ] . $el . scrollIntoView ( {
block : 'nearest' ,
inline : 'start ',
} )
} catch ( e ) { }
}
@ -796,15 +280,55 @@ setAdditionalValidations({
onMounted ( ( ) => {
jsep . plugins . register ( jsepCurlyHook )
} )
/ / c o n s t p r e d i c t F u n c t i o n = a s y n c ( ) = > {
/ / a w a i t _ p r e d i c t F u n c t i o n ( f o r m S t a t e , m e t a , s u p p o r t e d C o l u m n s , s u g g e s t i o n s L i s t , v M o d e l )
/ / }
< / script >
< template >
< div class = "formula-wrapper" >
< a -form -item v-bind ="validateInfos.formula_raw" :label="$t('datatype.Formula')" >
< div class = "formula-wrapper relative" >
< div
v - if = "suggestionPreviewed && suggestionPreviewed.type === 'function'"
class = "absolute -left-91 w-84 top-0 bg-white z-10 pl-3 pt-3 border-1 shadow-md rounded-xl"
>
< div class = "pr-3" >
< div class = "flex flex-row w-full justify-between pb-1 border-b-1" >
< div class = "flex items-center gap-x-1 font-semibold text-base" >
< component :is ="iconMap.function" class = "text-lg" / >
{ { suggestionPreviewed . text } }
< / div >
< NcButton type = "text" size = "small" @ click = "suggestionPreviewed = undefined" >
< GeneralIcon icon = "close" / >
< / NcButton >
< / div >
< / div >
< div class = "flex flex-col max-h-120 nc-scrollbar-md pr-2" >
< div class = "flex mt-3" > { { 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
v - for = "(example, index) of suggestionPreviewed.examples"
: key = "example"
class = "bg-gray-100 py-1 px-2"
: class = " {
'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 ,
} "
>
{ { example } }
< / div >
< / div >
< div class = "flex flex-row mt-1 mb-3 justify-end pr-3" >
< a target = "_blank" rel = "noopener noreferrer" :href ="suggestionPreviewed.docsUrl" >
< NcButton type = "text" class = "!text-gray-400 !hover:text-gray-800 !text-xs"
> View in Docs
< GeneralIcon icon = "openInNew" class = "ml-1" / >
< / NcButton >
< / a >
< / div >
< / div >
< a -form -item v-bind ="validateInfos.formula_raw" class="!pb-1" :label="$t('datatype.Formula')" >
<!-- < GeneralIcon
v - if = "isEeUI"
icon = "magic"
@ -815,7 +339,7 @@ onMounted(() => {
< a -textarea
ref = "formulaRef"
v - model : value = "vModel.formula_raw"
class = "mb-2 nc-formula-input"
class = "nc-formula-input !rounded-md !my-1 "
@ keydown . down . prevent = "suggestionListDown"
@ keydown . up . prevent = "suggestionListUp"
@ keydown . enter . prevent = "selectText"
@ -823,26 +347,15 @@ onMounted(() => {
/ >
< / a - f o r m - i t e m >
< div class = "text-gray-600 mt-2 mb-4 prose-sm" >
{ {
/ / A s u s i n g { } i n t r a n s l a t i o n w i l l b e t r e a t e d a s p l a c e h o l d e r , a n d t h i s t r a n s l a t i o n c o n t a i n { } a s p a r t o f t h t e x t
$t ( 'msg.formula.hintStart' , {
placeholder1 : '{}' ,
placeholder2 : '{column_name}' ,
} )
} }
< a
class = "prose-sm"
href = "https://docs.nocodb.com/setup-and-usages/formulas#available-formula-features"
target = "_blank"
rel = "noopener"
>
{ { $t ( 'msg.formula.hintEnd' ) } }
< / a >
< / div >
< div ref = "sugListRef" class = "h-[250px] overflow-auto nc-scrollbar-md" >
< template v-if ="suggestedFormulas.length > 0" >
< div class = "rounded-t-lg border-1 bg-gray-50 px-3 py-1 uppercase text-gray-600 text-xs" > Formulas < / div >
< div class = "h-[250px] overflow-auto scrollbar-thin-primary" >
< a -list ref = "sugListRef" :data-source ="suggestion" : locale = "{ emptyText: $t('msg.formula.noSuggestedFormulaFound') }" >
< a -list
: data - source = "suggestedFormulas"
: locale = "{ emptyText: $t('msg.formula.noSuggestedFormulaFound') }"
class = "border-1 border-t-0 rounded-b-lg !mb-4"
>
< template # renderItem = "{ item, index }" >
< a -list -item
: ref = "
@ -850,46 +363,74 @@ onMounted(() => {
sugOptionsRef [ index ] = el
}
"
class = "cursor-pointer"
class = "cursor-pointer !overflow-hidden hover:bg-gray-50"
: class = " {
'!bg-gray-100' : selected === index ,
} "
@ click . prevent . stop = "appendText(item)"
@ mouseenter = "suggestionPreviewed = item"
>
< a -list -item -meta >
< template # title >
< div class = "flex" >
< a -col :span ="6" >
< span class = "prose-sm text-gray-600" > { { item . text } } < / span >
< / a - c o l >
< a -col :span ="18" >
< div v-if ="item.type === 'function'" class="text-xs text-gray-500" >
{ { item . description } } < br / > < br / >
{ { $t ( 'labels.syntax' ) } } : < br / >
{ { item . syntax } } < br / > < br / >
{ { $t ( 'labels.examples' ) } } : < br / >
< div class = "flex items-center gap-x-1" >
< component :is ="iconMap.function" v -if = " item.type = = = ' function ' " class = "text-lg" / >
< div v-for ="(example, idx) of item.examples" :key ="idx" >
< div > ( { { idx + 1 } } ) : { { example } } < / div >
< / div >
< / div >
< component :is ="iconMap.calculator" v -if = " item.type = = = ' op ' " class = "text-lg" / >
< div v-if ="item.type === 'column'" class="float-right mr-5 -mt-2" >
< a -badge -ribbon :text ="item.uidt" color = "gray" / >
< / div >
< / a - c o l >
< component :is ="item.icon" v -if = " item.type = = = ' column ' " class = "text-lg" / >
< span class = "prose-sm text-gray-600" > { { item . text } } < / span >
< / div >
< / template >
< / a - l i s t - i t e m - m e t a >
< / a - l i s t - i t e m >
< / template >
< / a - l i s t >
< / template >
< template # avatar >
< component :is ="iconMap.function" v -if = " item.type = = = ' function ' " class = "text-lg" / >
< template v-if ="variableList.length > 0" >
< div class = "rounded-t-lg border-1 bg-gray-50 px-3 py-1 uppercase text-gray-600 text-xs" > Fields < / div >
< component :is ="iconMap.calculator" v -if = " item.type = = = ' op ' " class = "text-lg" / >
< a -list
ref = "variableListRef"
: data - source = "variableList"
: locale = "{ emptyText: $t('msg.formula.noSuggestedFormulaFound') }"
class = "border-1 border-t-0 rounded-b-lg !overflow-hidden"
>
< template # renderItem = "{ item, index }" >
< a -list -item
: ref = "
( el ) => {
sugOptionsRef [ index + suggestedFormulas . length ] = el
}
"
: class = " {
'!bg-gray-100' : selected === index + suggestedFormulas . length ,
} "
class = "cursor-pointer hover:bg-gray-50"
@ click . prevent . stop = "appendText(item)"
>
< a -list -item -meta >
< template # title >
< div class = "flex items-center gap-x-1" >
< component :is ="item.icon" class = "text-lg" / >
< component :is ="item.icon" v -if = " item.type = = = ' column ' " class = "text-lg" / >
< span class = "prose-sm text-gray-600" > { { item . text } } < / span >
< / div >
< / template >
< / a - l i s t - i t e m - m e t a >
< / a - l i s t - i t e m >
< / template >
< / a - l i s t >
< / template >
< div v-if ="suggestion.length === 0" >
< span class = "text-gray-500" > Empty < / span >
< / div >
< / div >
< / div >
< / template >
< style lang = "scss" scoped >
: deep ( . ant - list - item ) {
@ apply ! pt - 1.75 pb - 0.75 ! px - 2 ;
}
< / style >