diff --git a/packages/nc-gui/app.vue b/packages/nc-gui/app.vue index 933ebfc3d6..f59b35c76d 100644 --- a/packages/nc-gui/app.vue +++ b/packages/nc-gui/app.vue @@ -1,9 +1,11 @@ - + + @@ -472,16 +487,16 @@ onBeforeUnmount(reset) - +
- + - +
diff --git a/packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue b/packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue index 397d7eec5f..360f0c8f0b 100644 --- a/packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue +++ b/packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue @@ -1,12 +1,7 @@ diff --git a/packages/nc-gui/pages/[projectType]/[projectId]/index/index/auth.vue b/packages/nc-gui/pages/[projectType]/[projectId]/index/index/auth.vue index 0073f0c16f..dd3d59cd47 100644 --- a/packages/nc-gui/pages/[projectType]/[projectId]/index/index/auth.vue +++ b/packages/nc-gui/pages/[projectType]/[projectId]/index/index/auth.vue @@ -1,7 +1,3 @@ - - diff --git a/packages/nc-gui/pages/[projectType]/[projectId]/index/index/index.vue b/packages/nc-gui/pages/[projectType]/[projectId]/index/index/index.vue index 5cbdbf4492..3c793a71ba 100644 --- a/packages/nc-gui/pages/[projectType]/[projectId]/index/index/index.vue +++ b/packages/nc-gui/pages/[projectType]/[projectId]/index/index/index.vue @@ -1,9 +1,17 @@ @@ -63,7 +55,10 @@ function resetError() {
- +

{{ $t('title.resetPassword') }}

diff --git a/packages/nc-gui/pages/index/apps.vue b/packages/nc-gui/pages/index/apps.vue index 5b3d81bf2c..6780d02668 100644 --- a/packages/nc-gui/pages/index/apps.vue +++ b/packages/nc-gui/pages/index/apps.vue @@ -1,6 +1,6 @@ - +
diff --git a/packages/nc-gui/pages/index/user/index.vue b/packages/nc-gui/pages/index/user/index.vue index f8eacfa737..8684d74738 100644 --- a/packages/nc-gui/pages/index/user/index.vue +++ b/packages/nc-gui/pages/index/user/index.vue @@ -1,5 +1,146 @@ + + + + diff --git a/packages/nc-gui/pages/index/user/index/index.vue b/packages/nc-gui/pages/index/user/index/index.vue deleted file mode 100644 index ecf7834b47..0000000000 --- a/packages/nc-gui/pages/index/user/index/index.vue +++ /dev/null @@ -1,149 +0,0 @@ - - - - - diff --git a/packages/nc-gui/pages/signin.vue b/packages/nc-gui/pages/signin.vue index 86638e9c13..e826fc7ae2 100644 --- a/packages/nc-gui/pages/signin.vue +++ b/packages/nc-gui/pages/signin.vue @@ -1,35 +1,22 @@ @@ -84,7 +67,10 @@ function resetError() {
- +

{{ $t('general.signIn') }}

diff --git a/packages/nc-gui/pages/signup/[[token]].vue b/packages/nc-gui/pages/signup/[[token]].vue index 8e39c7de84..52e6905ca4 100644 --- a/packages/nc-gui/pages/signup/[[token]].vue +++ b/packages/nc-gui/pages/signup/[[token]].vue @@ -1,22 +1,9 @@ @@ -105,7 +85,10 @@ function resetError() {
- +

{{ $t('general.signUp') }} diff --git a/packages/nc-gui/plugins/a.dayjs.ts b/packages/nc-gui/plugins/a.dayjs.ts index 261c3d5fda..76a1c35bc9 100644 --- a/packages/nc-gui/plugins/a.dayjs.ts +++ b/packages/nc-gui/plugins/a.dayjs.ts @@ -1,4 +1,4 @@ -import dayjs from 'dayjs' +import { extend } from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime.js' import customParseFormat from 'dayjs/plugin/customParseFormat.js' import duration from 'dayjs/plugin/duration.js' @@ -7,9 +7,9 @@ import weekday from 'dayjs/plugin/weekday.js' import { defineNuxtPlugin } from '#imports' export default defineNuxtPlugin(() => { - dayjs.extend(utc) - dayjs.extend(relativeTime) - dayjs.extend(customParseFormat) - dayjs.extend(duration) - dayjs.extend(weekday) + extend(utc) + extend(relativeTime) + extend(customParseFormat) + extend(duration) + extend(weekday) }) diff --git a/packages/nc-gui/plugins/a.i18n.ts b/packages/nc-gui/plugins/a.i18n.ts index 55cf0472fb..a0c9c2a498 100644 --- a/packages/nc-gui/plugins/a.i18n.ts +++ b/packages/nc-gui/plugins/a.i18n.ts @@ -1,6 +1,5 @@ -import { defineNuxtPlugin } from 'nuxt/app' import { createI18n } from 'vue-i18n' -import { nextTick } from 'vue' +import { defineNuxtPlugin, nextTick } from '#imports' import type { Language, NocoI18n } from '~/lib' import { LanguageAlias } from '~/lib' diff --git a/packages/nc-gui/plugins/initializeFeedbackForm.ts b/packages/nc-gui/plugins/feedbackForm.ts similarity index 94% rename from packages/nc-gui/plugins/initializeFeedbackForm.ts rename to packages/nc-gui/plugins/feedbackForm.ts index 314131afc7..5c6e58955f 100644 --- a/packages/nc-gui/plugins/initializeFeedbackForm.ts +++ b/packages/nc-gui/plugins/feedbackForm.ts @@ -1,8 +1,9 @@ import dayjs from 'dayjs' -import { defineNuxtPlugin } from '#app' +import { defineNuxtPlugin, useGlobal, useNuxtApp } from '#imports' const handleFeedbackForm = async () => { let { feedbackForm: currentFeedbackForm } = $(useGlobal()) + if (!currentFeedbackForm) return const { $api } = useNuxtApp() @@ -10,6 +11,7 @@ const handleFeedbackForm = async () => { const isFirstTimePolling = !currentFeedbackForm.lastFormPollDate const now = dayjs() + const lastFormPolledDate = dayjs(currentFeedbackForm.lastFormPollDate) if (isFirstTimePolling || dayjs.duration(now.diff(lastFormPolledDate)).days() > 0) { diff --git a/packages/nc-gui/plugins/resizeDirective.ts b/packages/nc-gui/plugins/resizeDirective.ts index d36a1396ca..549131ee99 100644 --- a/packages/nc-gui/plugins/resizeDirective.ts +++ b/packages/nc-gui/plugins/resizeDirective.ts @@ -1,4 +1,4 @@ -import { defineNuxtPlugin } from '#app' +import { defineNuxtPlugin, getCurrentInstance } from '#imports' export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.directive('xc-ver-resize', { @@ -18,6 +18,7 @@ export default defineNuxtPlugin((nuxtApp) => { resizer.addEventListener('mousedown', initDrag, false) const instance = getCurrentInstance() + const emit = instance?.emit ?? ((arg, data) => { @@ -37,6 +38,8 @@ export default defineNuxtPlugin((nuxtApp) => { document.documentElement.addEventListener('mouseup', stopDrag, false) } + ;(el as any).initDrag = initDrag + let width: number | string // emit event on dragging @@ -55,5 +58,12 @@ export default defineNuxtPlugin((nuxtApp) => { emit('xcresized') } }, + beforeUnmount: (el: Element) => { + const resizer = el.querySelector('.resizer') + + if (resizer) { + resizer.removeEventListener('mousedown', (el as any).initDrag, false) + } + }, }) }) diff --git a/packages/nc-gui/plugins/state.ts b/packages/nc-gui/plugins/state.ts index 08cc6574c8..69384aadbe 100644 --- a/packages/nc-gui/plugins/state.ts +++ b/packages/nc-gui/plugins/state.ts @@ -17,7 +17,7 @@ import { Language, LanguageAlias } from '~/lib' export default defineNuxtPlugin(async () => { const state = useGlobal() - const { api } = useApi() + const { api } = useApi({ useGlobalInstance: true }) let currentLang = state.lang.value diff --git a/packages/nc-gui/plugins/tele.ts b/packages/nc-gui/plugins/tele.ts index 481dd133f3..cd9c0b1ca1 100644 --- a/packages/nc-gui/plugins/tele.ts +++ b/packages/nc-gui/plugins/tele.ts @@ -1,7 +1,6 @@ -import { defineNuxtPlugin } from 'nuxt/app' import type { Socket } from 'socket.io-client' import io from 'socket.io-client' -import type { UseGlobalReturn } from '~/composables/useGlobal/types' +import { defineNuxtPlugin, useGlobal, useRoute, useRouter, watch } from '#imports' // todo: ignore init if tele disabled export default defineNuxtPlugin(async (nuxtApp) => { @@ -59,6 +58,10 @@ export default defineNuxtPlugin(async (nuxtApp) => { if (vnode.el) vnode.el.addEventListener('click', getListener(binding)) else el.addEventListener('click', getListener(binding)) }, + beforeUnmount(el, binding, vnode) { + if (vnode.el) vnode.el.removeEventListener('click', getListener(binding)) + else el.removeEventListener('click', getListener(binding)) + }, }) function getListener(binding: any) { @@ -75,7 +78,7 @@ export default defineNuxtPlugin(async (nuxtApp) => { } } - watch((nuxtApp.$state as UseGlobalReturn).token, (newToken, oldToken) => { + watch((nuxtApp.$state as ReturnType).token, (newToken, oldToken) => { if (newToken && newToken !== oldToken) init(newToken) else if (!newToken) socket.disconnect() }) diff --git a/packages/nc-gui/test/vite.config.ts b/packages/nc-gui/test/vite.config.ts index 2c4d773b7a..89731cff35 100644 --- a/packages/nc-gui/test/vite.config.ts +++ b/packages/nc-gui/test/vite.config.ts @@ -18,10 +18,6 @@ export default defineConfig({ }), ], test: { - setupFiles: path.resolve(__dirname, './vuetify.config.js'), - deps: { - inline: ['vuetify'], - }, globals: true, environment: 'happy-dom', }, diff --git a/packages/nc-gui/test/vuetify.config.js b/packages/nc-gui/test/vuetify.config.js deleted file mode 100644 index 4aa920d287..0000000000 --- a/packages/nc-gui/test/vuetify.config.js +++ /dev/null @@ -1,2 +0,0 @@ -/** fix issue with testing: https://github.com/vuetifyjs/vuetify/issues/14749 */ -global.CSS = { supports: () => false } diff --git a/packages/nc-gui/utils/currencyUtils.ts b/packages/nc-gui/utils/currencyUtils.ts index 5ac1ccb46f..0cdb4e2110 100644 --- a/packages/nc-gui/utils/currencyUtils.ts +++ b/packages/nc-gui/utils/currencyUtils.ts @@ -1,4 +1,4 @@ -import locale from 'locale-codes' +import { all } from 'locale-codes' export const currencyCodes = [ 'AED', @@ -191,24 +191,14 @@ export function validateCurrencyCode(v: string) { } export function currencyLocales() { - const localeList = locale.all - .filter((l: Record) => { - try { - if (Intl.NumberFormat.supportedLocalesOf(l.tag).length > 0) { - return true - } - return false - } catch (e) { - return false - } - }) - .map((l: Record) => { + return all + .filter((l) => validateCurrencyLocale(l.tag)) + .map((l) => { return { text: `${l.name} (${l.tag})`, value: l.tag, } }) - return localeList } export function validateCurrencyLocale(v: string) { diff --git a/packages/nc-gui/utils/generateName.ts b/packages/nc-gui/utils/generateName.ts index ffd144cd51..9d60b74309 100644 --- a/packages/nc-gui/utils/generateName.ts +++ b/packages/nc-gui/utils/generateName.ts @@ -1,6 +1,6 @@ -import { adjectives, animals, starWars, uniqueNamesGenerator } from 'unique-names-generator' +export const generateUniqueName = async () => { + const { adjectives, animals, starWars, uniqueNamesGenerator } = await import('unique-names-generator') -export const generateUniqueName = () => { return uniqueNamesGenerator({ dictionaries: [[starWars], [adjectives, animals]][Math.floor(Math.random() * 2)], }) @@ -14,7 +14,7 @@ export const generateUniqueTitle = = Record { let c = 1 - while (arr.some((item) => item[predicate] === (`${title}-${c}` as keyof T))) { + while (arr.some((item) => item[predicate].includes(`${title}-${c}` as keyof T))) { c++ } diff --git a/packages/nc-gui/utils/iconUtils.ts b/packages/nc-gui/utils/iconUtils.ts index aff29794a7..1b437ee998 100644 --- a/packages/nc-gui/utils/iconUtils.ts +++ b/packages/nc-gui/utils/iconUtils.ts @@ -12,6 +12,29 @@ import MdiThumbUp from '~icons/mdi/thumb-up' import MdiThumbUpOutline from '~icons/mdi/thumb-up-outline' import MdiFlag from '~icons/mdi/flag' import MdiFlagOutline from '~icons/mdi/flag-outline' +import MdiTableLarge from '~icons/mdi/table-large' +import MdiEyeCircleOutline from '~icons/mdi/eye-circle-outline' +import MdiAccountGroup from '~icons/mdi/account-group' + +export const iconMap = { + 'mdi-check-bold': MdiCheckBold, + 'mdi-crop-square': MdiCropSquare, + 'mdi-check-circle-outline': MdiCheckCircleOutline, + 'mdi-checkbox-blank-circle-outline': MdiCheckboxBlankCircleOutline, + 'mdi-star': MdiStar, + 'mdi-star-outline': MdiStarOutline, + 'mdi-heart': MdiHeart, + 'mdi-heart-outline': MdiHeartOutline, + 'mdi-moon-full': MdiMoonFull, + 'mdi-moon-new': MdiMoonNew, + 'mdi-thumb-up': MdiThumbUp, + 'mdi-thumb-up-outline': MdiThumbUpOutline, + 'mdi-flag': MdiFlag, + 'mdi-flag-outline': MdiFlagOutline, + 'mdi-table-large': MdiTableLarge, + 'mdi-eye-circle-outline': MdiEyeCircleOutline, + 'mdi-account-group': MdiAccountGroup, +} as const export const getMdiIcon = (type: string): any => { switch (type) { @@ -43,5 +66,11 @@ export const getMdiIcon = (type: string): any => { return MdiFlag case 'mdi-flag-outline': return MdiFlagOutline + case 'mdi-table-large': + return MdiTableLarge + case 'mdi-eye-circle-outline': + return MdiEyeCircleOutline + case 'mdi-account-group': + return MdiAccountGroup } } diff --git a/packages/nc-gui/utils/parsers/CSVTemplateAdapter.ts b/packages/nc-gui/utils/parsers/CSVTemplateAdapter.ts index 258992587f..f68c9be56e 100644 --- a/packages/nc-gui/utils/parsers/CSVTemplateAdapter.ts +++ b/packages/nc-gui/utils/parsers/CSVTemplateAdapter.ts @@ -1,4 +1,4 @@ -import Papaparse from 'papaparse' +import { parse } from 'papaparse' import TemplateGenerator from './TemplateGenerator' export default class CSVTemplateAdapter extends TemplateGenerator { @@ -24,7 +24,7 @@ export default class CSVTemplateAdapter extends TemplateGenerator { } async init() { - this.csv = Papaparse.parse(this.csvData, { header: true }) + this.csv = parse(this.csvData, { header: true }) } parseData() { diff --git a/packages/nc-gui/utils/parsers/ExcelTemplateAdapter.ts b/packages/nc-gui/utils/parsers/ExcelTemplateAdapter.ts index 85c631d2a2..6e1b9ea4c9 100644 --- a/packages/nc-gui/utils/parsers/ExcelTemplateAdapter.ts +++ b/packages/nc-gui/utils/parsers/ExcelTemplateAdapter.ts @@ -1,9 +1,8 @@ -import XLSX from 'xlsx' import { UITypes } from 'nocodb-sdk' import TemplateGenerator from './TemplateGenerator' import { getCheckboxValue, isCheckboxType } from './parserHelpers' -const excelTypeToUidt: Record = { +const excelTypeToUidt: Record = { d: UITypes.DateTime, b: UITypes.Checkbox, n: UITypes.Number, @@ -11,39 +10,59 @@ const excelTypeToUidt: Record = { } export default class ExcelTemplateAdapter extends TemplateGenerator { - config: Record + config: { + maxRowsToParse: number + } & Record + name: string + excelData: any - project: Record - data: Record + + project: { + title: string + tables: any[] + } + + data: Record = {} + wb: any + + xlsx: typeof import('xlsx') + constructor(name = '', data = {}, parserConfig = {}) { super() this.config = { maxRowsToParse: 500, ...parserConfig, } + this.name = name + this.excelData = data + this.project = { title: this.name, tables: [], } - this.data = {} + + this.xlsx = {} as any } async init() { - const options: Record = { + this.xlsx = await import('xlsx') + + const options = { cellText: true, cellDates: true, } + if (this.name.slice(-3) === 'csv') { - this.wb = XLSX.read(new TextDecoder().decode(new Uint8Array(this.excelData)), { + this.wb = this.xlsx.read(new TextDecoder().decode(new Uint8Array(this.excelData)), { type: 'string', ...options, }) } else { - this.wb = XLSX.read(new Uint8Array(this.excelData), { + this.wb = this.xlsx.read(new Uint8Array(this.excelData), { type: 'array', ...options, }) @@ -51,9 +70,10 @@ export default class ExcelTemplateAdapter extends TemplateGenerator { } parse() { - const tableNamePrefixRef: any = {} + const tableNamePrefixRef: Record = {} + for (let i = 0; i < this.wb.SheetNames.length; i++) { - const columnNamePrefixRef: Record = { id: 0 } + const columnNamePrefixRef: Record = { id: 0 } const sheet: any = this.wb.SheetNames[i] let tn: string = (sheet || 'table').replace(/[` ~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/g, '_').trim() @@ -62,11 +82,11 @@ export default class ExcelTemplateAdapter extends TemplateGenerator { } tableNamePrefixRef[tn] = 0 - const table: Record = { table_name: tn, ref_table_name: tn, columns: [] } + const table = { table_name: tn, ref_table_name: tn, columns: [] as any[] } this.data[tn] = [] const ws: any = this.wb.Sheets[sheet] - const range = XLSX.utils.decode_range(ws['!ref']) - let rows: any = XLSX.utils.sheet_to_json(ws, { header: 1, blankrows: false, defval: null }) + const range = this.xlsx.utils.decode_range(ws['!ref']) + let rows: any = this.xlsx.utils.sheet_to_json(ws, { header: 1, blankrows: false, defval: null }) if (this.name.slice(-3) !== 'csv') { // fix precision bug & timezone offset issues introduced by xlsx @@ -77,7 +97,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator { const day_ms = 24 * 60 * 60 * 1000 // handle date1904 property const fixImportedDate = (date: Date) => { - const parsed = XLSX.SSF.parse_date_code((date.getTime() - dnthresh) / day_ms, { + const parsed = this.xlsx.SSF.parse_date_code((date.getTime() - dnthresh) / day_ms, { date1904: this.wb.Workbook.WBProps.date1904, }) return new Date(parsed.y, parsed.m, parsed.d, parsed.H, parsed.M, parsed.S) @@ -111,7 +131,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator { table.columns.push(column) // const cellId = `${col.toString(26).split('').map(s => (parseInt(s, 26) + 10).toString(36).toUpperCase())}2`; - const cellId = XLSX.utils.encode_cell({ + const cellId = this.xlsx.utils.encode_cell({ c: range.s.c + col, r: columnNameRowExist, }) @@ -173,7 +193,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator { } if ( rows.slice(1, this.config.maxRowsToParse).every((v: any, i: any) => { - const cellId = XLSX.utils.encode_cell({ + const cellId = this.xlsx.utils.encode_cell({ c: range.s.c + col, r: i + columnNameRowExist, }) @@ -188,7 +208,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator { } else if (column.uidt === UITypes.DateTime) { if ( rows.slice(1, this.config.maxRowsToParse).every((v: any, i: any) => { - const cellId = XLSX.utils.encode_cell({ + const cellId = this.xlsx.utils.encode_cell({ c: range.s.c + col, r: i + columnNameRowExist, }) @@ -209,7 +229,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator { if (table.columns[i].uidt === UITypes.Checkbox) { rowData[table.columns[i].column_name] = getCheckboxValue(row[i]) } else if (table.columns[i].uidt === UITypes.Currency) { - const cellId = XLSX.utils.encode_cell({ + const cellId = this.xlsx.utils.encode_cell({ c: range.s.c + i, r: rowIndex + columnNameRowExist, }) diff --git a/packages/nc-gui/utils/validation.ts b/packages/nc-gui/utils/validation.ts index 3b0a9b893b..be0571b5fd 100644 --- a/packages/nc-gui/utils/validation.ts +++ b/packages/nc-gui/utils/validation.ts @@ -80,18 +80,20 @@ export function validateColumnName(v: string, isGQL = false) { } export const projectTitleValidator = { - validator: (rule: any, value: any, callback: (errMsg?: string) => void) => { + validator: (rule: any, value: any) => { const { t } = getI18n().global - if (value?.length > 50) { - // callback('Project name exceeds 50 characters') - callback(t('msg.error.projectNameExceeds50Characters')) - } - if (value[0] === ' ') { - // callback('Project name cannot start with space') - callback(t('msg.error.projectNameCannotStartWithSpace')) - } - callback() + return new Promise((resolve, reject) => { + if (value?.length > 50) { + reject(new Error(t('msg.error.projectNameExceeds50Characters'))) + } + + if (value[0] === ' ') { + reject(new Error(t('msg.error.projectNameCannotStartWithSpace'))) + } + + resolve(true) + }) }, } diff --git a/packages/nocodb-sdk/package-lock.json b/packages/nocodb-sdk/package-lock.json index 497bd8f603..cbdb43f6b2 100644 --- a/packages/nocodb-sdk/package-lock.json +++ b/packages/nocodb-sdk/package-lock.json @@ -16087,4 +16087,4 @@ "dev": true } } -} \ No newline at end of file +} diff --git a/scripts/cypress/integration/common/00_pre_configurations.js b/scripts/cypress/integration/common/00_pre_configurations.js index ebc07f02b7..c4edc6c659 100644 --- a/scripts/cypress/integration/common/00_pre_configurations.js +++ b/scripts/cypress/integration/common/00_pre_configurations.js @@ -169,7 +169,7 @@ export const genTest = (apiType, dbType) => { function cy_createProjectBlock(proj, apiType, dbType) { // click home button - cy.get(".nc-noco-brand-icon").click(); + cy.getSettled(".nc-noco-brand-icon").click(); cy.get(".ant-table-content").then((obj) => { // if project already created, open // else, create a new one @@ -240,8 +240,13 @@ export const genTest = (apiType, dbType) => { } // close team & auth tab + + // wait for tab to be rendered completely cy.wait(2000); - cy.get("button.ant-tabs-tab-remove").should("exist").click(); + + cy.getSettled("button.ant-tabs-tab-remove") + .should("be.visible") + .click(); cy.get("button.ant-tabs-tab-remove").should("not.exist"); // first instance of updating local storage information diff --git a/scripts/cypress/integration/common/1b_table_column_operations.js b/scripts/cypress/integration/common/1b_table_column_operations.js index 75925ba475..f1d53f0895 100644 --- a/scripts/cypress/integration/common/1b_table_column_operations.js +++ b/scripts/cypress/integration/common/1b_table_column_operations.js @@ -10,6 +10,9 @@ export const genTest = (apiType, dbType) => { function addNewRow(index, cellValue) { cy.get(".nc-add-new-row-btn:visible").should("exist"); cy.get(".nc-add-new-row-btn").click(); + + cy.wait(2000); + // cy.get("#data-table-form-Title > input").first().type(cellValue); cy.get(".nc-expand-col-Title") .find(".nc-cell > input") @@ -143,6 +146,9 @@ export const genTest = (apiType, dbType) => { .trigger("mouseover", { force: true }); cy.get(".nc-row-expand").click({ force: true }); + // wait for page render to complete + cy.get('button:contains("Save row"):visible').should("exist"); + cy.get(".nc-expand-col-Title") .find(".nc-cell > input") .should("exist") diff --git a/scripts/cypress/integration/common/3f_link_to_another_record.js b/scripts/cypress/integration/common/3f_link_to_another_record.js index 3891fd5404..8e072ee185 100644 --- a/scripts/cypress/integration/common/3f_link_to_another_record.js +++ b/scripts/cypress/integration/common/3f_link_to_another_record.js @@ -5,6 +5,10 @@ import { isTestSuiteActive } from "../../support/page_objects/projectConstants"; export const genTest = (apiType, dbType) => { if (!isTestSuiteActive(apiType, dbType)) return; + // tbd: this needs a proper fix + let waitTime = 2000; + let clear; + describe(`${apiType.toUpperCase()} api - Link to another record`, () => { function fetchParentFromLabel(label) { cy.get("label").contains(label).parents(".ant-row").click(); @@ -115,10 +119,12 @@ export const genTest = (apiType, dbType) => { cy.wait("@unlinkCount"); } - // before(() => { - // // required for standalone test - // // loginPage.loginAndOpenProject(apiType, dbType); - // }); + before(() => { + cy.restoreLocalStorage(); + + clear = Cypress.LocalStorage.clear; + Cypress.LocalStorage.clear = () => {}; + }); afterEach(() => { cy.saveLocalStorage(); @@ -141,6 +147,8 @@ export const genTest = (apiType, dbType) => { cy.deleteTable("Sheet2"); cy.saveLocalStorage(); + + Cypress.LocalStorage.clear = clear; }); /////////////////////////////////////////////////// @@ -169,11 +177,15 @@ export const genTest = (apiType, dbType) => { // Expand form [Add new row] // it("Add HM, BT, MM Link, Expand form", () => { + // http://localhost:8080/api/v1/db/data/noco/p_0l53e1xgsxlecb/md_mn4xgb2jnq16a7?limit=10&offset=0&where=&fields[]=Title&fields[]=Id + cy.intercept("GET", `/api/v1/db/data/noco/**`).as("waitForCardLoad"); + cy.openTableTab("Sheet2", 0); // Click on `Add new row` button cy.get(".nc-add-new-row-btn:visible").should("exist"); cy.get(".nc-add-new-row-btn").click(); + cy.wait(waitTime); // Title cy.get(".nc-expand-col-Title") @@ -193,26 +205,32 @@ export const genTest = (apiType, dbType) => { .find(".nc-action-icon") .should("exist") .click({ force: true }); - cy.wait(1000); + cy.wait(waitTime); + cy.wait("@waitForCardLoad"); cy.getActiveModal(".nc-modal-link-record") .find(".ant-card") .should("exist") .eq(0) .click(); + cy.wait(waitTime); // MM cy.get(".nc-expand-col-Sheet1.List").find(".ant-btn-primary").click(); - cy.wait(1000); + cy.wait(waitTime); + cy.wait("@waitForCardLoad"); cy.getActiveModal(".nc-modal-link-record") .find(".ant-card") .should("exist") .eq(0) .click(); + cy.wait(waitTime); // HM cy.get(".nc-expand-col-Link2-1hm").find(".ant-btn-primary").click(); - cy.wait(1000); + cy.wait(waitTime); + cy.wait("@waitForCardLoad"); cy.getActiveModal().find(".ant-card").should("exist").eq(0).click(); + cy.wait(waitTime); // Save row cy.getActiveDrawer(".nc-drawer-expanded-form") @@ -238,12 +256,13 @@ export const genTest = (apiType, dbType) => { .getCell("Sheet1", 2) .find(".nc-action-icon") .click({ force: true }); + cy.wait(waitTime); cy.getActiveModal(".nc-modal-link-record") .find(".ant-card") .should("exist") .eq(1) .click(); - cy.wait(1000); + cy.wait(waitTime); // MM mainPage @@ -251,12 +270,13 @@ export const genTest = (apiType, dbType) => { .find(".nc-action-icon") .last() .click({ force: true }); + cy.wait(waitTime); cy.getActiveModal(".nc-modal-link-record") .find(".ant-card") .should("exist") .eq(1) .click(); - cy.wait(1000); + cy.wait(waitTime); // HM mainPage @@ -264,50 +284,65 @@ export const genTest = (apiType, dbType) => { .find(".nc-action-icon") .last() .click({ force: true }); + cy.wait(waitTime); cy.getActiveModal(".nc-modal-link-record") .find(".ant-card") .should("exist") .eq(1) .click(); + cy.wait(waitTime); }); // Existing row, expand record it("Add HM, BT, MM Link, expand record", () => { + // http://localhost:8080/api/v1/db/data/noco/p_0l53e1xgsxlecb/md_mn4xgb2jnq16a7?limit=10&offset=0&where=&fields[]=Title&fields[]=Id + cy.intercept("GET", `/api/v1/db/data/noco/**`).as("waitForCardLoad"); + addRow(3, "2c"); + + cy.wait(waitTime); cy.get(".nc-row-expand").eq(2).click({ force: true }); + cy.wait(waitTime); + + // wait for page render to complete + cy.get('button:contains("Save row"):visible').should("exist"); // BT - cy.wait(1000); + cy.wait(waitTime); cy.get(".nc-expand-col-Sheet1") .find(".nc-action-icon") .should("exist") .click({ force: true }); - cy.wait(1000); + cy.wait(waitTime); + // cy.wait("@waitForCardLoad"); cy.getActiveModal(".nc-modal-link-record") .find(".ant-card") .should("exist") .eq(2) .click(); + cy.wait(waitTime); // MM cy.get(".nc-expand-col-Sheet1.List").find(".ant-btn-primary").click(); - cy.wait(1000); + cy.wait(waitTime); + // cy.wait("@waitForCardLoad"); cy.getActiveModal(".nc-modal-link-record") .find(".ant-card") .should("exist") .eq(2) .click(); - cy.wait(1000); + cy.wait(waitTime); // HM cy.get(".nc-expand-col-Link2-1hm").find(".ant-btn-primary").click(); - cy.wait(1000); + cy.wait(waitTime); + // cy.wait("@waitForCardLoad"); cy.getActiveModal(".nc-modal-link-record") .find(".ant-card") .should("exist") .eq(2) .click(); - cy.wait(1000); + cy.wait(waitTime); cy.getActiveDrawer(".nc-drawer-expanded-form") .find("button") diff --git a/scripts/cypress/integration/common/4e_form_view_share.js b/scripts/cypress/integration/common/4e_form_view_share.js index d976cee30b..9d3c5ceb34 100644 --- a/scripts/cypress/integration/common/4e_form_view_share.js +++ b/scripts/cypress/integration/common/4e_form_view_share.js @@ -62,6 +62,9 @@ export const genTest = (apiType, dbType) => { // enable "Submit another form" check box cy.get("button.nc-form-checkbox-show-blank-form").click(); + // [kludge] CI-CD: title is being rendered initially in disabled state + cy.wait(2000); + // Update header & add some description, verify cy.get(".nc-form") .find('[placeholder="Form Title"]') diff --git a/scripts/cypress/integration/common/4f_grid_view_share.js b/scripts/cypress/integration/common/4f_grid_view_share.js index b815200d68..0fadfd5ee7 100644 --- a/scripts/cypress/integration/common/4f_grid_view_share.js +++ b/scripts/cypress/integration/common/4f_grid_view_share.js @@ -39,12 +39,17 @@ export const genTest = (apiType, dbType) => { cy.getActiveModal(".nc-modal-share-view").should("not.be.visible"); }; + let clear; + describe(`${apiType.toUpperCase()} api - GRID view (Share)`, () => { // Run once before test- create project (rest/graphql) // before(() => { cy.restoreLocalStorage(); cy.openTableTab("Address", 25); + + clear = Cypress.LocalStorage.clear; + Cypress.LocalStorage.clear = () => {}; }); beforeEach(() => { @@ -61,6 +66,8 @@ export const genTest = (apiType, dbType) => { cy.restoreLocalStorage(); cy.closeTableTab("Address"); cy.saveLocalStorage(); + + Cypress.LocalStorage.clear = clear; }); // Common routine to create/edit/delete GRID & GALLERY view diff --git a/scripts/cypress/integration/common/4g_table_view_expanded_form.js b/scripts/cypress/integration/common/4g_table_view_expanded_form.js index 1094a7e4df..3031a30654 100644 --- a/scripts/cypress/integration/common/4g_table_view_expanded_form.js +++ b/scripts/cypress/integration/common/4g_table_view_expanded_form.js @@ -31,10 +31,10 @@ export const genTest = (apiType, dbType) => { // open a table to work on views // - cy.openTableTab('Country', 25); + cy.openTableTab("Country", 25); clear = Cypress.LocalStorage.clear; - Cypress.LocalStorage.clear = () => {} + Cypress.LocalStorage.clear = () => {}; }); beforeEach(() => { @@ -91,6 +91,9 @@ export const genTest = (apiType, dbType) => { // click on first row-expand if grid & first card if its gallery if (viewType === "grid") { cy.get(".nc-row-expand").first().click({ force: true }); + + // wait for page render to complete + cy.get('button:contains("Save row"):visible').should("exist"); } else if (viewType === "gallery") { cy.get(".nc-gallery-container .ant-card").first().click(); } diff --git a/scripts/cypress/integration/common/5a_user_role.js b/scripts/cypress/integration/common/5a_user_role.js index 8ccba12e06..e52eadaf66 100644 --- a/scripts/cypress/integration/common/5a_user_role.js +++ b/scripts/cypress/integration/common/5a_user_role.js @@ -147,8 +147,11 @@ export const genTest = (apiType, dbType) => { if (roleType === "creator") { // kludge: wait for page load to finish // close team & auth tab - cy.wait(500); - cy.get("button.ant-tabs-tab-remove").should("exist").click(); + // cy.wait(500); + // cy.get("button.ant-tabs-tab-remove").should("exist").click(); + cy.getSettled("button.ant-tabs-tab-remove") + .should("be.visible") + .click(); cy.wait(500); } diff --git a/scripts/cypress/integration/common/8a_webhook.js b/scripts/cypress/integration/common/8a_webhook.js index f39b8a6935..4f9e633fc7 100644 --- a/scripts/cypress/integration/common/8a_webhook.js +++ b/scripts/cypress/integration/common/8a_webhook.js @@ -16,24 +16,13 @@ function createWebhook(hook, test) { cy.get(".nc-btn-create-webhook").should("exist").click(); // hardcode "Content-type: application/json" - cy.get(".ant-tabs-tab-btn").contains("Headers").should("exist").click(); - - // kludge : as neither scrollIntoView nor scrollTo didn't yield any results - // cy.getActiveSelection().find('.ant-select-item').contains('Content-Type).scrollIntoView(); - // cy.getActiveSelection().find('.rc-virtual-list').scrollTo('center'); - // cy.getActiveSelection().select('Content-Type', { force: true }); + cy.get(`.ant-tabs-tab-btn:contains("Headers")`).should("exist").click(); cy.get(".nc-input-hook-header-key") .should("exist") .click() .type("Content-Type{enter}"); - // cy.getActiveSelection(".nc-dropdown-webhook-header") - // .find(".ant-select-item-option-content") - // .contains("Content-Type") - // .should("exist") - // .click(); - cy.get("input.nc-input-hook-header-value") .should("exist") .clear({ force: true }) @@ -184,6 +173,9 @@ function updateRow(index, cellValue) { .eq(index - 1) .click({ force: true }); + // wait for page render to complete + cy.get('button:contains("Save row"):visible').should("exist"); + cy.get(".nc-expand-col-Title") .should("exist") .find(".nc-cell > input") diff --git a/scripts/cypress/integration/common/9a_QuickTest.js b/scripts/cypress/integration/common/9a_QuickTest.js index 24481a2604..f808667d0e 100644 --- a/scripts/cypress/integration/common/9a_QuickTest.js +++ b/scripts/cypress/integration/common/9a_QuickTest.js @@ -56,13 +56,18 @@ let cn = [ ]; function openWebhook(index) { + // http://localhost:8080/api/v1/db/meta/tables/md_dx81kkfdso115u/hooks + cy.intercept("GET", "/api/v1/db/meta/tables/*/hooks").as("getHooks"); + cy.get(".nc-actions-menu-btn").should("exist").click(); cy.getActiveMenu(".nc-dropdown-actions-menu") .find(".ant-dropdown-menu-title-content") .contains("Webhooks") .click(); - cy.get(".nc-hook").eq(index).click(); + cy.wait("@getHooks"); + + cy.get(`.nc-hook:eq(${index})`).should("exist").click(); } // to be invoked after open diff --git a/scripts/cypress/integration/common/9b_ERD.js b/scripts/cypress/integration/common/9b_ERD.js index 601ec9b202..0bff55311d 100644 --- a/scripts/cypress/integration/common/9b_ERD.js +++ b/scripts/cypress/integration/common/9b_ERD.js @@ -1,4 +1,4 @@ -import { mainPage } from "../../support/page_objects/mainPage"; +import { mainPage, settingsPage } from "../../support/page_objects/mainPage"; import {loginPage} from "../../support/page_objects/navigation"; import { isTestSuiteActive, mysqlSakilaSqlViews, mysqlSakilaTables, pgSakilaSqlViews, pgSakilaTables, sqliteSakilaSqlViews } from "../../support/page_objects/projectConstants"; @@ -59,6 +59,11 @@ export const genTest = (apiType, dbType) => { it(`Verify ERD Context menu in all table view`, () => { mainPage.openErdTab(); + cy.wait(2000) + // todo: Edges are not rendering properly in cypress in first render + settingsPage.openTab(settingsPage.UI_ACCESS_CONTROL) + settingsPage.openTab(settingsPage.ERD) + cy.get('.nc-erd-context-menu').should('be.visible'); cy.get('.nc-erd-context-menu').get('.nc-erd-histogram').should('be.visible'); cy.get('.nc-erd-context-menu').find('.ant-checkbox').should('have.length', 3); @@ -68,6 +73,10 @@ export const genTest = (apiType, dbType) => { cy.get('.nc-erd-context-menu').find('.nc-erd-showColumns-label').dblclick(); cy.get('.nc-erd-context-menu').find('.ant-checkbox').should('have.length', 5); + + // todo: Enabling and disabling showJunctionTableNames rerenders `mm` edges since `mm` edges is not getting rendered in cypress + cy.get('.nc-erd-context-menu').get('.nc-erd-showJunctionTableNames-checkbox').click(); + cy.get('.nc-erd-context-menu').get('.nc-erd-showJunctionTableNames-checkbox').click(); }); it("Verify ERD of all tables view and verify columns of actor and payment with default config", () => { @@ -311,6 +320,10 @@ export const genTest = (apiType, dbType) => { cy.get('.nc-erd-context-menu').get('.nc-erd-showViews-checkbox').click(); cy.get('.nc-erd-context-menu').get('.nc-erd-showMMTables-checkbox').click(); + // Enabling and disabling showJunctionTableNames rerenders `mm` edges since `mm` edges is not getting rendered in cypress + cy.get('.nc-erd-context-menu').get('.nc-erd-showJunctionTableNames-checkbox').click(); + cy.get('.nc-erd-context-menu').get('.nc-erd-showJunctionTableNames-checkbox').click(); + if(dbType === "mysql") { cy.get('.nc-erd-vue-flow').find('.nc-erd-table-node').should('have.length', 16) cy.get('.nc-erd-vue-flow').find('.vue-flow__edge').should('have.length', 26) diff --git a/scripts/cypress/integration/spec/roleValidation.spec.js b/scripts/cypress/integration/spec/roleValidation.spec.js index 6d08ac9273..fc03f211e2 100644 --- a/scripts/cypress/integration/spec/roleValidation.spec.js +++ b/scripts/cypress/integration/spec/roleValidation.spec.js @@ -162,6 +162,10 @@ export function _editData(roleType, mode) { .eq(0) .trigger("mouseover", { force: true }); cy.get(".nc-row-expand").should("exist").eq(10).click({ force: true }); + + // wait for page render to complete + cy.get('button:contains("Save row"):visible').should("exist"); + cy.getActiveDrawer(".nc-drawer-expanded-form") .find("button") .contains("Save row") @@ -175,6 +179,10 @@ export function _editData(roleType, mode) { // update cell contents option using row expander should be disabled // cy.get(".nc-row-expand").should("exist").eq(10).click({ force: true }); + + // wait for page render to complete + cy.get('button:contains("Save row"):visible').should("exist"); + cy.getActiveDrawer(".nc-drawer-expanded-form") .find("button:disabled") .contains("Save row") diff --git a/scripts/cypress/support/commands.js b/scripts/cypress/support/commands.js index 19c2e08fcd..245313264b 100644 --- a/scripts/cypress/support/commands.js +++ b/scripts/cypress/support/commands.js @@ -29,6 +29,36 @@ import { isXcdb, isPostgres } from "./page_objects/projectConstants"; require("@4tw/cypress-drag-drop"); +// recursively gets an element, returning only after it's determined to be attached to the DOM for good +Cypress.Commands.add('getSettled', (selector, opts = {}) => { + const retries = opts.retries || 3; + const delay = opts.delay || 400; + + const isAttached = (resolve, count = 0) => { + const el = Cypress.$(selector); + + // is element attached to the DOM? + count = Cypress.dom.isAttached(el) ? count + 1 : 0; + + // hit our base case, return the element + if (count >= retries) { + return resolve(el); + } + + // retry after a bit of a delay + setTimeout(() => isAttached(resolve, count), delay); + }; + + // wrap, so we can chain cypress commands off the result + return cy.wrap(null).then(() => { + return new Cypress.Promise((resolve) => { + return isAttached(resolve, 0); + }).then((el) => { + return cy.wrap(el); + }); + }); +}); + // for waiting until page load Cypress.Commands.add("waitForSpinners", () => { cy.visit("http://localhost:3000/signup", { @@ -252,9 +282,9 @@ Cypress.Commands.add("getActiveModal", (wrapperSelector) => { Cypress.Commands.add("getActiveMenu", (overlaySelector) => { if (overlaySelector) { - return cy.get(`${overlaySelector} .ant-dropdown-content:visible`); + return cy.getSettled(`${overlaySelector} .ant-dropdown-content:visible`); } - return cy.get(".ant-dropdown-content:visible").last(); + return cy.getSettled(".ant-dropdown-content:visible").last(); }); Cypress.Commands.add("getActivePopUp", () => { diff --git a/scripts/cypress/support/page_objects/mainPage.js b/scripts/cypress/support/page_objects/mainPage.js index e0376615c9..3a6f09b374 100644 --- a/scripts/cypress/support/page_objects/mainPage.js +++ b/scripts/cypress/support/page_objects/mainPage.js @@ -204,11 +204,11 @@ export class _mainPage { }; addColumn = (colName, tableName) => { - cy.get(".nc-column-add").click({ - force: true, - }); + cy.get(".nc-column-add").click(); - cy.getActiveMenu(".nc-dropdown-grid-add-column") + cy.wait(2000); + + cy.getActiveMenu(".nc-dropdown-grid-add-column:has(.nc-column-name-input)") .find("input.nc-column-name-input") .should("exist") .clear() @@ -219,22 +219,24 @@ export class _mainPage { .should("exist") .click(); cy.toastWait(`Column created`); + + cy.wait(2000); + cy.get(`th[data-title="${colName}"]`).should("exist"); }; addColumnWithType = (colName, colType, tableName) => { - cy.get(".nc-column-add").click({ - force: true, - }); + cy.get(".nc-column-add").click(); - cy.getActiveMenu(".nc-dropdown-grid-add-column") + cy.wait(2000); + + cy.getActiveMenu(".nc-dropdown-grid-add-column:has(.nc-column-name-input)") .find("input.nc-column-name-input") .should("exist") .clear() .type(colName); // change column type and verify - // cy.get(".nc-column-type-input").last().click(); cy.getActiveMenu(".nc-dropdown-grid-add-column") .find(".nc-column-type-input") .last() @@ -243,13 +245,15 @@ export class _mainPage { .find(".ant-select-item-option") .contains(colType) .click(); - // cy.get(".ant-btn-primary:visible").contains("Save").click(); cy.getActiveMenu(".nc-dropdown-grid-add-column") .find(".ant-btn-primary:visible") .contains("Save") .click(); cy.toastWait(`Column created`); + + cy.wait(2000); + cy.get(`th[data-title="${colName}"]`).should("exist"); }; @@ -435,7 +439,7 @@ export class _mainPage { .find(".ant-btn-primary") .contains("Add Sort Option") .click(); - cy.getActiveMenu(".nc-dropdown-sort-menu") + cy.getActiveMenu(".nc-dropdown-sort-menu:has(.nc-sort-field-select div)") .find(".nc-sort-field-select div") .first() .click(); @@ -473,7 +477,8 @@ export class _mainPage { .find(".ant-btn-primary") .contains("Add Filter") .click(); - cy.getActiveMenu(".nc-dropdown-filter-menu") + + cy.getActiveMenu(".nc-dropdown-filter-menu:has(.nc-filter-field-select)") .find(".nc-filter-field-select") .should("exist") .last() @@ -527,11 +532,10 @@ export class _mainPage { // one of the row would contain seggregation header ('other views) if (5 == $tableRow[0].childElementCount) { cy.wrap($tableRow).find(".nc-icon").last().click(); - cy.wait(100); + cy.toastWait("Deleted shared view successfully"); } }) .then(() => { - cy.toastWait("Deleted shared view successfully"); cy.getActiveModal() .find("button.ant-modal-close") .should("exist") diff --git a/scripts/cypress/support/page_objects/navigation.js b/scripts/cypress/support/page_objects/navigation.js index 050c7aaef8..20f664c7ee 100644 --- a/scripts/cypress/support/page_objects/navigation.js +++ b/scripts/cypress/support/page_objects/navigation.js @@ -108,7 +108,7 @@ export class _projectsPage { cy.wait("@waitForPageLoad"); // close team & auth tab - cy.get("button.ant-tabs-tab-remove").should("exist").click(); + cy.getSettled("button.ant-tabs-tab-remove").should("be.visible").click(); cy.get("button.ant-tabs-tab-remove").should("not.exist"); }