Browse Source

feat: Formula column creation

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/448/head
Pranav C 3 years ago committed by Pranav C
parent
commit
c7ea4f82b5
  1. 18
      packages/nc-gui/components/project/spreadsheet/components/editColumn.vue
  2. 31
      packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue
  3. 10
      packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue
  4. 14
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/formulaCell.vue
  5. 4
      packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue
  6. 18
      packages/nocodb/src/__tests__/formula.test.ts
  7. 18
      packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilder.ts

18
packages/nc-gui/components/project/spreadsheet/components/editColumn.vue

@ -36,11 +36,13 @@
@input="newColumn.altered = newColumn.altered || 8"
/>
</v-col>
<div
<v-container
fluid
:class="{
editDisabled :isEditDisabled
}"
>
<v-row>
<v-col v-if="relation" cols="12">
<div class="caption">
<p class="mb-1">
@ -327,7 +329,7 @@
</template>
</template>
<template v-else>
<v-col cols12>
<v-col cols="12">
<formula-options
ref="formula"
:column="newColumn"
@ -364,7 +366,8 @@
>here</a>.
</v-alert>
</div>
</div>
</v-row>
</v-container>
</v-row>
</v-container>
</v-form>
@ -495,9 +498,9 @@ export default {
return
}
try {
if (this.newColumn.uidt === 'Formula') {
return this.$toast.info('Coming Soon...').goAway(3000)
}
// if (this.newColumn.uidt === 'Formula') {
// return this.$toast.info('Coming Soon...').goAway(3000)
// }
if (this.isLinkToAnotherRecord && this.$refs.relation) {
await this.$refs.relation.saveRelation()
@ -506,6 +509,9 @@ export default {
if (this.isLookup && this.$refs.lookup) {
return await this.$refs.lookup.save()
}
if (this.newColumn.uidt === 'Formula' && this.$refs.formula) {
return await this.$refs.formula.save()
}
this.newColumn.tn = this.nodes.tn
this.newColumn._cn = this.newColumn.cn

31
packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue

@ -1,6 +1,7 @@
<template>
<v-text-field
v-model="formula.value"
dense
outlined
class="caption"
hide-details="auto"
@ -17,7 +18,37 @@ export default {
data: () => ({
formula: {},
formulas: ['AVERAGE()', 'COUNT()', 'COUNTA()', 'COUNTALL()', 'SUM()', 'MIN()', 'MAX()', 'AND()', 'OR()', 'TRUE()', 'FALSE()', 'NOT()', 'XOR()', 'ISERROR()', 'IF()', 'LEN()', 'MID()', 'LEFT()', 'RIGHT()', 'FIND()', 'CONCATENATE()', 'T()', 'VALUE()', 'ARRAYJOIN()', 'ARRAYUNIQUE()', 'ARRAYCOMPACT()', 'ARRAYFLATTEN()', 'ROUND()', 'ROUNDUP()', 'ROUNDDOWN()', 'INT()', 'EVEN()', 'ODD()', 'MOD()', 'LOG()', 'EXP()', 'POWER()', 'SQRT()', 'CEILING()', 'FLOOR()', 'ABS()', 'RECORD_ID()', 'CREATED_TIME()', 'ERROR()', 'BLANK()', 'YEAR()', 'MONTH()', 'DAY()', 'HOUR()', 'MINUTE()', 'SECOND()', 'TODAY()', 'NOW()', 'WORKDAY()', 'DATETIME_PARSE()', 'DATETIME_FORMAT()', 'SET_LOCALE()', 'SET_TIMEZONE()', 'DATESTR()', 'TIMESTR()', 'TONOW()', 'FROMNOW()', 'DATEADD()', 'WEEKDAY()', 'WEEKNUM()', 'DATETIME_DIFF()', 'WORKDAY_DIFF()', 'IS_BEFORE()', 'IS_SAME()', 'IS_AFTER()', 'REPLACE()', 'REPT()', 'LOWER()', 'UPPER()', 'TRIM()', 'SUBSTITUTE()', 'SEARCH()', 'SWITCH()', 'LAST_MODIFIED_TIME()', 'ENCODE_URL_COMPONENT()', 'REGEX_EXTRACT()', 'REGEX_MATCH()', 'REGEX_REPLACE()']
}),
methods: {
async save() {
try {
await this.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.nodes.dbAlias,
env: this.nodes.env,
tn: this.meta.tn,
force: true
})
const meta = JSON.parse(JSON.stringify(this.$store.state.meta.metas[this.meta.tn]))
meta.v.push({
_cn: this.alias,
formula: this.formula
})
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
meta
}])
return this.$emit('saved', this.alias)
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
}
}
}
</script>

10
packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue

@ -63,11 +63,17 @@
:column="column"
v-on="$listeners "
/>
<formula-cell
v-else-if="formula"
:row="row"
:column="column"
/>
</v-lazy>
</div>
</template>
<script>
import FormulaCell from '@/components/project/spreadsheet/components/virtualCell/formulaCell'
import hasManyCell from '@/components/project/spreadsheet/components/virtualCell/hasManyCell'
import LookupCell from '@/components/project/spreadsheet/components/virtualCell/lookupCell'
import manyToManyCell from '@/components/project/spreadsheet/components/virtualCell/manyToManyCell'
@ -78,6 +84,7 @@ import belongsToCell from '@/components/project/spreadsheet/components/virtualCe
export default {
name: 'VirtualCell',
components: {
FormulaCell,
LookupCell,
belongsToCell,
manyToManyCell,
@ -119,6 +126,9 @@ export default {
},
lookup() {
return this.column && this.column.lk
},
formula() {
return this.column && this.column.formula
}
},
methods: {

14
packages/nc-gui/components/project/spreadsheet/components/virtualCell/formulaCell.vue

@ -0,0 +1,14 @@
<template>
<div>{{ row[column._cn] }}</div>
</template>
<script>
export default {
name: 'FormulaCell',
props: ['column', 'row']
}
</script>
<style scoped>
</style>

4
packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue

@ -11,6 +11,9 @@
<v-icon v-else-if="column.mm" color="pink" x-small class="mr-1" v-on="on">
mdi-table-network
</v-icon>
<v-icon v-else-if="column.formula" x-small class="mr-1" v-on="on">
mdi-math-integral
</v-icon>
<template v-else-if="column.lk">
<v-icon v-if="column.lk.type === 'hm'" color="warning" x-small class="mr-1" v-on="on">
mdi-table-column-plus-before
@ -24,7 +27,6 @@
</template>
<span class="name flex-grow-1" :title="column._cn" v-on="on" v-html="alias">
<span v-if="column.rqd" class="error--text text--lighten-1" v-on="on">&nbsp;*</span>
</span>
</template>

18
packages/nocodb/src/__tests__/formula.test.ts

@ -34,21 +34,21 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
expect(formulaQueryBuilder("concat(city, ' _ ',city_id+country_id)", 'a',knexMysqlRef).toQuery()).eq('concat(`city`,\' _ \',`city_id` + `country_id`) as a')
expect(formulaQueryBuilder("concat(city, ' _ ',city_id+country_id)", 'a',knexPgRef).toQuery()).eq('concat("city",\' _ \',"city_id" + "country_id") as a')
expect(formulaQueryBuilder("concat(city, ' _ ',city_id+country_id)", 'a',knexMssqlRef).toQuery()).eq('concat([city],\' _ \',[city_id] + [country_id]) as a')
expect(formulaQueryBuilder("concat(city, ' _ ',city_id+country_id)", 'a',knexSqliteRef).toQuery()).eq('`city` || (\' _ \' || (`city_id` + `country_id`)) as a')
expect(formulaQueryBuilder("concat(city, ' _ ',city_id+country_id)", 'a',knexSqliteRef).toQuery()).eq('`city` || \' _ \' || (`city_id` + `country_id`) as a')
done()
});
it('Addition', function (done) {
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('`city_id` + (`country_id` + (2 + (3 + (4 + (5 + 4))))) as a')
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('"city_id" + ("country_id" + (2 + (3 + (4 + (5 + 4))))) as a')
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('[city_id] + ([country_id] + (2 + (3 + (4 + (5 + 4))))) as a')
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('`city_id` + (`country_id` + (2 + (3 + (4 + (5 + 4))))) as a')
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('"city_id" + "country_id" + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('[city_id] + [country_id] + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a')
done()
});
it('Average', function (done) {
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('(`city_id` + (`country_id` + (2 + (3 + (4 + (5 + 4)))))) / 7 as a')
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('("city_id" + ("country_id" + (2 + (3 + (4 + (5 + 4)))))) / 7 as a')
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('([city_id] + ([country_id] + (2 + (3 + (4 + (5 + 4)))))) / 7 as a')
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('(`city_id` + (`country_id` + (2 + (3 + (4 + (5 + 4)))))) / 7 as a')
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('("city_id" + "country_id" + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('([city_id] + [country_id] + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a')
done()
});
});

18
packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilder.ts

@ -4,7 +4,7 @@ import jsep from 'jsep';
// todo: switch function based on database
export default function formulaQueryBuilder(str, alias, knex) {
const fn = (pt, a?, nestedBinary?) => {
const fn = (pt, a?, prevBinaryOp ?) => {
const colAlias = a ? ` as ${a}` : '';
if (pt.type === 'CallExpression') {
switch (pt.callee.name) {
@ -16,9 +16,9 @@ export default function formulaQueryBuilder(str, alias, knex) {
operator: '+',
left: pt.arguments[0],
right: {...pt, arguments: pt.arguments.slice(1)}
}, a, nestedBinary)
}, a, prevBinaryOp)
} else {
return fn(pt.arguments[0], a, nestedBinary)
return fn(pt.arguments[0], a, prevBinaryOp)
}
break;
case 'AVG':
@ -28,9 +28,9 @@ export default function formulaQueryBuilder(str, alias, knex) {
operator: '/',
left: {...pt, callee: {name: 'SUM'}},
right: {type: 'Literal', value: pt.arguments.length}
}, a, nestedBinary)
}, a, prevBinaryOp)
} else {
return fn(pt.arguments[0], a, nestedBinary)
return fn(pt.arguments[0], a, prevBinaryOp)
}
break;
case 'concat':
@ -42,9 +42,9 @@ export default function formulaQueryBuilder(str, alias, knex) {
operator: '||',
left: pt.arguments[0],
right: {...pt, arguments: pt.arguments.slice(1)}
}, a, nestedBinary)
}, a, prevBinaryOp)
} else {
return fn(pt.arguments[0], a, nestedBinary)
return fn(pt.arguments[0], a, prevBinaryOp)
}
}
break;
@ -56,8 +56,8 @@ export default function formulaQueryBuilder(str, alias, knex) {
} else if (pt.type === 'Identifier') {
return knex.raw(`??${colAlias}`, [pt.name]);
} else if (pt.type === 'BinaryExpression') {
const query = knex.raw(`${fn(pt.left, null, true).toQuery()} ${pt.operator} ${fn(pt.right, null, true).toQuery()}${colAlias}`)
if (nestedBinary) {
const query = knex.raw(`${fn(pt.left, null, pt.operator).toQuery()} ${pt.operator} ${fn(pt.right, null, pt.operator).toQuery()}${colAlias}`)
if (prevBinaryOp && pt.operator !== prevBinaryOp) {
query.wrap('(', ')')
}
return query;

Loading…
Cancel
Save