diff --git a/.gitignore b/.gitignore index 82f59b54ce..2d333cbc36 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ _site/ *.pid *.gz *.tmp +*.tmp.md *.bak *.swp logs/ diff --git a/build-local-docker-image.sh b/build-local-docker-image.sh new file mode 100755 index 0000000000..a4ccab4a89 --- /dev/null +++ b/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 ' + diff --git a/packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue b/packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue index 59a89bfea8..ee722d89c9 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue +++ b/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" /> diff --git a/packages/nc-gui/components/webhook/Editor.vue b/packages/nc-gui/components/webhook/Editor.vue index ebabf7809c..a1858ddaf1 100644 --- a/packages/nc-gui/components/webhook/Editor.vue +++ b/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" /> diff --git a/packages/nc-gui/composables/useViewFilters.ts b/packages/nc-gui/composables/useViewFilters.ts index 0d864825e3..24bcfa3c80 100644 --- a/packages/nc-gui/composables/useViewFilters.ts +++ b/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) diff --git a/packages/nocodb/.gitignore b/packages/nocodb/.gitignore index 22a836d74a..1a182ed6dd 100644 --- a/packages/nocodb/.gitignore +++ b/packages/nocodb/.gitignore @@ -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 diff --git a/packages/nocodb/Dockerfile.local b/packages/nocodb/Dockerfile.local new file mode 100644 index 0000000000..dff9c15b7d --- /dev/null +++ b/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"] diff --git a/packages/nocodb/src/Noco.ts b/packages/nocodb/src/Noco.ts index a764c4e5bc..be7c7acda9 100644 --- a/packages/nocodb/src/Noco.ts +++ b/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; diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index dcfe1640d1..439c72ee0e 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/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) || []; diff --git a/packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts b/packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts index fb5dec62d9..118423df7b 100644 --- a/packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts +++ b/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; diff --git a/packages/nocodb/src/models/Model.ts b/packages/nocodb/src/models/Model.ts index dbc2f18dbe..52c8d21830 100644 --- a/packages/nocodb/src/models/Model.ts +++ b/packages/nocodb/src/models/Model.ts @@ -341,6 +341,11 @@ export default class Model implements TableType { ): Promise { 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, diff --git a/packages/nocodb/src/modules/event-emitter/fallback-event-emitter.ts b/packages/nocodb/src/modules/event-emitter/fallback-event-emitter.ts index 4ee3ac5020..76c8fbc90e 100644 --- a/packages/nocodb/src/modules/event-emitter/fallback-event-emitter.ts +++ b/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; diff --git a/packages/nocodb/src/modules/event-emitter/nestjs-event-emitter.ts b/packages/nocodb/src/modules/event-emitter/nestjs-event-emitter.ts index 14e83fe5fa..dedf7e9fb8 100644 --- a/packages/nocodb/src/modules/event-emitter/nestjs-event-emitter.ts +++ b/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) {} diff --git a/packages/nocodb/src/modules/metas/metas.module.ts b/packages/nocodb/src/modules/metas/metas.module.ts index 63ed0a964e..0aa6a9ae76 100644 --- a/packages/nocodb/src/modules/metas/metas.module.ts +++ b/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'; diff --git a/packages/nocodb/src/run/local.ts b/packages/nocodb/src/run/local.ts new file mode 100644 index 0000000000..df29c1092a --- /dev/null +++ b/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)); diff --git a/packages/nocodb/src/services/hook-handler.service.spec.ts b/packages/nocodb/src/services/hook-handler.service.spec.ts index 9b79fd5af6..977cba23fa 100644 --- a/packages/nocodb/src/services/hook-handler.service.spec.ts +++ b/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; diff --git a/packages/nocodb/src/services/hook-handler.service.ts b/packages/nocodb/src/services/hook-handler.service.ts index d7dac48aa4..5067de43dd 100644 --- a/packages/nocodb/src/services/hook-handler.service.ts +++ b/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, diff --git a/tests/playwright/pages/Dashboard/Grid/index.ts b/tests/playwright/pages/Dashboard/Grid/index.ts index 5de5733255..94b133b393 100644 --- a/tests/playwright/pages/Dashboard/Grid/index.ts +++ b/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, diff --git a/tests/playwright/pages/Dashboard/WebhookForm/index.ts b/tests/playwright/pages/Dashboard/WebhookForm/index.ts index 4ae388b61f..4317c810a9 100644 --- a/tests/playwright/pages/Dashboard/WebhookForm/index.ts +++ b/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 }) { diff --git a/tests/playwright/pages/Dashboard/common/Cell/index.ts b/tests/playwright/pages/Dashboard/common/Cell/index.ts index c0515715f8..8a716c15bf 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/index.ts +++ b/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) { - 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'); - } -} +} \ No newline at end of file diff --git a/tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts b/tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts index 699c84c0cd..50b8835f11 100644 --- a/tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts +++ b/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; } -} +} \ No newline at end of file diff --git a/tests/playwright/tests/db/filters.spec.ts b/tests/playwright/tests/db/filters.spec.ts index 48a91be267..247eaa9210 100644 --- a/tests/playwright/tests/db/filters.spec.ts +++ b/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)); }); }); diff --git a/tests/playwright/tests/db/undo-redo.spec.ts b/tests/playwright/tests/db/undo-redo.spec.ts index e55a27cf3e..c798910d7a 100644 --- a/tests/playwright/tests/db/undo-redo.spec.ts +++ b/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 }); });