diff --git a/packages/nc-gui/components/dlg/BulkUpdate.vue b/packages/nc-gui/components/dlg/BulkUpdate.vue new file mode 100644 index 0000000000..f5e27f2c79 --- /dev/null +++ b/packages/nc-gui/components/dlg/BulkUpdate.vue @@ -0,0 +1,508 @@ + + + + + diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 3673c818a3..659fe932e8 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -99,6 +99,8 @@ const contextMenu = computed({ }) const contextMenuClosing = ref(false) +const bulkUpdateDlg = ref(false) + const routeQuery = $computed(() => route.query as Record) const contextMenuTarget = ref<{ row: number; col: number } | null>(null) const expandedFormDlg = ref(false) @@ -128,6 +130,7 @@ const { getExpandedRowIndex, deleteRangeOfRows, bulkUpdateRows, + bulkUpdateView, } = useViewData(meta, view, xWhere) const { getMeta } = useMetas() @@ -977,7 +980,12 @@ function addEmptyRow(row?: number) { :style="{ height: rowHeight ? `${rowHeight * 1.8}rem` : `1.8rem` }" :data-testid="`grid-row-${rowIndex}`" > - +
- -
+ +
+ + + Update Selected Rows +
+
+ + +
+ {{ $t('activity.deleteSelectedRow') }}
@@ -1208,6 +1231,19 @@ function addEmptyRow(row?: number) { @prev="navigateToSiblingRow(NavigateDir.PREV)" /> + + + +
diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index 1e8ef11365..4a996fd75e 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -492,6 +492,8 @@ export function useViewData( updateArray.push({ ...updateData, ...pk }) } + await $api.dbTableRow.bulkUpdate(NOCO, metaValue?.project_id as string, metaValue?.id as string, updateArray) + if (!undo) { addUndo({ redo: { @@ -548,8 +550,6 @@ export function useViewData( }) } - await $api.dbTableRow.bulkUpdate(NOCO, metaValue?.project_id as string, metaValue?.id as string, updateArray) - for (const row of rows) { if (!undo) { /** update row data(to sync formula and other related columns) @@ -577,6 +577,19 @@ export function useViewData( } } + async function bulkUpdateView( + data: Record[], + { metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, + ) { + if (!viewMetaValue) return + + await $api.dbTableRow.bulkUpdateAll(NOCO, metaValue?.project_id as string, metaValue?.id as string, data, { + viewId: viewMetaValue.id, + }) + + await loadData() + } + async function changePage(page: number) { paginationData.value.page = page await loadData({ @@ -995,6 +1008,7 @@ export function useViewData( deleteRangeOfRows, updateOrSaveRow, bulkUpdateRows, + bulkUpdateView, selectedAllRecords, syncCount, syncPagination, diff --git a/packages/nocodb-sdk/src/lib/Api.ts b/packages/nocodb-sdk/src/lib/Api.ts index 122abbfcf9..b35b8e5267 100644 --- a/packages/nocodb-sdk/src/lib/Api.ts +++ b/packages/nocodb-sdk/src/lib/Api.ts @@ -7249,6 +7249,7 @@ export class Api< data: object, query?: { where?: string; + viewId?: string; }, params: RequestParams = {} ) => @@ -7289,6 +7290,7 @@ export class Api< data: object, query?: { where?: string; + viewId?: string; }, params: RequestParams = {} ) => diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index c67a2ea1de..efbdc09f4b 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -2493,7 +2493,7 @@ class BaseModelSqlv2 { } async bulkUpdateAll( - args: { where?: string; filterArr?: Filter[] } = {}, + args: { where?: string; filterArr?: Filter[]; viewId?: string } = {}, data, { cookie }: { cookie?: any } = {}, ) { @@ -2515,22 +2515,30 @@ class BaseModelSqlv2 { const aliasColObjMap = await this.model.getAliasColObjMap(); const filterObj = extractFilterFromXwhere(where, aliasColObjMap); - await conditionV2( - [ - new Filter({ - children: args.filterArr || [], - is_group: true, - logical_op: 'and', - }), + const conditionObj = [ + new Filter({ + children: args.filterArr || [], + is_group: true, + logical_op: 'and', + }), + new Filter({ + children: filterObj, + is_group: true, + logical_op: 'and', + }), + ]; + + if (args.viewId) { + conditionObj.push( new Filter({ - children: filterObj, + children: + (await Filter.rootFilterList({ viewId: args.viewId })) || [], is_group: true, - logical_op: 'and', }), - ], - qb, - this.dbDriver, - ); + ); + } + + await conditionV2(conditionObj, qb, this.dbDriver); qb.update(updateData); diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json index 84d509f0ef..5be2b7ddb3 100644 --- a/packages/nocodb/src/schema/swagger.json +++ b/packages/nocodb/src/schema/swagger.json @@ -10059,6 +10059,13 @@ }, "in": "query", "name": "where" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "viewId" } ], "patch": { diff --git a/tests/playwright/pages/Dashboard/BulkUpdate/index.ts b/tests/playwright/pages/Dashboard/BulkUpdate/index.ts new file mode 100644 index 0000000000..1d66a336d5 --- /dev/null +++ b/tests/playwright/pages/Dashboard/BulkUpdate/index.ts @@ -0,0 +1,192 @@ +import { expect, Locator } from '@playwright/test'; +import BasePage from '../../Base'; +import { DashboardPage } from '..'; +import { DateTimeCellPageObject } from '../common/Cell/DateTimeCell'; +import { getTextExcludeIconText } from '../../../tests/utils/general'; + +export class BulkUpdatePage extends BasePage { + readonly dashboard: DashboardPage; + readonly bulkUpdateButton: Locator; + readonly formHeader: Locator; + readonly columnsDrawer: Locator; + readonly form: Locator; + + constructor(dashboard: DashboardPage) { + super(dashboard.rootPage); + this.dashboard = dashboard; + this.bulkUpdateButton = this.dashboard.get().locator('.nc-bulk-update-save-btn'); + this.formHeader = this.dashboard.get().locator('.nc-bulk-update-bulk-update-header'); + this.columnsDrawer = this.dashboard.get().locator('.nc-columns-drawer'); + this.form = this.dashboard.get().locator('div.form'); + } + + get() { + return this.dashboard.get().locator(`.nc-drawer-bulk-update`); + } + + async close() { + return this.dashboard.rootPage.keyboard.press('Escape'); + } + + async getInactiveColumn(index: number) { + const inactiveColumns = await this.columnsDrawer.locator('.ant-card'); + return inactiveColumns.nth(index); + } + + async getActiveColumn(index: number) { + const activeColumns = await this.form.locator('[data-testid="nc-bulk-update-fields"]'); + return activeColumns.nth(index); + } + + async getInactiveColumns() { + const inactiveColumns = await this.columnsDrawer.locator('.ant-card'); + const inactiveColumnsCount = await inactiveColumns.count(); + const inactiveColumnsTitles = []; + // get title for each inactive column + for (let i = 0; i < inactiveColumnsCount; i++) { + const title = await getTextExcludeIconText(inactiveColumns.nth(i).locator('.ant-card-body')); + inactiveColumnsTitles.push(title); + } + + return inactiveColumnsTitles; + } + + async getActiveColumns() { + const activeColumns = await this.form.locator('[data-testid="nc-bulk-update-fields"]'); + const activeColumnsCount = await activeColumns.count(); + const activeColumnsTitles = []; + // get title for each active column + for (let i = 0; i < activeColumnsCount; i++) { + const title = await getTextExcludeIconText(activeColumns.nth(i).locator('[data-testid="nc-bulk-update-input-label"]')); + activeColumnsTitles.push(title); + } + + return activeColumnsTitles; + } + + async removeField(index: number) { + const removeFieldButton = await this.form.locator('[data-testid="nc-bulk-update-fields"]'); + const removeFieldButtonCount = await removeFieldButton.count(); + await removeFieldButton.nth(index).locator('[data-testid="nc-bulk-update-fields-remove-icon"]').click(); + const newRemoveFieldButtonCount = await removeFieldButton.count(); + expect(newRemoveFieldButtonCount).toBe(removeFieldButtonCount - 1); + } + + async addField(index: number) { + const addFieldButton = await this.columnsDrawer.locator('.ant-card'); + const addFieldButtonCount = await addFieldButton.count(); + await addFieldButton.nth(index).click(); + const newAddFieldButtonCount = await addFieldButton.count(); + expect(newAddFieldButtonCount).toBe(addFieldButtonCount - 1); + } + + ////////////////////////////////////////////////////////////////////////////// + + async fillField({ columnTitle, value, type = 'text' }: { columnTitle: string; value: string; type?: string }) { + let picker = null; + const field = this.form.locator(`[data-testid="nc-bulk-update-input-${columnTitle}"]`); + await field.scrollIntoViewIfNeeded(); + await field.hover(); + if (type !== 'checkbox' && type !== 'attachment') { + await field.click(); + } + switch (type) { + case 'text': + await field.locator('input').waitFor(); + await field.locator('input').fill(value); + break; + case 'longText': + await field.locator('textarea').waitFor(); + await field.locator('textarea').fill(value); + break; + case 'rating': + await field + .locator('.ant-rate-star') + .nth(Number(value) - 1) + .click(); + break; + case 'year': + picker = this.rootPage.locator('.ant-picker-dropdown.active'); + await picker.waitFor(); + await picker.locator(`td[title="${value}"]`).click(); + break; + case 'time': + picker = this.rootPage.locator('.ant-picker-dropdown.active'); + await picker.waitFor(); + // eslint-disable-next-line no-case-declarations + const time = value.split(':'); + // eslint-disable-next-line no-case-declarations + const timePanel = picker.locator('.ant-picker-time-panel-column'); + await timePanel.nth(0).locator('li').nth(+time[0]).click(); + await timePanel.nth(1).locator('li').nth(+time[1]).click(); + await picker.locator('.ant-picker-ok').click(); + break; + case 'singleSelect': + picker = this.rootPage.locator('.ant-select-dropdown.active'); + await picker.waitFor(); + await picker.locator(`.nc-select-option-SingleSelect-${value}`).click(); + break; + case 'multiSelect': + picker = this.rootPage.locator('.ant-select-dropdown.active'); + await picker.waitFor(); + for (const val of value.split(',')) { + await picker.locator(`.nc-select-option-MultiSelect-${val}`).click(); + } + break; + case 'checkbox': + if (value === 'true') { + await field.click(); + } + break; + case 'attachment': + // eslint-disable-next-line no-case-declarations + const attachFileAction = field.locator('[data-testid="attachment-cell-file-picker-button"]').click(); + await this.attachFile({ filePickUIAction: attachFileAction, filePath: value }); + break; + case 'date': + { + const values = value.split('-'); + const { year, month, day } = { year: values[0], month: values[1], day: values[2] }; + picker = this.rootPage.locator('.ant-picker-dropdown.active'); + const monthBtn = picker.locator('.ant-picker-month-btn'); + const yearBtn = picker.locator('.ant-picker-year-btn'); + + await yearBtn.click(); + await picker.waitFor(); + await picker.locator(`td[title="${year}"]`).click(); + + await monthBtn.click(); + await picker.waitFor(); + await picker.locator(`td[title="${year}-${month}"]`).click(); + + await picker.waitFor(); + await picker.locator(`td[title="${year}-${month}-${day}"]`).click(); + } + break; + } + } + + async save({ + awaitResponse = true, + }: { + awaitResponse?: boolean; + } = {}) { + await this.bulkUpdateButton.click(); + const confirmModal = await this.rootPage.locator('.ant-modal-confirm'); + + const saveRowAction = () => confirmModal.locator('.ant-btn-primary').click(); + if (!awaitResponse) { + await saveRowAction(); + } else { + await this.waitForResponse({ + uiAction: saveRowAction, + requestUrlPathToMatch: 'api/v1/db/data/noco/', + httpMethodsToMatch: ['GET'], + responseJsonMatcher: json => json['pageInfo'], + }); + } + + await this.get().waitFor({ state: 'hidden' }); + await this.rootPage.locator('[data-testid="grid-load-spinner"]').waitFor({ state: 'hidden' }); + } +} diff --git a/tests/playwright/pages/Dashboard/Grid/index.ts b/tests/playwright/pages/Dashboard/Grid/index.ts index 3614fcf2ee..9663395132 100644 --- a/tests/playwright/pages/Dashboard/Grid/index.ts +++ b/tests/playwright/pages/Dashboard/Grid/index.ts @@ -220,6 +220,19 @@ export class GridPage extends BasePage { await this.deleteSelectedRows(); } + async updateSelectedRows() { + await this.get().locator('[data-testid="nc-check-all"]').nth(0).click({ + button: 'right', + }); + await this.rootPage.locator('text=Update Selected Rows').click(); + await this.dashboard.waitForLoaderToDisappear(); + } + + async updateAll() { + await this.selectAll(); + await this.updateSelectedRows(); + } + async verifyTotalRowCount({ count }: { count: number }) { // wait for 100 ms and try again : 5 times let i = 0; diff --git a/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts b/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts index eb94373ce0..f4dc982ed1 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts +++ b/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts @@ -126,7 +126,7 @@ export class SelectOptionCellPageObject extends BasePage { await expect(this.rootPage.locator(`div.ant-select-item-option`).nth(counter)).toHaveText(option); counter++; } - await this.get({ index, columnHeader }).click(); + await this.rootPage.keyboard.press('Escape'); await this.rootPage.locator(`.nc-dropdown-single-select-cell`).nth(index).waitFor({ state: 'hidden' }); } diff --git a/tests/playwright/pages/Dashboard/common/Cell/TimeCell.ts b/tests/playwright/pages/Dashboard/common/Cell/TimeCell.ts new file mode 100644 index 0000000000..737d7b3d8d --- /dev/null +++ b/tests/playwright/pages/Dashboard/common/Cell/TimeCell.ts @@ -0,0 +1,23 @@ +import { CellPageObject } from '.'; +import BasePage from '../../../Base'; +import { expect } from '@playwright/test'; + +export class TimeCellPageObject extends BasePage { + readonly cell: CellPageObject; + + constructor(cell: CellPageObject) { + super(cell.rootPage); + this.cell = cell; + } + + get({ index, columnHeader }: { index?: number; columnHeader: string }) { + return this.cell.get({ index, columnHeader }); + } + + async verify({ index, columnHeader, value }: { index: number; columnHeader: string; value: string }) { + const cell = await this.get({ index, columnHeader }); + await cell.scrollIntoViewIfNeeded(); + await cell.locator(`input[title="${value}"]`).waitFor({ state: 'visible' }); + await expect(cell.locator(`[title="${value}"]`)).toBeVisible(); + } +} diff --git a/tests/playwright/pages/Dashboard/common/Cell/YearCell.ts b/tests/playwright/pages/Dashboard/common/Cell/YearCell.ts new file mode 100644 index 0000000000..d622bf1e63 --- /dev/null +++ b/tests/playwright/pages/Dashboard/common/Cell/YearCell.ts @@ -0,0 +1,23 @@ +import { CellPageObject } from '.'; +import BasePage from '../../../Base'; +import { expect } from '@playwright/test'; + +export class YearCellPageObject extends BasePage { + readonly cell: CellPageObject; + + constructor(cell: CellPageObject) { + super(cell.rootPage); + this.cell = cell; + } + + get({ index, columnHeader }: { index?: number; columnHeader: string }) { + return this.cell.get({ index, columnHeader }); + } + + async verify({ index, columnHeader, value }: { index: number; columnHeader: string; value: number }) { + const cell = await this.get({ index, columnHeader }); + await cell.scrollIntoViewIfNeeded(); + await cell.locator(`input[title="${value}"]`).waitFor({ state: 'visible' }); + await expect(cell.locator(`[title="${value}"]`)).toBeVisible(); + } +} diff --git a/tests/playwright/pages/Dashboard/common/Cell/index.ts b/tests/playwright/pages/Dashboard/common/Cell/index.ts index ad3daaedb6..02399664c7 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/index.ts +++ b/tests/playwright/pages/Dashboard/common/Cell/index.ts @@ -9,6 +9,8 @@ import { RatingCellPageObject } from './RatingCell'; import { DateCellPageObject } from './DateCell'; import { DateTimeCellPageObject } from './DateTimeCell'; import { GeoDataCellPageObject } from './GeoDataCell'; +import { YearCellPageObject } from './YearCell'; +import { TimeCellPageObject } from './TimeCell'; export interface CellProps { index?: number; @@ -21,6 +23,8 @@ export class CellPageObject extends BasePage { readonly attachment: AttachmentCellPageObject; readonly checkbox: CheckboxCellPageObject; readonly rating: RatingCellPageObject; + readonly year: YearCellPageObject; + readonly time: TimeCellPageObject; readonly geoData: GeoDataCellPageObject; readonly date: DateCellPageObject; readonly dateTime: DateTimeCellPageObject; @@ -32,6 +36,8 @@ export class CellPageObject extends BasePage { this.attachment = new AttachmentCellPageObject(this); this.checkbox = new CheckboxCellPageObject(this); this.rating = new RatingCellPageObject(this); + this.year = new YearCellPageObject(this); + this.time = new TimeCellPageObject(this); this.geoData = new GeoDataCellPageObject(this); this.date = new DateCellPageObject(this); this.dateTime = new DateTimeCellPageObject(this); diff --git a/tests/playwright/pages/Dashboard/index.ts b/tests/playwright/pages/Dashboard/index.ts index 31ff8a9fa7..661d204904 100644 --- a/tests/playwright/pages/Dashboard/index.ts +++ b/tests/playwright/pages/Dashboard/index.ts @@ -3,6 +3,7 @@ import BasePage from '../Base'; import { GridPage } from './Grid'; import { FormPage } from './Form'; import { ExpandedFormPage } from './ExpandedForm'; +import { BulkUpdatePage } from './BulkUpdate'; import { ChildList } from './Grid/Column/LTAR/ChildList'; import { LinkRecord } from './Grid/Column/LTAR/LinkRecord'; import { TreeViewPage } from './TreeView'; @@ -29,6 +30,7 @@ export class DashboardPage extends BasePage { readonly kanban: KanbanPage; readonly map: MapPage; readonly expandedForm: ExpandedFormPage; + readonly bulkUpdateForm: BulkUpdatePage; readonly webhookForm: WebhookFormPage; readonly findRowByScanOverlay: FindRowByScanOverlay; readonly childList: ChildList; @@ -51,6 +53,7 @@ export class DashboardPage extends BasePage { this.kanban = new KanbanPage(this); this.map = new MapPage(this); this.expandedForm = new ExpandedFormPage(this); + this.bulkUpdateForm = new BulkUpdatePage(this); this.webhookForm = new WebhookFormPage(this); this.findRowByScanOverlay = new FindRowByScanOverlay(this); this.childList = new ChildList(this); diff --git a/tests/playwright/setup/demoTable.ts b/tests/playwright/setup/demoTable.ts index eee37ee050..f94c49c3d8 100644 --- a/tests/playwright/setup/demoTable.ts +++ b/tests/playwright/setup/demoTable.ts @@ -116,6 +116,24 @@ const columns = { uidt: UITypes.Time, }, ], + + miscellaneous: [ + { + column_name: 'Id', + title: 'Id', + uidt: UITypes.ID, + }, + { + column_name: 'Checkbox', + title: 'Checkbox', + uidt: UITypes.Checkbox, + }, + { + column_name: 'Attachment', + title: 'Attachment', + uidt: UITypes.Attachment, + }, + ], }; async function createDemoTable({ @@ -198,6 +216,18 @@ async function createDemoTable({ console.error(e); } break; + case 'miscellaneous': + try { + for (let i = 0; i < recordCnt; i++) { + const row = { + Checkbox: rowMixedValue(columns.miscellaneous[1], i), + }; + rowAttributes.push(row); + } + } catch (e) { + console.error(e); + } + break; } await api.dbTableRow.bulkCreate('noco', context.project.id, table.id, rowAttributes); diff --git a/tests/playwright/tests/db/bulkUpdate.spec.ts b/tests/playwright/tests/db/bulkUpdate.spec.ts new file mode 100644 index 0000000000..c3b78d40a5 --- /dev/null +++ b/tests/playwright/tests/db/bulkUpdate.spec.ts @@ -0,0 +1,365 @@ +import { expect, test } from '@playwright/test'; +import setup from '../../setup'; +import { DashboardPage } from '../../pages/Dashboard'; +import { Api } from 'nocodb-sdk'; +import { createDemoTable } from '../../setup/demoTable'; +import { BulkUpdatePage } from '../../pages/Dashboard/BulkUpdate'; + +let bulkUpdateForm: BulkUpdatePage; +async function updateBulkFields(fields) { + // move all fields to active + for (let i = 0; i < fields.length; i++) { + await bulkUpdateForm.addField(0); + } + + // fill all fields + for (let i = 0; i < fields.length; i++) { + await bulkUpdateForm.fillField({ columnTitle: fields[i].title, value: fields[i].value, type: fields[i].type }); + } + + // save form + await bulkUpdateForm.save({ awaitResponse: true }); +} + +test.describe('Bulk update', () => { + let dashboard: DashboardPage; + let context: any; + let api: Api; + let table; + + test.beforeEach(async ({ page }) => { + context = await setup({ page, isEmptyProject: true }); + dashboard = new DashboardPage(page, context.project); + bulkUpdateForm = dashboard.bulkUpdateForm; + + api = new Api({ + baseURL: `http://localhost:8080/`, + headers: { + 'xc-auth': context.token, + }, + }); + + table = await createDemoTable({ context, type: 'textBased', recordCnt: 50 }); + await page.reload(); + + await dashboard.treeView.openTable({ title: 'textBased' }); + + // Open bulk update form + await dashboard.grid.updateAll(); + }); + + test('General- Click to add & remove', async () => { + let inactiveColumns = await bulkUpdateForm.getInactiveColumns(); + expect(inactiveColumns).toEqual(['SingleLineText', 'MultiLineText', 'Email', 'PhoneNumber', 'URL']); + + let activeColumns = await bulkUpdateForm.getActiveColumns(); + expect(activeColumns).toEqual([]); + + await bulkUpdateForm.addField(0); + await bulkUpdateForm.addField(0); + + inactiveColumns = await bulkUpdateForm.getInactiveColumns(); + expect(inactiveColumns).toEqual(['Email', 'PhoneNumber', 'URL']); + + activeColumns = await bulkUpdateForm.getActiveColumns(); + expect(activeColumns).toEqual(['SingleLineText', 'MultiLineText']); + }); + + test('General- Drag drop', async () => { + const src = await bulkUpdateForm.getInactiveColumn(0); + const dst = await bulkUpdateForm.form; + + await src.dragTo(dst); + expect(await bulkUpdateForm.getActiveColumns()).toEqual(['SingleLineText']); + expect(await bulkUpdateForm.getInactiveColumns()).toEqual(['MultiLineText', 'Email', 'PhoneNumber', 'URL']); + + const src2 = await bulkUpdateForm.getActiveColumn(0); + const dst2 = await bulkUpdateForm.columnsDrawer; + + await src2.dragTo(dst2); + expect(await bulkUpdateForm.getActiveColumns()).toEqual([]); + expect(await bulkUpdateForm.getInactiveColumns()).toEqual([ + 'SingleLineText', + 'MultiLineText', + 'Email', + 'PhoneNumber', + 'URL', + ]); + }); + + test('Text based', async () => { + const fields = [ + { title: 'SingleLineText', value: 'SingleLineText', type: 'text' }, + { title: 'Email', value: 'a@b.com', type: 'text' }, + { title: 'PhoneNumber', value: '987654321', type: 'text' }, + { title: 'URL', value: 'https://www.google.com', type: 'text' }, + { + title: 'MultiLineText', + value: 'Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ', + type: 'longText', + }, + ]; + + await updateBulkFields(fields); + + // verify data on grid + for (let i = 0; i < fields.length; i++) { + await dashboard.grid.cell.verify({ index: 5, columnHeader: fields[i].title, value: fields[i].value }); + } + + // verify api response + const updatedRecords = (await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 50 })).list; + for (let i = 0; i < updatedRecords.length; i++) { + for (let j = 0; j < fields.length; j++) { + expect(updatedRecords[i][fields[j].title]).toEqual(fields[j].value); + } + } + }); +}); + +test.describe('Bulk update', () => { + let dashboard: DashboardPage; + let context: any; + let api: Api; + let table; + + test.beforeEach(async ({ page }) => { + context = await setup({ page, isEmptyProject: true }); + dashboard = new DashboardPage(page, context.project); + bulkUpdateForm = dashboard.bulkUpdateForm; + + api = new Api({ + baseURL: `http://localhost:8080/`, + headers: { + 'xc-auth': context.token, + }, + }); + + table = await createDemoTable({ context, type: 'numberBased', recordCnt: 50 }); + await page.reload(); + + await dashboard.treeView.openTable({ title: 'numberBased' }); + + // Open bulk update form + await dashboard.grid.updateAll(); + }); + + test('Number based', async () => { + const fields = [ + { title: 'Number', value: '1', type: 'text' }, + { title: 'Decimal', value: '1.1', type: 'text' }, + { title: 'Currency', value: '1.1', type: 'text' }, + { title: 'Percent', value: '10', type: 'text' }, + { title: 'Duration', value: '16:40', type: 'text' }, + { title: 'Rating', value: '3', type: 'rating' }, + { title: 'Year', value: '2024', type: 'year' }, + { title: 'Time', value: '10:10', type: 'time' }, + ]; + + await updateBulkFields(fields); + + // verify data on grid + for (let i = 0; i < fields.length; i++) { + if (fields[i].type === 'rating') { + await dashboard.grid.cell.rating.verify({ index: 5, columnHeader: fields[i].title, rating: +fields[i].value }); + } else if (fields[i].type === 'year') { + await dashboard.grid.cell.year.verify({ index: 5, columnHeader: fields[i].title, value: +fields[i].value }); + } else if (fields[i].type === 'time') { + await dashboard.grid.cell.time.verify({ index: 5, columnHeader: fields[i].title, value: fields[i].value }); + } else { + await dashboard.grid.cell.verify({ index: 5, columnHeader: fields[i].title, value: fields[i].value }); + } + } + + // verify api response + // duration in seconds + const APIResponse = [1, 1.1, 1.1, 10, 60000, 3, 2024, '10:10:00']; + const updatedRecords = (await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 50 })).list; + for (let i = 0; i < updatedRecords.length; i++) { + for (let j = 0; j < fields.length; j++) { + if (fields[j].title === 'Time') { + expect(updatedRecords[i][fields[j].title]).toContain(APIResponse[j]); + } else { + expect(+updatedRecords[i][fields[j].title]).toEqual(APIResponse[j]); + } + } + } + }); +}); + +test.describe('Bulk update', () => { + let dashboard: DashboardPage; + let context: any; + let api: Api; + let table; + + test.beforeEach(async ({ page }) => { + context = await setup({ page, isEmptyProject: true }); + dashboard = new DashboardPage(page, context.project); + bulkUpdateForm = dashboard.bulkUpdateForm; + + api = new Api({ + baseURL: `http://localhost:8080/`, + headers: { + 'xc-auth': context.token, + }, + }); + + table = await createDemoTable({ context, type: 'selectBased', recordCnt: 50 }); + await page.reload(); + + await dashboard.treeView.openTable({ title: 'selectBased' }); + + // Open bulk update form + await dashboard.grid.updateAll(); + }); + + test('Select based', async () => { + const fields = [ + { title: 'SingleSelect', value: 'jan', type: 'singleSelect' }, + { title: 'MultiSelect', value: 'jan,feb,mar', type: 'multiSelect' }, + ]; + + await updateBulkFields(fields); + + // verify data on grid + const displayOptions = ['jan', 'feb', 'mar']; + for (let i = 0; i < fields.length; i++) { + if (fields[i].type === 'singleSelect') { + await dashboard.grid.cell.selectOption.verify({ + index: 5, + columnHeader: fields[i].title, + option: fields[i].value, + }); + } else { + await dashboard.grid.cell.selectOption.verifyOptions({ + index: 5, + columnHeader: fields[i].title, + options: displayOptions, + }); + } + } + + // verify api response + const updatedRecords = (await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 50 })).list; + for (let i = 0; i < updatedRecords.length; i++) { + for (let j = 0; j < fields.length; j++) { + expect(updatedRecords[i][fields[j].title]).toContain(fields[j].value); + } + } + }); +}); + +test.describe('Bulk update', () => { + let dashboard: DashboardPage; + let context: any; + let api: Api; + let table; + + test.beforeEach(async ({ page }) => { + context = await setup({ page, isEmptyProject: true }); + dashboard = new DashboardPage(page, context.project); + bulkUpdateForm = dashboard.bulkUpdateForm; + + api = new Api({ + baseURL: `http://localhost:8080/`, + headers: { + 'xc-auth': context.token, + }, + }); + + table = await createDemoTable({ context, type: 'miscellaneous', recordCnt: 50 }); + await page.reload(); + + await dashboard.treeView.openTable({ title: 'miscellaneous' }); + + // Open bulk update form + await dashboard.grid.updateAll(); + }); + + test('Miscellaneous (Checkbox, attachment)', async () => { + const fields = [ + { title: 'Checkbox', value: 'true', type: 'checkbox' }, + { title: 'Attachment', value: `${process.cwd()}/fixtures/sampleFiles/1.json`, type: 'attachment' }, + ]; + + await updateBulkFields(fields); + + // verify data on grid + for (let i = 0; i < fields.length; i++) { + if (fields[i].type === 'checkbox') { + await dashboard.grid.cell.checkbox.verifyChecked({ + index: 5, + columnHeader: fields[i].title, + }); + } else { + await dashboard.grid.cell.attachment.verifyFileCount({ + index: 5, + columnHeader: fields[i].title, + count: 1, + }); + } + } + + // verify api response + const updatedRecords = (await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 50 })).list; + for (let i = 0; i < updatedRecords.length; i++) { + for (let j = 0; j < fields.length; j++) { + expect(+updatedRecords[i]['Checkbox']).toBe(1); + expect(updatedRecords[i]['Attachment'][0].title).toBe('1.json'); + expect(updatedRecords[i]['Attachment'][0].mimetype).toBe('application/json'); + } + } + }); +}); + +test.describe('Bulk update', () => { + let dashboard: DashboardPage; + let context: any; + let api: Api; + let table; + + test.beforeEach(async ({ page }) => { + context = await setup({ page, isEmptyProject: true }); + dashboard = new DashboardPage(page, context.project); + bulkUpdateForm = dashboard.bulkUpdateForm; + + api = new Api({ + baseURL: `http://localhost:8080/`, + headers: { + 'xc-auth': context.token, + }, + }); + + table = await createDemoTable({ context, type: 'dateTimeBased', recordCnt: 50 }); + await page.reload(); + + await dashboard.treeView.openTable({ title: 'dateTimeBased' }); + + // Open bulk update form + await dashboard.grid.updateAll(); + }); + + test('Date Time Based', async () => { + const fields = [{ title: 'Date', value: '2024-08-04', type: 'date' }]; + + await updateBulkFields(fields); + + // verify data on grid + for (let i = 0; i < fields.length; i++) { + await dashboard.grid.cell.date.verify({ + index: 5, + columnHeader: fields[i].title, + date: fields[i].value, + }); + } + + // verify api response + const updatedRecords = (await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 50 })).list; + for (let i = 0; i < updatedRecords.length; i++) { + for (let j = 0; j < fields.length; j++) { + expect(updatedRecords[i]['Date']).toBe(fields[j].value); + } + } + }); +}); diff --git a/tests/playwright/tests/db/projectOperations.spec.ts b/tests/playwright/tests/db/projectOperations.spec.ts index 03054b5f49..e1e9acb99d 100644 --- a/tests/playwright/tests/db/projectOperations.spec.ts +++ b/tests/playwright/tests/db/projectOperations.spec.ts @@ -107,9 +107,9 @@ test.describe('Project operations', () => { const testProjectId = await projectList.list.find((p: any) => p.title === testProjectName); const dupeProjectId = await projectList.list.find((p: any) => p.title === dupeProjectName); const projectInfoOp: ProjectInfoApiUtil = new ProjectInfoApiUtil(context.token); - const orginal: Promise = projectInfoOp.extractProjectInfo(testProjectId.id); + const original: Promise = projectInfoOp.extractProjectInfo(testProjectId.id); const duplicate: Promise = projectInfoOp.extractProjectInfo(dupeProjectId.id); - await Promise.all([orginal, duplicate]).then(arr => { + await Promise.all([original, duplicate]).then(arr => { const ignoredFields: Set = new Set([ 'id', 'prefix',