Browse Source

chore: sync with develop

pull/5642/head
Wing-Kam Wong 2 years ago
parent
commit
ccc55dfdac
  1. 1
      .gitignore
  2. 42
      build-local-docker-image.sh
  3. 2
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  4. 2
      packages/nc-gui/components/webhook/Editor.vue
  5. 9
      packages/nc-gui/composables/useViewFilters.ts
  6. 3
      packages/nocodb/.gitignore
  7. 55
      packages/nocodb/Dockerfile.local
  8. 2
      packages/nocodb/src/Noco.ts
  9. 16
      packages/nocodb/src/db/BaseModelSqlv2.ts
  10. 85
      packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts
  11. 5
      packages/nocodb/src/models/Model.ts
  12. 2
      packages/nocodb/src/modules/event-emitter/fallback-event-emitter.ts
  13. 4
      packages/nocodb/src/modules/event-emitter/nestjs-event-emitter.ts
  14. 2
      packages/nocodb/src/modules/metas/metas.module.ts
  15. 18
      packages/nocodb/src/run/local.ts
  16. 3
      packages/nocodb/src/services/hook-handler.service.spec.ts
  17. 6
      packages/nocodb/src/services/hook-handler.service.ts
  18. 1
      tests/playwright/pages/Dashboard/Grid/index.ts
  19. 16
      tests/playwright/pages/Dashboard/WebhookForm/index.ts
  20. 22
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  21. 115
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  22. 26
      tests/playwright/tests/db/filters.spec.ts
  23. 6
      tests/playwright/tests/db/undo-redo.spec.ts

1
.gitignore vendored

@ -38,6 +38,7 @@ _site/
*.pid
*.gz
*.tmp
*.tmp.md
*.bak
*.swp
logs/

42
build-local-docker-image.sh

@ -0,0 +1,42 @@
#!/bin/bash
# script to build local docker image.
# highlevel steps involved
# 1. build nocodb-sdk
# 2. build nc-gui
# 2a. static build of nc-gui
# 2b. copy nc-gui build to nocodb dir
# 3. build nocodb
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
#build nocodb-sdk
echo "Building nocodb-sdk"
cd ${SCRIPT_DIR}/packages/nocodb-sdk
npm ci
npm run build
# build nc-gui
echo "Building nc-gui"
export NODE_OPTIONS="--max_old_space_size=16384"
# generate static build of nc-gui
cd ${SCRIPT_DIR}/packages/nc-gui
npm ci
npm run generate
# copy nc-gui build to nocodb dir
rsync -rvzh --delete ./dist/ ${SCRIPT_DIR}/packages/nocodb/docker/nc-gui/
#build nocodb
# build nocodb ( pack nocodb-sdk and nc-gui )
cd ${SCRIPT_DIR}/packages/nocodb
npm install
EE=true ./node_modules/.bin/webpack --config webpack.local.config.js
# remove nocodb-sdk since it's packed with the build
npm uninstall --save nocodb-sdk
# build docker
docker build . -f Dockerfile.local -t nocodb-local
echo 'docker image with tag "nocodb-local" built sussessfully. Use below sample command to run the container'
echo 'docker run -d -p 3333:8080 --name nocodb-local nocodb-local '

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

@ -64,6 +64,7 @@ const {
() => reloadDataHook.trigger(showLoading),
modelValue || nestedFilters.value,
!modelValue,
webHook,
)
const localNestedFilters = ref()
@ -249,6 +250,7 @@ defineExpose({
:parent-id="filter.id"
nested
:auto-save="autoSave"
:web-hook="webHook"
/>
</div>
</template>

2
packages/nc-gui/components/webhook/Editor.vue

@ -710,7 +710,7 @@ onMounted(async () => {
:auto-save="false"
:show-loading="false"
:hook-id="hook.id"
web-hook
:web-hook="true"
/>
</a-card>
</a-col>

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

@ -29,6 +29,7 @@ export function useViewFilters(
reloadData?: () => void,
_currentFilters?: Filter[],
isNestedRoot?: boolean,
isWebhook?: boolean,
) {
let currentFilters = $ref(_currentFilters)
@ -238,7 +239,7 @@ export function useViewFilters(
}
}
reloadData?.()
if (!isWebhook) reloadData?.()
} catch (e: any) {
console.log(e)
message.error(await extractSdkResponseErrorMsg(e))
@ -308,7 +309,7 @@ export function useViewFilters(
lastFilters.value = clone(filters.value)
reloadData?.()
if (!isWebhook) reloadData?.()
}
const deleteFilter = async (filter: Filter, i: number, undo = false) => {
@ -335,7 +336,7 @@ export function useViewFilters(
if (nestedMode.value) {
filters.value.splice(i, 1)
filters.value = [...filters.value]
reloadData?.()
if (!isWebhook) reloadData?.()
} else {
if (filter.id) {
// if auto-apply disabled mark it as disabled
@ -346,7 +347,7 @@ export function useViewFilters(
} else {
try {
await $api.dbTableFilter.delete(filter.id)
reloadData?.()
if (!isWebhook) reloadData?.()
filters.value.splice(i, 1)
} catch (e: any) {
console.log(e)

3
packages/nocodb/.gitignore vendored

@ -41,4 +41,7 @@ export/**
# test dbs
test_sakila_?.db
# ignoring to avoid commiting those files
# nc-gui dir is copied for building local docker.
/docker/main.js
/docker/nc-gui

55
packages/nocodb/Dockerfile.local

@ -0,0 +1,55 @@
###########
# Builder
###########
FROM node:16.17.0-alpine3.15 as builder
WORKDIR /usr/src/app
# install node-gyp dependencies
RUN apk add --no-cache python3 make g++
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure both package.json AND package-lock.json are copied.
# Copying this separately prevents re-running npm ci on every code change.
COPY ./package*.json ./
COPY ./docker/nc-gui/ ./docker/nc-gui/
COPY ./docker/main.js ./docker/index.js
COPY ./docker/start-local.sh /usr/src/appEntry/start.sh
COPY ./public/css/*.css ./docker/public/css/
COPY ./public/js/*.js ./docker/public/js/
COPY ./public/favicon.ico ./docker/public/
# install production dependencies,
# reduce node_module size with modclean & removing sqlite deps,
# package built code into app.tar.gz & add execute permission to start.sh
RUN npm ci --omit=dev --quiet \
&& npx modclean --patterns="default:*" --ignore="nc-lib-gui/**,dayjs/**,express-status-monitor/**,@azure/msal-node/dist/**" --run \
&& rm -rf ./node_modules/sqlite3/deps \
&& tar -czf ../appEntry/app.tar.gz ./* \
&& chmod +x /usr/src/appEntry/start.sh
##########
# Runner
##########
FROM alpine:3.15
WORKDIR /usr/src/app
ENV NC_DOCKER 0.6
ENV NODE_ENV production
ENV PORT 8080
ENV NC_TOOL_DIR=/usr/app/data/
RUN apk --update --no-cache add \
nodejs \
tar \
dumb-init \
curl \
jq
# Copy packaged production code & main entry file
COPY --from=builder /usr/src/appEntry/ /usr/src/appEntry/
EXPOSE 8080
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
# Start Nocodb
CMD ["/usr/src/appEntry/start.sh"]

2
packages/nocodb/src/Noco.ts

@ -8,9 +8,9 @@ import { AppModule } from './app.module';
import { NC_LICENSE_KEY } from './constants';
import Store from './models/Store';
import type { IEventEmitter } from './modules/event-emitter/event-emitter.interface';
import type { Express } from 'express';
import type * as http from 'http';
import { IEventEmitter } from './modules/event-emitter/event-emitter.interface'
export default class Noco {
private static _this: Noco;

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

@ -12,7 +12,6 @@ import {
isVirtualCol,
RelationTypes,
UITypes,
ViewTypes,
} from 'nocodb-sdk';
import Validator from 'validator';
import { customAlphabet } from 'nanoid';
@ -20,17 +19,7 @@ import DOMPurify from 'isomorphic-dompurify';
import { v4 as uuidv4 } from 'uuid';
import { NcError } from '../helpers/catchError';
import getAst from '../helpers/getAst';
import {
Audit,
Base,
Column,
Filter,
Model,
Project,
Sort,
View,
} from '../models';
import { Audit, Column, Filter, Model, Project, Sort, View } from '../models';
import { sanitize, unsanitize } from '../helpers/sqlSanitize';
import {
COMPARISON_OPS,
@ -2575,8 +2564,8 @@ class BaseModelSqlv2 {
modelId: this.model.id,
tnPath: this.tnPath,
});
/*
/*
const view = await View.get(this.viewId);
// handle form view data submission
@ -3478,7 +3467,6 @@ function validateFilterComparison(uidt: UITypes, op: any, sub_op?: any) {
function extractCondition(nestedArrayConditions, aliasColObjMap) {
return nestedArrayConditions?.map((str) => {
// eslint-disable-next-line prefer-const
let [logicOp, alias, op, value] =
str.match(/(?:~(and|or|not))?\((.*?),(\w+),(.*)\)/)?.slice(1) || [];

85
packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts

@ -200,7 +200,7 @@ class SqliteClient extends KnexClient {
table.integer('status').nullable();
table.dateTime('created');
table.timestamps();
}
},
);
log.debug('Table created:', `${args.tn}`, data);
} else {
@ -295,7 +295,7 @@ class SqliteClient extends KnexClient {
try {
const response = await this.sqlClient.raw(
`SELECT name as tn FROM sqlite_master where type = 'table'`
`SELECT name as tn FROM sqlite_master where type = 'table'`,
);
result.data.list = [];
@ -359,7 +359,7 @@ class SqliteClient extends KnexClient {
try {
const response = await this.sqlClient.raw(
`PRAGMA table_info("${args.tn}")`
`PRAGMA table_info("${args.tn}")`,
);
const triggerList = (await this.triggerList(args)).data.list;
@ -420,7 +420,8 @@ class SqliteClient extends KnexClient {
response[i].dtxs = '';
response[i].au = !!triggerList.find(
({ trigger }) => trigger === `xc_trigger_${args.tn}_${response[i].cn}`
({ trigger }) =>
trigger === `xc_trigger_${args.tn}_${response[i].cn}`,
);
}
@ -466,7 +467,7 @@ class SqliteClient extends KnexClient {
// PRAGMA index_xinfo('idx_fk_original_language_id');
const response = await this.sqlClient.raw(
`PRAGMA index_list("${args.tn}")`
`PRAGMA index_list("${args.tn}")`,
);
const rows = [];
@ -478,7 +479,7 @@ class SqliteClient extends KnexClient {
response[i].unique = response[i].unique === 1 ? 1 : 0;
const colsInIndex = await this.sqlClient.raw(
`PRAGMA index_info('${response[i].key_name}')`
`PRAGMA index_info('${response[i].key_name}')`,
);
if (colsInIndex.length === 1) {
@ -531,7 +532,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`PRAGMA foreign_key_list('${args.tn}')`
`PRAGMA foreign_key_list('${args.tn}')`,
);
for (let i = 0; i < response.length; ++i) {
@ -582,7 +583,7 @@ class SqliteClient extends KnexClient {
for (let i = 0; i < tables.length; ++i) {
const response = await this.sqlClient.raw(
`PRAGMA foreign_key_list('${tables[i].tn}')`
`PRAGMA foreign_key_list('${tables[i].tn}')`,
);
for (let j = 0; j < response.length; ++j) {
@ -633,7 +634,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`select *, name as trigger_name from sqlite_master where type = 'trigger' and tbl_name='${args.tn}';`
`select *, name as trigger_name from sqlite_master where type = 'trigger' and tbl_name='${args.tn}';`,
);
for (let i = 0; i < response.length; ++i) {
@ -676,7 +677,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`show function status where db='${args.databaseName}'`
`show function status where db='${args.databaseName}'`,
);
if (response.length === 2) {
@ -730,7 +731,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`show procedure status where db='${args.databaseName}'`
`show procedure status where db='${args.databaseName}'`,
);
if (response.length === 2) {
@ -775,7 +776,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SELECT * FROM sqlite_master WHERE type = 'view'`
`SELECT * FROM sqlite_master WHERE type = 'view'`,
);
for (let i = 0; i < response.length; ++i) {
@ -813,7 +814,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SHOW CREATE FUNCTION ${args.function_name};`
`SHOW CREATE FUNCTION ${args.function_name};`,
);
if (response.length === 2) {
@ -865,7 +866,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`show create procedure ${args.procedure_name};`
`show create procedure ${args.procedure_name};`,
);
if (response.length === 2) {
@ -911,7 +912,7 @@ class SqliteClient extends KnexClient {
try {
const response = await this.sqlClient.raw(
`SELECT * FROM sqlite_master WHERE type = 'view' AND name = '${args.view_name}'`
`SELECT * FROM sqlite_master WHERE type = 'view' AND name = '${args.view_name}'`,
);
for (let i = 0; i < response.length; ++i) {
@ -938,7 +939,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SHOW FULL TABLES IN ${args.databaseName} WHERE TABLE_TYPE LIKE 'VIEW';`
`SHOW FULL TABLES IN ${args.databaseName} WHERE TABLE_TYPE LIKE 'VIEW';`,
);
if (response.length === 2) {
@ -970,7 +971,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`create database ${args.database_name}`
`create database ${args.database_name}`,
);
return rows;
}
@ -981,7 +982,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`drop database ${args.database_name}`
`drop database ${args.database_name}`,
);
return rows;
}
@ -1011,7 +1012,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`DROP FUNCTION IF EXISTS ${args.function_name}`
`DROP FUNCTION IF EXISTS ${args.function_name}`,
);
return rows;
}
@ -1022,7 +1023,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`DROP PROCEDURE IF EXISTS ${args.procedure_name}`
`DROP PROCEDURE IF EXISTS ${args.procedure_name}`,
);
return rows;
}
@ -1042,7 +1043,7 @@ class SqliteClient extends KnexClient {
this._version = result.data.object;
log.debug(
`Version was empty for ${args.func}: population version for database as`,
this._version
this._version,
);
}
@ -1073,7 +1074,7 @@ class SqliteClient extends KnexClient {
log.api(`${func}:args:`, args);
try {
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
);
result.data.list = rows;
} catch (e) {
@ -1101,7 +1102,7 @@ class SqliteClient extends KnexClient {
try {
await this.sqlClient.raw(`DROP TRIGGER ${args.function_name}`);
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
);
result.data.list = rows;
} catch (e) {
@ -1128,7 +1129,7 @@ class SqliteClient extends KnexClient {
log.api(`${func}:args:`, args);
try {
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
);
result.data.list = rows;
} catch (e) {
@ -1156,7 +1157,7 @@ class SqliteClient extends KnexClient {
try {
await this.sqlClient.raw(`DROP TRIGGER ${args.procedure_name}`);
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
);
result.data.list = rows;
} catch (e) {
@ -1216,7 +1217,7 @@ class SqliteClient extends KnexClient {
try {
await this.sqlClient.raw(`DROP TRIGGER ${args.trigger_name}`);
await this.sqlClient.raw(
`CREATE TRIGGER \`${args.trigger_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
`CREATE TRIGGER \`${args.trigger_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
);
const upQuery = `DROP TRIGGER ${args.trigger_name};\nCREATE TRIGGER \`${args.trigger_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`;
@ -1507,13 +1508,13 @@ class SqliteClient extends KnexClient {
args.table,
args.columns[i],
oldColumn,
upQuery
upQuery,
);
downQuery += this.alterTableAddColumn(
args.table,
oldColumn,
args.columns[i],
downQuery
downQuery,
);
} else if (args.columns[i].altered & 2 || args.columns[i].altered & 8) {
// col edit
@ -1521,7 +1522,7 @@ class SqliteClient extends KnexClient {
args.table,
args.columns[i],
oldColumn,
upQuery
upQuery,
);
downQuery += ';';
// downQuery += this.alterTableChangeColumn(
@ -1537,7 +1538,7 @@ class SqliteClient extends KnexClient {
args.table,
args.columns[i],
oldColumn,
upQuery
upQuery,
);
downQuery += ';';
// downQuery += alterTableRemoveColumn(
@ -1553,7 +1554,7 @@ class SqliteClient extends KnexClient {
const pkQuery = this.alterTablePK(
args.columns,
args.originalColumns,
upQuery
upQuery,
);
await this.sqlClient.raw('PRAGMA foreign_keys = OFF;');
@ -1572,7 +1573,7 @@ class SqliteClient extends KnexClient {
if (pkQuery) {
await trx.schema.alterTable(args.table, (table) => {
for (const pk of pkQuery.oldPks.filter(
(el) => !pkQuery.newPks.includes(el)
(el) => !pkQuery.newPks.includes(el),
)) {
table.dropPrimary(pk);
}
@ -1858,7 +1859,7 @@ class SqliteClient extends KnexClient {
/* Filter relations for current table */
if (args.tn) {
relations = relations.filter(
(r) => r.tn === args.tn || r.rtn === args.tn
(r) => r.tn === args.tn || r.rtn === args.tn,
);
}
@ -1867,7 +1868,7 @@ class SqliteClient extends KnexClient {
let columns: any = await this.columnList({ tn: tables[i].tn });
columns = columns.data.list;
console.log(
`Sequelize model created: ${tables[i].tn}(${columns.length})\n`
`Sequelize model created: ${tables[i].tn}(${columns.length})\n`,
);
// let SqliteSequelizeRender = require('./SqliteSequelizeRender');
@ -1967,7 +1968,7 @@ class SqliteClient extends KnexClient {
query += this.genQuery(
`ALTER TABLE ?? DROP COLUMN ??`,
[t, n.cn],
shouldSanitize
shouldSanitize,
);
return query;
}
@ -2007,14 +2008,14 @@ class SqliteClient extends KnexClient {
const backupOldColumnQuery = this.genQuery(
`ALTER TABLE ?? RENAME COLUMN ?? TO ??;`,
[t, o.cn, `${o.cno}_nc_${suffix}`],
shouldSanitize
shouldSanitize,
);
let addNewColumnQuery = '';
addNewColumnQuery += this.genQuery(
` ADD ?? ${this.sanitiseDataType(n.dt)}`,
[n.cn],
shouldSanitize
shouldSanitize,
);
addNewColumnQuery += n.dtxp && n.dt !== 'text' ? `(${n.dtxp})` : '';
addNewColumnQuery += n.cdf
@ -2026,19 +2027,19 @@ class SqliteClient extends KnexClient {
addNewColumnQuery = this.genQuery(
`ALTER TABLE ?? ${addNewColumnQuery};`,
[t],
shouldSanitize
shouldSanitize,
);
const updateNewColumnQuery = this.genQuery(
`UPDATE ?? SET ?? = ??;`,
[t, n.cn, `${o.cno}_nc_${suffix}`],
shouldSanitize
shouldSanitize,
);
const dropOldColumnQuery = this.genQuery(
`ALTER TABLE ?? DROP COLUMN ??;`,
[t, `${o.cno}_nc_${suffix}`],
shouldSanitize
shouldSanitize,
);
query = `${backupOldColumnQuery}${addNewColumnQuery}${updateNewColumnQuery}${dropOldColumnQuery}`;
@ -2105,12 +2106,12 @@ class SqliteClient extends KnexClient {
try {
const tables = await this.sqlClient.raw(
`SELECT name FROM sqlite_master WHERE type='table';`
`SELECT name FROM sqlite_master WHERE type='table';`,
);
let count = 0;
for (const tb of tables) {
const tmp = await this.sqlClient.raw(
`SELECT COUNT(*) as ct FROM '${tb.name}';`
`SELECT COUNT(*) as ct FROM '${tb.name}';`,
);
if (tmp && tmp.length) {
count += tmp[0].ct;

5
packages/nocodb/src/models/Model.ts

@ -341,6 +341,11 @@ export default class Model implements TableType {
): Promise<BaseModelSqlv2> {
const model = args?.model || (await this.get(args.id, ncMeta));
if (!args?.viewId) {
const view = await View.getDefaultView(model.id, ncMeta);
args.viewId = view.id;
}
return new BaseModelSqlv2({
dbDriver: args.dbDriver,
viewId: args.viewId,

2
packages/nocodb/src/modules/event-emitter/fallback-event-emitter.ts

@ -1,5 +1,5 @@
import Emittery from 'emittery';
import { IEventEmitter } from './event-emitter.interface';
import type { IEventEmitter } from './event-emitter.interface';
export class FallbackEventEmitter implements IEventEmitter {
private readonly emitter: Emittery;

4
packages/nocodb/src/modules/event-emitter/nestjs-event-emitter.ts

@ -1,5 +1,5 @@
import { EventEmitter2 } from '@nestjs/event-emitter';
import { IEventEmitter } from './event-emitter.interface';
import type { EventEmitter2 } from '@nestjs/event-emitter';
import type { IEventEmitter } from './event-emitter.interface';
export class NestjsEventEmitter implements IEventEmitter {
constructor(private readonly eventEmitter: EventEmitter2) {}

2
packages/nocodb/src/modules/metas/metas.module.ts

@ -67,7 +67,7 @@ import { UtilsService } from '../../services/utils.service';
import { ViewColumnsService } from '../../services/view-columns.service';
import { ViewsService } from '../../services/views.service';
import { ApiDocsService } from '../../services/api-docs/api-docs.service';
import { EventEmitterModule } from '../event-emitter/event-emitter.module'
import { EventEmitterModule } from '../event-emitter/event-emitter.module';
import { GlobalModule } from '../global/global.module';
import { ProjectUsersController } from '../../controllers/project-users.controller';
import { ProjectUsersService } from '../../services/project-users/project-users.service';

18
packages/nocodb/src/run/local.ts

@ -0,0 +1,18 @@
import path from 'path';
import cors from 'cors';
import express from 'express';
import Noco from '../Noco';
const server = express();
server.enable('trust proxy');
server.use(cors());
server.use('/dashboard', express.static(path.join(__dirname, 'nc-gui')));
server.set('view engine', 'ejs');
(async () => {
const httpServer = server.listen(process.env.PORT || 8080, () => {
console.log(`App started successfully.\nVisit -> ${Noco.dashboardUrl}`);
});
server.use(await Noco.init({}, httpServer, server));
})().catch((e) => console.log(e));

3
packages/nocodb/src/services/hook-handler.service.spec.ts

@ -1,5 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { HookHandlerService } from './hook-handler.service';
import type { TestingModule } from '@nestjs/testing';
describe('HookHandlerService', () => {
let service: HookHandlerService;

6
packages/nocodb/src/services/hook-handler.service.ts

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'
import { Inject, Injectable } from '@nestjs/common';
import { UITypes, ViewTypes } from 'nocodb-sdk';
import ejs from 'ejs';
import NcPluginMgrv2 from '../helpers/NcPluginMgrv2';
@ -18,7 +18,9 @@ export const HANDLE_WEBHOOK = '__nc_handleHooks';
export class HookHandlerService implements OnModuleInit, OnModuleDestroy {
private unsubscribe: () => void;
constructor(@Inject('IEventEmitter') private readonly eventEmitter: IEventEmitter) {}
constructor(
@Inject('IEventEmitter') private readonly eventEmitter: IEventEmitter,
) {}
private async handleHooks({
hookName,

1
tests/playwright/pages/Dashboard/Grid/index.ts

@ -52,6 +52,7 @@ export class GridPage extends BasePage {
private async _fillRow({ index, columnHeader, value }: { index: number; columnHeader: string; value: string }) {
const cell = this.cell.get({ index, columnHeader });
await cell.waitFor({ state: 'visible' });
await this.cell.dblclick({
index,
columnHeader,

16
tests/playwright/pages/Dashboard/WebhookForm/index.ts

@ -83,7 +83,7 @@ export class WebhookFormPage extends BasePage {
}
if (save) {
await this.save(true);
await this.save();
await this.close();
}
}
@ -91,27 +91,20 @@ export class WebhookFormPage extends BasePage {
async deleteCondition(p: { save: boolean }) {
await this.get().locator(`.nc-filter-item-remove-btn`).click();
if (p.save) {
await this.save(true);
await this.save();
await this.close();
}
}
async save(condition = false) {
async save() {
const saveAction = () => this.saveButton.click();
await this.waitForResponse({
uiAction: saveAction,
requestUrlPathToMatch: '/hooks',
httpMethodsToMatch: ['POST', 'PATCH'],
});
if (condition) {
await this.waitForResponse({
uiAction: saveAction,
requestUrlPathToMatch: '/filters',
httpMethodsToMatch: ['POST', 'PATCH', 'DELETE'],
});
}
await this.verifyToast({ message: 'Webhook details updated successfully' });
}
@ -142,6 +135,7 @@ export class WebhookFormPage extends BasePage {
await this.toolbar.clickActions();
await this.toolbar.actions.click('Webhooks');
await this.dashboard.get().locator(`.nc-hook`).nth(index).click();
await this.get().locator('.nc-check-box-enable-webhook').waitFor({ state: 'visible' });
}
async openForm({ index }: { index: number }) {

22
tests/playwright/pages/Dashboard/common/Cell/index.ts

@ -291,7 +291,11 @@ export class CellPageObject extends BasePage {
// arrow expand doesn't exist for bt columns
if (await arrow_expand.count()) {
await arrow_expand.click();
await this.waitForResponse({
uiAction: () => arrow_expand.click(),
requestUrlPathToMatch: '/api/v1/db',
httpMethodsToMatch: ['GET'],
});
// wait for child list to open
await this.rootPage.waitForSelector('.nc-modal-child-list:visible');
@ -309,7 +313,11 @@ export class CellPageObject extends BasePage {
async unlinkVirtualCell({ index, columnHeader }: CellProps) {
const cell = this.get({ index, columnHeader });
await cell.click();
await cell.locator('.unlink-icon').first().click();
await this.waitForResponse({
uiAction: () => cell.locator('.unlink-icon').first().click(),
requestUrlPathToMatch: '/api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
});
}
async verifyRoleAccess(param: { role: string }) {
@ -352,12 +360,4 @@ export class CellPageObject extends BasePage {
await this.get({ index, columnHeader }).press((await this.isMacOs()) ? 'Meta+C' : 'Control+C');
await this.verifyToast({ message: 'Copied to clipboard' });
}
async pasteFromClipboard({ index, columnHeader }: CellProps, ...clickOptions: Parameters<Locator['click']>) {
await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
await this.get({ index, columnHeader }).click(...clickOptions);
await (await this.get({ index, columnHeader }).elementHandle()).waitForElementState('stable');
await this.get({ index, columnHeader }).press((await this.isMacOs()) ? 'Meta+V' : 'Control+V');
}
}
}

115
tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

@ -45,6 +45,7 @@ export class ToolbarFilterPage extends BasePage {
locallySaved = false,
dataType,
openModal = false,
skipWaitingResponse = false, // used for undo (single request, less stable)
}: {
title: string;
operation: string;
@ -53,21 +54,32 @@ export class ToolbarFilterPage extends BasePage {
locallySaved?: boolean;
dataType?: string;
openModal?: boolean;
skipWaitingResponse?: boolean;
}) {
if (!openModal) await this.get().locator(`button:has-text("Add Filter")`).first().click();
const selectedField = await getTextExcludeIconText(await this.rootPage.locator('.nc-filter-field-select .ant-select-selection-item'));
const selectedField = await getTextExcludeIconText(
await this.rootPage.locator('.nc-filter-field-select .ant-select-selection-item')
);
if (selectedField !== title) {
await this.rootPage.locator('.nc-filter-field-select').last().click();
await this.waitForResponse({
uiAction: () => this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list')
.locator(`div[label="${title}"]:visible`)
.click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
if (skipWaitingResponse) {
this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list')
.locator(`div[label="${title}"]:visible`)
.click();
} else {
await this.waitForResponse({
uiAction: () =>
this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list')
.locator(`div[label="${title}"]:visible`)
.click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
}
}
const selectedOpType = await getTextExcludeIconText(await this.rootPage.locator('.nc-filter-operation-select'));
@ -75,15 +87,24 @@ export class ToolbarFilterPage extends BasePage {
await this.rootPage.locator('.nc-filter-operation-select').click();
// first() : filter list has >, >=
await this.waitForResponse({
uiAction: () => this.rootPage
.locator('.nc-dropdown-filter-comp-op')
.locator(`.ant-select-item:has-text("${operation}")`)
.first()
.click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
if (skipWaitingResponse) {
this.rootPage
.locator('.nc-dropdown-filter-comp-op')
.locator(`.ant-select-item:has-text("${operation}")`)
.first()
.click();
} else {
await this.waitForResponse({
uiAction: () =>
this.rootPage
.locator('.nc-dropdown-filter-comp-op')
.locator(`.ant-select-item:has-text("${operation}")`)
.first()
.click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
}
}
// subtype for date
@ -95,15 +116,24 @@ export class ToolbarFilterPage extends BasePage {
await this.rootPage.locator('.nc-filter-sub_operation-select').click();
// first() : filter list has >, >=
await this.waitForResponse({
uiAction: () => this.rootPage
.locator('.nc-dropdown-filter-comp-sub-op')
.locator(`.ant-select-item:has-text("${subOperation}")`)
.first()
.click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
if (skipWaitingResponse) {
this.rootPage
.locator('.nc-dropdown-filter-comp-sub-op')
.locator(`.ant-select-item:has-text("${subOperation}")`)
.first()
.click();
} else {
await this.waitForResponse({
uiAction: () =>
this.rootPage
.locator('.nc-dropdown-filter-comp-sub-op')
.locator(`.ant-select-item:has-text("${subOperation}")`)
.first()
.click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
}
}
}
@ -135,11 +165,16 @@ export class ToolbarFilterPage extends BasePage {
if (subOperation === 'exact date') {
await this.get().locator('.nc-filter-value-select').click();
await this.rootPage.locator(`.ant-picker-dropdown:visible`);
await this.waitForResponse({
uiAction: () => this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
if (skipWaitingResponse) {
this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click();
} else {
await this.waitForResponse({
uiAction: () => this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
}
} else {
fillFilter = () => this.rootPage.locator('.nc-filter-value-select > input').last().fill(value);
await this.waitForResponse({
@ -152,11 +187,15 @@ export class ToolbarFilterPage extends BasePage {
}
break;
case UITypes.Duration:
await this.waitForResponse({
uiAction: () => this.get().locator('.nc-filter-value-select').locator('input').fill(value),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
if (skipWaitingResponse) {
this.get().locator('.nc-filter-value-select').locator('input').fill(value);
} else {
await this.waitForResponse({
uiAction: () => this.get().locator('.nc-filter-value-select').locator('input').fill(value),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
}
break;
case UITypes.Rating:
await this.get()
@ -259,4 +298,4 @@ export class ToolbarFilterPage extends BasePage {
return opListText;
}
}
}

26
tests/playwright/tests/db/filters.spec.ts

@ -336,7 +336,31 @@ test.describe('Filter Tests: Numerical', () => {
});
test('Filter: Time', async () => {
await numBasedFilterTest('Time', '02:02:00', '04:04:00');
const getTime = date => {
let hours = date.getHours();
let minutes = date.getMinutes();
let seconds = date.getSeconds();
// let ap = hours >= 12 ? 'pm' : 'am';
hours = hours % 12;
hours = hours ? hours : 12;
hours = hours.toString().padStart(2, '0');
minutes = minutes.toString().padStart(2, '0');
seconds = seconds.toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
};
// compute timezone offset
const offset = new Date().getTimezoneOffset();
const timezoneOffset =
(offset <= 0 ? '+' : '-') +
String(Math.abs(Math.round(offset / 60))).padStart(2, '0') +
':' +
String(Math.abs(offset % 60)).padStart(2, '0');
const date1 = new Date(`1999-01-01 02:02:00${timezoneOffset}`);
const date2 = new Date(`1999-01-01 04:04:00${timezoneOffset}`);
await numBasedFilterTest('Time', getTime(date1), getTime(date2));
});
});

6
tests/playwright/tests/db/undo-redo.spec.ts

@ -273,15 +273,19 @@ test.describe('Undo Redo', () => {
}
await toolbar.clickFilter();
await toolbar.filter.add({ title: 'Number', operation: '=', value: '33' });
await toolbar.filter.add({ title: 'Number', operation: '=', value: '33', skipWaitingResponse: true });
await toolbar.clickFilter();
await verifyRecords({ filtered: true });
await toolbar.filter.reset();
await verifyRecords({ filtered: false });
// undo: remove filter
await undo({ page });
await verifyRecords({ filtered: true });
// undo: update filter
await undo({ page });
// undo: add filter
await undo({ page });
await verifyRecords({ filtered: false });
});

Loading…
Cancel
Save