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. 610
      packages/nc-gui/components/project/spreadsheet/components/editColumn.vue
  2. 33
      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

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

@ -36,308 +36,310 @@
@input="newColumn.altered = newColumn.altered || 8" @input="newColumn.altered = newColumn.altered || 8"
/> />
</v-col> </v-col>
<div <v-container
fluid
:class="{ :class="{
editDisabled :isEditDisabled editDisabled :isEditDisabled
}" }"
> >
<v-col v-if="relation" cols="12"> <v-row>
<div class="caption"> <v-col v-if="relation" cols="12">
<p class="mb-1"> <div class="caption">
Foreign Key <p class="mb-1">
</p> Foreign Key
</p>
<v-icon small class="mt-n1">
mdi-table <v-icon small class="mt-n1">
</v-icon> mdi-table
<span class="text-capitalize font-weight-bold body-1"> {{ relation._rtn }}</span> </v-icon>
<v-icon <span class="text-capitalize font-weight-bold body-1"> {{ relation._rtn }}</span>
v-ge="['columns','fk-delete']" <v-icon
small v-ge="['columns','fk-delete']"
class="ml-3 mt-n1" small
color="error" class="ml-3 mt-n1"
@click="deleteRelation('showDialog', column)" color="error"
> @click="deleteRelation('showDialog', column)"
mdi-delete-forever >
</v-icon> mdi-delete-forever
<span v-if="relation.type=== 'virtual'" class="caption">(v)</span> </v-icon>
</div> <span v-if="relation.type=== 'virtual'" class="caption">(v)</span>
</v-col> </div>
<template v-else> </v-col>
<v-col cols="12"> <template v-else>
<v-autocomplete <v-col cols="12">
v-model="newColumn.uidt" <v-autocomplete
hide-details v-model="newColumn.uidt"
item-value="name" hide-details
item-text="name" item-value="name"
class="caption ui-type" item-text="name"
:class="{'primary lighten-5' : newColumn.uidt }" class="caption ui-type"
label="Column type" :class="{'primary lighten-5' : newColumn.uidt }"
dense label="Column type"
outlined dense
:items="uiTypes" outlined
@change="onUiTypeChange" :items="uiTypes"
> @change="onUiTypeChange"
<template #selection="{item}"> >
<div> <template #selection="{item}">
<v-icon color="grey darken-4" small class="mr-1"> <div>
{{ item.icon }} <v-icon color="grey darken-4" small class="mr-1">
</v-icon> {{ item.icon }}
<span class="caption grey--text text--darken-4"> {{ item.name }}</span> </v-icon>
</div> <span class="caption grey--text text--darken-4"> {{ item.name }}</span>
</template> </div>
</template>
<template #item="{item}"> <template #item="{item}">
<div class="caption"> <div class="caption">
<v-icon small class="mr-1"> <v-icon small class="mr-1">
{{ item.icon }} {{ item.icon }}
</v-icon> </v-icon>
{{ item.name }} {{ item.name }}
</div> </div>
</template> </template>
</v-autocomplete> </v-autocomplete>
<!-- <v-list dense max-height="calc(100vh - 300px)" style="overflow: auto">--> <!-- <v-list dense max-height="calc(100vh - 300px)" style="overflow: auto">-->
<!-- <v-list-item v-for="item in uiTypes" @click.stop :key="item">--> <!-- <v-list-item v-for="item in uiTypes" @click.stop :key="item">-->
<!-- <span class="caption">{{ item }}</span>--> <!-- <span class="caption">{{ item }}</span>-->
<!-- </v-list-item>--> <!-- </v-list-item>-->
<!-- </v-list>--> <!-- </v-list>-->
</v-col>
<template v-if="newColumn.uidt !== 'Formula'">
<v-col
v-if="isLookup"
cols="12"
>
<lookup-options
ref="lookup"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:is-m-s-s-q-l="isMSSQL"
v-on="$listeners"
/>
</v-col>
<v-col
v-if="isLinkToAnotherRecord"
cols="12"
>
<linked-to-another-options
ref="relation"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:is-m-s-s-q-l="isMSSQL"
@onColumnSelect="onRelColumnSelect"
/>
</v-col>
<v-col
v-if="isRelation"
cols="12"
>
<relation-options
ref="relation"
:column="newColumn"
:nodes="nodes"
:is-m-s-s-q-l="isMSSQL"
:is-s-q-lite="isSQLite"
@onColumnSelect="onRelColumnSelect"
/>
</v-col> </v-col>
<v-col v-if="isSelect" cols="12"> <template v-if="newColumn.uidt !== 'Formula'">
<custom-select-options <v-col
v-model="newColumn.dtxp" v-if="isLookup"
@input="newColumn.altered = newColumn.altered || 2" cols="12"
/> >
</v-col> <lookup-options
ref="lookup"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:is-m-s-s-q-l="isMSSQL"
v-on="$listeners"
/>
</v-col>
<v-col
v-if="isLinkToAnotherRecord"
cols="12"
>
<linked-to-another-options
ref="relation"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:is-m-s-s-q-l="isMSSQL"
@onColumnSelect="onRelColumnSelect"
/>
</v-col>
<v-col
v-if="isRelation"
cols="12"
>
<relation-options
ref="relation"
:column="newColumn"
:nodes="nodes"
:is-m-s-s-q-l="isMSSQL"
:is-s-q-lite="isSQLite"
@onColumnSelect="onRelColumnSelect"
/>
</v-col>
<template v-if="newColumn.cn && newColumn.uidt && !isLinkToAnotherRecord && !isLookup"> <v-col v-if="isSelect" cols="12">
<v-col cols="12"> <custom-select-options
<v-container fluid class="wrapper"> v-model="newColumn.dtxp"
<v-row> @input="newColumn.altered = newColumn.altered || 2"
<v-col cols="12"> />
<div class="d-flex justify-space-between caption">
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.rqd"
:disabled="newColumn.pk || !sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="NN"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">NN</span>
</template>
</v-checkbox>
</div>
</template>
<span>Not Null</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.pk"
:disabled="!sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="PK"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">PK</span>
</template>
</v-checkbox>
</div>
</template>
<span>Primary Key</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.ai"
:disabled="sqlUi.colPropUNDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="AI"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">AI</span>
</template>
</v-checkbox>
</div>
</template>
<span>Auto Increment</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.un"
class="mr-2 mt-0"
dense
hide-details
label="UN"
:disabled="sqlUi.colPropUNDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">UN</span>
</template>
</v-checkbox>
</div>
</template>
<span>Unsigned</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.au"
class="mr-2 mt-0"
dense
hide-details
label="UN"
:disabled=" sqlUi.colPropAuDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">AU</span>
</template>
</v-checkbox>
</div>
</template>
<span>Auto Update Timestamp</span>
</v-tooltip>
</div>
</v-col>
<v-col cols="12">
<v-autocomplete
v-model="newColumn.dt"
hide-details
class="caption data-type"
label="Type in Database"
dense
outlined
:items="dataTypes"
@change="onDataTypeChange"
/>
</v-col>
<v-col :cols="sqlUi.showScale(newColumn) && !isSelect ? 6 : 12">
<v-text-field
v-if="!isSelect"
v-model="newColumn.dtxp"
dense
:disabled="sqlUi.getDefaultLengthIsDisabled(newColumn.dt) || !sqlUi.columnEditable(newColumn)"
class="caption"
label="Length / Values"
outlined
hide-details
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
<v-col v-if="sqlUi.showScale(newColumn)" :cols="isSelect ?12 : 6">
<v-text-field
v-model="newColumn.dtxs"
dense
:disabled=" !sqlUi.columnEditable(newColumn)"
class="caption"
label="Scale"
outlined
hide-details
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="newColumn.cdf"
label="Default value"
:hint="sqlUi.getDefaultValueForDatatype(newColumn.dt)"
persistent-hint
rows="3"
outlined
dense
class="caption"
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
</v-row>
</v-container>
</v-col> </v-col>
<template v-if="newColumn.cn && newColumn.uidt && !isLinkToAnotherRecord && !isLookup">
<v-col cols="12">
<v-container fluid class="wrapper">
<v-row>
<v-col cols="12">
<div class="d-flex justify-space-between caption">
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.rqd"
:disabled="newColumn.pk || !sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="NN"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">NN</span>
</template>
</v-checkbox>
</div>
</template>
<span>Not Null</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.pk"
:disabled="!sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="PK"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">PK</span>
</template>
</v-checkbox>
</div>
</template>
<span>Primary Key</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.ai"
:disabled="sqlUi.colPropUNDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="AI"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">AI</span>
</template>
</v-checkbox>
</div>
</template>
<span>Auto Increment</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.un"
class="mr-2 mt-0"
dense
hide-details
label="UN"
:disabled="sqlUi.colPropUNDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">UN</span>
</template>
</v-checkbox>
</div>
</template>
<span>Unsigned</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.au"
class="mr-2 mt-0"
dense
hide-details
label="UN"
:disabled=" sqlUi.colPropAuDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">AU</span>
</template>
</v-checkbox>
</div>
</template>
<span>Auto Update Timestamp</span>
</v-tooltip>
</div>
</v-col>
<v-col cols="12">
<v-autocomplete
v-model="newColumn.dt"
hide-details
class="caption data-type"
label="Type in Database"
dense
outlined
:items="dataTypes"
@change="onDataTypeChange"
/>
</v-col>
<v-col :cols="sqlUi.showScale(newColumn) && !isSelect ? 6 : 12">
<v-text-field
v-if="!isSelect"
v-model="newColumn.dtxp"
dense
:disabled="sqlUi.getDefaultLengthIsDisabled(newColumn.dt) || !sqlUi.columnEditable(newColumn)"
class="caption"
label="Length / Values"
outlined
hide-details
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
<v-col v-if="sqlUi.showScale(newColumn)" :cols="isSelect ?12 : 6">
<v-text-field
v-model="newColumn.dtxs"
dense
:disabled=" !sqlUi.columnEditable(newColumn)"
class="caption"
label="Scale"
outlined
hide-details
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="newColumn.cdf"
label="Default value"
:hint="sqlUi.getDefaultValueForDatatype(newColumn.dt)"
persistent-hint
rows="3"
outlined
dense
class="caption"
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
</v-row>
</v-container>
</v-col>
</template>
</template> </template>
</template> <template v-else>
<template v-else> <v-col cols="12">
<v-col cols12> <formula-options
<formula-options ref="formula"
ref="formula" :column="newColumn"
:column="newColumn" :nodes="nodes"
:nodes="nodes" :meta="meta"
:meta="meta" :is-s-q-lite="isSQLite"
:is-s-q-lite="isSQLite" :alias="newColumn.cn"
:alias="newColumn.cn" :is-m-s-s-q-l="isMSSQL"
:is-m-s-s-q-l="isMSSQL" v-on="$listeners"
v-on="$listeners" />
/>
<!-- <v-autocomplete <!-- <v-autocomplete
label="Formula" label="Formula"
@ -351,20 +353,21 @@
<span class="green&#45;&#45;text text&#45;&#45;darken-2 caption font-weight-regular">{{ item }}</span> <span class="green&#45;&#45;text text&#45;&#45;darken-2 caption font-weight-regular">{{ item }}</span>
</template> </template>
</v-autocomplete>--> </v-autocomplete>-->
</v-col> </v-col>
</template>
</template> </template>
</template>
<div class="disabled-info" :class="{'d-none':!isEditDisabled}">
<div class="disabled-info" :class="{'d-none':!isEditDisabled}"> <v-alert dense type="warning" icon="info" class="caption mx-2" outlined>
<v-alert dense type="warning" icon="info" class="caption mx-2" outlined> This spreadsheet is connected to an SQLite DB.<br>
This spreadsheet is connected to an SQLite DB.<br> For production please see <a
For production please see <a href="https://github.com/nocodb/nocodb#production-setup"
href="https://github.com/nocodb/nocodb#production-setup" target="_blank"
target="_blank" >here</a>.
>here</a>. </v-alert>
</v-alert> </div>
</div> </v-row>
</div> </v-container>
</v-row> </v-row>
</v-container> </v-container>
</v-form> </v-form>
@ -495,9 +498,9 @@ export default {
return return
} }
try { try {
if (this.newColumn.uidt === 'Formula') { // if (this.newColumn.uidt === 'Formula') {
return this.$toast.info('Coming Soon...').goAway(3000) // return this.$toast.info('Coming Soon...').goAway(3000)
} // }
if (this.isLinkToAnotherRecord && this.$refs.relation) { if (this.isLinkToAnotherRecord && this.$refs.relation) {
await this.$refs.relation.saveRelation() await this.$refs.relation.saveRelation()
@ -506,6 +509,9 @@ export default {
if (this.isLookup && this.$refs.lookup) { if (this.isLookup && this.$refs.lookup) {
return await this.$refs.lookup.save() 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.tn = this.nodes.tn
this.newColumn._cn = this.newColumn.cn this.newColumn._cn = this.newColumn.cn

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

@ -1,6 +1,7 @@
<template> <template>
<v-text-field <v-text-field
v-model="formula.value" v-model="formula.value"
dense
outlined outlined
class="caption" class="caption"
hide-details="auto" hide-details="auto"
@ -17,7 +18,37 @@ export default {
data: () => ({ data: () => ({
formula: {}, 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()'] 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> </script>

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

@ -63,11 +63,17 @@
:column="column" :column="column"
v-on="$listeners " v-on="$listeners "
/> />
<formula-cell
v-else-if="formula"
:row="row"
:column="column"
/>
</v-lazy> </v-lazy>
</div> </div>
</template> </template>
<script> <script>
import FormulaCell from '@/components/project/spreadsheet/components/virtualCell/formulaCell'
import hasManyCell from '@/components/project/spreadsheet/components/virtualCell/hasManyCell' import hasManyCell from '@/components/project/spreadsheet/components/virtualCell/hasManyCell'
import LookupCell from '@/components/project/spreadsheet/components/virtualCell/lookupCell' import LookupCell from '@/components/project/spreadsheet/components/virtualCell/lookupCell'
import manyToManyCell from '@/components/project/spreadsheet/components/virtualCell/manyToManyCell' import manyToManyCell from '@/components/project/spreadsheet/components/virtualCell/manyToManyCell'
@ -78,6 +84,7 @@ import belongsToCell from '@/components/project/spreadsheet/components/virtualCe
export default { export default {
name: 'VirtualCell', name: 'VirtualCell',
components: { components: {
FormulaCell,
LookupCell, LookupCell,
belongsToCell, belongsToCell,
manyToManyCell, manyToManyCell,
@ -119,6 +126,9 @@ export default {
}, },
lookup() { lookup() {
return this.column && this.column.lk return this.column && this.column.lk
},
formula() {
return this.column && this.column.formula
} }
}, },
methods: { 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"> <v-icon v-else-if="column.mm" color="pink" x-small class="mr-1" v-on="on">
mdi-table-network mdi-table-network
</v-icon> </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"> <template v-else-if="column.lk">
<v-icon v-if="column.lk.type === 'hm'" color="warning" x-small class="mr-1" v-on="on"> <v-icon v-if="column.lk.type === 'hm'" color="warning" x-small class="mr-1" v-on="on">
mdi-table-column-plus-before mdi-table-column-plus-before
@ -24,7 +27,6 @@
</template> </template>
<span class="name flex-grow-1" :title="column._cn" v-on="on" v-html="alias"> <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 v-if="column.rqd" class="error--text text--lighten-1" v-on="on">&nbsp;*</span>
</span> </span>
</template> </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',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',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',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() done()
}); });
it('Addition', function (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',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',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',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',knexSqliteRef).toQuery()).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a')
done() done()
}); });
it('Average', function (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',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',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',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',knexSqliteRef).toQuery()).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a')
done() 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 // todo: switch function based on database
export default function formulaQueryBuilder(str, alias, knex) { export default function formulaQueryBuilder(str, alias, knex) {
const fn = (pt, a?, nestedBinary?) => { const fn = (pt, a?, prevBinaryOp ?) => {
const colAlias = a ? ` as ${a}` : ''; const colAlias = a ? ` as ${a}` : '';
if (pt.type === 'CallExpression') { if (pt.type === 'CallExpression') {
switch (pt.callee.name) { switch (pt.callee.name) {
@ -16,9 +16,9 @@ export default function formulaQueryBuilder(str, alias, knex) {
operator: '+', operator: '+',
left: pt.arguments[0], left: pt.arguments[0],
right: {...pt, arguments: pt.arguments.slice(1)} right: {...pt, arguments: pt.arguments.slice(1)}
}, a, nestedBinary) }, a, prevBinaryOp)
} else { } else {
return fn(pt.arguments[0], a, nestedBinary) return fn(pt.arguments[0], a, prevBinaryOp)
} }
break; break;
case 'AVG': case 'AVG':
@ -28,9 +28,9 @@ export default function formulaQueryBuilder(str, alias, knex) {
operator: '/', operator: '/',
left: {...pt, callee: {name: 'SUM'}}, left: {...pt, callee: {name: 'SUM'}},
right: {type: 'Literal', value: pt.arguments.length} right: {type: 'Literal', value: pt.arguments.length}
}, a, nestedBinary) }, a, prevBinaryOp)
} else { } else {
return fn(pt.arguments[0], a, nestedBinary) return fn(pt.arguments[0], a, prevBinaryOp)
} }
break; break;
case 'concat': case 'concat':
@ -42,9 +42,9 @@ export default function formulaQueryBuilder(str, alias, knex) {
operator: '||', operator: '||',
left: pt.arguments[0], left: pt.arguments[0],
right: {...pt, arguments: pt.arguments.slice(1)} right: {...pt, arguments: pt.arguments.slice(1)}
}, a, nestedBinary) }, a, prevBinaryOp)
} else { } else {
return fn(pt.arguments[0], a, nestedBinary) return fn(pt.arguments[0], a, prevBinaryOp)
} }
} }
break; break;
@ -56,8 +56,8 @@ export default function formulaQueryBuilder(str, alias, knex) {
} else if (pt.type === 'Identifier') { } else if (pt.type === 'Identifier') {
return knex.raw(`??${colAlias}`, [pt.name]); return knex.raw(`??${colAlias}`, [pt.name]);
} else if (pt.type === 'BinaryExpression') { } else if (pt.type === 'BinaryExpression') {
const query = knex.raw(`${fn(pt.left, null, true).toQuery()} ${pt.operator} ${fn(pt.right, null, true).toQuery()}${colAlias}`) const query = knex.raw(`${fn(pt.left, null, pt.operator).toQuery()} ${pt.operator} ${fn(pt.right, null, pt.operator).toQuery()}${colAlias}`)
if (nestedBinary) { if (prevBinaryOp && pt.operator !== prevBinaryOp) {
query.wrap('(', ')') query.wrap('(', ')')
} }
return query; return query;

Loading…
Cancel
Save