(core) updates from grist-core

This commit is contained in:
Paul Fitzpatrick
2023-08-15 12:15:16 -04:00
13 changed files with 688 additions and 36 deletions

BIN
test/fixtures/docs/ShiftSelection.grist vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,213 @@
import {assert, driver, Key, WebElement} from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import {setupTestSuite} from 'test/nbrowser/testUtils';
interface CellSelection {
/** 0-based column index. */
colStart: number;
/** 0-based column index. */
colEnd: number;
/** 0-based row index. */
rowStart: number;
/** 0-based row index. */
rowEnd: number;
}
interface SelectionRange {
/** 0-based index. */
start: number;
/** 0-based index. */
end: number;
}
describe('ShiftSelection', function () {
this.timeout(20000);
const cleanup = setupTestSuite();
gu.bigScreen();
before(async function() {
const session = await gu.session().personalSite.login();
await session.tempDoc(cleanup, 'ShiftSelection.grist');
});
async function getSelectionRange(parent: WebElement, selector: string): Promise<SelectionRange|undefined> {
let start, end;
let selectionActive = false;
const elements = await parent.findAll(selector);
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const isSelected = await element.matches('.selected');
if (isSelected && !selectionActive) {
start = i;
selectionActive = true;
continue;
}
if (!isSelected && selectionActive) {
end = i - 1;
break;
}
}
if (start === undefined) {
return undefined;
}
if (end === undefined) {
end = elements.length - 1;
}
return {
start: start,
end: end,
};
}
async function getSelectedCells(): Promise<CellSelection|undefined> {
const activeSection = await driver.find('.active_section');
const colSelection = await getSelectionRange(activeSection, '.column_names .column_name');
if (!colSelection) {
return undefined;
}
const rowSelection = await getSelectionRange(activeSection, '.gridview_row .gridview_data_row_num');
if (!rowSelection) {
// Edge case if only a cell in the "new" row is selected
// Not relevant for our tests
return undefined;
}
return {
colStart: colSelection.start,
colEnd: colSelection.end,
rowStart: rowSelection.start,
rowEnd: rowSelection.end,
};
}
async function assertCellSelection(expected: CellSelection|undefined) {
const currentSelection = await getSelectedCells();
assert.deepEqual(currentSelection, expected);
}
it('Shift+Up extends the selection up', async function () {
await gu.getCell(1, 2).click();
await gu.sendKeys(Key.chord(Key.SHIFT, Key.UP));
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 0, rowEnd: 1});
});
it('Shift+Down extends the selection down', async function () {
await gu.getCell(1, 2).click();
await gu.sendKeys(Key.chord(Key.SHIFT, Key.DOWN));
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 1, rowEnd: 2});
});
it('Shift+Left extends the selection left', async function () {
await gu.getCell(1, 2).click();
await gu.sendKeys(Key.chord(Key.SHIFT, Key.LEFT));
await assertCellSelection({colStart: 0, colEnd: 1, rowStart: 1, rowEnd: 1});
});
it('Shift+Right extends the selection right', async function () {
await gu.getCell(1, 2).click();
await gu.sendKeys(Key.chord(Key.SHIFT, Key.RIGHT));
await assertCellSelection({colStart: 1, colEnd: 2, rowStart: 1, rowEnd: 1});
});
it('Shift+Right + Shift+Left leads to the initial selection', async function () {
await gu.getCell(1, 2).click();
await gu.sendKeys(Key.chord(Key.SHIFT, Key.RIGHT));
await gu.sendKeys(Key.chord(Key.SHIFT, Key.LEFT));
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 1, rowEnd: 1});
});
it('Shift+Up + Shift+Down leads to the initial selection', async function () {
await gu.getCell(1, 2).click();
await gu.sendKeys(Key.chord(Key.SHIFT, Key.UP));
await gu.sendKeys(Key.chord(Key.SHIFT, Key.DOWN));
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 1, rowEnd: 1});
});
it('Ctrl+Shift+Up extends the selection blockwise up', async function () {
await gu.getCell(5, 7).click();
const ctrlKey = await gu.modKey();
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 6});
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 2, rowEnd: 6});
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 0, rowEnd: 6});
});
it('Ctrl+Shift+Down extends the selection blockwise down', async function () {
await gu.getCell(5, 5).click();
const ctrlKey = await gu.modKey();
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.DOWN));
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 6});
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.DOWN));
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 8});
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.DOWN));
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 10});
});
it('Ctrl+Shift+Left extends the selection blockwise left', async function () {
await gu.getCell(6, 5).click();
const ctrlKey = await gu.modKey();
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.LEFT));
await assertCellSelection({colStart: 4, colEnd: 6, rowStart: 4, rowEnd: 4});
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.LEFT));
await assertCellSelection({colStart: 2, colEnd: 6, rowStart: 4, rowEnd: 4});
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.LEFT));
await assertCellSelection({colStart: 0, colEnd: 6, rowStart: 4, rowEnd: 4});
});
it('Ctrl+Shift+Right extends the selection blockwise right', async function () {
await gu.getCell(4, 5).click();
const ctrlKey = await gu.modKey();
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.RIGHT));
await assertCellSelection({colStart: 4, colEnd: 6, rowStart: 4, rowEnd: 4});
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.RIGHT));
await assertCellSelection({colStart: 4, colEnd: 8, rowStart: 4, rowEnd: 4});
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.RIGHT));
await assertCellSelection({colStart: 4, colEnd: 10, rowStart: 4, rowEnd: 4});
});
it('Ctrl+Shift+* extends the selection until all the next cells are empty', async function () {
await gu.getCell(3, 7).click();
const ctrlKey = await gu.modKey();
await gu.sendKeys(Key.chord(Key.SHIFT, Key.RIGHT));
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
await assertCellSelection({colStart: 3, colEnd: 4, rowStart: 2, rowEnd: 6});
await gu.getCell(4, 7).click();
await gu.sendKeys(Key.chord(Key.SHIFT, Key.LEFT));
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
await assertCellSelection({colStart: 3, colEnd: 4, rowStart: 4, rowEnd: 6});
});
});

View File

@@ -5,7 +5,7 @@ import {arrayRepeat} from 'app/common/gutil';
import {WebhookSummary} from 'app/common/Triggers';
import {DocAPI, DocState, UserAPIImpl} from 'app/common/UserAPI';
import {testDailyApiLimitFeatures} from 'app/gen-server/entity/Product';
import {AddOrUpdateRecord, Record as ApiRecord} from 'app/plugin/DocApiTypes';
import {AddOrUpdateRecord, Record as ApiRecord, ColumnsPut, RecordWithStringId} from 'app/plugin/DocApiTypes';
import {CellValue, GristObjCode} from 'app/plugin/GristData';
import {
applyQueryParameters,
@@ -443,6 +443,26 @@ function testDocApi() {
});
});
it('GET /docs/{did}/tables/{tid}/records honors the "hidden" param', async function () {
const params = { hidden: true };
const resp = await axios.get(
`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/records`,
{...chimpy, params }
);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data.records[0], {
id: 1,
fields: {
manualSort: 1,
A: 'hello',
B: '',
C: '',
D: null,
E: 'HELLO',
},
});
});
it("GET /docs/{did}/tables/{tid}/records handles errors and hidden columns", async function () {
let resp = await axios.get(`${serverUrl}/api/docs/${docIds.ApiDataRecordsTest}/tables/Table1/records`, chimpy);
assert.equal(resp.status, 200);
@@ -610,6 +630,21 @@ function testDocApi() {
);
});
it('GET /docs/{did}/tables/{tid}/columns retrieves hidden columns when "hidden" is set', async function () {
const params = { hidden: true };
const resp = await axios.get(
`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/columns`,
{ ...chimpy, params }
);
assert.equal(resp.status, 200);
const columnsMap = new Map(resp.data.columns.map(({id, fields}: {id: string, fields: object}) => [id, fields]));
assert.include([...columnsMap.keys()], "manualSort");
assert.deepInclude(columnsMap.get("manualSort"), {
colRef: 1,
type: 'ManualSortPos',
});
});
it("GET/POST/PATCH /docs/{did}/tables and /columns", async function () {
// POST /tables: Create new tables
let resp = await axios.post(`${serverUrl}/api/docs/${docIds.Timesheets}/tables`, {
@@ -842,6 +877,126 @@ function testDocApi() {
assert.equal(resp.status, 200);
});
describe("PUT /docs/{did}/columns", function () {
async function generateDocAndUrl() {
const wid = (await userApi.getOrgWorkspaces('current')).find((w) => w.name === 'Private')!.id;
const docId = await userApi.newDoc({name: 'ColumnsPut'}, wid);
const url = `${serverUrl}/api/docs/${docId}/tables/Table1/columns`;
return { url, docId };
}
async function getColumnFieldsMapById(url: string, params: any) {
const result = await axios.get(url, {...chimpy, params});
assert.equal(result.status, 200);
return new Map<string, object>(
result.data.columns.map(
({id, fields}: {id: string, fields: object}) => [id, fields]
)
);
}
async function checkPut(
columns: [RecordWithStringId, ...RecordWithStringId[]],
params: Record<string, any>,
expectedFieldsByColId: Record<string, object>,
opts?: { getParams?: any }
) {
const {url} = await generateDocAndUrl();
const body: ColumnsPut = { columns };
const resp = await axios.put(url, body, {...chimpy, params});
assert.equal(resp.status, 200);
const fieldsByColId = await getColumnFieldsMapById(url, opts?.getParams);
assert.deepEqual(
[...fieldsByColId.keys()],
Object.keys(expectedFieldsByColId),
"The updated table should have the expected columns"
);
for (const [colId, expectedFields] of Object.entries(expectedFieldsByColId)) {
assert.deepInclude(fieldsByColId.get(colId), expectedFields);
}
}
const COLUMN_TO_ADD = {
id: "Foo",
fields: {
type: "Text",
label: "FooLabel",
}
};
const COLUMN_TO_UPDATE = {
id: "A",
fields: {
type: "Numeric",
colId: "NewA"
}
};
it('should create new columns', async function () {
await checkPut([COLUMN_TO_ADD], {}, {
A: {}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
});
});
it('should update existing columns and create new ones', async function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {}, {
NewA: {type: "Numeric", label: "A"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
});
});
it('should only update existing columns when noadd is set', async function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noadd: "1"}, {
NewA: {type: "Numeric"}, B: {}, C: {}
});
});
it('should only add columns when noupdate is set', async function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noupdate: "1"}, {
A: {type: "Any"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
});
});
it('should remove existing columns if replaceall is set', async function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, {
NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields
});
});
it('should NOT remove hidden columns even when replaceall is set', async function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, {
manualSort: {type: "ManualSortPos"}, NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields
}, { getParams: { hidden: true } });
});
it('should forbid update by viewers', async function () {
// given
const { url, docId } = await generateDocAndUrl();
await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}});
// when
const resp = await axios.put(url, { columns: [ COLUMN_TO_ADD ] }, kiwi);
// then
assert.equal(resp.status, 403);
});
it("should return 404 when table is not found", async function() {
// given
const { url } = await generateDocAndUrl();
const notFoundUrl = url.replace("Table1", "NonExistingTable");
// when
const resp = await axios.put(notFoundUrl, { columns: [ COLUMN_TO_ADD ] }, chimpy);
// then
assert.equal(resp.status, 404);
assert.equal(resp.data.error, 'Table not found "NonExistingTable"');
});
});
it("GET /docs/{did}/tables/{tid}/data returns 404 for non-existent doc", async function () {
const resp = await axios.get(`${serverUrl}/api/docs/typotypotypo/tables/Table1/data`, chimpy);
assert.equal(resp.status, 404);