diff --git a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue index c607a8ccbd..b6875a6174 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -43,6 +43,9 @@ + + mdi-function + {{ it.text }} @@ -70,10 +73,12 @@ + + {{ it.icon }} + {{ it.text }} - Column @@ -87,6 +92,9 @@ + + mdi-calculator-variant + {{ it.text }} @@ -109,6 +117,7 @@ import debounce from 'debounce' import jsep from 'jsep' import { UITypes, jsepCurlyHook } from 'nocodb-sdk' +import { getUIDTIcon } from '../../helpers/uiTypes' import formulaList, { formulas, formulaTypes } from '../../../../../helpers/formulaList' import { getWordUntilCaret, insertAtCursor } from '@/helpers' import NcAutocompleteTree from '@/helpers/NcAutocompleteTree' @@ -146,6 +155,7 @@ export default { ...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({ text: c.title, type: 'column', + icon: getUIDTIcon(c.uidt), c })), ...this.availableBinOps.map(op => ({ @@ -534,13 +544,18 @@ export default { return 'N/A' } }, + isCurlyBracketBalanced() { + // count number of opening curly brackets and closing curly brackets + const cntCurlyBrackets = (this.$refs.input.$el.querySelector('input').value.match(/\{|}/g) || []).reduce((acc, cur) => (acc[cur] = (acc[cur] || 0) + 1, acc), {}) + return (cntCurlyBrackets['{'] || 0) === (cntCurlyBrackets['}'] || 0) + }, appendText(it) { const text = it.text const len = this.wordToComplete.length if (it.type === 'function') { this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len, 1)) } else if (it.type === 'column') { - this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), '{' + text + '}', len)) + this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), '{' + text + '}', len + (!this.isCurlyBracketBalanced()))) } else { this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len)) } @@ -560,6 +575,9 @@ export default { const parts = query.split(/\W+/) this.wordToComplete = parts.pop() this.suggestion = this.acTree.complete(this.wordToComplete)?.sort((x, y) => this.sortOrder[x.type] - this.sortOrder[y.type]) + if (!this.isCurlyBracketBalanced()) { + this.suggestion = this.suggestion.filter(v => v.type === 'column') + } this.autocomplete = !!this.suggestion.length }, selectText() { diff --git a/packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js b/packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js index 693b147017..11f85acfec 100644 --- a/packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js +++ b/packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js @@ -151,10 +151,20 @@ const uiTypes = [ ] const getUIDTIcon = (uidt) => { - return ([...uiTypes, { - name: 'CreateTime', - icon: 'mdi-calendar-clock' - }].find(t => t.name === uidt) || {}).icon + return ([...uiTypes, + { + name: 'CreateTime', + icon: 'mdi-calendar-clock' + }, + { + name: 'ID', + icon: 'mdi-identifier' + }, + { + name: 'ForeignKey', + icon: 'mdi-link-variant' + } + ].find(t => t.name === uidt) || {}).icon } export { diff --git a/packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts b/packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts index 35e2ecab30..a73841eb05 100644 --- a/packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts +++ b/packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts @@ -612,7 +612,21 @@ export default async function formulaQueryBuilderv2( return knex.raw( `${pt.callee.name}(${pt.arguments - .map(arg => fn(arg).toQuery()) + .map(arg => { + const query = fn(arg).toQuery(); + if (pt.callee.name === 'CONCAT') { + if (knex.clientType() === 'mysql2') { + // mysql2: CONCAT() returns NULL if any argument is NULL. + // adding IFNULL to convert NULL values to empty strings + return `IFNULL(${query}, '')`; + } else { + // do nothing + // pg / mssql: Concatenate all arguments. NULL arguments are ignored. + // sqlite3: special handling - See BinaryExpression + } + } + return query; + }) .join()})${colAlias}` ); } else if (pt.type === 'Literal') { @@ -637,13 +651,16 @@ export default async function formulaQueryBuilderv2( pt.left.fnName = pt.left.fnName || 'ARITH'; pt.right.fnName = pt.right.fnName || 'ARITH'; - const query = knex.raw( - `${fn(pt.left, null, pt.operator).toQuery()} ${pt.operator} ${fn( - pt.right, - null, - pt.operator - ).toQuery()}${colAlias}` - ); + const left = fn(pt.left, null, pt.operator).toQuery(); + const right = fn(pt.right, null, pt.operator).toQuery(); + let sql = `${left} ${pt.operator} ${right}${colAlias}`; + + // handle NULL values when calling CONCAT for sqlite3 + if (pt.left.fnName === 'CONCAT' && knex.clientType() === 'sqlite3') { + sql = `COALESCE(${left}, '') ${pt.operator} COALESCE(${right},'')${colAlias}`; + } + + const query = knex.raw(sql); if (prevBinaryOp && pt.operator !== prevBinaryOp) { query.wrap('(', ')'); } diff --git a/packages/nocodb/src/lib/noco-models/Column.ts b/packages/nocodb/src/lib/noco-models/Column.ts index 63d942599f..0592a79bc2 100644 --- a/packages/nocodb/src/lib/noco-models/Column.ts +++ b/packages/nocodb/src/lib/noco-models/Column.ts @@ -541,7 +541,7 @@ export default class Column implements ColumnType { title: col?.title }) ) - await FormulaColumn.update(formula.id, formula, ncMeta); + await FormulaColumn.update(formulaCol.id, formula, ncMeta); } } diff --git a/packages/nocodb/src/lib/noco/meta/api/columnApis.ts b/packages/nocodb/src/lib/noco/meta/api/columnApis.ts index 5c02957984..b00d90e6ee 100644 --- a/packages/nocodb/src/lib/noco/meta/api/columnApis.ts +++ b/packages/nocodb/src/lib/noco/meta/api/columnApis.ts @@ -649,7 +649,7 @@ export async function columnUpdate(req: Request, res: Response) { formula, [new_column] ); - await FormulaColumn.update(f.id, { + await FormulaColumn.update(c.id, { formula_raw: new_formula_raw }); }