Browse Source

Merge pull request #7574 from nocodb/nc-feat/formula-imp

Nc feat/formula imp
pull/7577/head
Raju Udava 10 months ago committed by GitHub
parent
commit
00fd48fc5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 36
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  2. 18
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  3. 23
      packages/nc-gui/components/virtual-cell/Formula.vue
  4. 33
      packages/nc-gui/composables/useViewFilters.ts
  5. 53
      packages/nc-gui/utils/filterUtils.ts
  6. 17
      packages/nocodb-sdk/src/lib/UITypes.ts
  7. 1
      packages/nocodb-sdk/src/lib/index.ts
  8. 6
      packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts
  9. 1
      packages/nocodb/src/db/BaseModelSqlv2.ts
  10. 82
      packages/nocodb/src/db/conditionV2.ts
  11. 4
      packages/nocodb/src/db/sortV2.ts

36
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -76,6 +76,7 @@ const {
isComparisonSubOpAllowed,
loadBtLookupTypes,
btLookupTypesMap,
types,
} = useViewFilters(
activeView,
parentId?.value,
@ -114,8 +115,9 @@ const isFilterDraft = (filter: Filter, col: ColumnType) => {
}
if (
comparisonOpList(col.uidt as UITypes, col?.meta?.date_format).find((compOp) => compOp.value === filter.comparison_op)
?.ignoreVal
comparisonOpList(types.value[col.id] as UITypes, col?.meta?.date_format).find(
(compOp) => compOp.value === filter.comparison_op,
)?.ignoreVal
) {
return false
}
@ -143,7 +145,7 @@ const filterUpdateCondition = (filter: FilterType, i: number) => {
// hence remove the previous value
filter.value = null
filter.comparison_sub_op = null
} else if (isDateType(col.uidt as UITypes)) {
} else if (isDateType(types.value[col.id] as UITypes)) {
// for date / datetime,
// the input type could be decimal or datepicker / datetime picker
// hence remove the previous value
@ -173,17 +175,6 @@ const filterUpdateCondition = (filter: FilterType, i: number) => {
})
}
const types = computed(() => {
if (!meta.value?.columns?.length) {
return {}
}
return meta.value?.columns?.reduce((obj: any, col: any) => {
obj[col.id] = col.uidt
return obj
}, {})
})
watch(
() => activeView.value?.id,
(n, o) => {
@ -237,11 +228,11 @@ const selectFilterField = (filter: Filter, index: number) => {
// since the existing one may not be supported for the new field
// e.g. `eq` operator is not supported in checkbox field
// hence, get the first option of the supported operators of the new field
filter.comparison_op = comparisonOpList(col.uidt as UITypes, col?.meta?.date_format).find((compOp) =>
filter.comparison_op = comparisonOpList(types.value[col.id] as UITypes, col?.meta?.date_format).find((compOp) =>
isComparisonOpAllowed(filter, compOp),
)?.value as FilterType['comparison_op']
if (isDateType(col.uidt as UITypes) && !['blank', 'notblank'].includes(filter.comparison_op!)) {
if (isDateType(types.value[col.id] as UITypes) && !['blank', 'notblank'].includes(filter.comparison_op!)) {
if (filter.comparison_op === 'isWithin') {
filter.comparison_sub_op = 'pastNumberOfDays'
} else {
@ -319,8 +310,9 @@ const showFilterInput = (filter: Filter) => {
(op) => op.value === filter.comparison_sub_op,
)?.ignoreVal
} else {
return !comparisonOpList(col?.uidt as UITypes, col?.meta?.date_format).find((op) => op.value === filter.comparison_op)
?.ignoreVal
return !comparisonOpList(types.value[col?.id] as UITypes, col?.meta?.date_format).find(
(op) => op.value === filter.comparison_op,
)?.ignoreVal
}
}
@ -462,7 +454,7 @@ function isDateType(uidt: UITypes) {
@change="filterUpdateCondition(filter, i)"
>
<template
v-for="compOp of comparisonOpList(getColumn(filter)?.uidt, getColumn(filter)?.meta?.date_format)"
v-for="compOp of comparisonOpList(types[filter.fk_column_id], getColumn(filter)?.meta?.date_format)"
:key="compOp.value"
>
<a-select-option v-if="isComparisonOpAllowed(filter, compOp)" :value="compOp.value">
@ -481,7 +473,7 @@ function isDateType(uidt: UITypes) {
<div v-if="['blank', 'notblank'].includes(filter.comparison_op)" class="flex flex-grow"></div>
<NcSelect
v-else-if="isDateType(getColumn(filter)?.uidt)"
v-else-if="isDateType(types[filter.fk_column_id])"
v-model:value="filter.comparison_sub_op"
v-e="['c:filter:sub-comparison-op:select']"
:dropdown-match-select-width="false"
@ -529,12 +521,12 @@ function isDateType(uidt: UITypes) {
<SmartsheetToolbarFilterInput
v-if="showFilterInput(filter)"
class="nc-filter-value-select rounded-md min-w-34"
:column="getColumn(filter)"
:column="{ ...getColumn(filter), uidt: types[filter.fk_column_id] }"
:filter="filter"
@update-filter-value="(value) => updateFilterValue(value, filter, i)"
@click.stop
/>
<div v-else-if="!isDateType(getColumn(filter)?.uidt)" class="flex-grow"></div>
<div v-else-if="!isDateType(types[filter.fk_column_id])" class="flex-grow"></div>
<NcButton
v-if="!filter.readOnly"

18
packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { PlanLimitTypes, RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { PlanLimitTypes, RelationTypes, UITypes, getEquivalentUIType, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import {
ActiveViewInj,
@ -75,8 +75,20 @@ const availableColumns = computed(() => {
})
const getColumnUidtByID = (key?: string) => {
if (!key) return ''
return columnByID.value[key]?.uidt || ''
if (!key || !columnByID.value[key]) return ''
const column = columnByID.value[key]
let uidt = column.uidt
if (column.uidt === UITypes.Formula) {
uidt =
getEquivalentUIType({
formulaColumn: column,
}) || uidt
}
return uidt || ''
}
const open = ref(false)

23
packages/nc-gui/components/virtual-cell/Formula.vue

@ -1,8 +1,19 @@
<script lang="ts" setup>
import { handleTZ } from 'nocodb-sdk'
import { FormulaDataTypes, handleTZ } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { CellValueInj, ColumnInj, computed, inject, renderValue, replaceUrlsWithLink, useBase } from '#imports'
import {
CellValueInj,
ColumnInj,
IsExpandedFormOpenInj,
computed,
inject,
ref,
renderValue,
replaceUrlsWithLink,
useBase,
useShowNotEditableWarning,
} from '#imports'
// todo: column type doesn't have required property `error` - throws in typecheck
const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }>
@ -19,10 +30,16 @@ const urls = computed(() => replaceUrlsWithLink(result.value))
const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activateShowEditNonEditableFieldWarning } =
useShowNotEditableWarning()
const isNumber = computed(() => (column.value.colOptions as any)?.parsed_tree?.dataType === FormulaDataTypes.NUMERIC)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const isGrid = inject(IsGridInj, ref(false))
</script>
<template>
<div>
<div class="w-full" :class="{ 'text-right': isNumber && isGrid && !isExpandedFormOpen }">
<a-tooltip v-if="column && column.colOptions && column.colOptions.error" placement="bottom" class="text-orange-700">
<template #title>
<span class="font-bold">{{ column.colOptions.error }}</span>

33
packages/nc-gui/composables/useViewFilters.ts

@ -1,4 +1,13 @@
import type { ColumnType, FilterType, LinkToAnotherRecordType, LookupType, ViewType } from 'nocodb-sdk'
import {
type ColumnType,
type FilterType,
FormulaDataTypes,
type FormulaType,
type LinkToAnotherRecordType,
type LookupType,
type ViewType,
getEquivalentUIType,
} from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import type { SelectProps } from 'ant-design-vue'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
@ -103,8 +112,15 @@ export function useViewFilters(
}
return meta.value?.columns?.reduce((obj: any, col: any) => {
if (col.uidt === UITypes.Formula) {
const formulaUIType = getEquivalentUIType({
formulaColumn: col,
})
obj[col.id] = formulaUIType || col.uidt
}
// if column is a lookup column, then use the lookup type extracted from the column
if (btLookupTypesMap.value[col.id]) {
else if (btLookupTypesMap.value[col.id]) {
obj[col.id] = btLookupTypesMap.value[col.id].uidt
} else {
obj[col.id] = col.uidt
@ -137,9 +153,11 @@ export function useViewFilters(
},
) => {
const isNullOrEmptyOp = ['empty', 'notempty', 'null', 'notnull'].includes(compOp.value)
const uidt = types.value[filter.fk_column_id]
if (compOp.includedTypes) {
// include allowed values only if selected column type matches
if (filter.fk_column_id && compOp.includedTypes.includes(types.value[filter.fk_column_id])) {
if (filter.fk_column_id && compOp.includedTypes.includes(uidt)) {
// for 'empty', 'notempty', 'null', 'notnull',
// show them based on `showNullAndEmptyInFilter` in Base Settings
return isNullOrEmptyOp ? baseMeta.value.showNullAndEmptyInFilter : true
@ -148,7 +166,7 @@ export function useViewFilters(
}
} else if (compOp.excludedTypes) {
// include not allowed values only if selected column type not matches
if (filter.fk_column_id && !compOp.excludedTypes.includes(types.value[filter.fk_column_id])) {
if (filter.fk_column_id && !compOp.excludedTypes.includes(uidt)) {
// for 'empty', 'notempty', 'null', 'notnull',
// show them based on `showNullAndEmptyInFilter` in Base Settings
return isNullOrEmptyOp ? baseMeta.value.showNullAndEmptyInFilter : true
@ -170,12 +188,14 @@ export function useViewFilters(
excludedTypes?: UITypes[]
},
) => {
const uidt = types.value[filter.fk_column_id]
if (compOp.includedTypes) {
// include allowed values only if selected column type matches
return filter.fk_column_id && compOp.includedTypes.includes(types.value[filter.fk_column_id])
return filter.fk_column_id && compOp.includedTypes.includes(uidt)
} else if (compOp.excludedTypes) {
// include not allowed values only if selected column type not matches
return filter.fk_column_id && !compOp.excludedTypes.includes(types.value[filter.fk_column_id])
return filter.fk_column_id && !compOp.excludedTypes.includes(uidt)
}
}
@ -479,5 +499,6 @@ export function useViewFilters(
isComparisonSubOpAllowed,
loadBtLookupTypes,
btLookupTypesMap,
types
}
}

53
packages/nc-gui/utils/filterUtils.ts

@ -4,9 +4,15 @@ const getEqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType) || fieldUiType === UITypes.Time) {
return '='
} else if (
[UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord, UITypes.Date, UITypes.DateTime].includes(
fieldUiType,
)
[
UITypes.SingleSelect,
UITypes.Collaborator,
UITypes.LinkToAnotherRecord,
UITypes.Date,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.DateTime,
].includes(fieldUiType)
) {
return 'is'
}
@ -17,9 +23,15 @@ const getNeqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType) || fieldUiType === UITypes.Time) {
return '!='
} else if (
[UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord, UITypes.Date, UITypes.DateTime].includes(
fieldUiType,
)
[
UITypes.SingleSelect,
UITypes.Collaborator,
UITypes.LinkToAnotherRecord,
UITypes.Date,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.DateTime,
].includes(fieldUiType)
) {
return 'is not'
}
@ -41,28 +53,28 @@ const getNotLikeText = (fieldUiType: UITypes) => {
}
const getGtText = (fieldUiType: UITypes) => {
if ([UITypes.Date, UITypes.DateTime].includes(fieldUiType)) {
if ([UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(fieldUiType)) {
return 'is after'
}
return '>'
}
const getLtText = (fieldUiType: UITypes) => {
if ([UITypes.Date, UITypes.DateTime].includes(fieldUiType)) {
if ([UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(fieldUiType)) {
return 'is before'
}
return '<'
}
const getGteText = (fieldUiType: UITypes) => {
if ([UITypes.Date, UITypes.DateTime].includes(fieldUiType)) {
if ([UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(fieldUiType)) {
return 'is on or after'
}
return '>='
}
const getLteText = (fieldUiType: UITypes) => {
if ([UITypes.Date, UITypes.DateTime].includes(fieldUiType)) {
if ([UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(fieldUiType)) {
return 'is on or before'
}
return '<='
@ -131,6 +143,8 @@ export const comparisonOpList = (
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.Time,
...numericUITypes,
],
@ -149,6 +163,8 @@ export const comparisonOpList = (
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.Time,
...numericUITypes,
],
@ -170,6 +186,8 @@ export const comparisonOpList = (
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.Time,
...numericUITypes,
],
@ -191,6 +209,8 @@ export const comparisonOpList = (
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.Time,
...numericUITypes,
],
@ -213,6 +233,8 @@ export const comparisonOpList = (
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.Time,
],
},
@ -234,6 +256,8 @@ export const comparisonOpList = (
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.Time,
],
},
@ -304,7 +328,14 @@ export const comparisonOpList = (
text: getLteText(fieldUiType),
value: 'lte',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
includedTypes: [
...numericUITypes,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
],
},
{
text: 'is within',

17
packages/nocodb-sdk/src/lib/UITypes.ts

@ -1,4 +1,5 @@
import { ColumnReqType, ColumnType } from './Api';
import { FormulaDataTypes } from './formulaHelpers';
enum UITypes {
ID = 'ID',
@ -185,4 +186,20 @@ export function isLinksOrLTAR(
);
}
export const getEquivalentUIType = ({
formulaColumn,
}: {
formulaColumn: ColumnType;
}): void | UITypes => {
switch ((formulaColumn?.colOptions as any)?.parsed_tree?.dataType) {
case FormulaDataTypes.NUMERIC:
return UITypes.Number;
case FormulaDataTypes.DATE:
return UITypes.DateTime;
case FormulaDataTypes.LOGICAL:
case FormulaDataTypes.BOOLEAN:
return UITypes.Checkbox;
}
};
export default UITypes;

1
packages/nocodb-sdk/src/lib/index.ts

@ -17,6 +17,7 @@ export {
isCreatedOrLastModifiedTimeCol,
isCreatedOrLastModifiedByCol,
isHiddenCol,
getEquivalentUIType,
} from '~/lib/UITypes';
export { default as CustomAPI, FileType } from '~/lib/CustomAPI';
export { default as TemplateGenerator } from '~/lib/TemplateGenerator';

6
packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts

@ -915,10 +915,6 @@ export class SqliteUi {
static getUnsupportedFnList() {
return [
'LOG',
'EXP',
'POWER',
'SQRT',
'XOR',
'REGEX_MATCH',
'REGEX_EXTRACT',
@ -926,8 +922,6 @@ export class SqliteUi {
'VALUE',
'COUNTA',
'COUNT',
'ROUNDDOWN',
'ROUNDUP',
'DATESTR',
'DAY',
'MONTH',

1
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -427,7 +427,6 @@ class BaseModelSqlv2 {
const proto = await this.getProto();
let data;
try {
data = await this.execAndParse(qb);
} catch (e) {

82
packages/nocodb/src/db/conditionV2.ts

@ -1,5 +1,6 @@
import {
FormulaDataTypes,
getEquivalentUIType,
isDateMonthFormat,
isNumericCol,
RelationTypes,
@ -568,12 +569,18 @@ const parseConditionV2 = async (
return (qb: Knex.QueryBuilder) => {
let [field, val] = [_field, _val];
// based on custom where clause(builder), we need to change the field and val
// todo: refactor this to use a better approach to make it more readable and clean
let genVal = customWhereClause ? field : val;
const dateFormat =
qb?.client?.config?.client === 'mysql2'
? 'YYYY-MM-DD HH:mm:ss'
: 'YYYY-MM-DD HH:mm:ssZ';
if (
(column.uidt === UITypes.Formula &&
getEquivalentUIType({ formulaColumn: column }) ==
UITypes.DateTime) ||
[
UITypes.Date,
UITypes.DateTime,
@ -586,82 +593,91 @@ const parseConditionV2 = async (
if (dateFormatFromMeta && isDateMonthFormat(dateFormatFromMeta)) {
// reset to 1st
now = dayjs(now).date(1);
if (val) val = dayjs(val).date(1);
if (val) genVal = dayjs(val).date(1);
}
// handle sub operation
switch (filter.comparison_sub_op) {
case 'today':
val = now;
genVal = now;
break;
case 'tomorrow':
val = now.add(1, 'day');
genVal = now.add(1, 'day');
break;
case 'yesterday':
val = now.add(-1, 'day');
genVal = now.add(-1, 'day');
break;
case 'oneWeekAgo':
val = now.add(-1, 'week');
genVal = now.add(-1, 'week');
break;
case 'oneWeekFromNow':
val = now.add(1, 'week');
genVal = now.add(1, 'week');
break;
case 'oneMonthAgo':
val = now.add(-1, 'month');
genVal = now.add(-1, 'month');
break;
case 'oneMonthFromNow':
val = now.add(1, 'month');
genVal = now.add(1, 'month');
break;
case 'daysAgo':
if (!val) return;
val = now.add(-val, 'day');
genVal = now.add(-genVal, 'day');
break;
case 'daysFromNow':
if (!val) return;
val = now.add(val, 'day');
genVal = now.add(genVal, 'day');
break;
case 'exactDate':
if (!val) return;
if (!genVal) return;
break;
// sub-ops for `isWithin` comparison
case 'pastWeek':
val = now.add(-1, 'week');
genVal = now.add(-1, 'week');
break;
case 'pastMonth':
val = now.add(-1, 'month');
genVal = now.add(-1, 'month');
break;
case 'pastYear':
val = now.add(-1, 'year');
genVal = now.add(-1, 'year');
break;
case 'nextWeek':
val = now.add(1, 'week');
genVal = now.add(1, 'week');
break;
case 'nextMonth':
val = now.add(1, 'month');
genVal = now.add(1, 'month');
break;
case 'nextYear':
val = now.add(1, 'year');
genVal = now.add(1, 'year');
break;
case 'pastNumberOfDays':
if (!val) return;
val = now.add(-val, 'day');
genVal = now.add(-genVal, 'day');
break;
case 'nextNumberOfDays':
if (!val) return;
val = now.add(val, 'day');
if (!genVal) return;
genVal = now.add(genVal, 'day');
break;
}
if (dayjs.isDayjs(val)) {
if (dayjs.isDayjs(genVal)) {
// turn `val` in dayjs object format to string
val = val.format(dateFormat).toString();
genVal = genVal.format(dateFormat).toString();
// keep YYYY-MM-DD only for date
val = column.uidt === UITypes.Date ? val.substring(0, 10) : val;
genVal =
column.uidt === UITypes.Date ? genVal.substring(0, 10) : genVal;
}
}
if (isNumericCol(column.uidt) && typeof val === 'string') {
if (isNumericCol(column.uidt) && typeof genVal === 'string') {
// convert to number
val = +val;
genVal = +genVal;
}
// if customWhereClause(builder) is provided, replace field with raw value
// or assign value to val
if (customWhereClause) {
field = knex.raw('?', [genVal]);
} else {
val = genVal;
}
switch (filter.comparison_op) {
@ -681,6 +697,9 @@ const parseConditionV2 = async (
) {
qb = qb.where(field, val);
} else if (
(column.uidt === UITypes.Formula &&
getEquivalentUIType({ formulaColumn: column }) ==
UITypes.DateTime) ||
column.ct === 'timestamp' ||
column.ct === 'date' ||
column.ct === 'datetime'
@ -692,6 +711,9 @@ const parseConditionV2 = async (
}
} else {
if (
(column.uidt === UITypes.Formula &&
getEquivalentUIType({ formulaColumn: column }) ==
UITypes.DateTime) ||
[
UITypes.DateTime,
UITypes.CreatedTime,
@ -1038,18 +1060,24 @@ const parseConditionV2 = async (
case 'isWithin': {
let now = dayjs(new Date()).format(dateFormat).toString();
now = column.uidt === UITypes.Date ? now.substring(0, 10) : now;
// switch between arg based on customWhereClause(builder)
const [firstArg, rangeArg] = [
customWhereClause ? val : field,
customWhereClause ? field : val,
];
switch (filter.comparison_sub_op) {
case 'pastWeek':
case 'pastMonth':
case 'pastYear':
case 'pastNumberOfDays':
qb = qb.whereBetween(field, [val, now]);
qb = qb.whereBetween(firstArg, [rangeArg, now]);
break;
case 'nextWeek':
case 'nextMonth':
case 'nextYear':
case 'nextNumberOfDays':
qb = qb.whereBetween(field, [now, val]);
qb = qb.whereBetween(firstArg, [now, rangeArg]);
break;
}
}

4
packages/nocodb/src/db/sortV2.ts

@ -65,9 +65,11 @@ export default async function sortV2(
(
await column.getColOptions<FormulaColumn>()
).formula,
alias,
null,
model,
column,
{},
alias
)
).builder;
qb.orderBy(builder, sort.direction || 'asc', nulls);

Loading…
Cancel
Save