From e3a5999329eed51594ed27f87c44a8774da4310f Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Thu, 5 May 2022 20:00:57 +0800 Subject: [PATCH 01/21] fix: handle column name referenced by $ --- .../components/editColumn/formulaOptions.vue | 11 +++++++---- packages/nocodb-sdk/src/lib/formulaHelpers.ts | 5 ++++- .../src/lib/noco/meta/helpers/formulaHelpers.ts | 3 ++- 3 files changed, 13 insertions(+), 6 deletions(-) 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 66431a4ff2..d5af9bc103 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue @@ -20,7 +20,7 @@ hide-details="auto" label="Formula" persistent-hint - hint="Available formulas are ADD, AVG, CONCAT, +, -, /" + hint="Hint: If you reference columns, use '$' to wrap the column name, e.g: $column_name$." :rules="[v => !!v || 'Required', v => parseAndValidateFormula(v)]" autocomplete="off" @input="handleInputDeb" @@ -165,7 +165,6 @@ export default { this.$toast.error(e.message).goAway(3000) } }, - // todo: validate formula based on meta parseAndValidateFormula(formula) { try { const pt = jsep(formula) @@ -195,8 +194,12 @@ export default { } pt.arguments.map(arg => this.validateAgainstMeta(arg, arr)) } else if (pt.type === 'Identifier') { - if (this.meta.columns.filter(c => !this.column || this.column.id !== c.id).every(c => c.title !== pt.name)) { - arr.push(`Column with name '${pt.name}' is not available`) + // $column_name$ -> column_name + const sanitizedPtName = pt.name.substring(1, pt.name.length - 1) + if (this.meta.columns.filter(c => !this.column || this.column.id !== c.id).find(c => c.title === pt.name)) { + arr.push(`Column '${pt.name}' needs to be wrapped by $. Try $${pt.name}$ instead`) + } else if (this.meta.columns.filter(c => !this.column || this.column.id !== c.id).every(c => c.title !== sanitizedPtName)) { + arr.push(`Column '${pt.name}' is not available`) } } else if (pt.type === 'BinaryExpression') { if (!this.availableBinOps.includes(pt.operator)) { diff --git a/packages/nocodb-sdk/src/lib/formulaHelpers.ts b/packages/nocodb-sdk/src/lib/formulaHelpers.ts index 492b362ecd..a1f97b20a3 100644 --- a/packages/nocodb-sdk/src/lib/formulaHelpers.ts +++ b/packages/nocodb-sdk/src/lib/formulaHelpers.ts @@ -15,7 +15,7 @@ export function substituteColumnIdWithAliasInFormula( } else if (pt.type === 'Literal') { return; } else if (pt.type === 'Identifier') { - const colNameOrId = pt.name; + const colNameOrId = pt?.name; const column = columns.find( (c) => c.id === colNameOrId || @@ -23,6 +23,9 @@ export function substituteColumnIdWithAliasInFormula( c.title === colNameOrId ); pt.name = column?.title || ptRaw?.name || pt?.name; + if (pt.name[0] != '$' && pt.name[pt.name.length - 1] != '$') { + pt.name = '$' + pt.name + '$'; + } } else if (pt.type === 'BinaryExpression') { substituteId(pt.left, ptRaw?.left); substituteId(pt.right, ptRaw?.right); diff --git a/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts b/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts index 8bc4159710..f49927f09c 100644 --- a/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts +++ b/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts @@ -14,7 +14,8 @@ export async function substituteColumnAliasWithIdInFormula( } else if (pt.type === 'Literal') { return; } else if (pt.type === 'Identifier') { - const colNameOrId = pt.name; + const sanitizedPtName = pt.name.substring(1, pt.name.length - 1); + const colNameOrId = sanitizedPtName; const column = columns.find( c => c.id === colNameOrId || From d19c3462c74a6fe81d18edd1ddb9984e725ca034 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Thu, 5 May 2022 20:01:51 +0800 Subject: [PATCH 02/21] fix: update formula_raw when fk column name is changed --- .../src/lib/noco/meta/api/columnApis.ts | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/packages/nocodb/src/lib/noco/meta/api/columnApis.ts b/packages/nocodb/src/lib/noco/meta/api/columnApis.ts index 180b5df871..4959cf206b 100644 --- a/packages/nocodb/src/lib/noco/meta/api/columnApis.ts +++ b/packages/nocodb/src/lib/noco/meta/api/columnApis.ts @@ -19,6 +19,7 @@ import { isVirtualCol, LinkToAnotherRecordType, RelationTypes, + substituteColumnIdWithAliasInFormula, TableType, UITypes } from 'nocodb-sdk'; @@ -32,6 +33,8 @@ import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT'; import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue'; import NcConnectionMgrv2 from '../../common/NcConnectionMgrv2'; import { metaApiMetrics } from '../helpers/apiMetrics'; +import FormulaColumn from '../../../noco-models/FormulaColumn'; +import { MetaTable } from '../../../utils/globals'; const randomID = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 10); @@ -606,20 +609,47 @@ export async function columnUpdate(req: Request, res: Response) { cn: c.column_name, cno: c.column_name })), - columns: table.columns.map(c => { - if (c.id === req.params.columnId) { - return { - ...c, - ...colBody, - cn: colBody.column_name, - cno: c.column_name, - altered: Altered.UPDATE_COLUMN - }; - } else { - (c as any).cn = c.column_name; - } - return c; - }) + columns: await Promise.all( + table.columns.map(async c => { + if (c.id === req.params.columnId) { + const res = { + ...c, + ...colBody, + cn: colBody.column_name, + cno: c.column_name, + altered: Altered.UPDATE_COLUMN + }; + + // update formula with new column name + if (c.column_name != colBody.column_name) { + const formulas = await Noco.ncMeta + .knex(MetaTable.COL_FORMULA) + .where('formula', 'like', `%${c.id}%`); + if (formulas) { + const new_column = c; + new_column.column_name = colBody.column_name; + new_column.title = colBody.title; + for (const f of formulas) { + // the formula with column IDs only + const formula = f.formula; + // replace column IDs with alias to get the new formula_raw + const new_formula_raw = substituteColumnIdWithAliasInFormula( + formula, + [new_column] + ); + await FormulaColumn.update(f.id, { + formula_raw: new_formula_raw + }); + } + } + } + return Promise.resolve(res); + } else { + (c as any).cn = c.column_name; + } + return Promise.resolve(c); + }) + ) }; const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id }); From d7013c8c224f3cae2331b0907cb2e598d69f4add Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Thu, 5 May 2022 20:15:40 +0800 Subject: [PATCH 03/21] docs: update formula content --- .../content/en/setup-and-usages/formulas.md | 109 +++++++++--------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/packages/noco-docs/content/en/setup-and-usages/formulas.md b/packages/noco-docs/content/en/setup-and-usages/formulas.md index a20b49a477..7944427f5c 100644 --- a/packages/noco-docs/content/en/setup-and-usages/formulas.md +++ b/packages/noco-docs/content/en/setup-and-usages/formulas.md @@ -18,10 +18,10 @@ menuTitle: "Formulas" ### 4. Insert required formula -- Can use column names in equation -- Can use explicit numberical values/ strings as needed +- You can reference column names in equation with `$`, e.g. `$column_name$`. +- You can use explicit numerical values/ strings as needed, e.g. `123` (numeric) or `"123"` (string). - Table below lists supported formula & associated syntax -- Nested formula (formula equation referring to another formula column) are not supported +- Nested formula (formula equation referring to another formula column) is supported ### 5. Click on 'Save' @@ -31,99 +31,98 @@ menuTitle: "Formulas" | Name | Syntax | Sample | Output | |-------------|----------------------------|----------------------------------|------------------------------------------------------------------| -| **ABS** | `ABS(value)` | `ABS(Column)` | Absolute value of the input parameter | -| **ADD** | `ADD(value1,[value2,...])` | `ADD(Column1, Column1)` | Sum of input parameters | -| **AVG** | `AVG(value1,[value2,...])` | `AVG(Column1, Column1)` | Average of input parameters | -| **CEILING** | `CEILING(value)` | `CEILING(Column)` | Rounded next largest integer value of input parameter | -| **EXP** | `EXP(value)` | `EXP(Column)` | Exponential value of input parameter (`e^x`) | -| **FLOOR** | `FLOOR(value)` | `FLOOR(Column)` | Rounded largest integer less than or equal to input parameter | -| **INT** | `INT(value)` | `INT(Column)` | Integer value of input parameter | -| **LOG** | `LOG([base], value)` | `LOG(10, Column)` | Logarithm of input parameter to the base (default = e) specified | -| **MAX** | `MAX(value1,[value2,...])` | `MAX(Column1, Column2, Column3)` | Maximum value amongst input parameters | -| **MIN** | `MIN(value1,[value2,...])` | `MIN(Column1, Column2, Column3)` | Minimum value amongst input parameters | -| **MOD** | `MOD(value1, value2)` | `MOD(Column, 2)` | Remainder after integer division of input parameters | -| **POWER** | `POWER(base, exponent)` | `POWER(Column, 3)` | `base` to the `exponent` power, as in `base^exponent` | -| **ROUND** | `ROUND(value)` | `ROUND(Column)` | Nearest integer to the input parameter | -| **SQRT** | `SQRT(value)` | `SQRT(Column)` | Square root of the input parameter | +| **ABS** | `ABS(value)` | `ABS($Column$)` | Absolute value of the input parameter | +| **ADD** | `ADD(value1,[value2,...])` | `ADD($Column1$, $Column2$)` | Sum of input parameters | +| **AVG** | `AVG(value1,[value2,...])` | `AVG($Column1$, $Column2$)` | Average of input parameters | +| **CEILING** | `CEILING(value)` | `CEILING($Column$)` | Rounded next largest integer value of input parameter | +| **EXP** | `EXP(value)` | `EXP($Column$)` | Exponential value of input parameter (`e^x`) | +| **FLOOR** | `FLOOR(value)` | `FLOOR($Column$)` | Rounded largest integer less than or equal to input parameter | +| **INT** | `INT(value)` | `INT($Column$)` | Integer value of input parameter | +| **LOG** | `LOG([base], value)` | `LOG(10, $Column$)` | Logarithm of input parameter to the base (default = e) specified | +| **MAX** | `MAX(value1,[value2,...])` | `MAX($Column1$, $Column2$, $Column3$)` | Maximum value amongst input parameters | +| **MIN** | `MIN(value1,[value2,...])` | `MIN($Column1$, $Column2$, $Column3$)` | Minimum value amongst input parameters | +| **MOD** | `MOD(value1, value2)` | `MOD($Column$, 2)` | Remainder after integer division of input parameters | +| **POWER** | `POWER(base, exponent)` | `POWER($Column$, 3)` | `base` to the `exponent` power, as in `base ^ exponent` | +| **ROUND** | `ROUND(value)` | `ROUND($Column$)` | Nearest integer to the input parameter | +| **SQRT** | `SQRT(value)` | `SQRT($Column$)` | Square root of the input parameter | ### Numeric Operators | Operator | Sample | Description | | -------- | ----------------------- | -------------------------------- | -| `+` | `column1 + column2 + 2` | Addition of numeric values | -| `-` | `column1 - column2` | Subtraction of numeric values | -| `*` | `column1 * column2` | Multiplication of numeric values | -| `/` | `column1 / column2` | Division of numeric values | +| `+` | `$Column1$ + $Column2$ + 2` | Addition of numeric values | +| `-` | `$Column1$ - $Column2$` | Subtraction of numeric values | +| `*` | `$Column1$ * $Column2$` | Multiplication of numeric values | +| `/` | `$Column1$ / $Column2$` | Division of numeric values | Tip :To change the order of arithmetic operation, you can use round bracket parantheses ().
-Example: (column1 + (column2 * column3) / (3 - column4 )) +Example: ($Column1$ + ($Column2$ * $Column3$) / (3 - $Column4$ ))
### String Functions | Name | Syntax | Sample | Output | |-------------|----------------------------------|---------------------------------|---------------------------------------------------------------------------| -| **CONCAT** | `CONCAT(str1, [str2,...])` | `CONCAT(fName, ' ', lName)` | Concatenated string of input parameters | -| **LEFT** | `LEFT(str1, [str2,...])` | `LEFT(Column, 3)` | `n` characters from the beginning of input parameter | -| **LEN** | `LEN(str)` | `LEN(Title)` | Input parameter character length | -| **LOWER** | `LOWER(str)` | `LOWER(Title)` | Lower case converted string of input parameter | -| **MID** | `SUBTR(str, position, [count])` | `MID(Column, 3, 2)` | Alias for `SUBSTR` | -| **REPEAT** | `REPEAT(str, count)` | `REPEAT(Column, 2)` | Specified copies of the input parameter string concatenated together | -| **REPLACE** | `REPLACE(str, srchStr, rplcStr)` | `REPLACE(Column, 'int', 'num')` | String, after replacing all occurrences of `srchStr` with `rplcStr` | -| **RIGHT** | `RIGHT(str, count)` | `RIGHT(Column, 3)` | `n` characters from the end of input parameter | -| **SEARCH** | `SEARCH(str, srchStr)` | `SEARCH(Column, 'str')` | Index of `srchStr` specified if found, 0 otherwise | -| **SUBSTR** | `SUBTR(str, position, [count])` | `SUBSTR(Column, 3, 2)` | Substring of length 'count' of input string, from the postition specified | -| **TRIM** | `TRIM(str)` | `TRIM(Title)` | Remove trailing and leading whitespaces from input parameter | -| **UPPER** | `UPPER(str)` | `UPPER(Title)` | Upper case converted string of input parameter | -| **URL** | `URL(str)` | `URL(Column)` | Convert to a hyperlink if it is a valid URL | +| **CONCAT** | `CONCAT(str1, [str2,...])` | `CONCAT($Column1$, ' ', $Column2$)` | Concatenated string of input parameters | +| **LEFT** | `LEFT(str1, [str2,...])` | `LEFT($Column$, 3)` | `n` characters from the beginning of input parameter | +| **LEN** | `LEN(str)` | `LEN($Column$)` | Input parameter character length | +| **LOWER** | `LOWER(str)` | `LOWER($Column$)` | Lower case converted string of input parameter | +| **MID** | `SUBTR(str, position, [count])` | `MID($Column$, 3, 2)` | Alias for `SUBSTR` | +| **REPEAT** | `REPEAT(str, count)` | `REPEAT($Column$, 2)` | Specified copies of the input parameter string concatenated together | +| **REPLACE** | `REPLACE(str, srchStr, rplcStr)` | `REPLACE($Column$, 'int', 'num')` | String, after replacing all occurrences of `srchStr` with `rplcStr` | +| **RIGHT** | `RIGHT(str, count)` | `RIGHT($Column$, 3)` | `n` characters from the end of input parameter | +| **SEARCH** | `SEARCH(str, srchStr)` | `SEARCH($Column$, 'str')` | Index of `srchStr` specified if found, 0 otherwise | +| **SUBSTR** | `SUBTR(str, position, [count])` | `SUBSTR($Column$, 3, 2)` | Substring of length 'count' of input string, from the postition specified | +| **TRIM** | `TRIM(str)` | `TRIM($Column$)` | Remove trailing and leading whitespaces from input parameter | +| **UPPER** | `UPPER(str)` | `UPPER($Column$)` | Upper case converted string of input parameter | +| **URL** | `URL(str)` | `URL($Column$)` | Convert to a hyperlink if it is a valid URL | ### Date Functions | Name | Syntax | Sample | Output | Remark | |---|---|---|---|---| -| **DATEADD** | `DATEADD(DATE_COL, 1, 'day')` | `DATEADD(date, 1, 'day')` | Supposing the DATE_COL is 2022-03-14. The result is 2022-03-15. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` | -| | `DATEADD(DATE_COL, 2, 'month')` | `DATEADD(date, 2, 'month')` | Supposing the DATE_COL is 2022-03-14 03:14. The result is 2022-05-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -2, 'month')` | -| | `IF(NOW() < DATE_COL, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than DATE_COL, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | -| | `IF(NOW() < DATEADD(DATE_COL,10,'day'), "true", "false")` | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than DATE_COL plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | +| **DATEADD** | `DATEADD($DATE_COL$, 1, 'day')` | `DATEADD(date, 1, 'day')` | Supposing $DATE_COL$ is 2022-03-14. The result is 2022-03-15. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` | +| | `DATEADD($DATE_COL$, 2, 'month')` | `DATEADD(date, 2, 'month')` | Supposing $DATE_COL$ is 2022-03-14 03:14. The result is 2022-05-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -2, 'month')` | +| | `IF(NOW() < $DATE_COL$, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than $DATE_COL$, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | +| | `IF(NOW() < DATEADD($DATE_COL$,10,'day'), "true", "false")` | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than $DATE_COL$ plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | ### Logical Operators | Operator | Sample | Description | | -------- | -------------------- | ------------------------ | -| `<` | `column1 < column2` | Less than | -| `>` | `column1 > column2` | Greater than | -| `<=` | `column1 <= column2` | Less than or equal to | -| `>=` | `column1 >= column2` | Greater than or equal to | -| `==` | `column1 == column2` | Equal to | -| `!=` | `column1 != column2` | Not equal to | +| `<` | `$Column1$ < $Column2$` | Less than | +| `>` | `$Column1$ > $Column2$` | Greater than | +| `<=` | `$Column1$ <= $Column2$` | Less than or equal to | +| `>=` | `$Column1$ >= $Column2$` | Greater than or equal to | +| `==` | `$Column1$ == $Column2$` | Equal to | +| `!=` | `$Column1$ != $Column2$` | Not equal to | ### Conditional Expressions | Name | Syntax | Sample | Output | |------------|------------------------------------------------|---------------------------------------------|-------------------------------------------------------------| -| **IF** | `IF(expr, successCase, [failCase])` | `IF(Column > 1, Value1, Value2)` | successCase if `expr` evaluates to TRUE, elseCase otherwise | -| **SWITCH** | `SWITCH(expr, [pattern, value, ..., default])` | `SWITCH(Column1, 1, 'One', 2, 'Two', '--')` | Switch case value based on `expr` output | -| **AND** | `AND(expr1, [expr2,...])` | `AND(Column > 2, Column < 10)` | TRUE if all `expr` evaluate to TRUE | -| **OR** | `OR(expr1, [expr2,...])` | `OR(Column > 2, Column < 10)` | TRUE if at least one `expr` evaluates to TRUE | +| **IF** | `IF(expr, successCase, [failCase])` | `IF($Column$ > 1, Value1, Value2)` | successCase if `expr` evaluates to TRUE, elseCase otherwise | +| **SWITCH** | `SWITCH(expr, [pattern, value, ..., default])` | `SWITCH($Column$, 1, 'One', 2, 'Two', '--')` | Switch case value based on `expr` output | +| **AND** | `AND(expr1, [expr2,...])` | `AND($Column$ > 2, $Column$ < 10)` | TRUE if all `expr` evaluate to TRUE | +| **OR** | `OR(expr1, [expr2,...])` | `OR($Column$ > 2, $Column$ < 10)` | TRUE if at least one `expr` evaluates to TRUE | Logical operators, along with Numerical operators can be used to build conditional `expressions`. Examples: -```bash -IF(marksSecured > 80, "GradeA", "GradeB") +``` +IF($marksSecured$ > 80, "GradeA", "GradeB") ``` -```bash -SWITCH(quarterNumber, +``` +SWITCH($quarterNumber$, 1, 'Jan-Mar', 2, 'Apr-Jun', 3, 'Jul-Sep', 4, 'Oct-Dec', 'INVALID' ) -``` - +``` \ No newline at end of file From 7745c8cc4f2d692a78d0f2e9b84a6ec30bac0ce4 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Fri, 6 May 2022 12:06:36 +0800 Subject: [PATCH 04/21] cypress: wrap column name by $ --- scripts/cypress/integration/common/3b_formula_column.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/cypress/integration/common/3b_formula_column.js b/scripts/cypress/integration/common/3b_formula_column.js index ae7354cdc1..b292fb33c0 100644 --- a/scripts/cypress/integration/common/3b_formula_column.js +++ b/scripts/cypress/integration/common/3b_formula_column.js @@ -189,7 +189,7 @@ export const genTest = (apiType, dbType) => { it("Formula: ADD, AVG, LEN", () => { addFormulaBasedColumn( "NC_MATH_0", - "ADD(CityId, CountryId) + AVG(CityId, CountryId) + LEN(City)" + "ADD($CityId$, $CountryId$) + AVG($CityId$, $CountryId$) + LEN($City$)" ); rowValidation("NC_MATH_0", RESULT_MATH_0); }); @@ -198,7 +198,7 @@ export const genTest = (apiType, dbType) => { editColumnByName( "NC_MATH_0", "NC_STR_1", - `CONCAT(UPPER(City), LOWER(City), TRIM(' trimmed '))` + `CONCAT(UPPER($City$), LOWER($City$), TRIM(' trimmed '))` ); rowValidation("NC_STR_1", RESULT_STRING); }); @@ -207,7 +207,7 @@ export const genTest = (apiType, dbType) => { editColumnByName( "NC_STR_1", "NC_MATH_1", - `CEILING(1.4) + FLOOR(1.6) + ROUND(2.5) + MOD(CityId, 3) + MIN(CityId, CountryId) + MAX(CityId, CountryId)` + `CEILING(1.4) + FLOOR(1.6) + ROUND(2.5) + MOD($CityId$, 3) + MIN($CityId$, $CountryId$) + MAX($CityId$, $CountryId$)` ); rowValidation("NC_MATH_1", RESULT_MATH_1); }); @@ -218,7 +218,7 @@ export const genTest = (apiType, dbType) => { editColumnByName( "NC_MATH_1", "NC_MATH_2", - `LOG(CityId) + EXP(CityId) + POWER(CityId, 3) + SQRT(CountryId)` + `LOG($CityId$) + EXP($CityId$) + POWER($CityId$, 3) + SQRT($CountryId$)` ); rowValidation("NC_MATH_2", RESULT_MATH_2); } @@ -237,6 +237,7 @@ export const genTest = (apiType, dbType) => { * * @author Pranav C Balan * @author Raju Udava + * @author Wing-Kam Wong * * @license GNU AGPL version 3 or any later version * From 389c8f9a1dddfc6dd0174fd681666b8ec36b6aaf Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Fri, 6 May 2022 14:53:59 +0800 Subject: [PATCH 05/21] enhancement: formula option UI --- .../components/editColumn/formulaOptions.vue | 125 ++++++++++++------ 1 file changed, 87 insertions(+), 38 deletions(-) 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 d5af9bc103..97f2a7a8d3 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue @@ -1,35 +1,28 @@ @@ -87,7 +123,7 @@ export default { type: 'function' })), ...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({ - text: c.title, + text: '$' + c.title + '$', type: 'column', c })), @@ -266,4 +302,17 @@ export default { From c39c8f516fb1bacd9ed47226dd6d643c36feaa3a Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Fri, 6 May 2022 18:31:32 +0800 Subject: [PATCH 06/21] refactor: put column after function & remove '$' from the list --- .../components/editColumn/formulaOptions.vue | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 97f2a7a8d3..390e07563a 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue @@ -112,7 +112,12 @@ export default { suggestion: null, wordToComplete: '', selected: 0, - tooltip: true + tooltip: true, + sortOrder: { + column: 0, + function: 1, + op: 2 + } }), computed: { suggestionsList() { @@ -123,7 +128,7 @@ export default { type: 'function' })), ...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({ - text: '$' + c.title + '$', + text: c.title, type: 'column', c })), @@ -251,6 +256,8 @@ export default { 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)) } else { this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len)) } @@ -267,7 +274,7 @@ export default { const query = getWordUntilCaret(this.$refs.input.$el.querySelector('input')) const parts = query.split(/\W+/) this.wordToComplete = parts.pop() - this.suggestion = this.acTree.complete(this.wordToComplete) + this.suggestion = this.acTree.complete(this.wordToComplete)?.sort((x, y) => this.sortOrder[x.type] - this.sortOrder[y.type]) this.autocomplete = !!this.suggestion.length }, selectText() { From 27f3cc7df9d598cdf038f3f4393b88f6b6ee96a8 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Fri, 6 May 2022 19:42:16 +0800 Subject: [PATCH 07/21] fix: function & autocomplete behaviour --- .../components/editColumn/formulaOptions.vue | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 390e07563a..2881395470 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue @@ -19,7 +19,7 @@ Hint: Use $ to reference columns, e.g: $column_name$. For more, please check out Formulas. - + Suggestions @@ -41,7 +41,7 @@ - {{ it.text }}(...) + {{ it.text }} @@ -124,7 +124,7 @@ export default { const unsupportedFnList = this.sqlUi.getUnsupportedFnList() return [ ...this.availableFunctions.filter(fn => !unsupportedFnList.includes(fn)).map(fn => ({ - text: fn, + text: fn + '()', type: 'function' })), ...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({ @@ -255,12 +255,13 @@ export default { 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)) + 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)) } else { this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len)) } + this.autocomplete = false }, _handleInputDeb: debounce(async function(self) { await self.handleInput() @@ -278,9 +279,10 @@ export default { this.autocomplete = !!this.suggestion.length }, selectText() { - if (this.selected > -1 && this.selected < this.suggestion.length) { + if (this.suggestion && this.selected > -1 && this.selected < this.suggestion.length) { this.appendText(this.suggestion[this.selected]) this.autocomplete = false + this.suggestion = null } }, suggestionListDown() { From fa6d6cbf2a55dd4244eca45221d7a799278fb509 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Fri, 6 May 2022 20:00:28 +0800 Subject: [PATCH 08/21] chore: close suggestion after selection --- .../spreadsheet/components/editColumn/formulaOptions.vue | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 2881395470..e1708d9851 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue @@ -262,6 +262,7 @@ export default { this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len)) } this.autocomplete = false + this.suggestion = null }, _handleInputDeb: debounce(async function(self) { await self.handleInput() @@ -281,8 +282,6 @@ export default { selectText() { if (this.suggestion && this.selected > -1 && this.selected < this.suggestion.length) { this.appendText(this.suggestion[this.selected]) - this.autocomplete = false - this.suggestion = null } }, suggestionListDown() { From a5f77a50f3a273aa5aaf3f59dda25026f9748997 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Mon, 16 May 2022 19:23:06 +0800 Subject: [PATCH 09/21] feat: introduce formula curly hook --- .../components/editColumn/FormulaOptions.vue | 13 ++- packages/nc-gui/helpers/formulaCurlyHook.js | 24 +++++ packages/nc-gui/package-lock.json | 18 ++-- packages/nc-gui/package.json | 2 +- packages/nocodb-sdk/package-lock.json | 14 +-- packages/nocodb-sdk/package.json | 2 +- packages/nocodb-sdk/src/lib/formulaHelpers.ts | 33 ++++++- packages/nocodb/package-lock.json | 18 ++-- packages/nocodb/package.json | 2 +- .../lib/noco/meta/helpers/formulaHelpers.ts | 94 ++++++++++++------- 10 files changed, 147 insertions(+), 73 deletions(-) create mode 100644 packages/nc-gui/helpers/formulaCurlyHook.js 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 e1708d9851..a543a0375c 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -16,7 +16,7 @@ @keydown.enter.prevent="selectText" />
- Hint: Use $ to reference columns, e.g: $column_name$. For more, please check out + Hint: Use {} to reference columns, e.g: {column_name}. For more, please check out Formulas.
@@ -97,6 +97,7 @@ import debounce from 'debounce' import jsep from 'jsep' import { UITypes } from 'nocodb-sdk' import formulaList, { validations } from '../../../../../helpers/formulaList' +import curly from '../../../../../helpers/formulaCurlyHook' import { getWordUntilCaret, insertAtCursor } from '@/helpers' import NcAutocompleteTree from '@/helpers/NcAutocompleteTree' @@ -161,6 +162,7 @@ export default { }, created() { this.formula = { value: this.value || '' } + jsep.plugins.register(curly) }, methods: { async save() { @@ -219,6 +221,7 @@ export default { } }, validateAgainstMeta(pt, arr = []) { + console.log('pt.type= ' + pt.type) if (pt.type === 'CallExpression') { if (!this.availableFunctions.includes(pt.callee.name)) { arr.push(`'${pt.callee.name}' function is not available`) @@ -235,11 +238,7 @@ export default { } pt.arguments.map(arg => this.validateAgainstMeta(arg, arr)) } else if (pt.type === 'Identifier') { - // $column_name$ -> column_name - const sanitizedPtName = pt.name.substring(1, pt.name.length - 1) - if (this.meta.columns.filter(c => !this.column || this.column.id !== c.id).find(c => c.title === pt.name)) { - arr.push(`Column '${pt.name}' needs to be wrapped by $. Try $${pt.name}$ instead`) - } else if (this.meta.columns.filter(c => !this.column || this.column.id !== c.id).every(c => c.title !== sanitizedPtName)) { + if (this.meta.columns.filter(c => !this.column || this.column.id !== c.id).every(c => c.title !== pt.name)) { arr.push(`Column '${pt.name}' is not available`) } } else if (pt.type === 'BinaryExpression') { @@ -257,7 +256,7 @@ export default { 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)) } else { this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len)) } diff --git a/packages/nc-gui/helpers/formulaCurlyHook.js b/packages/nc-gui/helpers/formulaCurlyHook.js new file mode 100644 index 0000000000..3661b088f6 --- /dev/null +++ b/packages/nc-gui/helpers/formulaCurlyHook.js @@ -0,0 +1,24 @@ +const OCURLY_CODE = 123; // { +const CCURLY_CODE = 125; // } + +export default { + name: 'curly', + init(jsep) { + jsep.hooks.add('gobble-token', function gobbleCurlyLiteral(env) { + const { context } = env + if (!jsep.isIdentifierStart(context.code) && context.code === OCURLY_CODE) { + context.index += 1 + let nodes = context.gobbleExpressions(CCURLY_CODE) + if (context.code === CCURLY_CODE) { + context.index += 1 + if (nodes.length > 0) { + env.node = nodes[0] + } + return env.node + } else { + context.throwError('Unclosed }') + } + } + }); + } +} \ No newline at end of file diff --git a/packages/nc-gui/package-lock.json b/packages/nc-gui/package-lock.json index 31f19b9f86..66b60bcf64 100644 --- a/packages/nc-gui/package-lock.json +++ b/packages/nc-gui/package-lock.json @@ -22,7 +22,7 @@ "fix-path": "^3.0.0", "httpsnippet": "^2.0.0", "inflection": "^1.12.0", - "jsep": "^0.4.0", + "jsep": "^1.3.6", "material-design-icons-iconfont": "^5.0.1", "monaco-editor": "^0.19.3", "monaco-themes": "^0.2.5", @@ -76,7 +76,7 @@ "license": "MIT", "dependencies": { "axios": "^0.21.1", - "jsep": "^0.4.0" + "jsep": "^1.3.6" }, "devDependencies": { "@ava/typescript": "^1.1.1", @@ -9952,9 +9952,9 @@ } }, "node_modules/jsep": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.4.0.tgz", - "integrity": "sha512-UDkrzhJK8hmgXeGK8WIiecc/cuW4Vnx5nnrRma7yaxK0WXlvZ4VerGrcxPzifd/CA6QdcI1hpXqr22tHKXpcQA==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.6.tgz", + "integrity": "sha512-o7fP1eZVROIChADx7HKiwGRVI0tUqgUUGhaok6DP7cMxpDeparuooREDBDeNk2G5KIB49MBSkRYsCOu4PmZ+1w==", "engines": { "node": ">= 10.16.0" } @@ -24714,9 +24714,9 @@ } }, "jsep": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.4.0.tgz", - "integrity": "sha512-UDkrzhJK8hmgXeGK8WIiecc/cuW4Vnx5nnrRma7yaxK0WXlvZ4VerGrcxPzifd/CA6QdcI1hpXqr22tHKXpcQA==" + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.6.tgz", + "integrity": "sha512-o7fP1eZVROIChADx7HKiwGRVI0tUqgUUGhaok6DP7cMxpDeparuooREDBDeNk2G5KIB49MBSkRYsCOu4PmZ+1w==" }, "jsesc": { "version": "2.5.2", @@ -25389,7 +25389,7 @@ "eslint-plugin-import": "^2.22.0", "eslint-plugin-prettier": "^4.0.0", "gh-pages": "^3.1.0", - "jsep": "^0.4.0", + "jsep": "^1.3.6", "npm-run-all": "^4.1.5", "nyc": "^15.1.0", "open-cli": "^6.0.1", diff --git a/packages/nc-gui/package.json b/packages/nc-gui/package.json index 9305f5b655..057b4ded37 100644 --- a/packages/nc-gui/package.json +++ b/packages/nc-gui/package.json @@ -25,7 +25,7 @@ "fix-path": "^3.0.0", "httpsnippet": "^2.0.0", "inflection": "^1.12.0", - "jsep": "^0.4.0", + "jsep": "^1.3.6", "material-design-icons-iconfont": "^5.0.1", "monaco-editor": "^0.19.3", "monaco-themes": "^0.2.5", diff --git a/packages/nocodb-sdk/package-lock.json b/packages/nocodb-sdk/package-lock.json index 2d1bb01701..1b51d927c4 100644 --- a/packages/nocodb-sdk/package-lock.json +++ b/packages/nocodb-sdk/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "axios": "^0.21.1", - "jsep": "^0.4.0" + "jsep": "^1.3.6" }, "devDependencies": { "@ava/typescript": "^1.1.1", @@ -6764,9 +6764,9 @@ } }, "node_modules/jsep": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.4.0.tgz", - "integrity": "sha512-UDkrzhJK8hmgXeGK8WIiecc/cuW4Vnx5nnrRma7yaxK0WXlvZ4VerGrcxPzifd/CA6QdcI1hpXqr22tHKXpcQA==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.6.tgz", + "integrity": "sha512-o7fP1eZVROIChADx7HKiwGRVI0tUqgUUGhaok6DP7cMxpDeparuooREDBDeNk2G5KIB49MBSkRYsCOu4PmZ+1w==", "engines": { "node": ">= 10.16.0" } @@ -15535,9 +15535,9 @@ } }, "jsep": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.4.0.tgz", - "integrity": "sha512-UDkrzhJK8hmgXeGK8WIiecc/cuW4Vnx5nnrRma7yaxK0WXlvZ4VerGrcxPzifd/CA6QdcI1hpXqr22tHKXpcQA==" + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.6.tgz", + "integrity": "sha512-o7fP1eZVROIChADx7HKiwGRVI0tUqgUUGhaok6DP7cMxpDeparuooREDBDeNk2G5KIB49MBSkRYsCOu4PmZ+1w==" }, "jsesc": { "version": "2.5.2", diff --git a/packages/nocodb-sdk/package.json b/packages/nocodb-sdk/package.json index 96a2dbeace..985cfa4649 100644 --- a/packages/nocodb-sdk/package.json +++ b/packages/nocodb-sdk/package.json @@ -44,7 +44,7 @@ }, "dependencies": { "axios": "^0.21.1", - "jsep": "^0.4.0" + "jsep": "^1.3.6" }, "devDependencies": { "@ava/typescript": "^1.1.1", diff --git a/packages/nocodb-sdk/src/lib/formulaHelpers.ts b/packages/nocodb-sdk/src/lib/formulaHelpers.ts index a1f97b20a3..1472f4fa40 100644 --- a/packages/nocodb-sdk/src/lib/formulaHelpers.ts +++ b/packages/nocodb-sdk/src/lib/formulaHelpers.ts @@ -1,6 +1,7 @@ import jsep from 'jsep'; import { ColumnType } from './Api'; + export function substituteColumnIdWithAliasInFormula( formula, columns: ColumnType[], @@ -23,15 +24,39 @@ export function substituteColumnIdWithAliasInFormula( c.title === colNameOrId ); pt.name = column?.title || ptRaw?.name || pt?.name; - if (pt.name[0] != '$' && pt.name[pt.name.length - 1] != '$') { - pt.name = '$' + pt.name + '$'; - } } else if (pt.type === 'BinaryExpression') { substituteId(pt.left, ptRaw?.left); substituteId(pt.right, ptRaw?.right); } }; + // register curly hook + jsep.plugins.register({ + name: 'curly', + init(jsep) { + jsep.hooks.add('gobble-token', function gobbleCurlyLiteral(env) { + const OCURLY_CODE = 123; // { + const CCURLY_CODE = 125; // } + const { context } = env; + if ( + !jsep.isIdentifierStart(context.code) && + context.code === OCURLY_CODE + ) { + context.index += 1; + const nodes = context.gobbleExpressions(CCURLY_CODE); + if (context.code === CCURLY_CODE) { + context.index += 1; + if (nodes.length > 0) { + env.node = nodes[0]; + } + return env.node; + } else { + context.throwError('Unclosed }'); + } + } + }); + }, + } as jsep.IPlugin); const parsedFormula = jsep(formula); const parsedRawFormula = rawFormula && jsep(rawFormula); substituteId(parsedFormula, parsedRawFormula); @@ -65,7 +90,7 @@ export function jsepTreeToFormula(node) { } if (node.type === 'Identifier') { - return node.name; + return '{' + node.name + '}'; } if (node.type === 'Literal') { diff --git a/packages/nocodb/package-lock.json b/packages/nocodb/package-lock.json index 9b7f90dce5..4425d71f66 100644 --- a/packages/nocodb/package-lock.json +++ b/packages/nocodb/package-lock.json @@ -52,7 +52,7 @@ "ioredis-mock": "^7.1.0", "is-docker": "^2.2.1", "js-beautify": "^1.11.0", - "jsep": "^0.4.0", + "jsep": "^1.3.6", "json2csv": "^5.0.6", "jsonfile": "^6.1.0", "jsonwebtoken": "^8.5.1", @@ -160,7 +160,7 @@ "license": "MIT", "dependencies": { "axios": "^0.21.1", - "jsep": "^0.4.0" + "jsep": "^1.3.6" }, "devDependencies": { "@ava/typescript": "^1.1.1", @@ -13834,9 +13834,9 @@ "optional": true }, "node_modules/jsep": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.4.0.tgz", - "integrity": "sha512-UDkrzhJK8hmgXeGK8WIiecc/cuW4Vnx5nnrRma7yaxK0WXlvZ4VerGrcxPzifd/CA6QdcI1hpXqr22tHKXpcQA==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.6.tgz", + "integrity": "sha512-o7fP1eZVROIChADx7HKiwGRVI0tUqgUUGhaok6DP7cMxpDeparuooREDBDeNk2G5KIB49MBSkRYsCOu4PmZ+1w==", "engines": { "node": ">= 10.16.0" } @@ -35846,9 +35846,9 @@ "optional": true }, "jsep": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.4.0.tgz", - "integrity": "sha512-UDkrzhJK8hmgXeGK8WIiecc/cuW4Vnx5nnrRma7yaxK0WXlvZ4VerGrcxPzifd/CA6QdcI1hpXqr22tHKXpcQA==" + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.6.tgz", + "integrity": "sha512-o7fP1eZVROIChADx7HKiwGRVI0tUqgUUGhaok6DP7cMxpDeparuooREDBDeNk2G5KIB49MBSkRYsCOu4PmZ+1w==" }, "jsesc": { "version": "2.5.2", @@ -37849,7 +37849,7 @@ "eslint-plugin-import": "^2.22.0", "eslint-plugin-prettier": "^4.0.0", "gh-pages": "^3.1.0", - "jsep": "^0.4.0", + "jsep": "^1.3.6", "npm-run-all": "^4.1.5", "nyc": "^15.1.0", "open-cli": "^6.0.1", diff --git a/packages/nocodb/package.json b/packages/nocodb/package.json index cc2cae2723..67778b1c7d 100644 --- a/packages/nocodb/package.json +++ b/packages/nocodb/package.json @@ -134,7 +134,7 @@ "ioredis-mock": "^7.1.0", "is-docker": "^2.2.1", "js-beautify": "^1.11.0", - "jsep": "^0.4.0", + "jsep": "^1.3.6", "json2csv": "^5.0.6", "jsonfile": "^6.1.0", "jsonwebtoken": "^8.5.1", diff --git a/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts b/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts index f49927f09c..b87441f351 100644 --- a/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts +++ b/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts @@ -14,53 +14,79 @@ export async function substituteColumnAliasWithIdInFormula( } else if (pt.type === 'Literal') { return; } else if (pt.type === 'Identifier') { - const sanitizedPtName = pt.name.substring(1, pt.name.length - 1); - const colNameOrId = sanitizedPtName; + const colNameOrId = pt.name; const column = columns.find( c => c.id === colNameOrId || c.column_name === colNameOrId || c.title === colNameOrId ); - pt.name = column.id; + pt.name = '{' + column.id + '}'; } else if (pt.type === 'BinaryExpression') { await substituteId(pt.left); await substituteId(pt.right); } }; - + // register curly hook + jsep.plugins.register({ + name: 'curly', + init(jsep) { + jsep.hooks.add('gobble-token', function gobbleCurlyLiteral(env) { + const OCURLY_CODE = 123; // { + const CCURLY_CODE = 125; // } + const { context } = env; + if ( + !jsep.isIdentifierStart(context.code) && + context.code === OCURLY_CODE + ) { + context.index += 1; + const nodes = context.gobbleExpressions(CCURLY_CODE); + if (context.code === CCURLY_CODE) { + context.index += 1; + if (nodes.length > 0) { + env.node = nodes[0]; + } + return env.node; + } else { + context.throwError('Unclosed }'); + } + } + }); + } + } as jsep.IPlugin); const parsedFormula = jsep(formula); await substituteId(parsedFormula); return jsepTreeToFormula(parsedFormula); } -export function substituteColumnIdWithAliasInFormula( - formula, - columns: Column[] -) { - const substituteId = (pt: any) => { - if (pt.type === 'CallExpression') { - for (const arg of pt.arguments || []) { - substituteId(arg); - } - } else if (pt.type === 'Literal') { - return; - } else if (pt.type === 'Identifier') { - const colNameOrId = pt.name; - const column = columns.find( - c => - c.id === colNameOrId || - c.column_name === colNameOrId || - c.title === colNameOrId - ); - pt.name = column.id; - } else if (pt.type === 'BinaryExpression') { - substituteId(pt.left); - substituteId(pt.right); - } - }; - - const parsedFormula = jsep(formula); - substituteId(parsedFormula); - return jsepTreeToFormula(parsedFormula); -} +// not in use +// export function substituteColumnIdWithAliasInFormula( +// formula, +// columns: Column[] +// ) { +// const substituteId = (pt: any) => { +// if (pt.type === 'CallExpression') { +// for (const arg of pt.arguments || []) { +// substituteId(arg); +// } +// } else if (pt.type === 'Literal') { +// return; +// } else if (pt.type === 'Identifier') { +// const colNameOrId = pt.name; +// const column = columns.find( +// c => +// c.id === colNameOrId || +// c.column_name === colNameOrId || +// c.title === colNameOrId +// ); +// pt.name = column.id; +// } else if (pt.type === 'BinaryExpression') { +// substituteId(pt.left); +// substituteId(pt.right); +// } +// }; +// +// const parsedFormula = jsep(formula); +// substituteId(parsedFormula); +// return jsepTreeToFormula(parsedFormula); +// } From 116a064c6c4dce6d2427f9c45813062dc86fe112 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Mon, 16 May 2022 19:25:15 +0800 Subject: [PATCH 10/21] cypress: wrap column name by {} --- scripts/cypress/integration/common/3b_formula_column.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/cypress/integration/common/3b_formula_column.js b/scripts/cypress/integration/common/3b_formula_column.js index b292fb33c0..d3c9ef69c6 100644 --- a/scripts/cypress/integration/common/3b_formula_column.js +++ b/scripts/cypress/integration/common/3b_formula_column.js @@ -189,7 +189,7 @@ export const genTest = (apiType, dbType) => { it("Formula: ADD, AVG, LEN", () => { addFormulaBasedColumn( "NC_MATH_0", - "ADD($CityId$, $CountryId$) + AVG($CityId$, $CountryId$) + LEN($City$)" + "ADD({CityId}, {CountryId}) + AVG({CityId}, {CountryId}) + LEN({City})" ); rowValidation("NC_MATH_0", RESULT_MATH_0); }); @@ -198,7 +198,7 @@ export const genTest = (apiType, dbType) => { editColumnByName( "NC_MATH_0", "NC_STR_1", - `CONCAT(UPPER($City$), LOWER($City$), TRIM(' trimmed '))` + `CONCAT(UPPER({City}), LOWER({City}), TRIM(' trimmed '))` ); rowValidation("NC_STR_1", RESULT_STRING); }); @@ -207,7 +207,7 @@ export const genTest = (apiType, dbType) => { editColumnByName( "NC_STR_1", "NC_MATH_1", - `CEILING(1.4) + FLOOR(1.6) + ROUND(2.5) + MOD($CityId$, 3) + MIN($CityId$, $CountryId$) + MAX($CityId$, $CountryId$)` + `CEILING(1.4) + FLOOR(1.6) + ROUND(2.5) + MOD({CityId}, 3) + MIN({CityId}, {CountryId}) + MAX({CityId}, {CountryId})` ); rowValidation("NC_MATH_1", RESULT_MATH_1); }); @@ -218,7 +218,7 @@ export const genTest = (apiType, dbType) => { editColumnByName( "NC_MATH_1", "NC_MATH_2", - `LOG($CityId$) + EXP($CityId$) + POWER($CityId$, 3) + SQRT($CountryId$)` + `LOG({CityId}) + EXP({CityId}) + POWER({CityId}, 3) + SQRT({CountryId})` ); rowValidation("NC_MATH_2", RESULT_MATH_2); } From 11c8b5b737d365443a12da423a4fb60339cf8ff3 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Mon, 16 May 2022 20:00:09 +0800 Subject: [PATCH 11/21] fix: wrap non-functional identifiers --- packages/nocodb-sdk/src/lib/formulaHelpers.ts | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/nocodb-sdk/src/lib/formulaHelpers.ts b/packages/nocodb-sdk/src/lib/formulaHelpers.ts index 1472f4fa40..e534325fa3 100644 --- a/packages/nocodb-sdk/src/lib/formulaHelpers.ts +++ b/packages/nocodb-sdk/src/lib/formulaHelpers.ts @@ -90,7 +90,44 @@ export function jsepTreeToFormula(node) { } if (node.type === 'Identifier') { - return '{' + node.name + '}'; + const formulas = [ + 'AVG', + 'ADD', + 'DATEADD', + 'AND', + 'OR', + 'CONCAT', + 'TRIM', + 'UPPER', + 'LOWER', + 'LEN', + 'MIN', + 'MAX', + 'CEILING', + 'FLOOR', + 'ROUND', + 'MOD', + 'REPEAT', + 'LOG', + 'EXP', + 'POWER', + 'SQRT', + 'SQRT', + 'ABS', + 'NOW', + 'REPLACE', + 'SEARCH', + 'INT', + 'RIGHT', + 'LEFT', + 'SUBSTR', + 'MID', + 'IF', + 'SWITCH', + 'URL', + ]; + if (!formulas.includes(node.name)) return '{' + node.name + '}'; + return node.name; } if (node.type === 'Literal') { From 2fc29b4d815b4e7f829ba0e8f16e24e4cf5f0a5c Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Mon, 16 May 2022 20:00:28 +0800 Subject: [PATCH 12/21] docs: replace $column_name$ with {column_name} --- .../content/en/setup-and-usages/formulas.md | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/packages/noco-docs/content/en/setup-and-usages/formulas.md b/packages/noco-docs/content/en/setup-and-usages/formulas.md index 7944427f5c..c667cfb27c 100644 --- a/packages/noco-docs/content/en/setup-and-usages/formulas.md +++ b/packages/noco-docs/content/en/setup-and-usages/formulas.md @@ -18,8 +18,8 @@ menuTitle: "Formulas" ### 4. Insert required formula -- You can reference column names in equation with `$`, e.g. `$column_name$`. - You can use explicit numerical values/ strings as needed, e.g. `123` (numeric) or `"123"` (string). +- You can reference column names in equation with `{}`, e.g. `{column_name}`, if the column name conflicts with literals - Table below lists supported formula & associated syntax - Nested formula (formula equation referring to another formula column) is supported @@ -31,94 +31,94 @@ menuTitle: "Formulas" | Name | Syntax | Sample | Output | |-------------|----------------------------|----------------------------------|------------------------------------------------------------------| -| **ABS** | `ABS(value)` | `ABS($Column$)` | Absolute value of the input parameter | -| **ADD** | `ADD(value1,[value2,...])` | `ADD($Column1$, $Column2$)` | Sum of input parameters | -| **AVG** | `AVG(value1,[value2,...])` | `AVG($Column1$, $Column2$)` | Average of input parameters | -| **CEILING** | `CEILING(value)` | `CEILING($Column$)` | Rounded next largest integer value of input parameter | -| **EXP** | `EXP(value)` | `EXP($Column$)` | Exponential value of input parameter (`e^x`) | -| **FLOOR** | `FLOOR(value)` | `FLOOR($Column$)` | Rounded largest integer less than or equal to input parameter | -| **INT** | `INT(value)` | `INT($Column$)` | Integer value of input parameter | -| **LOG** | `LOG([base], value)` | `LOG(10, $Column$)` | Logarithm of input parameter to the base (default = e) specified | -| **MAX** | `MAX(value1,[value2,...])` | `MAX($Column1$, $Column2$, $Column3$)` | Maximum value amongst input parameters | -| **MIN** | `MIN(value1,[value2,...])` | `MIN($Column1$, $Column2$, $Column3$)` | Minimum value amongst input parameters | -| **MOD** | `MOD(value1, value2)` | `MOD($Column$, 2)` | Remainder after integer division of input parameters | -| **POWER** | `POWER(base, exponent)` | `POWER($Column$, 3)` | `base` to the `exponent` power, as in `base ^ exponent` | -| **ROUND** | `ROUND(value)` | `ROUND($Column$)` | Nearest integer to the input parameter | -| **SQRT** | `SQRT(value)` | `SQRT($Column$)` | Square root of the input parameter | +| **ABS** | `ABS(value)` | `ABS({Column})` | Absolute value of the input parameter | +| **ADD** | `ADD(value1,[value2,...])` | `ADD({Column1}, {Column2})` | Sum of input parameters | +| **AVG** | `AVG(value1,[value2,...])` | `AVG({Column1}, {Column2})` | Average of input parameters | +| **CEILING** | `CEILING(value)` | `CEILING({Column})` | Rounded next largest integer value of input parameter | +| **EXP** | `EXP(value)` | `EXP({Column})` | Exponential value of input parameter (`e^x`) | +| **FLOOR** | `FLOOR(value)` | `FLOOR({Column})` | Rounded largest integer less than or equal to input parameter | +| **INT** | `INT(value)` | `INT({Column})` | Integer value of input parameter | +| **LOG** | `LOG([base], value)` | `LOG(10, {Column})` | Logarithm of input parameter to the base (default = e) specified | +| **MAX** | `MAX(value1,[value2,...])` | `MAX({Column1}, {Column2}, {Column3})` | Maximum value amongst input parameters | +| **MIN** | `MIN(value1,[value2,...])` | `MIN({Column1}, {Column2}, {Column3})` | Minimum value amongst input parameters | +| **MOD** | `MOD(value1, value2)` | `MOD({Column}, 2)` | Remainder after integer division of input parameters | +| **POWER** | `POWER(base, exponent)` | `POWER({Column}, 3)` | `base` to the `exponent` power, as in `base ^ exponent` | +| **ROUND** | `ROUND(value)` | `ROUND({Column})` | Nearest integer to the input parameter | +| **SQRT** | `SQRT(value)` | `SQRT({Column})` | Square root of the input parameter | ### Numeric Operators | Operator | Sample | Description | | -------- | ----------------------- | -------------------------------- | -| `+` | `$Column1$ + $Column2$ + 2` | Addition of numeric values | -| `-` | `$Column1$ - $Column2$` | Subtraction of numeric values | -| `*` | `$Column1$ * $Column2$` | Multiplication of numeric values | -| `/` | `$Column1$ / $Column2$` | Division of numeric values | +| `+` | `{Column1} + {Column2} + 2` | Addition of numeric values | +| `-` | `{Column1} - {Column2}` | Subtraction of numeric values | +| `*` | `{Column1} * {Column2}` | Multiplication of numeric values | +| `/` | `{Column1} / {Column2}` | Division of numeric values | Tip :To change the order of arithmetic operation, you can use round bracket parantheses ().
-Example: ($Column1$ + ($Column2$ * $Column3$) / (3 - $Column4$ )) +Example: ({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ ))
### String Functions | Name | Syntax | Sample | Output | |-------------|----------------------------------|---------------------------------|---------------------------------------------------------------------------| -| **CONCAT** | `CONCAT(str1, [str2,...])` | `CONCAT($Column1$, ' ', $Column2$)` | Concatenated string of input parameters | -| **LEFT** | `LEFT(str1, [str2,...])` | `LEFT($Column$, 3)` | `n` characters from the beginning of input parameter | -| **LEN** | `LEN(str)` | `LEN($Column$)` | Input parameter character length | -| **LOWER** | `LOWER(str)` | `LOWER($Column$)` | Lower case converted string of input parameter | -| **MID** | `SUBTR(str, position, [count])` | `MID($Column$, 3, 2)` | Alias for `SUBSTR` | -| **REPEAT** | `REPEAT(str, count)` | `REPEAT($Column$, 2)` | Specified copies of the input parameter string concatenated together | -| **REPLACE** | `REPLACE(str, srchStr, rplcStr)` | `REPLACE($Column$, 'int', 'num')` | String, after replacing all occurrences of `srchStr` with `rplcStr` | -| **RIGHT** | `RIGHT(str, count)` | `RIGHT($Column$, 3)` | `n` characters from the end of input parameter | -| **SEARCH** | `SEARCH(str, srchStr)` | `SEARCH($Column$, 'str')` | Index of `srchStr` specified if found, 0 otherwise | -| **SUBSTR** | `SUBTR(str, position, [count])` | `SUBSTR($Column$, 3, 2)` | Substring of length 'count' of input string, from the postition specified | -| **TRIM** | `TRIM(str)` | `TRIM($Column$)` | Remove trailing and leading whitespaces from input parameter | -| **UPPER** | `UPPER(str)` | `UPPER($Column$)` | Upper case converted string of input parameter | -| **URL** | `URL(str)` | `URL($Column$)` | Convert to a hyperlink if it is a valid URL | +| **CONCAT** | `CONCAT(str1, [str2,...])` | `CONCAT({Column1}, ' ', {Column2})` | Concatenated string of input parameters | +| **LEFT** | `LEFT(str1, [str2,...])` | `LEFT({Column}, 3)` | `n` characters from the beginning of input parameter | +| **LEN** | `LEN(str)` | `LEN({Column})` | Input parameter character length | +| **LOWER** | `LOWER(str)` | `LOWER({Column})` | Lower case converted string of input parameter | +| **MID** | `SUBTR(str, position, [count])` | `MID({Column}, 3, 2)` | Alias for `SUBSTR` | +| **REPEAT** | `REPEAT(str, count)` | `REPEAT({Column}, 2)` | Specified copies of the input parameter string concatenated together | +| **REPLACE** | `REPLACE(str, srchStr, rplcStr)` | `REPLACE({Column}, 'int', 'num')` | String, after replacing all occurrences of `srchStr` with `rplcStr` | +| **RIGHT** | `RIGHT(str, count)` | `RIGHT({Column}, 3)` | `n` characters from the end of input parameter | +| **SEARCH** | `SEARCH(str, srchStr)` | `SEARCH({Column}, 'str')` | Index of `srchStr` specified if found, 0 otherwise | +| **SUBSTR** | `SUBTR(str, position, [count])` | `SUBSTR({Column}, 3, 2)` | Substring of length 'count' of input string, from the postition specified | +| **TRIM** | `TRIM(str)` | `TRIM({Column})` | Remove trailing and leading whitespaces from input parameter | +| **UPPER** | `UPPER(str)` | `UPPER({Column})` | Upper case converted string of input parameter | +| **URL** | `URL(str)` | `URL({Column})` | Convert to a hyperlink if it is a valid URL | ### Date Functions | Name | Syntax | Sample | Output | Remark | |---|---|---|---|---| -| **DATEADD** | `DATEADD($DATE_COL$, 1, 'day')` | `DATEADD(date, 1, 'day')` | Supposing $DATE_COL$ is 2022-03-14. The result is 2022-03-15. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` | -| | `DATEADD($DATE_COL$, 2, 'month')` | `DATEADD(date, 2, 'month')` | Supposing $DATE_COL$ is 2022-03-14 03:14. The result is 2022-05-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -2, 'month')` | -| | `IF(NOW() < $DATE_COL$, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than $DATE_COL$, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | -| | `IF(NOW() < DATEADD($DATE_COL$,10,'day'), "true", "false")` | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than $DATE_COL$ plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | +| **DATEADD** | `DATEADD({DATE_COL}, 1, 'day')` | `DATEADD(date, 1, 'day')` | Supposing {DATE_COL} is 2022-03-14. The result is 2022-03-15. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` | +| | `DATEADD({DATE_COL}, 2, 'month')` | `DATEADD(date, 2, 'month')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-05-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -2, 'month')` | +| | `IF(NOW() < {DATE_COL}, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than {DATE_COL}, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | +| | `IF(NOW() < DATEADD({DATE_COL},10,'day'), "true", "false")` | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | ### Logical Operators | Operator | Sample | Description | | -------- | -------------------- | ------------------------ | -| `<` | `$Column1$ < $Column2$` | Less than | -| `>` | `$Column1$ > $Column2$` | Greater than | -| `<=` | `$Column1$ <= $Column2$` | Less than or equal to | -| `>=` | `$Column1$ >= $Column2$` | Greater than or equal to | -| `==` | `$Column1$ == $Column2$` | Equal to | -| `!=` | `$Column1$ != $Column2$` | Not equal to | +| `<` | `{Column1} < {Column2}` | Less than | +| `>` | `{Column1} > {Column2}` | Greater than | +| `<=` | `{Column1} <= {Column2}` | Less than or equal to | +| `>=` | `{Column1} >= {Column2}` | Greater than or equal to | +| `==` | `{Column1} == {Column2}` | Equal to | +| `!=` | `{Column1} != {Column2}` | Not equal to | ### Conditional Expressions | Name | Syntax | Sample | Output | |------------|------------------------------------------------|---------------------------------------------|-------------------------------------------------------------| -| **IF** | `IF(expr, successCase, [failCase])` | `IF($Column$ > 1, Value1, Value2)` | successCase if `expr` evaluates to TRUE, elseCase otherwise | -| **SWITCH** | `SWITCH(expr, [pattern, value, ..., default])` | `SWITCH($Column$, 1, 'One', 2, 'Two', '--')` | Switch case value based on `expr` output | -| **AND** | `AND(expr1, [expr2,...])` | `AND($Column$ > 2, $Column$ < 10)` | TRUE if all `expr` evaluate to TRUE | -| **OR** | `OR(expr1, [expr2,...])` | `OR($Column$ > 2, $Column$ < 10)` | TRUE if at least one `expr` evaluates to TRUE | +| **IF** | `IF(expr, successCase, [failCase])` | `IF({Column} > 1, Value1, Value2)` | successCase if `expr` evaluates to TRUE, elseCase otherwise | +| **SWITCH** | `SWITCH(expr, [pattern, value, ..., default])` | `SWITCH({Column}, 1, 'One', 2, 'Two', '--')` | Switch case value based on `expr` output | +| **AND** | `AND(expr1, [expr2,...])` | `AND({Column} > 2, {Column} < 10)` | TRUE if all `expr` evaluate to TRUE | +| **OR** | `OR(expr1, [expr2,...])` | `OR({Column} > 2, {Column} < 10)` | TRUE if at least one `expr` evaluates to TRUE | Logical operators, along with Numerical operators can be used to build conditional `expressions`. Examples: ``` -IF($marksSecured$ > 80, "GradeA", "GradeB") +IF({marksSecured} > 80, "GradeA", "GradeB") ``` ``` -SWITCH($quarterNumber$, +SWITCH({quarterNumber}, 1, 'Jan-Mar', 2, 'Apr-Jun', 3, 'Jul-Sep', From 8b6b2fd3519d83b71ed38adaab9f693a40f331a4 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 17 May 2022 11:51:35 +0800 Subject: [PATCH 13/21] cypress: skip parsing special character sequences and type the text exactly as written --- scripts/cypress/integration/common/3b_formula_column.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/cypress/integration/common/3b_formula_column.js b/scripts/cypress/integration/common/3b_formula_column.js index d3c9ef69c6..9fba2ab366 100644 --- a/scripts/cypress/integration/common/3b_formula_column.js +++ b/scripts/cypress/integration/common/3b_formula_column.js @@ -61,7 +61,7 @@ export const genTest = (apiType, dbType) => { .contains("Formula") .parent() .click() - .type(formula) + .type(formula, { parseSpecialCharSequences: false }) .click(); // click on Save @@ -115,7 +115,7 @@ export const genTest = (apiType, dbType) => { .parent() .find("input") .clear() - .type(newFormula) + .type(newFormula, { parseSpecialCharSequences: false }) .click(); cy.get(".nc-col-create-or-edit-card") From a9fd82885907d8702484297d89ee3b8c41752f37 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 17 May 2022 12:21:43 +0800 Subject: [PATCH 14/21] refactor: remove substituteColumnIdWithAliasInFormula as now it is in sdk --- .../lib/noco/meta/helpers/formulaHelpers.ts | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts b/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts index b87441f351..8f114447b6 100644 --- a/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts +++ b/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts @@ -58,35 +58,3 @@ export async function substituteColumnAliasWithIdInFormula( await substituteId(parsedFormula); return jsepTreeToFormula(parsedFormula); } - -// not in use -// export function substituteColumnIdWithAliasInFormula( -// formula, -// columns: Column[] -// ) { -// const substituteId = (pt: any) => { -// if (pt.type === 'CallExpression') { -// for (const arg of pt.arguments || []) { -// substituteId(arg); -// } -// } else if (pt.type === 'Literal') { -// return; -// } else if (pt.type === 'Identifier') { -// const colNameOrId = pt.name; -// const column = columns.find( -// c => -// c.id === colNameOrId || -// c.column_name === colNameOrId || -// c.title === colNameOrId -// ); -// pt.name = column.id; -// } else if (pt.type === 'BinaryExpression') { -// substituteId(pt.left); -// substituteId(pt.right); -// } -// }; -// -// const parsedFormula = jsep(formula); -// substituteId(parsedFormula); -// return jsepTreeToFormula(parsedFormula); -// } From 7fd4d6caa3fff90ece01137b03a02104d3a5ebc0 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 17 May 2022 12:57:55 +0800 Subject: [PATCH 15/21] refactor: remove console.log --- .../project/spreadsheet/components/editColumn/FormulaOptions.vue | 1 - 1 file changed, 1 deletion(-) 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 a543a0375c..60aff9e658 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -221,7 +221,6 @@ export default { } }, validateAgainstMeta(pt, arr = []) { - console.log('pt.type= ' + pt.type) if (pt.type === 'CallExpression') { if (!this.availableFunctions.includes(pt.callee.name)) { arr.push(`'${pt.callee.name}' function is not available`) From 9d5bce4a6afe96349fbc4af25d754a0f968ea77e Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 17 May 2022 13:03:30 +0800 Subject: [PATCH 16/21] refactor: move formula helpers to sdk --- packages/nocodb-sdk/src/lib/formulaHelpers.ts | 88 +++++++++++++------ .../sql/formulav2/formulaQueryBuilderv2.ts | 10 ++- .../src/lib/noco/meta/api/columnApis.ts | 9 +- .../lib/noco/meta/helpers/formulaHelpers.ts | 60 ------------- .../jobs/ncProjectUpgraderV2_0090000.ts | 8 +- 5 files changed, 81 insertions(+), 94 deletions(-) delete mode 100644 packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts diff --git a/packages/nocodb-sdk/src/lib/formulaHelpers.ts b/packages/nocodb-sdk/src/lib/formulaHelpers.ts index e534325fa3..97407da52f 100644 --- a/packages/nocodb-sdk/src/lib/formulaHelpers.ts +++ b/packages/nocodb-sdk/src/lib/formulaHelpers.ts @@ -2,6 +2,65 @@ import jsep from 'jsep'; import { ColumnType } from './Api'; +export const jsepCurlyHook = { + name: 'curly', + init(jsep) { + jsep.hooks.add('gobble-token', function gobbleCurlyLiteral(env) { + const OCURLY_CODE = 123; // { + const CCURLY_CODE = 125; // } + const { context } = env; + if ( + !jsep.isIdentifierStart(context.code) && + context.code === OCURLY_CODE + ) { + context.index += 1; + const nodes = context.gobbleExpressions(CCURLY_CODE); + if (context.code === CCURLY_CODE) { + context.index += 1; + if (nodes.length > 0) { + env.node = nodes[0]; + } + return env.node; + } else { + context.throwError('Unclosed }'); + } + } + }); + }, +} as jsep.IPlugin; + +export async function substituteColumnAliasWithIdInFormula( + formula, + columns: ColumnType[] +) { + const substituteId = async (pt: any) => { + if (pt.type === 'CallExpression') { + for (const arg of pt.arguments || []) { + await substituteId(arg); + } + } else if (pt.type === 'Literal') { + return; + } else if (pt.type === 'Identifier') { + const colNameOrId = pt.name; + const column = columns.find( + (c) => + c.id === colNameOrId || + c.column_name === colNameOrId || + c.title === colNameOrId + ); + pt.name = '{' + column.id + '}'; + } else if (pt.type === 'BinaryExpression') { + await substituteId(pt.left); + await substituteId(pt.right); + } + }; + // register jsep curly hook + jsep.plugins.register(jsepCurlyHook); + const parsedFormula = jsep(formula); + await substituteId(parsedFormula); + return jsepTreeToFormula(parsedFormula); +} + export function substituteColumnIdWithAliasInFormula( formula, columns: ColumnType[], @@ -30,33 +89,8 @@ export function substituteColumnIdWithAliasInFormula( } }; - // register curly hook - jsep.plugins.register({ - name: 'curly', - init(jsep) { - jsep.hooks.add('gobble-token', function gobbleCurlyLiteral(env) { - const OCURLY_CODE = 123; // { - const CCURLY_CODE = 125; // } - const { context } = env; - if ( - !jsep.isIdentifierStart(context.code) && - context.code === OCURLY_CODE - ) { - context.index += 1; - const nodes = context.gobbleExpressions(CCURLY_CODE); - if (context.code === CCURLY_CODE) { - context.index += 1; - if (nodes.length > 0) { - env.node = nodes[0]; - } - return env.node; - } else { - context.throwError('Unclosed }'); - } - } - }); - }, - } as jsep.IPlugin); + // register jsep curly hook + jsep.plugins.register(jsepCurlyHook); const parsedFormula = jsep(formula); const parsedRawFormula = rawFormula && jsep(rawFormula); substituteId(parsedFormula, parsedRawFormula); 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 7bc9327ab5..3a10ea4bd6 100644 --- a/packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts +++ b/packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts @@ -7,7 +7,7 @@ import FormulaColumn from '../../../../noco-models/FormulaColumn'; import { XKnex } from '../../..'; import LinkToAnotherRecordColumn from '../../../../noco-models/LinkToAnotherRecordColumn'; import LookupColumn from '../../../../noco-models/LookupColumn'; -import { UITypes } from 'nocodb-sdk'; +import { jsepCurlyHook, UITypes } from 'nocodb-sdk'; // todo: switch function based on database @@ -51,6 +51,8 @@ export default async function formulaQueryBuilderv2( model: Model, aliasToColumn = {} ) { + // register jsep curly hook + jsep.plugins.register(jsepCurlyHook); const tree = jsep(_tree); // todo: improve - implement a common solution for filter, sort, formula, etc @@ -647,7 +649,11 @@ export default async function formulaQueryBuilderv2( return query; } else if (pt.type === 'UnaryExpression') { const query = knex.raw( - `${pt.operator}${fn(pt.argument, null, pt.operator).toQuery()}${colAlias}` + `${pt.operator}${fn( + pt.argument, + null, + pt.operator + ).toQuery()}${colAlias}` ); if (prevBinaryOp && pt.operator !== prevBinaryOp) { query.wrap('(', ')'); diff --git a/packages/nocodb/src/lib/noco/meta/api/columnApis.ts b/packages/nocodb/src/lib/noco/meta/api/columnApis.ts index 96c68b5885..d7c3a99123 100644 --- a/packages/nocodb/src/lib/noco/meta/api/columnApis.ts +++ b/packages/nocodb/src/lib/noco/meta/api/columnApis.ts @@ -3,7 +3,6 @@ import Model from '../../../noco-models/Model'; import ProjectMgrv2 from '../../../sqlMgr/v2/ProjectMgrv2'; import Base from '../../../noco-models/Base'; import Column from '../../../noco-models/Column'; -import { substituteColumnAliasWithIdInFormula } from '../helpers/formulaHelpers'; import validateParams from '../helpers/validateParams'; import { Tele } from 'nc-help'; @@ -19,6 +18,7 @@ import { isVirtualCol, LinkToAnotherRecordType, RelationTypes, + substituteColumnAliasWithIdInFormula, substituteColumnIdWithAliasInFormula, TableType, UITypes @@ -496,9 +496,12 @@ export async function columnAdd(req: Request, res: Response) { }> = (await sqlClient.columnList({ tn: table.table_name }))?.data?.list; const insertedColumnMeta = - columns.find(c => c.cn === colBody.column_name) || {} as any; + columns.find(c => c.cn === colBody.column_name) || ({} as any); - if (colBody.uidt === UITypes.SingleSelect || colBody.uidt === UITypes.MultiSelect) { + if ( + colBody.uidt === UITypes.SingleSelect || + colBody.uidt === UITypes.MultiSelect + ) { insertedColumnMeta.dtxp = colBody.dtxp; } diff --git a/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts b/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts deleted file mode 100644 index 8f114447b6..0000000000 --- a/packages/nocodb/src/lib/noco/meta/helpers/formulaHelpers.ts +++ /dev/null @@ -1,60 +0,0 @@ -import jsep from 'jsep'; -import jsepTreeToFormula from '../../common/helpers/jsepTreeToFormula'; -import Column from '../../../noco-models/Column'; - -export async function substituteColumnAliasWithIdInFormula( - formula, - columns: Column[] -) { - const substituteId = async (pt: any) => { - if (pt.type === 'CallExpression') { - for (const arg of pt.arguments || []) { - await substituteId(arg); - } - } else if (pt.type === 'Literal') { - return; - } else if (pt.type === 'Identifier') { - const colNameOrId = pt.name; - const column = columns.find( - c => - c.id === colNameOrId || - c.column_name === colNameOrId || - c.title === colNameOrId - ); - pt.name = '{' + column.id + '}'; - } else if (pt.type === 'BinaryExpression') { - await substituteId(pt.left); - await substituteId(pt.right); - } - }; - // register curly hook - jsep.plugins.register({ - name: 'curly', - init(jsep) { - jsep.hooks.add('gobble-token', function gobbleCurlyLiteral(env) { - const OCURLY_CODE = 123; // { - const CCURLY_CODE = 125; // } - const { context } = env; - if ( - !jsep.isIdentifierStart(context.code) && - context.code === OCURLY_CODE - ) { - context.index += 1; - const nodes = context.gobbleExpressions(CCURLY_CODE); - if (context.code === CCURLY_CODE) { - context.index += 1; - if (nodes.length > 0) { - env.node = nodes[0]; - } - return env.node; - } else { - context.throwError('Unclosed }'); - } - } - }); - } - } as jsep.IPlugin); - const parsedFormula = jsep(formula); - await substituteId(parsedFormula); - return jsepTreeToFormula(parsedFormula); -} diff --git a/packages/nocodb/src/lib/noco/upgrader/jobs/ncProjectUpgraderV2_0090000.ts b/packages/nocodb/src/lib/noco/upgrader/jobs/ncProjectUpgraderV2_0090000.ts index d2765134af..6b451221c7 100644 --- a/packages/nocodb/src/lib/noco/upgrader/jobs/ncProjectUpgraderV2_0090000.ts +++ b/packages/nocodb/src/lib/noco/upgrader/jobs/ncProjectUpgraderV2_0090000.ts @@ -4,11 +4,15 @@ import User from '../../../noco-models/User'; import Project from '../../../noco-models/Project'; import ProjectUser from '../../../noco-models/ProjectUser'; import Model from '../../../noco-models/Model'; -import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk'; +import { + ModelTypes, + substituteColumnAliasWithIdInFormula, + UITypes, + ViewTypes +} from 'nocodb-sdk'; import Column from '../../../noco-models/Column'; import LinkToAnotherRecordColumn from '../../../noco-models/LinkToAnotherRecordColumn'; import NcHelp from '../../../utils/NcHelp'; -import { substituteColumnAliasWithIdInFormula } from '../../meta/helpers/formulaHelpers'; import RollupColumn from '../../../noco-models/RollupColumn'; import View from '../../../noco-models/View'; import GridView from '../../../noco-models/GridView'; From c624a0e75ed7464240539567bc6634ef16aab73d Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 17 May 2022 13:56:44 +0800 Subject: [PATCH 17/21] refactor: use jsepCurlyHook from nocodb-sdk --- .../components/editColumn/FormulaOptions.vue | 5 ++-- packages/nc-gui/helpers/formulaCurlyHook.js | 24 ------------------- 2 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 packages/nc-gui/helpers/formulaCurlyHook.js 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 60aff9e658..429e335155 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -95,9 +95,8 @@ import debounce from 'debounce' import jsep from 'jsep' -import { UITypes } from 'nocodb-sdk' +import { UITypes, jsepCurlyHook } from 'nocodb-sdk' import formulaList, { validations } from '../../../../../helpers/formulaList' -import curly from '../../../../../helpers/formulaCurlyHook' import { getWordUntilCaret, insertAtCursor } from '@/helpers' import NcAutocompleteTree from '@/helpers/NcAutocompleteTree' @@ -162,7 +161,7 @@ export default { }, created() { this.formula = { value: this.value || '' } - jsep.plugins.register(curly) + jsep.plugins.register(jsepCurlyHook) }, methods: { async save() { diff --git a/packages/nc-gui/helpers/formulaCurlyHook.js b/packages/nc-gui/helpers/formulaCurlyHook.js deleted file mode 100644 index 3661b088f6..0000000000 --- a/packages/nc-gui/helpers/formulaCurlyHook.js +++ /dev/null @@ -1,24 +0,0 @@ -const OCURLY_CODE = 123; // { -const CCURLY_CODE = 125; // } - -export default { - name: 'curly', - init(jsep) { - jsep.hooks.add('gobble-token', function gobbleCurlyLiteral(env) { - const { context } = env - if (!jsep.isIdentifierStart(context.code) && context.code === OCURLY_CODE) { - context.index += 1 - let nodes = context.gobbleExpressions(CCURLY_CODE) - if (context.code === CCURLY_CODE) { - context.index += 1 - if (nodes.length > 0) { - env.node = nodes[0] - } - return env.node - } else { - context.throwError('Unclosed }') - } - } - }); - } -} \ No newline at end of file From 9cae93c98933a110c486e8cf3b17a70546c07ec0 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 17 May 2022 16:33:10 +0800 Subject: [PATCH 18/21] fix: handle column name with spaces --- packages/nocodb-sdk/src/lib/formulaHelpers.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/nocodb-sdk/src/lib/formulaHelpers.ts b/packages/nocodb-sdk/src/lib/formulaHelpers.ts index 97407da52f..6666c097b8 100644 --- a/packages/nocodb-sdk/src/lib/formulaHelpers.ts +++ b/packages/nocodb-sdk/src/lib/formulaHelpers.ts @@ -17,9 +17,10 @@ export const jsepCurlyHook = { const nodes = context.gobbleExpressions(CCURLY_CODE); if (context.code === CCURLY_CODE) { context.index += 1; - if (nodes.length > 0) { - env.node = nodes[0]; - } + env.node = { + type: jsep.IDENTIFIER, + name: nodes.map((node) => node.name).join(' '), + }; return env.node; } else { context.throwError('Unclosed }'); From ccf276a2d2cc0f244190a5b13c7301a7279fd905 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 17 May 2022 18:19:24 +0800 Subject: [PATCH 19/21] refactor: toast error --- .../nc-gui/components/project/spreadsheet/RowsXcDataTable.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue b/packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue index 4dcd1ac69b..c3b0287866 100644 --- a/packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue +++ b/packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue @@ -1425,7 +1425,7 @@ export default { })) // } } catch (e) { - console.log(e) + this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000) } this.loadingData = false }, From 1ec0f982bf196cf9a21baf733a8bc005fb971645 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 17 May 2022 18:19:52 +0800 Subject: [PATCH 20/21] fix: undefined suggestion & toast sdk error msg --- .../components/editColumn/FormulaOptions.vue | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 429e335155..03f1a503ae 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -177,7 +177,7 @@ export default { this.$toast.success('Formula column saved successfully').goAway(3000) return this.$emit('saved', this.alias) } catch (e) { - this.$toast.error(e.message).goAway(3000) + this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000) } }, async update() { @@ -282,12 +282,16 @@ export default { } }, suggestionListDown() { - this.selected = ++this.selected % this.suggestion.length - this.scrollToSelectedOption() + if (this.suggestion) { + this.selected = ++this.selected % this.suggestion.length + this.scrollToSelectedOption() + } }, suggestionListUp() { - this.selected = --this.selected > -1 ? this.selected : this.suggestion.length - 1 - this.scrollToSelectedOption() + if (this.suggestion) { + this.selected = --this.selected > -1 ? this.selected : this.suggestion.length - 1 + this.scrollToSelectedOption() + } }, scrollToSelectedOption() { this.$nextTick(() => { From d04ddf80194636c5ffa5102a763331f9e04a3842 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 17 May 2022 22:56:32 +0800 Subject: [PATCH 21/21] fix: @click.stop.prevent -> @click.stop --- .../project/spreadsheet/components/EditVirtualColumn.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nc-gui/components/project/spreadsheet/components/EditVirtualColumn.vue b/packages/nc-gui/components/project/spreadsheet/components/EditVirtualColumn.vue index e224f269d5..d27b92a556 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/EditVirtualColumn.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/EditVirtualColumn.vue @@ -7,7 +7,7 @@ class=" card nc-col-create-or-edit-card" > - +