mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Fixing cursor position for filtered linked section.
Summary: In a selector table, when a selected row is filtered out of view, linked widgets should update based on the newly selected row. There were a few bugs that contributed to this wrong behavior: - Gridview wasn't subscribing to the current row id, and the row with id 'new' was being converted to the first row - Cursor was keeping track of the currently selected row id, it was hiding a problem behind the proper rowIndex - Undo/redo somehow leveraged the wrong rowId from the cursor during the position restore. The `No data` text was also changed to be more meaningful. Test Plan: Added and updated. Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3937
This commit is contained in:
@@ -7,30 +7,134 @@ describe('LinkingSelector', function() {
|
||||
|
||||
const cleanup = setupTestSuite({team: true});
|
||||
let session: gu.Session;
|
||||
let docId: string;
|
||||
|
||||
afterEach(() => gu.checkForErrors());
|
||||
|
||||
before(async function() {
|
||||
session = await gu.session().login();
|
||||
const doc = await session.tempDoc(cleanup, 'Class Enrollment.grist', {load: false});
|
||||
await session.loadDoc(`/doc/${doc.id}/p/7`);
|
||||
docId = (await session.tempDoc(cleanup, 'Class Enrollment.grist')).id;
|
||||
});
|
||||
|
||||
interface CursorSelectorInfo {
|
||||
linkSelector: false | number;
|
||||
cursor: false | {rowNum: number, col: number};
|
||||
}
|
||||
it('should update linked grid view on filter change', async function() {
|
||||
// Add new page with summarized classes by Start_Date.
|
||||
await gu.addNewPage('Table', 'Classes', {
|
||||
summarize: ['Start_Date'],
|
||||
});
|
||||
|
||||
async function getCursorSelectorInfo(section: WebElement): Promise<CursorSelectorInfo> {
|
||||
const hasCursor = await section.find('.active_cursor').isPresent();
|
||||
const hasSelector = await section.find('.link_selector_row').isPresent();
|
||||
return {
|
||||
linkSelector: hasSelector && Number(await section.find('.link_selector_row .gridview_data_row_num').getText()),
|
||||
cursor: hasCursor && await gu.getCursorPosition(section),
|
||||
};
|
||||
}
|
||||
// Add a new linked widget that shows classes by this date.
|
||||
await gu.addNewSection('Table', 'Classes', {
|
||||
selectBy: 'CLASSES [by Start_Date]'
|
||||
});
|
||||
|
||||
// Click on the first date.
|
||||
await gu.getCell('Start_Date', 1, 'CLASSES [by Start_Date]').click();
|
||||
// Make sure we know which row is selected.
|
||||
assert.equal(await gu.getCell('Start_Date', 1).getText(), '2018-09-13');
|
||||
|
||||
// Show only Start_Date in the linked section.
|
||||
await gu.selectSectionByTitle('CLASSES');
|
||||
await gu.openWidgetPanel();
|
||||
await gu.selectAllVisibleColumns();
|
||||
await gu.toggleVisibleColumn('Start_Date');
|
||||
await gu.hideVisibleColumns();
|
||||
|
||||
// Make sure we see the same date.
|
||||
await gu.getCell('Start_Date', 1, 'CLASSES').click();
|
||||
assert.equal(await gu.getCell('Start_Date', 1).getText(), '2018-09-13');
|
||||
|
||||
// Now filter the summary table to not show the selected date.
|
||||
await gu.selectSectionByTitle('CLASSES [by Start_Date]');
|
||||
await gu.filterBy('Start_Date', false, ['2018-09-14']);
|
||||
// Make sure this is filtered out.
|
||||
assert.equal(await gu.getCell('Start_Date', 1, 'CLASSES [by Start_Date]').getText(), '2018-09-14');
|
||||
// Make sure the linked section is updated.
|
||||
assert.equal(await gu.getCell('Start_Date', 1, 'CLASSES').getText(), '2018-09-14');
|
||||
});
|
||||
|
||||
it('should update linked card on filter change with same record linking', async function() {
|
||||
// Test the same for the card view and same record linking (it uses different code).
|
||||
// Add a list and a card view of the same table and link them together.
|
||||
await gu.addNewPage('Table', 'Classes');
|
||||
|
||||
// Hide all columns in the list view.
|
||||
await gu.openWidgetPanel();
|
||||
await gu.selectAllVisibleColumns();
|
||||
await gu.toggleVisibleColumn('Start_Date');
|
||||
await gu.hideVisibleColumns();
|
||||
|
||||
// Rename it to list.
|
||||
await gu.renameActiveSection('List');
|
||||
|
||||
// Now add a card view.
|
||||
await gu.addNewSection('Card', 'Classes', {
|
||||
'selectBy': 'List'
|
||||
});
|
||||
await gu.renameActiveSection('Card');
|
||||
await gu.selectAllVisibleColumns();
|
||||
await gu.toggleVisibleColumn('Start_Date');
|
||||
await gu.hideVisibleColumns();
|
||||
|
||||
// Select the second row.
|
||||
await gu.selectSectionByTitle('List');
|
||||
await gu.getCell('Start_Date', 2).click();
|
||||
// Make sure we know the second row is selected.
|
||||
assert.equal(await gu.getCell('Start_Date', 2).getText(), '2018-09-14');
|
||||
assert.equal((await gu.getCursorPosition()).rowNum, 2);
|
||||
|
||||
// Make sure it was also updated in the card view.
|
||||
await gu.selectSectionByTitle('Card');
|
||||
assert.equal(await gu.getDetailCell('Start_Date', 1).getText(), '2018-09-14');
|
||||
|
||||
// Now filter it out, using pinned filters (to not alter the cursor position).
|
||||
await gu.selectSectionByTitle('List');
|
||||
// Pin the filter to the panel by just adding it (it is pinned by default).
|
||||
await gu.sortAndFilter()
|
||||
.then(x => x.addColumn())
|
||||
.then(x => x.clickColumn('Start_Date'))
|
||||
.then(x => x.close())
|
||||
.then(x => x.click());
|
||||
|
||||
// Open the pinned filter, and filter out the date.
|
||||
await gu.openPinnedFilter('Start_Date')
|
||||
.then(x => x.toggleValue('2018-09-14'))
|
||||
.then(x => x.close());
|
||||
|
||||
// Make sure we see it as the first row
|
||||
assert.equal(await gu.getCell('Start_Date', 1).getText(), '2018-09-13');
|
||||
assert.equal(await gu.getCell('Start_Date', 2).getText(), '2019-01-27');
|
||||
// And cursor was moved to the first row.
|
||||
assert.equal((await gu.getCursorPosition()).rowNum, 1);
|
||||
// Make sure the card view is updated.
|
||||
await gu.selectSectionByTitle('Card');
|
||||
assert.equal(await gu.getDetailCell('Start_Date', 1).getText(), '2018-09-13');
|
||||
});
|
||||
|
||||
it('should update linked section for the first row', async function() {
|
||||
// There was a bug here. First row wasn't somehow triggering the linked section to update itself,
|
||||
// when it was filtered out, for self-linking.
|
||||
await gu.selectSectionByTitle('List');
|
||||
await gu.removeFilters();
|
||||
|
||||
// Select first row.
|
||||
await gu.getCell('Start_Date', 1).click();
|
||||
// Make sure we know what we selected.
|
||||
assert.equal(await gu.getCell('Start_Date', 1, 'List').getText(), '2018-09-13');
|
||||
// Make sure that card reflects it.
|
||||
assert.equal(await gu.getDetailCell('Start_Date', 1, 'Card').getText(), '2018-09-13');
|
||||
// Now unfilter it.
|
||||
await gu.openColumnFilter('Start_Date')
|
||||
.then(x => x.toggleValue('2018-09-13'))
|
||||
.then(x => x.close());
|
||||
// Make sure that List is updated in the first row.
|
||||
assert.equal(await gu.getCell('Start_Date', 1, 'List').getText(), '2018-09-14');
|
||||
// Make sure that Card is updated accordingly.
|
||||
assert.equal(await gu.getDetailCell('Start_Date', 1, 'Card').getText(), '2018-09-14');
|
||||
});
|
||||
|
||||
it('should mark selected row used for linking', async function() {
|
||||
await session.loadDoc(`/doc/${docId}/p/7`);
|
||||
|
||||
const families = gu.getSection('FAMILIES');
|
||||
const students = gu.getSection('STUDENTS');
|
||||
const enrollments = gu.getSection('ENROLLMENTS');
|
||||
@@ -94,3 +198,18 @@ describe('LinkingSelector', function() {
|
||||
assert.deepEqual(await getCursorSelectorInfo(families), {linkSelector: 3, cursor: false});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
interface CursorSelectorInfo {
|
||||
linkSelector: false | number;
|
||||
cursor: false | {rowNum: number, col: number};
|
||||
}
|
||||
|
||||
async function getCursorSelectorInfo(section: WebElement): Promise<CursorSelectorInfo> {
|
||||
const hasCursor = await section.find('.active_cursor').isPresent();
|
||||
const hasSelector = await section.find('.link_selector_row').isPresent();
|
||||
return {
|
||||
linkSelector: hasSelector && Number(await section.find('.link_selector_row .gridview_data_row_num').getText()),
|
||||
cursor: hasCursor && await gu.getCursorPosition(section),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('Views.ntest', function() {
|
||||
await gu.openColumnMenu('A');
|
||||
await $(`.grist-floating-menu .test-sort-asc`).click();
|
||||
// Delete the table.
|
||||
await gu.removeTable("Table4");
|
||||
await gu.sendActions([['RemoveTable', 'Table4']]);
|
||||
await gu.actions.selectTabView('Table1');
|
||||
// Assert that the default section (Table1 record) is now active.
|
||||
assert.equal(await $('.active_section > .viewsection_title').text(), 'TABLE1');
|
||||
|
||||
@@ -1467,6 +1467,29 @@ export async function moveToHidden(col: string) {
|
||||
await waitForServer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks `Select All` in visible columns section.
|
||||
*/
|
||||
export async function selectAllVisibleColumns() {
|
||||
await driver.find('.test-vfc-visible-fields-select-all').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle checkbox for a column in visible columns section.
|
||||
*/
|
||||
export async function toggleVisibleColumn(col: string) {
|
||||
const row = await driver.findContent(".test-vfc-visible-fields .kf_draggable_content", exactMatch(col));
|
||||
await row.find('input').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks `Hide Columns` button in visible columns section.
|
||||
*/
|
||||
export async function hideVisibleColumns() {
|
||||
await driver.find('.test-vfc-visible-hide').click();
|
||||
await waitForServer();
|
||||
}
|
||||
|
||||
export async function search(what: string) {
|
||||
await driver.find('.test-tb-search-icon').click();
|
||||
await driver.sleep(500);
|
||||
@@ -2815,18 +2838,123 @@ export async function scrollActiveViewTop() {
|
||||
* Filters a column in a Grid using the filter menu.
|
||||
*/
|
||||
export async function filterBy(col: IColHeader|string, save: boolean, values: (string|RegExp)[]) {
|
||||
await openColumnMenu(col, 'Filter');
|
||||
// Select none at start
|
||||
await driver.findContent('.test-filter-menu-bulk-action', /None/).click();
|
||||
for(const value of values) {
|
||||
await driver.findContent('.test-filter-menu-list label', value).click();
|
||||
const filter = await openColumnFilter(col);
|
||||
await filter.none();
|
||||
for (const value of values) {
|
||||
await filter.toggleValue(value);
|
||||
}
|
||||
// Save filters
|
||||
await driver.find('.test-filter-menu-apply-btn').click();
|
||||
await filter.close();
|
||||
if (save) {
|
||||
await driver.find('.test-section-menu-small-btn-save').click();
|
||||
await filter.save();
|
||||
}
|
||||
await waitForServer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a filter menu for a column and returns a controller for it.
|
||||
*/
|
||||
export async function openColumnFilter(col: IColHeader|string) {
|
||||
await openColumnMenu(col, 'Filter');
|
||||
return filterController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a filter menu for a column and returns a controller for it.
|
||||
*/
|
||||
export async function openPinnedFilter(col: string) {
|
||||
const filterBar = driver.find('.active_section .test-filter-bar');
|
||||
const pinnedFilter = filterBar.findContent('.test-filter-field', col);
|
||||
await pinnedFilter.click();
|
||||
return filterController;
|
||||
}
|
||||
|
||||
const filterController = {
|
||||
async toggleValue(value: string|RegExp) {
|
||||
await driver.findContent('.test-filter-menu-list label', value).click();
|
||||
return this;
|
||||
},
|
||||
async none() {
|
||||
await driver.findContent('.test-filter-menu-bulk-action', /None/).click();
|
||||
return this;
|
||||
},
|
||||
async all() {
|
||||
await driver.findContent('.test-filter-menu-bulk-action', /All/).click();
|
||||
return this;
|
||||
},
|
||||
async close() {
|
||||
await driver.find('.test-filter-menu-apply-btn').click();
|
||||
return this;
|
||||
},
|
||||
async cancel() {
|
||||
await driver.find('.test-filter-menu-cancel-btn').click();
|
||||
return this;
|
||||
},
|
||||
async save() {
|
||||
await driver.find('.test-section-menu-small-btn-save').click();
|
||||
await waitForServer();
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the filter menu in the current section, and removes all filters. Optionally saves it.
|
||||
*/
|
||||
export async function removeFilters(save = false) {
|
||||
const sectionFilter = await sortAndFilter();
|
||||
for(const filter of await sectionFilter.filters()) {
|
||||
await filter.remove();
|
||||
}
|
||||
if (save) {
|
||||
await sectionFilter.save();
|
||||
} else {
|
||||
await sectionFilter.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the filter icon in the current section, and returns a controller for it for interactions.
|
||||
*/
|
||||
export async function sortAndFilter() {
|
||||
const ctrl = {
|
||||
async addColumn() {
|
||||
await driver.find('.test-filter-config-add-filter-btn').click();
|
||||
return this;
|
||||
},
|
||||
async clickColumn(col: string) {
|
||||
await driver.findContent(".test-sd-searchable-list-item", col).click();
|
||||
return this;
|
||||
},
|
||||
async close() {
|
||||
await driver.find('.test-filter-menu-apply-btn').click();
|
||||
return this;
|
||||
},
|
||||
async save() {
|
||||
await driver.find('.test-section-menu-btn-save').click();
|
||||
await waitForServer();
|
||||
return this;
|
||||
},
|
||||
/**
|
||||
* Clicks the filter icon in the current section (can be used to close the filter menu or open it)
|
||||
*/
|
||||
async click() {
|
||||
await driver.find('.active_section .test-section-menu-filter-icon').click();
|
||||
return this;
|
||||
},
|
||||
async filters() {
|
||||
const items = await driver.findAll('.test-filter-config-filter');
|
||||
return items.map(item => ({
|
||||
async remove() {
|
||||
await item.find('.test-filter-config-remove-filter').click();
|
||||
return this;
|
||||
},
|
||||
async togglePin() {
|
||||
await item.find('.test-filter-config-pin-filter').click();
|
||||
return this;
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
await ctrl.click();
|
||||
return ctrl;
|
||||
}
|
||||
|
||||
export interface PinnedFilter {
|
||||
|
||||
Reference in New Issue
Block a user