Browse Source

Merge pull request #6489 from nocodb/feat/6430-group-by

feat: Add group by support on virtual columns
pull/6431/head
Pranav C 12 months ago committed by GitHub
parent
commit
45acf476d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 69
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  2. 22
      packages/nc-gui/composables/useViewGroupBy.ts
  3. 214
      packages/nocodb/src/db/BaseModelSqlv2.ts
  4. 163
      packages/nocodb/src/db/generateBTLookupSelectQuery.ts
  5. 22
      packages/nocodb/tests/unit/factory/column.ts
  6. 132
      packages/nocodb/tests/unit/rest/tests/groupby.test.ts
  7. 16
      packages/nocodb/tests/unit/rest/tests/tableRow.test.ts
  8. 6
      packages/nocodb/tests/unit/rest/tests/viewRow.test.ts
  9. 4
      tests/playwright/startPlayWrightServer.sh
  10. 19
      tests/playwright/tests/db/general/groupCRUD.spec.ts

69
packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import { type ColumnType, UITypes } from 'nocodb-sdk'
import GroupIcon from '~icons/nc-icons/group'
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import {
ActiveViewInj,
IsLockedInj,
@ -8,11 +8,14 @@ import {
computed,
getSortDirectionOptions,
inject,
onMounted,
ref,
useMenuCloseOnEsc,
useMetas,
useNuxtApp,
useSmartsheetStoreOrThrow,
} from '#imports'
import GroupIcon from '~icons/nc-icons/group'
const groupingUidt = [
UITypes.SingleSelect,
@ -21,6 +24,10 @@ const groupingUidt = [
UITypes.Date,
UITypes.SingleLineText,
UITypes.Number,
UITypes.Rollup,
UITypes.Lookup,
UITypes.Links,
UITypes.Formula,
]
const meta = inject(MetaInj, ref())
@ -33,6 +40,8 @@ const { $e } = useNuxtApp()
const _groupBy = ref<{ fk_column_id?: string; sort: string; order: number }[]>([])
const { getMeta } = useMetas()
const groupBy = computed<{ fk_column_id?: string; sort: string; order: number }[]>(() => {
const tempGroupBy: { fk_column_id?: string; sort: string; order: number }[] = []
Object.values(gridViewCols.value).forEach((col) => {
@ -53,12 +62,19 @@ const groupedByColumnIds = computed(() => groupBy.value.map((g) => g.fk_column_i
const { eventBus } = useSmartsheetStoreOrThrow()
const { isMobileMode } = useGlobal()
const btLookups = ref([])
const fieldsToGroupBy = computed(() => {
const fields = meta.value?.columns || []
return fields.filter((field) => {
return groupingUidt.includes(field.uidt as UITypes)
if (!groupingUidt.includes(field.uidt as UITypes)) return false
if (field.uidt === UITypes.Lookup) {
return btLookups.value.includes(field.id)
}
return true
})
})
@ -144,6 +160,53 @@ watch(open, () => {
}
}
})
const loadBtLookups = async () => {
const filteredLookupCols = []
try {
for (const col of meta.value?.columns || []) {
if (col.uidt !== UITypes.Lookup) continue
let nextCol = col
let btLookup = true
// check all the relation of nested lookup columns is bt or not
// include the column only if all only if all relations are bt
while (btLookup && nextCol && nextCol.uidt === UITypes.Lookup) {
const lookupRelation = (await getMeta(nextCol.fk_model_id))?.columns?.find(
(c) => c.id === (nextCol.colOptions as LookupType).fk_relation_column_id,
)
if ((lookupRelation.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO) {
btLookup = false
continue
}
const relatedTableMeta = await getMeta((lookupRelation.colOptions as LinkToAnotherRecordType).fk_related_model_id)
nextCol = relatedTableMeta?.columns?.find(
(c) => c.id === (nextCol.colOptions as LinkToAnotherRecordType).fk_lookup_column_id,
)
// if next column is same as root lookup column then break the loop
// since it's going to be a circular loop, and ignore the column
if (nextCol.id === col.id) {
btLookup = false
break
}
}
if (btLookup) filteredLookupCols.push(col.id)
}
btLookups.value = filteredLookupCols
} catch (e) {
console.error(e)
}
}
onMounted(async () => {
await loadBtLookups()
})
</script>
<template>

22
packages/nc-gui/composables/useViewGroupBy.ts

@ -161,12 +161,10 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
if (group.nestedIn.length > groupBy.value.length) return
if (group.nestedIn.length === 0) nextGroupColor.value = colors.value[0]
const groupby = groupBy.value[group.nestedIn.length]
const nestedWhere = calculateNestedWhere(group.nestedIn, where?.value)
if (!groupby || !groupby.column.column_name) return
if (!groupby || !(groupby.column.title)) return
if (isPublic.value && !sharedView.value?.uuid) {
return
@ -181,7 +179,7 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
where: `${nestedWhere}`,
sort: `${groupby.sort === 'desc' ? '-' : ''}${groupby.column.title}`,
column_name: groupby.column.column_name,
column_name: groupby.column.title,
} as any)
: await api.public.dataGroupBy(sharedView.value!.uuid!, {
offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByLimit),
@ -189,30 +187,32 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
...params,
where: nestedWhere,
sort: `${groupby.sort === 'desc' ? '-' : ''}${groupby.column.title}`,
column_name: groupby.column.column_name,
column_name: groupby.column.title,
sortsArr: sorts.value,
filtersArr: nestedFilters.value,
})
const tempList: Group[] = response.list.reduce((acc: Group[], curr: Record<string, any>) => {
const keyExists = acc.find((a) => a.key === valueToTitle(curr[groupby.column.column_name!], groupby.column))
const keyExists = acc.find(
(a) => a.key === valueToTitle(curr[groupby.column.column_name!] ?? curr[groupby.column.title!], groupby.column),
)
if (keyExists) {
keyExists.count += +curr.count
keyExists.paginationData = { page: 1, pageSize: groupByLimit, totalRows: keyExists.count }
return acc
}
if (groupby.column.title && groupby.column.column_name && groupby.column.uidt) {
if (groupby.column.title && groupby.column.uidt) {
acc.push({
key: valueToTitle(curr[groupby.column.column_name!], groupby.column),
key: valueToTitle(curr[(groupby.column.title)!], groupby.column),
column: groupby.column,
count: +curr.count,
color: findKeyColor(curr[groupby.column.column_name!], groupby.column),
color: findKeyColor(curr[(groupby.column.title)!], groupby.column),
nestedIn: [
...group!.nestedIn,
{
title: groupby.column.title,
column_name: groupby.column.column_name!,
key: valueToTitle(curr[groupby.column.column_name!], groupby.column),
column_name: (groupby.column.title)!,
key: valueToTitle(curr[(groupby.column.title)!], groupby.column),
column_uidt: groupby.column.uidt,
},
],

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

@ -18,6 +18,7 @@ import Validator from 'validator';
import { customAlphabet } from 'nanoid';
import DOMPurify from 'isomorphic-dompurify';
import { v4 as uuidv4 } from 'uuid';
import type LookupColumn from '~/models/LookupColumn';
import type { Knex } from 'knex';
import type { XKnex } from '~/db/CustomKnex';
import type {
@ -34,6 +35,7 @@ import type {
SelectOption,
} from '~/models';
import type { SortType } from 'nocodb-sdk';
import generateBTLookupSelectQuery from '~/db/generateBTLookupSelectQuery';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import conditionV2 from '~/db/conditionV2';
@ -338,7 +340,6 @@ class BaseModelSqlv2 {
const proto = await this.getProto();
let data;
try {
data = await this.execAndParse(qb);
} catch (e) {
@ -498,21 +499,95 @@ class BaseModelSqlv2 {
args.column_name = args.column_name || '';
const groupByColumns = await this.model.getColumns().then((cols) =>
args.column_name.split(',').map((col) => {
const cols = await this.model.getColumns();
const groupByColumns: Record<string, Column> = {};
const selectors = [];
const groupBySelectors = [];
await Promise.all(
args.column_name.split(',').map(async (col) => {
const column = cols.find(
(c) => c.column_name === col || c.title === col,
);
groupByColumns[column.id] = column;
if (!column) {
throw NcError.notFound('Column not found');
}
return column.column_name;
switch (column.uidt) {
case UITypes.Links:
case UITypes.Rollup:
selectors.push(
(
await genRollupSelectv2({
baseModelSqlv2: this,
knex: this.dbDriver,
columnOptions: (await column.getColOptions()) as RollupColumn,
})
).builder.as(sanitize(column.title)),
);
groupBySelectors.push(sanitize(column.title));
break;
case UITypes.Formula:
{
let selectQb;
try {
const _selectQb = await this.getSelectQueryBuilderForFormula(
column,
);
selectQb = this.dbDriver.raw(`?? as ??`, [
_selectQb.builder,
sanitize(column.title),
]);
} catch (e) {
console.log(e);
// return dummy select
selectQb = this.dbDriver.raw(`'ERR' as ??`, [
sanitize(column.title),
]);
}
selectors.push(selectQb);
groupBySelectors.push(column.title);
}
break;
case UITypes.Lookup:
{
const _selectQb = await generateBTLookupSelectQuery({
baseModelSqlv2: this,
column,
alias: null,
model: this.model,
});
const selectQb = this.dbDriver.raw(`?? as ??`, [
this.dbDriver.raw(_selectQb.builder).wrap('(', ')'),
sanitize(column.title),
]);
selectors.push(selectQb);
groupBySelectors.push(sanitize(column.title));
}
break;
default:
selectors.push(
this.dbDriver.raw('?? as ??', [column.column_name, column.title]),
);
groupBySelectors.push(sanitize(column.title));
break;
}
}),
);
const qb = this.dbDriver(this.tnPath);
// get aggregated count of each group
qb.count(`${this.model.primaryKey?.column_name || '*'} as count`);
qb.select(...groupByColumns);
// get each group
qb.select(...selectors);
if (+rest?.shuffle) {
await this.shuffle({ qb });
@ -549,14 +624,28 @@ class BaseModelSqlv2 {
qb,
);
qb.groupBy(...groupByColumns);
if (!sorts)
sorts = args.sortArr?.length
? args.sortArr
: await Sort.list({ viewId: this.viewId });
if (sorts) await sortV2(this, sorts, qb);
// if sort is provided filter out the group by columns sort and apply
// since we are grouping by the column and applying sort on any other column is not required
for (const sort of sorts) {
if (!groupByColumns[sort.fk_column_id]) {
continue;
}
qb.orderBy(
groupByColumns[sort.fk_column_id].title,
sort.direction,
sort.direction === 'desc' ? 'LAST' : 'FIRST',
);
}
// group by using the column aliases
qb.groupBy(...groupBySelectors);
applyPaginate(qb, rest);
return await qb;
}
@ -567,29 +656,99 @@ class BaseModelSqlv2 {
limit?;
offset?;
filterArr?: Filter[];
// skip sort for count
// sort?: string | string[];
// sortArr?: Sort[];
}) {
const { where, ..._rest } = this._getListArgs(args as any);
const { where } = this._getListArgs(args as any);
args.column_name = args.column_name || '';
const groupByColumns = await this.model.getColumns().then((cols) =>
args.column_name.split(',').map((col) => {
const column = cols.find(
(c) => c.column_name === col || c.title === col,
);
if (!column) {
throw NcError.notFound('Column not found');
}
return column.column_name;
}),
const selectors = [];
const groupBySelectors = [];
await this.model.getColumns().then((cols) =>
Promise.all(
args.column_name.split(',').map(async (col) => {
const column = cols.find(
(c) => c.column_name === col || c.title === col,
);
if (!column) {
throw NcError.notFound('Column not found');
}
switch (column.uidt) {
case UITypes.Rollup:
case UITypes.Links:
selectors.push(
(
await genRollupSelectv2({
baseModelSqlv2: this,
// tn: this.title,
knex: this.dbDriver,
// column,
// alias,
columnOptions:
(await column.getColOptions()) as RollupColumn,
})
).builder.as(sanitize(column.title)),
);
groupBySelectors.push(sanitize(column.title));
break;
case UITypes.Formula:
let selectQb;
try {
const _selectQb = await this.getSelectQueryBuilderForFormula(
column,
);
selectQb = this.dbDriver.raw(`?? as ??`, [
_selectQb.builder,
sanitize(column.title),
]);
} catch (e) {
console.log(e);
// return dummy select
selectQb = this.dbDriver.raw(`'ERR' as ??`, [
sanitize(column.title),
]);
}
selectors.push(selectQb);
groupBySelectors.push(column.title);
break;
case UITypes.Lookup:
{
const _selectQb = await generateBTLookupSelectQuery({
baseModelSqlv2: this,
column,
alias: null,
model: this.model,
});
const selectQb = this.dbDriver.raw(`?? as ??`, [
this.dbDriver.raw(_selectQb.builder).wrap('(', ')'),
sanitize(column.title),
]);
selectors.push(selectQb);
groupBySelectors.push(sanitize(column.title));
}
break;
default:
selectors.push(
this.dbDriver.raw('?? as ??', [
column.column_name,
column.title,
]),
);
groupBySelectors.push(sanitize(column.title));
break;
}
}),
),
);
const qb = this.dbDriver(this.tnPath);
qb.count(`${this.model.primaryKey?.column_name || '*'} as count`);
qb.select(...groupByColumns);
qb.select(...selectors);
const aliasColObjMap = await this.model.getAliasColObjMap();
@ -620,11 +779,12 @@ class BaseModelSqlv2 {
qb,
);
qb.groupBy(...groupByColumns);
qb.groupBy(...groupBySelectors);
const qbP = this.dbDriver
.count('*', { as: 'count' })
.from(qb.as('groupby'));
return (await qbP.first())?.count;
}
@ -4497,14 +4657,16 @@ export function extractSortsObject(
let sorts = _sorts;
if (!Array.isArray(sorts)) sorts = sorts.split(',');
if (!Array.isArray(sorts)) sorts = sorts.split(/\s*,\s*/);
return sorts.map((s) => {
const sort: SortType = { direction: 'asc' };
if (s.startsWith('-')) {
sort.direction = 'desc';
sort.fk_column_id = aliasColObjMap[s.slice(1)]?.id;
} else sort.fk_column_id = aliasColObjMap[s]?.id;
}
// replace + at the beginning if present
else sort.fk_column_id = aliasColObjMap[s.replace(/^\+/, '')]?.id;
return new Sort(sort);
});

163
packages/nocodb/src/db/generateBTLookupSelectQuery.ts

@ -0,0 +1,163 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
import type LookupColumn from '../models/LookupColumn';
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
import type {
Column,
FormulaColumn,
LinkToAnotherRecordColumn,
Model,
RollupColumn,
} from '~/models';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import { NcError } from '~/helpers/catchError';
export default async function generateBTLookupSelectQuery({
column,
baseModelSqlv2,
alias,
model,
}: {
column: Column;
baseModelSqlv2: BaseModelSqlv2;
alias: string;
model: Model;
}): Promise<any> {
const knex = baseModelSqlv2.dbDriver;
const rootAlias = alias;
{
let aliasCount = 0,
selectQb;
const alias = `__nc_lk_${aliasCount++}`;
const lookup = await column.getColOptions<LookupColumn>();
{
const relationCol = await lookup.getRelationColumn();
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if not belongs to then throw error as we don't support
if (relation.type !== RelationTypes.BELONGS_TO)
NcError.badRequest('HasMany/ManyToMany lookup is not supported');
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb = knex(
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`,
).where(
`${alias}.${parentColumn.column_name}`,
knex.raw(`??`, [
`${rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)}.${
childColumn.column_name
}`,
]),
);
}
let lookupColumn = await lookup.getLookupColumn();
let prevAlias = alias;
while (lookupColumn.uidt === UITypes.Lookup) {
const nestedAlias = `__nc_lk_nested_${aliasCount++}`;
const nestedLookup = await lookupColumn.getColOptions<LookupColumn>();
const relationCol = await nestedLookup.getRelationColumn();
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if any of the relation in nested lookup is
// not belongs to then throw error as we don't support
if (relation.type !== RelationTypes.BELONGS_TO)
NcError.badRequest('HasMany/ManyToMany lookup is not supported');
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb.join(
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${nestedAlias}`,
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`,
);
lookupColumn = await nestedLookup.getLookupColumn();
prevAlias = nestedAlias;
}
switch (lookupColumn.uidt) {
case UITypes.Links:
case UITypes.Rollup:
{
const builder = (
await genRollupSelectv2({
baseModelSqlv2,
knex,
columnOptions:
(await lookupColumn.getColOptions()) as RollupColumn,
alias: prevAlias,
})
).builder;
selectQb.select(builder);
}
break;
case UITypes.LinkToAnotherRecord:
{
const nestedAlias = `__nc_sort${aliasCount++}`;
const relation =
await lookupColumn.getColOptions<LinkToAnotherRecordColumn>();
if (relation.type !== 'bt') return;
const colOptions =
(await column.getColOptions()) as LinkToAnotherRecordColumn;
const childColumn = await colOptions.getChildColumn();
const parentColumn = await colOptions.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb
.join(
`${baseModelSqlv2.getTnPath(
parentModel.table_name,
)} as ${nestedAlias}`,
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`,
)
.select(parentModel?.displayValue?.column_name);
}
break;
case UITypes.Formula:
{
const builder = (
await formulaQueryBuilderv2(
baseModelSqlv2,
(
await column.getColOptions<FormulaColumn>()
).formula,
null,
model,
column,
)
).builder;
selectQb.select(builder);
}
break;
default:
{
selectQb.select(`${prevAlias}.${lookupColumn.column_name}`);
}
break;
}
return { builder: selectQb };
}
}

22
packages/nocodb/tests/unit/factory/column.ts

@ -233,12 +233,14 @@ const createLookupColumn = async (
table,
relatedTableName,
relatedTableColumnTitle,
relationColumnId,
}: {
project: Project;
title: string;
table: Model;
relatedTableName: string;
relatedTableColumnTitle: string;
relationColumnId?: string;
},
) => {
const childBases = await project.getBases();
@ -258,12 +260,20 @@ const createLookupColumn = async (
);
}
const ltarColumn = (await table.getColumns()).find(
(column) =>
(column.uidt === UITypes.Links ||
column.uidt === UITypes.LinkToAnotherRecord) &&
column.colOptions?.fk_related_model_id === childTable.id,
);
let ltarColumn;
if (relationColumnId)
ltarColumn = (await table.getColumns()).find(
(column) => column.id === relationColumnId,
);
else {
ltarColumn = (await table.getColumns()).find(
(column) =>
(column.uidt === UITypes.Links ||
column.uidt === UITypes.LinkToAnotherRecord) &&
column.colOptions?.fk_related_model_id === childTable.id,
);
}
const lookupColumn = await createColumn(context, table, {
title: title,
uidt: UITypes.Lookup,

132
packages/nocodb/tests/unit/rest/tests/groupby.test.ts

@ -1,9 +1,14 @@
import { UITypes } from 'nocodb-sdk';
import request from 'supertest';
import { assert, expect } from 'chai';
import { createColumn, createLookupColumn } from '../../factory/column';
import { createProject, createSakilaProject } from '../../factory/project';
import { listRow } from '../../factory/row';
import { getTable } from '../../factory/table';
import { getView, updateView } from '../../factory/view';
import init from '../../init';
import type { Column, Model, Project, View } from '../../../../src/models';
import 'mocha';
function groupByTests() {
let context;
@ -234,6 +239,133 @@ function groupByTests() {
)
throw new Error('Invalid GroupBy With Filters');
});
it('Check One GroupBy Column with Links/Rollup', async function () {
const actorsColumn = filmColumns.find((c) => c.title === 'Actors');
const response = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/groupby`)
.set('xc-auth', context.token)
.query({
column_name: actorsColumn.title,
sort: `-${actorsColumn.title}`,
})
.expect(200);
expect(response.body.list[0]['Actors']).not.equal('0');
expect(response.body.list[0]['count']).not.equal('10');
expect(+response.body.list[0]['Actors']).to.be.gte(
+response.body.list[1]['Actors'],
);
});
it('Check One GroupBy Column with BT Lookup', async function () {
// get the row list and extract the correct language column name which have the values
// this is to avoid issue since there is 2 language column
const rows = await listRow({
table: filmTable,
project: sakilaProject,
options: {
limit: 1,
offset: 0,
},
});
const language = await rows[0]['Language']();
const ltarColumn = filmColumns.find(
(c) => c.title === (language ? 'Language' : 'Language1'),
);
await createLookupColumn(context, {
project: sakilaProject,
title: 'LanguageName',
table: filmTable,
relatedTableName: 'language',
relatedTableColumnTitle: 'Name',
relationColumnId: ltarColumn.id,
});
const response = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/groupby`)
.set('xc-auth', context.token)
.query({
column_name: 'LanguageName',
sort: `-LanguageName`,
})
.expect(200);
assert.match(response.body.list[0]['LanguageName'], /^English/);
expect(+response.body.list[0]['count']).to.gt(0);
expect(response.body.list.length).to.equal(1);
});
it('Check One GroupBy Column with MM Lookup which is not supported', async function () {
await createLookupColumn(context, {
project: sakilaProject,
title: 'ActorNames',
table: filmTable,
relatedTableName: 'actor',
relatedTableColumnTitle: 'FirstName',
});
const res = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/groupby`)
.set('xc-auth', context.token)
.query({
column_name: 'ActorNames',
})
.expect(400);
assert.match(res.body.msg, /not supported/);
});
it('Check One GroupBy Column with Formula and Formula referring another formula', async function () {
const formulaColumnTitle = 'Formula';
await createColumn(context, filmTable, {
uidt: UITypes.Formula,
title: formulaColumnTitle,
formula: `ADD({RentalDuration}, 10)`,
});
const res = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/groupby`)
.set('xc-auth', context.token)
.query({
column_name: formulaColumnTitle,
sort: `-${formulaColumnTitle}`,
})
.expect(200);
expect(res.body.list[0][formulaColumnTitle]).to.be.gte(
res.body.list[0][formulaColumnTitle],
);
expect(+res.body.list[0].count).to.gte(1);
// generate a formula column which refers to another formula column
const nestedFormulaColumnTitle = 'FormulaNested';
await createColumn(context, filmTable, {
uidt: UITypes.Formula,
title: nestedFormulaColumnTitle,
formula: `ADD(1000,{${formulaColumnTitle}})`,
});
const res1 = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/groupby`)
.set('xc-auth', context.token)
.query({
column_name: nestedFormulaColumnTitle,
sort: `-${nestedFormulaColumnTitle}`,
})
.expect(200);
expect(res1.body.list[0][nestedFormulaColumnTitle]).to.be.gte(
res1.body.list[0][nestedFormulaColumnTitle],
);
expect(res1.body.list[0][nestedFormulaColumnTitle]).to.be.gte(1000);
expect(+res1.body.list[0][nestedFormulaColumnTitle]).to.equal(
1000 + +res.body.list[0][formulaColumnTitle],
);
expect(+res1.body.list[res1.body.list.length - 1].count).to.gte(0);
});
}
export default function () {

16
packages/nocodb/tests/unit/rest/tests/tableRow.test.ts

@ -1485,7 +1485,7 @@ function tableTest() {
});
const visibleColumns = [firstNameColumn];
const sortInfo = `-FirstName, +${rollupColumn.title}`;
const sortInfo = `-FirstName`;
const response = await request(context.app)
.get(
@ -1609,7 +1609,7 @@ function tableTest() {
});
const visibleColumns = [firstNameColumn];
const sortInfo = `-FirstName, +${rollupColumn.title}`;
const sortInfo = `-FirstName`;
const response = await request(context.app)
.get(
@ -1619,18 +1619,18 @@ function tableTest() {
.query({
fields: visibleColumns.map((c) => c.title),
sort: sortInfo,
column_name: firstNameColumn.column_name,
column_name: firstNameColumn.title,
})
.expect(200);
if (
response.body.list[4]['first_name'] !== 'WILLIE' ||
response.body.list[4][firstNameColumn.title] !== 'WILLIE' ||
parseInt(response.body.list[4]['count']) !== 2
)
throw new Error('Wrong groupby');
});
it('Groupby desc sorted and with rollup table data list with required columns', async function () {
it('Groupby desc sorted and with rollup tabl e data list with required columns', async function () {
const firstNameColumn = customerColumns.find(
(col) => col.title === 'FirstName',
);
@ -1645,7 +1645,7 @@ function tableTest() {
});
const visibleColumns = [firstNameColumn];
const sortInfo = `-FirstName, +${rollupColumn.title}`;
const sortInfo = `-FirstName`;
const response = await request(context.app)
.get(
@ -1655,13 +1655,13 @@ function tableTest() {
.query({
fields: visibleColumns.map((c) => c.title),
sort: sortInfo,
column_name: firstNameColumn.column_name,
column_name: firstNameColumn.title,
offset: 4,
})
.expect(200);
if (
response.body.list[0]['first_name'] !== 'WILLIE' ||
response.body.list[0][firstNameColumn.title] !== 'WILLIE' ||
parseInt(response.body.list[0]['count']) !== 2
)
throw new Error('Wrong groupby');

6
packages/nocodb/tests/unit/rest/tests/viewRow.test.ts

@ -940,7 +940,7 @@ function viewRowTests() {
.expect(200);
if (
response.body.list[4]['first_name'] !== 'WILLIE' ||
response.body.list[4]['FirstName'] !== 'WILLIE' ||
parseInt(response.body.list[4]['count']) !== 2
)
throw new Error('Wrong groupby');
@ -986,13 +986,13 @@ function viewRowTests() {
.query({
fields: visibleColumns.map((c) => c.title),
sort: sortInfo,
column_name: firstNameColumn.column_name,
column_name: firstNameColumn.title,
offset: 4,
})
.expect(200);
if (
response.body.list[0]['first_name'] !== 'WILLIE' ||
response.body.list[0]['FirstName'] !== 'WILLIE' ||
parseInt(response.body.list[0]['count']) !== 2
)
throw new Error('Wrong groupby');

4
tests/playwright/startPlayWrightServer.sh

@ -2,10 +2,10 @@
if ! curl --output /dev/null --silent --head --fail http://localhost:31000
then
echo "Starting PlayWright Server"
PWDEBUG=console pnpm dlx playwright run-server --port 31000 &
PWDEBUG=console pnpm exec playwright run-server --port 31000 &
# Wait for server to start
while ! curl --output /dev/null --silent --head --fail http://localhost:31000; do
sleep 0.2
done
fi
fi

19
tests/playwright/tests/db/general/groupCRUD.spec.ts

@ -30,7 +30,7 @@ test.describe('GroupBy CRUD Operations', () => {
let context: any;
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: true });
context = await setup({ page, isEmptyProject: false });
dashboard = new DashboardPage(page, context.project);
toolbar = dashboard.grid.toolbar;
topbar = dashboard.grid.topbar;
@ -248,4 +248,21 @@ test.describe('GroupBy CRUD Operations', () => {
value: 'Zzzzzzzzzzzzzzzzzzz',
});
});
test('Single GroupBy CRUD Operations - Links', async ({ page }) => {
await dashboard.treeView.openTable({ title: 'Film' });
await toolbar.clickGroupBy();
await toolbar.groupBy.add({ title: 'Actors', ascending: false, locallySaved: false });
await dashboard.grid.groupPage.openGroup({ indexMap: [2] });
await dashboard.grid.groupPage.validateFirstRow({
indexMap: [2],
rowIndex: 0,
columnHeader: 'Title',
value: 'ARABIA DOGMA',
});
});
});

Loading…
Cancel
Save