)
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}`"
>
-
+ |
-
-
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',
|