apps/nestjs-backend/test/undo-redo.e2e-spec.ts
/* eslint-disable sonarjs/no-duplicate-string */
import type { INestApplication } from '@nestjs/common';
import type { IFieldRo, IFieldVo, ILinkFieldOptions, IRollupFieldOptions } from '@teable/core';
import {
CellValueType,
DbFieldType,
FieldKeyType,
FieldType,
getRandomString,
Relationship,
ViewType,
} from '@teable/core';
import {
axios,
clear,
convertField,
copy,
createField,
createRecords,
createView,
deleteField,
deleteFields,
deleteRecord,
deleteRecords,
deleteSelection,
deleteView,
getField,
getFields,
getRecord,
getRecords,
getView,
getViewList,
paste,
redo,
undo,
updateRecord,
updateRecordOrders,
updateRecords,
updateViewColumnMeta,
updateViewDescription,
updateViewFilter,
updateViewName,
updateViewOrder,
} from '@teable/openapi';
import type { ITableFullVo } from '@teable/openapi';
import { EventEmitterService } from '../src/event-emitter/event-emitter.service';
import { Events } from '../src/event-emitter/events';
import { createAwaitWithEvent } from './utils/event-promise';
import { initApp, permanentDeleteTable, createTable, updateRecordByApi } from './utils/init-app';
describe('Undo Redo (e2e)', () => {
let app: INestApplication;
let table: ITableFullVo;
let eventEmitterService: EventEmitterService;
let awaitWithEvent: <T>(fn: () => Promise<T>) => Promise<T>;
const baseId = globalThis.testConfig.baseId;
beforeAll(async () => {
const appCtx = await initApp();
app = appCtx.app;
eventEmitterService = app.get(EventEmitterService);
const windowId = 'win' + getRandomString(8);
axios.interceptors.request.use((config) => {
config.headers['X-Window-Id'] = windowId;
return config;
});
awaitWithEvent = createAwaitWithEvent(eventEmitterService, Events.OPERATION_PUSH);
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
table = await createTable(baseId, { name: 'table1' });
});
afterEach(async () => {
await permanentDeleteTable(baseId, table.id);
});
it('should undo / redo create records', async () => {
await awaitWithEvent(() => createField(table.id, { type: FieldType.CreatedTime }));
await awaitWithEvent(() => createField(table.id, { type: FieldType.LastModifiedTime }));
const record1 = (
await awaitWithEvent(() =>
createRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
records: [{ fields: { [table.fields[0].id]: 'record1' } }],
order: {
viewId: table.views[0].id,
anchorId: table.records[0].id,
position: 'after',
},
})
)
).data.records[0];
const allRecords = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
expect(allRecords.data.records).toHaveLength(4);
await undo(table.id);
const allRecordsAfterUndo = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
expect(allRecordsAfterUndo.data.records).toHaveLength(3);
expect(allRecordsAfterUndo.data.records.find((r) => r.id === record1.id)).toBeUndefined();
await redo(table.id);
const allRecordsAfterRedo = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
expect(allRecordsAfterRedo.data.records).toHaveLength(4);
// back to index 1
expect(allRecordsAfterRedo.data.records[1]).toMatchObject(record1);
await updateRecord(table.id, record1.id, {
fieldKeyType: FieldKeyType.Id,
record: { fields: { [table.fields[0].id]: 'new value' } },
});
});
it('should undo / redo delete record', async () => {
await awaitWithEvent(() => createField(table.id, { type: FieldType.CreatedTime }));
await awaitWithEvent(() => createField(table.id, { type: FieldType.LastModifiedTime }));
// index 1
const record1 = (
await createRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
records: [{ fields: { [table.fields[0].id]: 'record1' } }],
order: {
viewId: table.views[0].id,
anchorId: table.records[0].id,
position: 'after',
},
})
).data.records[0];
await awaitWithEvent(() => deleteRecord(table.id, record1.id));
const allRecords = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
// 4 -> 3
expect(allRecords.data.records).toHaveLength(3);
await undo(table.id);
const allRecordsAfterUndo = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
// 3 -> 4
expect(allRecordsAfterUndo.data.records).toHaveLength(4);
// back to index 1
expect(allRecordsAfterUndo.data.records[1]).toMatchObject(record1);
await redo(table.id);
const allRecordsAfterRedo = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
expect(allRecordsAfterRedo.data.records).toHaveLength(3);
expect(allRecordsAfterRedo.data.records.find((r) => r.id === record1.id)).toBeUndefined();
});
it('should undo / redo delete selection records', async () => {
await awaitWithEvent(() => createField(table.id, { type: FieldType.CreatedTime }));
await awaitWithEvent(() => createField(table.id, { type: FieldType.LastModifiedTime }));
// index 1
const record1 = (
await createRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
records: [{ fields: { [table.fields[0].id]: 'record1' } }],
order: {
viewId: table.views[0].id,
anchorId: table.records[0].id,
position: 'after',
},
})
).data.records[0];
// delete index 1
await awaitWithEvent(() =>
deleteSelection(table.id, {
viewId: table.views[0].id,
ranges: [
[0, 1],
[1, 1],
],
})
);
const allRecords = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
expect(allRecords.data.records.find((r) => r.id === record1.id)).toBeUndefined();
// 4 -> 3
expect(allRecords.data.records).toHaveLength(3);
await undo(table.id);
const allRecordsAfterUndo = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
// 3 -> 4
expect(allRecordsAfterUndo.data.records).toHaveLength(4);
// back to index 1
expect(allRecordsAfterUndo.data.records[1]).toMatchObject(record1);
await redo(table.id);
const allRecordsAfterRedo = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
expect(allRecordsAfterRedo.data.records).toHaveLength(3);
expect(allRecordsAfterRedo.data.records.find((r) => r.id === record1.id)).toBeUndefined();
});
it('should undo / redo delete multiple records', async () => {
await awaitWithEvent(() => createField(table.id, { type: FieldType.CreatedTime }));
await awaitWithEvent(() => createField(table.id, { type: FieldType.LastModifiedTime }));
// index 1
const record1 = (
await createRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
records: [{ fields: { [table.fields[0].id]: 'record1' } }],
order: {
viewId: table.views[0].id,
anchorId: table.records[0].id,
position: 'after',
},
})
).data.records[0];
// delete index 1
await awaitWithEvent(() => deleteRecords(table.id, [record1.id]));
const allRecords = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
expect(allRecords.data.records.find((r) => r.id === record1.id)).toBeUndefined();
// 4 -> 3
expect(allRecords.data.records).toHaveLength(3);
await undo(table.id);
const allRecordsAfterUndo = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
// 3 -> 4
expect(allRecordsAfterUndo.data.records).toHaveLength(4);
// back to index 1
expect(allRecordsAfterUndo.data.records[1]).toMatchObject(record1);
await redo(table.id);
const allRecordsAfterRedo = await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
});
expect(allRecordsAfterRedo.data.records).toHaveLength(3);
expect(allRecordsAfterRedo.data.records.find((r) => r.id === record1.id)).toBeUndefined();
});
it('should undo / redo update record', async () => {
await awaitWithEvent(() => createField(table.id, { type: FieldType.CreatedTime }));
await awaitWithEvent(() => createField(table.id, { type: FieldType.LastModifiedTime }));
await awaitWithEvent(() =>
updateRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
record: { fields: { [table.fields[0].id]: 'A' } },
})
);
const updatedRecord = (
await awaitWithEvent(() =>
updateRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
record: { fields: { [table.fields[0].id]: 'B' } },
})
)
).data;
expect(updatedRecord.fields[table.fields[0].id]).toEqual('B');
await undo(table.id);
const updatedRecordAfter = (
await getRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
})
).data;
expect(updatedRecordAfter.fields[table.fields[0].id]).toEqual('A');
await undo(table.id);
const updatedRecordAfter2 = (
await getRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
})
).data;
expect(updatedRecordAfter2.fields[table.fields[0].id]).toBeUndefined();
await redo(table.id);
const updatedRecordAfterRedo = (
await getRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
})
).data;
expect(updatedRecordAfterRedo.fields[table.fields[0].id]).toEqual('A');
await redo(table.id);
const updatedRecordAfterRedo2 = (
await getRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
})
).data;
expect(updatedRecordAfterRedo2.fields[table.fields[0].id]).toEqual('B');
});
it('should undo / redo clear records', async () => {
await awaitWithEvent(() =>
updateRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
record: { fields: { [table.fields[0].id]: 'A' } },
})
);
await awaitWithEvent(() =>
clear(table.id, {
viewId: table.views[0].id,
ranges: [
[0, 0],
[1, 0],
],
})
);
const record = await getRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
});
expect(record.data.fields[table.fields[0].id]).toBeUndefined();
await undo(table.id);
const updatedRecordAfter = (
await getRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
})
).data;
expect(updatedRecordAfter.fields[table.fields[0].id]).toEqual('A');
await redo(table.id);
const updatedRecordAfterRedo = (
await getRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
})
).data;
expect(updatedRecordAfterRedo.fields[table.fields[0].id]).toBeUndefined();
});
it('should undo / redo update record value with order', async () => {
// update and move 0 to 2
const recordId = table.records[0].id;
await awaitWithEvent(() =>
updateRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
record: { fields: { [table.fields[0].id]: 'A' } },
order: {
viewId: table.views[0].id,
anchorId: table.records[2].id,
position: 'after',
},
})
);
const records = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(records.records[2].fields[table.fields[0].id]).toEqual('A');
await undo(table.id);
const recordsAfterUndo = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(recordsAfterUndo.records[0].id).toEqual(recordId);
expect(recordsAfterUndo.records[0].fields[table.fields[0].id]).toBeUndefined();
await redo(table.id);
const recordsAfterRedo = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(recordsAfterRedo.records[2].fields[table.fields[0].id]).toEqual('A');
});
it('should undo / redo update record order in view', async () => {
// update and move 0 to 2
const recordId = table.records[0].id;
await awaitWithEvent(() =>
updateRecordOrders(table.id, table.views[0].id, {
anchorId: table.records[2].id,
position: 'after',
recordIds: [table.records[0].id],
})
);
const records = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(records.records[2].id).toEqual(recordId);
await undo(table.id);
const recordsAfterUndo = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(recordsAfterUndo.records[0].id).toEqual(recordId);
await redo(table.id);
const recordsAfterRedo = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(recordsAfterRedo.records[2].id).toEqual(recordId);
});
it('should undo / redo delete field', async () => {
// update and move 0 to 2
const fieldId = table.fields[1].id;
await awaitWithEvent(() =>
updateRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
record: { fields: { [table.fields[1].id]: 666 } },
})
);
await awaitWithEvent(() => deleteField(table.id, fieldId));
const fields = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fields.length).toEqual(2);
await undo(table.id);
const fieldsAfterUndo = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fieldsAfterUndo[1].id).toEqual(fieldId);
const recordsAfterUndo = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(recordsAfterUndo.records[0].fields[fieldId]).toEqual(666);
await redo(table.id);
const fieldsAfterRedo = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fieldsAfterRedo.length).toEqual(2);
});
it('should undo / redo create field', async () => {
const field = await awaitWithEvent(() =>
createField(table.id, {
type: FieldType.SingleLineText,
order: {
viewId: table.views[0].id,
orderIndex: 0.5,
},
})
);
const fieldId = field.data.id;
const fields = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fields[1].id).toEqual(fieldId);
await undo(table.id);
const fieldsAfterUndo = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fieldsAfterUndo.length).toEqual(3);
await redo(table.id);
const fieldsAfterRedo = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fieldsAfterRedo[1].id).toEqual(fieldId);
});
it('should undo / redo delete multiple fields', async () => {
const fieldId = table.fields[1].id;
await awaitWithEvent(() =>
updateRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
record: { fields: { [table.fields[1].id]: 666 } },
})
);
const formulaField = (
await awaitWithEvent(() =>
createField(table.id, {
type: FieldType.Formula,
options: {
expression: `{${table.fields[1].id}}`,
},
})
)
).data;
// delete 1 3
await awaitWithEvent(() => deleteFields(table.id, [fieldId, formulaField.id]));
const fields = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fields.length).toEqual(2);
const result = await undo(table.id);
expect(result.data.status).toEqual('fulfilled');
// get back 1 3
const fieldsAfterUndo = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fieldsAfterUndo[1].id).toEqual(fieldId);
expect(fieldsAfterUndo[3].id).toEqual(formulaField.id);
expect(fieldsAfterUndo[3].hasError).toBeFalsy();
const recordsAfterUndo = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(recordsAfterUndo.records[0].fields[fieldId]).toEqual(666);
await redo(table.id);
const fieldsAfterRedo = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fieldsAfterRedo.length).toEqual(2);
});
// event throw error because of sqlite(record history create many)
it('should undo / redo delete field with outgoing references', async () => {
// update and move 0 to 2
const fieldId = table.fields[1].id;
await awaitWithEvent(() =>
updateRecord(table.id, table.records[0].id, {
fieldKeyType: FieldKeyType.Id,
record: { fields: { [table.fields[1].id]: 666 } },
})
);
const formulaField = await awaitWithEvent(() =>
createField(table.id, {
type: FieldType.Formula,
options: {
expression: `{${table.fields[1].id}}`,
},
})
);
await awaitWithEvent(() => deleteField(table.id, fieldId));
const fields = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fields.length).toEqual(3);
expect(fields[2].hasError).toBeTruthy();
await undo(table.id);
const fieldsAfterUndo = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fieldsAfterUndo[1].id).toEqual(fieldId);
expect(fieldsAfterUndo[3].id).toEqual(formulaField.data.id);
expect(fieldsAfterUndo[3].hasError).toBeFalsy();
const recordsAfterUndo = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(recordsAfterUndo.records[0].fields[fieldId]).toEqual(666);
await redo(table.id);
const fieldsAfterRedo = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(fieldsAfterRedo.length).toEqual(3);
});
it('should undo / redo paste simple selection', async () => {
await updateRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
records: [
{
id: table.records[0].id,
fields: { [table.fields[0].id]: 'A', [table.fields[1].id]: 1 },
},
],
});
const { content, header } = (
await copy(table.id, {
viewId: table.views[0].id,
ranges: [
[0, 0],
[0, 0],
],
})
).data;
await awaitWithEvent(() =>
paste(table.id, {
viewId: table.views[0].id,
content,
header,
ranges: [
[0, 1],
[0, 1],
],
})
);
const records = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(records.records[1].fields[table.fields[0].id]).toEqual('A');
await undo(table.id);
const recordsAfterUndo = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(recordsAfterUndo.records[1].fields[table.fields[0].id]).toBeUndefined();
await redo(table.id);
const recordsAfterRedo = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
expect(recordsAfterRedo.records[1].fields[table.fields[0].id]).toEqual('A');
});
it('should undo / redo paste expanding selection', async () => {
await awaitWithEvent(() =>
updateRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
records: [
{
id: table.records[0].id,
fields: { [table.fields[0].id]: 'A', [table.fields[1].id]: 1 },
},
{
id: table.records[1].id,
fields: { [table.fields[0].id]: 'B', [table.fields[1].id]: 2 },
},
],
})
);
const { content, header } = (
await copy(table.id, {
viewId: table.views[0].id,
ranges: [
[0, 0],
[1, 1],
],
})
).data;
await awaitWithEvent(() =>
paste(table.id, {
viewId: table.views[0].id,
content,
header,
ranges: [
[2, 2],
[2, 2],
],
})
);
const records = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
const fields = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(records.records[2].fields[fields[2].id]).toEqual('A');
expect(records.records[2].fields[fields[3].id]).toEqual(1);
expect(records.records[3].fields[fields[2].id]).toEqual('B');
expect(records.records[3].fields[fields[3].id]).toEqual(2);
await undo(table.id);
const recordsAfterUndo = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
const fieldsAfterUndo = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(recordsAfterUndo.records[2].fields[fieldsAfterUndo[2].id]).toBeUndefined();
expect(recordsAfterUndo.records.length).toEqual(3);
expect(fieldsAfterUndo.length).toEqual(3);
await redo(table.id);
const recordsAfterRedo = (
await getRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
viewId: table.views[0].id,
})
).data;
const fieldsAfterRedo = (
await getFields(table.id, {
viewId: table.views[0].id,
})
).data;
expect(recordsAfterRedo.records[2].fields[fieldsAfterRedo[2].id]).toEqual('A');
expect(recordsAfterRedo.records[2].fields[fieldsAfterRedo[3].id]).toEqual(1);
expect(recordsAfterRedo.records[3].fields[fieldsAfterRedo[2].id]).toEqual('B');
expect(recordsAfterRedo.records[3].fields[fieldsAfterRedo[3].id]).toEqual(2);
});
it('should undo / redo create view', async () => {
const view = (
await awaitWithEvent(() =>
createView(table.id, {
type: ViewType.Grid,
name: 'view1',
})
)
).data;
await undo(table.id);
const viewsAfterUndo = (await getViewList(table.id)).data;
expect(viewsAfterUndo.find((v) => v.id === view.id)).toBeUndefined();
await redo(table.id);
const viewsAfterRedo = (await getViewList(table.id)).data;
expect(viewsAfterRedo.find((v) => v.id === view.id)).toMatchObject({
id: view.id,
name: view.name,
type: view.type,
});
});
it('should undo / redo delete view', async () => {
const view = (
await awaitWithEvent(() =>
createView(table.id, {
type: ViewType.Grid,
name: 'view1',
})
)
).data;
await awaitWithEvent(() => deleteView(table.id, view.id));
await undo(table.id);
const viewsAfterUndo = (await getViewList(table.id)).data;
expect(viewsAfterUndo.find((v) => v.id === view.id)).toMatchObject({
id: view.id,
name: view.name,
type: view.type,
});
await redo(table.id);
const viewsAfterRedo = (await getViewList(table.id)).data;
expect(viewsAfterRedo.find((v) => v.id === view.id)).toBeUndefined();
});
it('should undo / redo update view property', async () => {
// name
const view = table.views[0];
(await awaitWithEvent(() => updateViewName(table.id, view.id, { name: 'newName' }))).data;
await undo(table.id);
expect((await getView(table.id, view.id)).data.name).toEqual(view.name);
await redo(table.id);
expect((await getView(table.id, view.id)).data.name).toEqual('newName');
// description
(
await awaitWithEvent(() =>
updateViewDescription(table.id, view.id, { description: 'newName' })
)
).data;
await undo(table.id);
expect((await getView(table.id, view.id)).data.description).toEqual(view.description);
await redo(table.id);
expect((await getView(table.id, view.id)).data.description).toEqual('newName');
// filter
(
await awaitWithEvent(() =>
updateViewFilter(table.id, view.id, {
filter: {
filterSet: [
{
fieldId: table.fields![0].id,
value: 'text',
operator: 'is',
},
],
conjunction: 'and',
},
})
)
).data;
await undo(table.id);
expect((await getView(table.id, view.id)).data.filter).toEqual(view.filter);
await redo(table.id);
expect((await getView(table.id, view.id)).data.filter).toEqual({
filterSet: [
{
fieldId: table.fields![0].id,
value: 'text',
operator: 'is',
},
],
conjunction: 'and',
});
});
it('should undo / redo update view column meta', async () => {
const view = table.views[0];
(
await awaitWithEvent(() =>
updateViewColumnMeta(table.id, view.id, [
{
fieldId: table.fields[1].id,
columnMeta: {
order: 10,
},
},
])
)
).data;
const fields = (await getFields(table.id, { viewId: view.id })).data;
expect(fields[2].id).toEqual(table.fields[1].id);
await undo(table.id);
const fieldsAfterUndo = (await getFields(table.id, { viewId: view.id })).data;
expect(fieldsAfterUndo[1].id).toEqual(table.fields[1].id);
await redo(table.id);
const fieldsAfterRedo = (await getFields(table.id, { viewId: view.id })).data;
expect(fieldsAfterRedo[2].id).toEqual(table.fields[1].id);
});
it('should undo / redo update view order', async () => {
const view = table.views[0];
const view1 = (
await awaitWithEvent(() =>
createView(table.id, {
type: ViewType.Grid,
name: 'view1',
})
)
).data;
(
await awaitWithEvent(() =>
updateViewOrder(table.id, view.id, { anchorId: view1.id, position: 'after' })
)
).data;
await undo(table.id);
const viewsAfterUndo = (await getViewList(table.id)).data;
expect(viewsAfterUndo[0].id).toMatchObject(view.id);
await redo(table.id);
const viewsAfterRedo = (await getViewList(table.id)).data;
expect(viewsAfterRedo[1].id).toMatchObject(view.id);
});
describe('modify field constraint', () => {
it('should undo modify field constraint', async () => {
await awaitWithEvent(() =>
convertField(table.id, table.fields[0].id, {
...table.fields[0],
unique: true,
})
);
await expect(
updateRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
records: [
{
id: table.records[0].id,
fields: { [table.fields[0].id]: 'A' },
},
{
id: table.records[1].id,
fields: { [table.fields[0].id]: 'A' },
},
],
})
).rejects.toThrowError();
await undo(table.id);
await updateRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
records: [
{
id: table.records[0].id,
fields: { [table.fields[0].id]: 'A' },
},
{
id: table.records[1].id,
fields: { [table.fields[0].id]: 'A' },
},
],
});
});
it('should redo modify field constraint', async () => {
await awaitWithEvent(() =>
convertField(table.id, table.fields[0].id, {
...table.fields[0],
unique: true,
})
);
await undo(table.id);
await redo(table.id);
await expect(
updateRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
records: [
{
id: table.records[0].id,
fields: { [table.fields[0].id]: 'A' },
},
{
id: table.records[1].id,
fields: { [table.fields[0].id]: 'A' },
},
],
})
).rejects.toThrowError();
});
});
describe('link related', () => {
let table1: ITableFullVo;
let table2: ITableFullVo;
let table3: ITableFullVo;
const refField1Ro: IFieldRo = {
type: FieldType.SingleLineText,
};
const refField2Ro: IFieldRo = {
type: FieldType.Number,
};
let refField1: IFieldVo;
let refField2: IFieldVo;
beforeEach(async () => {
table1 = await createTable(baseId, { name: 'table1' });
table2 = await createTable(baseId, { name: 'table2' });
table3 = await createTable(baseId, { name: 'table3' });
console.log('table1', table1.id);
console.log('table2', table2.id);
console.log('table3', table3.id);
refField1 = (await createField(table1.id, refField1Ro)).data;
refField2 = (await createField(table1.id, refField2Ro)).data;
await updateRecordByApi(table1.id, table1.records[0].id, refField1.id, 'x');
await updateRecordByApi(table1.id, table1.records[1].id, refField1.id, 'y');
await updateRecordByApi(table1.id, table1.records[0].id, refField2.id, 1);
await updateRecordByApi(table1.id, table1.records[1].id, refField2.id, 2);
});
afterEach(async () => {
await permanentDeleteTable(baseId, table1.id);
await permanentDeleteTable(baseId, table2.id);
await permanentDeleteTable(baseId, table3.id);
});
it('should undo / redo delete record with link', async () => {
const linkFieldRo: IFieldRo = {
type: FieldType.Link,
options: {
relationship: Relationship.ManyOne,
foreignTableId: table2.id,
},
};
const linkField = (await createField(table1.id, linkFieldRo)).data;
const record = await updateRecordByApi(table1.id, table1.records[0].id, linkField.id, {
id: table2.records[0].id,
});
console.log('updated:record', record);
await deleteRecord(table1.id, table1.records[0].id);
await undo(table1.id);
const recordAfterUndo = (
await getRecord(table1.id, table1.records[0].id, { fieldKeyType: FieldKeyType.Id })
).data;
expect(recordAfterUndo.fields[linkField.id]).toMatchObject({
id: table2.records[0].id,
});
await redo(table1.id);
const recordsAfterRedo = (
await getRecords(table1.id, { fieldKeyType: FieldKeyType.Id, viewId: table1.views[0].id })
).data;
expect(recordsAfterRedo.records.length).toEqual(2);
});
it('should undo / redo convert link to single line text', async () => {
const sourceFieldRo: IFieldRo = {
type: FieldType.Link,
options: {
relationship: Relationship.ManyOne,
foreignTableId: table2.id,
},
};
const newFieldRo: IFieldRo = {
type: FieldType.SingleLineText,
};
// set primary key in table2
await updateRecordByApi(table2.id, table2.records[0].id, table2.fields[0].id, 'B1');
const sourceLinkField = (await createField(table1.id, sourceFieldRo)).data;
await updateRecordByApi(table1.id, table1.records[0].id, sourceLinkField.id, {
id: table2.records[0].id,
});
const newLinkField = (
await awaitWithEvent(() => convertField(table1.id, sourceLinkField.id, newFieldRo))
).data;
await undo(table1.id);
const newLinkFieldAfterUndo = (await getField(table1.id, newLinkField.id)).data;
expect(newLinkFieldAfterUndo).toMatchObject(sourceLinkField);
// make sure records has been updated
const recordsAfterUndo = (await getRecords(table1.id, { fieldKeyType: FieldKeyType.Id }))
.data;
expect(recordsAfterUndo.records[0].fields[newLinkFieldAfterUndo.id]).toEqual({
id: table2.records[0].id,
title: 'B1',
});
await redo(table1.id);
const newLinkFieldAfterRedo = (await getField(table1.id, newLinkField.id)).data;
expect(newLinkFieldAfterRedo).toMatchObject(newLinkField);
// make sure records has been updated
const recordsAfterRedo = (await getRecords(table1.id, { fieldKeyType: FieldKeyType.Id }))
.data;
expect(recordsAfterRedo.records[0].fields[newLinkFieldAfterRedo.id]).toEqual('B1');
});
it('should undo / redo convert link when convert link from one table to another', async () => {
const sourceFieldRo: IFieldRo = {
type: FieldType.Link,
options: {
relationship: Relationship.ManyOne,
foreignTableId: table2.id,
},
};
const newFieldRo: IFieldRo = {
type: FieldType.Link,
options: {
relationship: Relationship.ManyOne,
foreignTableId: table3.id,
},
};
// set primary key in table2
await updateRecordByApi(table2.id, table2.records[0].id, table2.fields[0].id, 'B1');
// set primary key in table3
await updateRecordByApi(table3.id, table3.records[0].id, table3.fields[0].id, 'C1');
const sourceLinkField = (await createField(table1.id, sourceFieldRo)).data;
const lookupFieldRo: IFieldRo = {
type: FieldType.SingleLineText,
isLookup: true,
lookupOptions: {
foreignTableId: table2.id,
lookupFieldId: table2.fields[0].id,
linkFieldId: sourceLinkField.id,
},
};
const sourceLookupField = (await awaitWithEvent(() => createField(table1.id, lookupFieldRo)))
.data;
const formulaLinkFieldRo: IFieldRo = {
type: FieldType.Formula,
options: {
expression: `{${sourceLinkField.id}}`,
},
};
const formulaLookupFieldRo: IFieldRo = {
type: FieldType.Formula,
options: {
expression: `{${sourceLookupField.id}}`,
},
};
const sourceFormulaLinkField = (
await awaitWithEvent(() => createField(table1.id, formulaLinkFieldRo))
).data;
const sourceFormulaLookupField = (
await awaitWithEvent(() => createField(table1.id, formulaLookupFieldRo))
).data;
await updateRecordByApi(table1.id, table1.records[0].id, sourceLinkField.id, {
id: table2.records[0].id,
});
// make sure records has been updated
const { records: rs } = (await getRecords(table1.id, { fieldKeyType: FieldKeyType.Id })).data;
expect(rs[0].fields[sourceLinkField.id]).toEqual({ id: table2.records[0].id, title: 'B1' });
expect(rs[0].fields[sourceLookupField.id]).toEqual('B1');
expect(rs[0].fields[sourceFormulaLinkField.id]).toEqual('B1');
expect(rs[0].fields[sourceFormulaLookupField.id]).toEqual('B1');
const newLinkField = (
await awaitWithEvent(() => convertField(table1.id, sourceLinkField.id, newFieldRo))
).data;
await undo(table1.id);
const newLinkFieldAfterUndo = (await getField(table1.id, newLinkField.id)).data;
expect(newLinkFieldAfterUndo).toMatchObject(sourceLinkField);
const targetLookupFieldAfterUndo = (await getField(table1.id, sourceLookupField.id)).data;
expect(targetLookupFieldAfterUndo.hasError).toBeUndefined();
await redo(table1.id);
const newLinkFieldAfterRedo = (await getField(table1.id, newLinkField.id)).data;
expect(newLinkFieldAfterRedo).toMatchObject(newLinkField);
await updateRecordByApi(table1.id, table1.records[0].id, newLinkFieldAfterRedo.id, {
id: table3.records[0].id,
});
const targetLookupField = (await getField(table1.id, sourceLookupField.id)).data;
const targetFormulaLinkField = (await getField(table1.id, sourceFormulaLinkField.id)).data;
const targetFormulaLookupField = (await getField(table1.id, sourceFormulaLookupField.id))
.data;
expect(targetLookupField.hasError).toBeTruthy();
expect(targetFormulaLinkField.hasError).toBeUndefined();
expect(targetFormulaLookupField.hasError).toBeUndefined();
// make sure records has been updated
const { records } = (await getRecords(table1.id, { fieldKeyType: FieldKeyType.Id })).data;
expect(records[0].fields[newLinkFieldAfterRedo.id]).toEqual({
id: table3.records[0].id,
title: 'C1',
});
expect(records[0].fields[targetLookupField.id]).toEqual('B1');
expect(records[0].fields[targetFormulaLinkField.id]).toEqual('C1');
expect(records[0].fields[targetFormulaLookupField.id]).toEqual('B1');
});
it('should undo / redo convert two-way to one-way link', async () => {
const sourceFieldRo: IFieldRo = {
type: FieldType.Link,
options: {
relationship: Relationship.OneMany,
foreignTableId: table2.id,
},
};
const newFieldRo: IFieldRo = {
type: FieldType.Link,
options: {
relationship: Relationship.OneMany,
foreignTableId: table2.id,
isOneWay: true,
},
};
const sourceField = (await createField(table1.id, sourceFieldRo)).data;
(await convertField(table1.id, sourceField.id, newFieldRo)).data;
await undo(table1.id);
const fieldAfterUndo = (await getField(table1.id, sourceField.id)).data;
expect(fieldAfterUndo).toMatchObject({
cellValueType: CellValueType.String,
dbFieldType: DbFieldType.Json,
type: FieldType.Link,
options: {
relationship: Relationship.OneMany,
foreignTableId: table2.id,
lookupFieldId: table2.fields[0].id,
isOneWay: false,
},
});
await redo(table1.id);
const fieldAfterRedo = (await getField(table1.id, sourceField.id)).data;
expect(fieldAfterRedo).toMatchObject({
cellValueType: CellValueType.String,
dbFieldType: DbFieldType.Json,
type: FieldType.Link,
options: {
relationship: Relationship.OneMany,
foreignTableId: table2.id,
lookupFieldId: table2.fields[0].id,
isOneWay: true,
},
});
const symmetricFieldId = (fieldAfterRedo.options as ILinkFieldOptions).symmetricFieldId;
expect(symmetricFieldId).toBeUndefined();
});
it('should undo / redo convert one-way link to two-way link', async () => {
const sourceFieldRo: IFieldRo = {
type: FieldType.Link,
options: {
relationship: Relationship.OneMany,
foreignTableId: table2.id,
isOneWay: true,
},
};
const newFieldRo: IFieldRo = {
type: FieldType.Link,
options: {
relationship: Relationship.OneMany,
foreignTableId: table2.id,
isOneWay: false,
},
};
// set primary key in table2
await updateRecordByApi(table2.id, table2.records[0].id, table2.fields[0].id, 'x');
await updateRecordByApi(table2.id, table2.records[1].id, table2.fields[0].id, 'y');
await updateRecordByApi(table2.id, table2.records[2].id, table2.fields[0].id, 'zzz');
const sourceField = (await createField(table1.id, sourceFieldRo)).data;
await updateRecordByApi(table1.id, table1.records[0].id, sourceField.id, [
{ id: table2.records[0].id },
{ id: table2.records[1].id },
]);
await createField(table1.id, {
type: FieldType.SingleLineText,
isLookup: true,
lookupOptions: {
foreignTableId: table2.id,
lookupFieldId: table2.fields[0].id,
linkFieldId: sourceField.id,
},
});
await createField(table1.id, {
type: FieldType.Rollup,
options: {
expression: `count({values})`,
formatting: {
precision: 2,
type: 'decimal',
},
} as IRollupFieldOptions,
lookupOptions: {
foreignTableId: table2.id,
lookupFieldId: table2.fields[0].id,
linkFieldId: sourceField.id,
},
});
(await convertField(table1.id, sourceField.id, newFieldRo)).data;
await undo(table1.id);
const fieldAfterUndo = (await getField(table1.id, sourceField.id)).data;
expect(fieldAfterUndo).toMatchObject({
cellValueType: CellValueType.String,
dbFieldType: DbFieldType.Json,
type: FieldType.Link,
options: {
relationship: Relationship.OneMany,
foreignTableId: table2.id,
lookupFieldId: table2.fields[0].id,
isOneWay: true,
},
});
// perform redo
await redo(table1.id);
const fieldAfterRedo = (await getField(table1.id, sourceField.id)).data;
expect(fieldAfterRedo).toMatchObject({
cellValueType: CellValueType.String,
dbFieldType: DbFieldType.Json,
type: FieldType.Link,
options: {
relationship: Relationship.OneMany,
foreignTableId: table2.id,
lookupFieldId: table2.fields[0].id,
isOneWay: false,
},
});
const symmetricFieldId = (fieldAfterRedo.options as ILinkFieldOptions).symmetricFieldId;
expect(symmetricFieldId).toBeDefined();
const symmetricField = (await getField(table2.id, symmetricFieldId as string)).data;
expect(symmetricField).toMatchObject({
cellValueType: CellValueType.String,
dbFieldType: DbFieldType.Json,
type: FieldType.Link,
options: {
relationship: Relationship.ManyOne,
foreignTableId: table1.id,
lookupFieldId: table1.fields[0].id,
},
});
const { records } = (await getRecords(table2.id, { fieldKeyType: FieldKeyType.Id })).data;
expect(records[0].fields[symmetricField.id]).toMatchObject({ id: table1.records[0].id });
expect(records[1].fields[symmetricField.id]).toMatchObject({ id: table1.records[0].id });
});
});
});