mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) updates from grist-core
This commit is contained in:
BIN
test/fixtures/docs/ShiftSelection.grist
vendored
Normal file
BIN
test/fixtures/docs/ShiftSelection.grist
vendored
Normal file
Binary file not shown.
213
test/nbrowser/ShiftSelection.ts
Normal file
213
test/nbrowser/ShiftSelection.ts
Normal 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});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user