diff --git a/packages/nc-gui/components.d.ts b/packages/nc-gui/components.d.ts index 4a230a0102..6e9b243c03 100644 --- a/packages/nc-gui/components.d.ts +++ b/packages/nc-gui/components.d.ts @@ -96,8 +96,6 @@ declare module '@vue/runtime-core' { MaterialSymbolsDarkModeOutline: typeof import('~icons/material-symbols/dark-mode-outline')['default'] MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default'] MaterialSymbolsKeyboardReturn: typeof import('~icons/material-symbols/keyboard-return')['default'] - MaterialSymbolsKeyboardShift: typeof import('~icons/material-symbols/keyboard-shift')['default'] - MaterialSymbolsLightMode: typeof import('~icons/material-symbols/light-mode')['default'] MaterialSymbolsLightModeOutline: typeof import('~icons/material-symbols/light-mode-outline')['default'] MaterialSymbolsRocketLaunchOutline: typeof import('~icons/material-symbols/rocket-launch-outline')['default'] MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default'] diff --git a/packages/nc-gui/components/cell/MultiSelect.vue b/packages/nc-gui/components/cell/MultiSelect.vue index f3f09fecba..dcfd660be9 100644 --- a/packages/nc-gui/components/cell/MultiSelect.vue +++ b/packages/nc-gui/components/cell/MultiSelect.vue @@ -1,19 +1,23 @@ @@ -152,17 +240,20 @@ useSelectedCellKeyupListener(active, (e) => { mode="multiple" class="w-full" :bordered="false" + clear-icon :show-arrow="!readOnly" - :show-search="false" + :show-search="active || editable" + :open="isOpen && (active || editable)" :disabled="readOnly" :class="{ '!ml-[-8px]': readOnly }" :dropdown-class-name="`nc-dropdown-multi-select-cell ${isOpen ? 'active' : ''}`" - @keydown.enter.stop + @search="search" + @keydown.stop @click="isOpen = (active || editable) && !isOpen" > { + + + + + Create new option named {{ searchVal }} + + + + { @apply "flex overflow-hidden"; } - diff --git a/packages/nc-gui/components/cell/SingleSelect.vue b/packages/nc-gui/components/cell/SingleSelect.vue index b7561d2e97..ab20e724a8 100644 --- a/packages/nc-gui/components/cell/SingleSelect.vue +++ b/packages/nc-gui/components/cell/SingleSelect.vue @@ -1,4 +1,5 @@ @@ -96,13 +157,15 @@ useSelectedCellKeyupListener(active, (e) => { class="w-full" :allow-clear="!column.rqd && active" :bordered="false" - :open="isOpen" + :open="isOpen && (active || editable)" :disabled="readOnly" :show-arrow="!readOnly && (active || editable || vModel === null)" :dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen ? 'active' : ''}`" + :show-search="active || editable" @select="isOpen = false" - @keydown.enter.stop - @click="isOpen = (active || editable) && !isOpen" + @keydown.stop + @search="search" + @click="toggleMenu" > { + + + + + + Create new option named {{ searchVal }} + + + @@ -141,6 +213,3 @@ useSelectedCellKeyupListener(active, (e) => { opacity: 1; } - diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index d0d6fe7867..9b08d29554 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -133,7 +133,7 @@ export function useMultiSelect( const onKeyDown = async (e: KeyboardEvent) => { // invoke the keyEventHandler if provided and return if it returns true if (await keyEventHandler?.(e)) { - return + return true } if ( @@ -268,7 +268,7 @@ export function useMultiSelect( } if (unref(editEnabled) || e.ctrlKey || e.altKey || e.metaKey) { - return + return true } /** on letter key press make cell editable and empty */ diff --git a/packages/nc-gui/nuxt.config.ts b/packages/nc-gui/nuxt.config.ts index 885cbe78d9..afba37c6ba 100644 --- a/packages/nc-gui/nuxt.config.ts +++ b/packages/nc-gui/nuxt.config.ts @@ -141,7 +141,6 @@ export default defineNuxtConfig({ 'process.env.DEBUG': 'false', 'process.nextTick': () => {}, 'process.env.ANT_MESSAGE_DURATION': process.env.ANT_MESSAGE_DURATION, - 'process.env.NC_BACKEND_URL': process.env.NC_BACKEND_URL, }, server: { watch: { diff --git a/packages/noco-docs/content/en/getting-started/installation.md b/packages/noco-docs/content/en/getting-started/installation.md index 107158e4ed..2f341aee2a 100644 --- a/packages/noco-docs/content/en/getting-started/installation.md +++ b/packages/noco-docs/content/en/getting-started/installation.md @@ -478,7 +478,7 @@ It is mandatory to configure `NC_DB` environment variables for production usecas | NC_JWT_EXPIRES_IN | No | JWT token expiry time | `10h` | | | NC_CONNECT_TO_EXTERNAL_DB_DISABLED | No | Disable Project creation with external database | | | | NC_INVITE_ONLY_SIGNUP | No | Allow users to signup only via invite url, value should be any non-empty string. | | | -| NC_BACKEND_URL | No | Custom Backend URL | ``http://localhost:8080`` will be used | | +| NUXT_PUBLIC_NC_BACKEND_URL | No | Custom Backend URL | ``http://localhost:8080`` will be used | | | NC_REQUEST_BODY_SIZE | No | Request body size [limit](https://expressjs.com/en/resources/middleware/body-parser.html#limit) | `1048576` | | | NC_EXPORT_MAX_TIMEOUT | No | After NC_EXPORT_MAX_TIMEOUT csv gets downloaded in batches | Default value 5000(in millisecond) will be used | | | NC_DISABLE_TELE | No | Disable telemetry | | | diff --git a/packages/nocodb/src/lib/Noco.ts b/packages/nocodb/src/lib/Noco.ts index 91cf8baac9..0dc767c026 100644 --- a/packages/nocodb/src/lib/Noco.ts +++ b/packages/nocodb/src/lib/Noco.ts @@ -213,7 +213,6 @@ export default class Noco { }); // to get ip addresses - this.router.use(requestIp.mw()); this.router.use(cookieParser()); this.router.use( diff --git a/packages/nocodb/src/lib/meta/api/testApis.ts b/packages/nocodb/src/lib/meta/api/testApis.ts index 76fcfcd842..73cba3ceb4 100644 --- a/packages/nocodb/src/lib/meta/api/testApis.ts +++ b/packages/nocodb/src/lib/meta/api/testApis.ts @@ -6,6 +6,7 @@ export async function reset(req: Request, res) { parallelId: req.body.parallelId, dbType: req.body.dbType, isEmptyProject: req.body.isEmptyProject, + workerId: req.body.workerId }); res.json(await service.process()); diff --git a/packages/nocodb/src/lib/services/test/TestResetService/index.ts b/packages/nocodb/src/lib/services/test/TestResetService/index.ts index 59d7372565..ae9837ce3e 100644 --- a/packages/nocodb/src/lib/services/test/TestResetService/index.ts +++ b/packages/nocodb/src/lib/services/test/TestResetService/index.ts @@ -23,11 +23,13 @@ const loginRootUser = async () => { const projectTitleByType = { sqlite: 'sampleREST', mysql: 'externalREST', - pg: 'pgExtREST', + pg: 'pgExtREST' }; export class TestResetService { private readonly parallelId; + // todo: Hack to resolve issue with pg resetting + private readonly workerId; private readonly dbType; private readonly isEmptyProject: boolean; @@ -35,14 +37,17 @@ export class TestResetService { parallelId, dbType, isEmptyProject, + workerId }: { parallelId: string; dbType: string; isEmptyProject: boolean; + workerId: string; }) { this.parallelId = parallelId; this.dbType = dbType; this.isEmptyProject = isEmptyProject; + this.workerId = workerId; } async process() { @@ -68,6 +73,7 @@ export class TestResetService { token, dbType: this.dbType, parallelId: this.parallelId, + workerId: this.workerId }); try { @@ -90,10 +96,12 @@ export class TestResetService { token, dbType, parallelId, + workerId }: { token: string; dbType: string; parallelId: string; + workerId: string; }) { const title = `${projectTitleByType[dbType]}${parallelId}`; const project: Project | undefined = await Project.getByTitle(title); @@ -115,7 +123,7 @@ export class TestResetService { token, title, parallelId, - isEmptyProject: this.isEmptyProject, + isEmptyProject: this.isEmptyProject }); } else if (dbType == 'mysql') { await resetMysqlSakilaProject({ @@ -123,20 +131,20 @@ export class TestResetService { title, parallelId, oldProject: project, - isEmptyProject: this.isEmptyProject, + isEmptyProject: this.isEmptyProject }); } else if (dbType == 'pg') { await resetPgSakilaProject({ token, title, - parallelId, + parallelId: workerId, oldProject: project, - isEmptyProject: this.isEmptyProject, + isEmptyProject: this.isEmptyProject }); } return { - project: await Project.getByTitle(title), + project: await Project.getByTitle(title) }; } } @@ -169,7 +177,7 @@ const removeProjectUsersFromCache = async (project: Project) => { const projectUsers: ProjectUser[] = await ProjectUser.getUsersList({ project_id: project.id, limit: 1000, - offset: 0, + offset: 0 }); for (const projectUser of projectUsers) { diff --git a/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts b/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts index bdd4adc18d..82953a7124 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts +++ b/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts @@ -112,4 +112,48 @@ export class SelectOptionCellPageObject extends BasePage { await this.get({ index, columnHeader }).click(); await this.rootPage.locator(`.nc-dropdown-single-select-cell`).nth(index).waitFor({ state: 'hidden' }); } + + async addNewOption({ + index, + columnHeader, + option, + multiSelect, + }: { + index: number; + columnHeader: string; + option: string; + multiSelect?: boolean; + }) { + const selectCell = this.get({ index, columnHeader }); + + // check if cell active + if (!(await selectCell.getAttribute('class')).includes('active')) { + await selectCell.click(); + } + + await selectCell.locator('.ant-select-selection-search-input').type(option); + + await selectCell.locator('.ant-select-selection-search-input').press('Enter'); + + if (multiSelect) await selectCell.locator('.ant-select-selection-search-input').press('Escape'); + // todo: wait for update api call + } + + async verifySelectedOptions({ + index, + options, + columnHeader, + }: { + columnHeader: string; + options: string[]; + index: number; + }) { + const selectCell = this.get({ index, columnHeader }); + + let counter = 0; + for (const option of options) { + await expect(selectCell.locator(`.nc-selected-option`).nth(counter)).toHaveText(option); + counter++; + } + } } diff --git a/tests/playwright/setup/db.ts b/tests/playwright/setup/db.ts index d42591f406..103af14789 100644 --- a/tests/playwright/setup/db.ts +++ b/tests/playwright/setup/db.ts @@ -11,17 +11,18 @@ const isSqlite = (context: NcContext) => context.dbType === 'sqlite'; const isPg = (context: NcContext) => context.dbType === 'pg'; -const pg_credentials = () => ({ +const pg_credentials = (context: NcContext) => ({ user: 'postgres', host: 'localhost', - database: `sakila_${process.env.TEST_PARALLEL_INDEX}`, + // todo: Hack to resolve issue with pg resetting + database: `sakila_${context.workerId}`, password: 'password', port: 5432, }); -const pgExec = async (query: string) => { +const pgExec = async (query: string, context: NcContext) => { // open pg client connection - const client = new Client(pg_credentials()); + const client = new Client(pg_credentials(context)); await client.connect(); await client.query(query); diff --git a/tests/playwright/setup/index.ts b/tests/playwright/setup/index.ts index a738d5c095..8d04b75464 100644 --- a/tests/playwright/setup/index.ts +++ b/tests/playwright/setup/index.ts @@ -1,10 +1,14 @@ import { Page, selectors } from '@playwright/test'; import axios from 'axios'; +const workerCount = {}; + export interface NcContext { project: any; token: string; dbType?: string; + // todo: Hack to resolve issue with pg resetting + workerId?: string; } selectors.setTestIdAttribute('data-testid'); @@ -13,11 +17,23 @@ const setup = async ({ page, isEmptyProject }: { page: Page; isEmptyProject?: bo let dbType = process.env.CI ? process.env.E2E_DB_TYPE : process.env.E2E_DEV_DB_TYPE; dbType = dbType || 'sqlite'; + let workerId; + // todo: Hack to resolve issue with pg resetting + if (dbType === 'pg') { + const workerIndex = process.env.TEST_PARALLEL_INDEX; + if (!workerCount[workerIndex]) { + workerCount[workerIndex] = 0; + } + workerCount[workerIndex]++; + workerId = String(Number(workerIndex) + Number(workerCount[workerIndex]) * 4); + } + // if (!process.env.CI) console.time(`setup ${process.env.TEST_PARALLEL_INDEX}`); let response; try { response = await axios.post(`http://localhost:8080/api/v1/meta/test/reset`, { parallelId: process.env.TEST_PARALLEL_INDEX, + workerId: workerId, dbType, isEmptyProject, }); @@ -59,7 +75,7 @@ const setup = async ({ page, isEmptyProject }: { page: Page; isEmptyProject?: bo await page.goto(`/#/nc/${project.id}/auth`, { waitUntil: 'networkidle' }); - return { project, token, dbType } as NcContext; + return { project, token, dbType, workerId } as NcContext; }; export default setup; diff --git a/tests/playwright/tests/columnMultiSelect.spec.ts b/tests/playwright/tests/columnMultiSelect.spec.ts index 042d0466ac..5eff82acfa 100644 --- a/tests/playwright/tests/columnMultiSelect.spec.ts +++ b/tests/playwright/tests/columnMultiSelect.spec.ts @@ -128,4 +128,28 @@ test.describe('Multi select', () => { await grid.column.delete({ title: 'MultiSelect' }); }); + + test('Add new option directly from cell', async () => { + await grid.cell.selectOption.addNewOption({ + index: 0, + columnHeader: 'MultiSelect', + option: 'Option added from cell 1', + multiSelect: true, + }); + + await grid.cell.selectOption.addNewOption({ + index: 0, + columnHeader: 'MultiSelect', + option: 'Option added from cell 2', + multiSelect: true, + }); + + await grid.cell.selectOption.verifySelectedOptions({ + index: 0, + columnHeader: 'MultiSelect', + options: ['Option added from cell 1', 'Option added from cell 2'], + }); + + await grid.column.delete({ title: 'MultiSelect' }); + }); }); diff --git a/tests/playwright/tests/columnSingleSelect.spec.ts b/tests/playwright/tests/columnSingleSelect.spec.ts index 4596d4ba2b..2b2d737889 100644 --- a/tests/playwright/tests/columnSingleSelect.spec.ts +++ b/tests/playwright/tests/columnSingleSelect.spec.ts @@ -69,4 +69,16 @@ test.describe('Single select', () => { await grid.column.delete({ title: 'SingleSelect' }); }); + + test('Add new option directly from cell', async () => { + await grid.cell.selectOption.addNewOption({ + index: 0, + columnHeader: 'SingleSelect', + option: 'Option added from cell', + }); + + await grid.cell.selectOption.verify({ index: 0, columnHeader: 'SingleSelect', option: 'Option added from cell' }); + + await grid.column.delete({ title: 'SingleSelect' }); + }); }); diff --git a/tests/playwright/tests/metaSync.spec.ts b/tests/playwright/tests/metaSync.spec.ts index 086d2bcb10..bec73023bd 100644 --- a/tests/playwright/tests/metaSync.spec.ts +++ b/tests/playwright/tests/metaSync.spec.ts @@ -24,7 +24,7 @@ test.describe('Meta sync', () => { dbExec = mysqlExec; break; case 'pg': - dbExec = pgExec; + dbExec = query => pgExec(query, context); break; } });