From a1c62f32f4542d8e072a0fa70c0ea8f050df82a7 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Thu, 30 Nov 2023 20:41:51 +0200 Subject: [PATCH] (core) Fix selecting new row in chain of filter links Summary: When the 'new' row of a table is selected, another table filter linked to the first shows no data. This diff ensures that a third table filtered by the second also shows no data, i.e. that it behaves the same as if the second table was also on the 'new' row. Video of the bug: https://grist.slack.com/archives/C069RUP71/p1692622810900179 The functional code is copied almost verbatim from https://github.com/gristlabs/grist-core/pull/666 by @jvorob which was working correctly. A comment there mentioned a possible bug where: > ...you can have the grayed-out "No row selected" text from disableEditing but still have rows showing up in the section. Haven't been able to reproduce... I noticed this behaviour when I copied only part of the fix, but it disappeared after copying the whole thing, so it seems likely to me that this is why it couldn't be reproduced. Test Plan: Added a new nbrowser test with a new fixture, which also tests filter link chains and selecting the new row more generally, since I couldn't find other tests of this. Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: jvorob Differential Revision: https://phab.getgrist.com/D4126 --- app/client/components/LinkingState.ts | 66 +++++++----- test/fixtures/docs/FilterLinkChain.grist | Bin 0 -> 172032 bytes test/nbrowser/FilterLinkChain.ts | 130 +++++++++++++++++++++++ 3 files changed, 170 insertions(+), 26 deletions(-) create mode 100644 test/fixtures/docs/FilterLinkChain.grist create mode 100644 test/nbrowser/FilterLinkChain.ts diff --git a/app/client/components/LinkingState.ts b/app/client/components/LinkingState.ts index 5481ff38..8fb13a09 100644 --- a/app/client/components/LinkingState.ts +++ b/app/client/components/LinkingState.ts @@ -350,7 +350,11 @@ export class LinkingState extends Disposable { * Returns a boolean indicating whether editing should be disabled in the destination section. */ public disableEditing(): boolean { - return Boolean(this.filterState) && this._srcSection.activeRowId() === 'new'; + if (!this.filterState) { + return false; + } + const srcRowId = this._srcSection.activeRowId(); + return srcRowId === 'new' || srcRowId === null; } @@ -438,11 +442,6 @@ export class LinkingState extends Disposable { //Get selector-rowId const srcRowId = this._srcSection.activeRowId(); - if (srcRowId === null) { - console.warn("LinkingState._makeFilterObs activeRowId is null"); - return EmptyFilterState; - } - //Get values from selector row const selectorCellVal = selectorValGetter(srcRowId); const displayCellVal = displayValGetter(srcRowId); @@ -473,30 +472,45 @@ export class LinkingState extends Disposable { } } - //Need to use 'intersects' for ChoiceLists or RefLists + // ==== Determine operation to use for filter ==== + // Common case: use 'in' for single vals, or 'intersects' for ChoiceLists & RefLists let operation = (tgtColId && isListType(tgtCol!.type())) ? 'intersects' : 'in'; - // If selectorVal is a blank-cell value, need to change operation for correct behavior with lists + // # Special case 1: // Blank selector shouldn't mean "show no records", it should mean "show records where tgt column is also blank" - if(srcRowId !== 'new') { //(EXCEPTION: the add-row, which is when we ACTUALLY want to show no records) - - // If tgtCol is a list (RefList or Choicelist) and selectorVal is null/blank, operation must be 'empty' - if (tgtCol?.type() === "ChoiceList" && !isSrcRefList && selectorCellVal === "") { operation = 'empty'; } - else if (isTgtRefList && !isSrcRefList && selectorCellVal === 0) { operation = 'empty'; } - else if (isTgtRefList && isSrcRefList && filterValues.length === 0) { operation = 'empty'; } - // other types can have falsey values when non-blank (e.g. for numbers, 0 is a valid value; blank cell is null) - // However, we don't need to check for those here, since we only care about lists (Reflist or Choicelist) - - // If tgtCol is a single ref, nullness is represented by [0], not by [], so need to create that null explicitly - else if (!isTgtRefList && isSrcRefList && filterValues.length === 0) { - filterValues = [0]; - displayValues = ['']; - } + // This is the default behavior for single-ref -> single-ref links + // However, if tgtCol is a list and the selectorVal is blank/empty, the default behavior ([] intersects tgtlist) + // doesn't work, we need to explicitly specify the operation to be 'empty', to select empty cells + if (tgtCol?.type() === "ChoiceList" && !isSrcRefList && selectorCellVal === "") { operation = 'empty'; } + else if (isTgtRefList && !isSrcRefList && selectorCellVal === 0) { operation = 'empty'; } + else if (isTgtRefList && isSrcRefList && filterValues.length === 0) { operation = 'empty'; } + // Note, we check each case separately since they have different "blank" values" + // Other types can have different falsey values when non-blank (e.g. a Ref=0 is a blank cell, but for numbers, + // 0 would be a valid value, and to check for an empty number-cell you'd check for null) + // However, we don't need to check for those here, since they can't be linked to list types + + // NOTES ON CHOICELISTS: they only show up in a few cases. + // - ChoiceList can only ever appear in links as the tgtcol + // (ChoiceLists can only be linked from summ. tables, and summary flattens lists, so srcCol would be 'Choice') + // - empty Choice is [""]. + + // # Special case 2: + // If tgtCol is a single ref, blankness is represented by [0] + // However if srcCol is a RefList, blankness is represented by [], which won't match the [0]. + // We create the 0 explicitly so the filter will select the blank Refs + else if (!isTgtRefList && isSrcRefList && filterValues.length === 0) { + filterValues = [0]; + displayValues = ['']; + } - // NOTES ON CHOICELISTS: they only show up in a few cases. - // - ChoiceList can only ever appear in links as the tgtcol - // (ChoiceLists can only be linked from summ. tables, and summary flattens lists, so srcCol would be 'Choice') - // - empty choicelist is [""]. + // # Special case 3: + // If the srcSection has no row selected (cursor on the add-row, or no data in srcSection), we should + // show no rows in tgtSection. (we also gray it out and show the "No row selected in $SRCSEC" msg) + // This should line up with when this.disableEditing() returns true + if (srcRowId === 'new' || srcRowId === null) { + operation = 'in'; + filterValues = []; + displayValues = []; } // Run values through formatters (for dates, numerics, Refs with visCol = rowId) diff --git a/test/fixtures/docs/FilterLinkChain.grist b/test/fixtures/docs/FilterLinkChain.grist new file mode 100644 index 0000000000000000000000000000000000000000..03587d14e71bef5d729bfe095b411b1f93ef6b7d GIT binary patch literal 172032 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 Cza&Kf literal 0 HcmV?d00001 diff --git a/test/nbrowser/FilterLinkChain.ts b/test/nbrowser/FilterLinkChain.ts new file mode 100644 index 00000000..0e8ba832 --- /dev/null +++ b/test/nbrowser/FilterLinkChain.ts @@ -0,0 +1,130 @@ +import {assert} from 'mocha-webdriver'; +import * as gu from 'test/nbrowser/gristUtils'; +import {setupTestSuite} from 'test/nbrowser/testUtils'; + +describe('FilterLinkChain', function () { + this.timeout(10000); + const cleanup = setupTestSuite(); + + before(async function () { + const session = await gu.session().teamSite.login(); + await session.tempDoc(cleanup, 'FilterLinkChain.grist'); + }); + + it('should work with chains of filter links', async function () { + async function checkCells(sectionName: string, cols: string[], expected: string[]) { + assert.deepEqual( + await gu.getVisibleGridCells({section: sectionName, cols, rowNums: [1, 2]}), + expected + ); + + // Sanity-check the selectors in checkSectionEmpty() below. + const section = gu.getSection(sectionName); + assert.isEmpty(await section.findAll('.disable_viewpane')); + assert.isNotEmpty(await section.findAll('.gridview_row .gridview_data_row_num')); + assert.isNotEmpty(await section.findAll('.gridview_row .record')); + assert.isNotEmpty(await section.findAll('.gridview_row .field')); + } + + async function checkTopCells(expected: string[]) { + await checkCells('TOP', ['Top'], expected); + } + + async function checkMiddleCells(expected: string[]) { + await checkCells('MIDDLE', ['Top', 'Middle'], expected); + } + + async function checkBottomCells(expected: string[]) { + await checkCells('BOTTOM', ['Top', 'Middle', 'Bottom'], expected); + } + + async function checkSectionEmpty(sectionName: string, text: string) { + const section = gu.getSection(sectionName); + assert.equal(await section.find('.disable_viewpane').getText(), text); + assert.isEmpty(await section.findAll('.gridview_row .gridview_data_row_num')); + assert.isEmpty(await section.findAll('.gridview_row .record')); + assert.isEmpty(await section.findAll('.gridview_row .field')); + } + + async function checkBottomEmpty() { + await checkSectionEmpty('BOTTOM', 'No row selected in MIDDLE'); + } + + async function checkMiddleEmpty() { + await checkSectionEmpty('MIDDLE', 'No row selected in TOP'); + } + + // The initially visible data. + // The bottom section is selected by the middle section, + // which is selected by the top section. + await checkTopCells([ + 'A', // selected initially + 'B', + ]); + // Filtered to 'A' + await checkMiddleCells([ + 'A', 'A1', // selected initially + 'A', 'A2', + ]); + // Filtered to 'A1' + await checkBottomCells([ + 'A', 'A1', '1', + 'A', 'A1', '2', + ]); + + // Select 'A2' + await gu.getCell({section: 'MIDDLE', col: 'Middle', rowNum: 2}).click(); + await checkBottomCells([ + 'A', 'A2', '3', + 'A', 'A2', '4', + ]); + + // Select the 'new' row + await gu.getCell({section: 'MIDDLE', col: 'Middle', rowNum: 3}).click(); + await checkSectionEmpty('BOTTOM', 'No row selected in MIDDLE'); + + // Select 'B' + await gu.getCell({section: 'TOP', col: 'Top', rowNum: 2}).click(); + await checkMiddleCells([ + 'B', 'B1', // selected initially + 'B', 'B2', + ]); + // Filtered to 'B1' + await checkBottomCells([ + 'B', 'B1', '5', + 'B', 'B1', '6', + ]); + + // Select 'B2' + await gu.getCell({section: 'MIDDLE', col: 'Middle', rowNum: 2}).click(); + await checkBottomCells([ + 'B', 'B2', '7', + 'B', 'B2', '8', + ]); + + // Select the 'new' row, making the bottom empty + await gu.getCell({section: 'MIDDLE', col: 'Middle', rowNum: 3}).click(); + await checkBottomEmpty(); + + // Select the 'new' in the top section, which makes middle empty, which means bottom stays empty. + await gu.getCell({section: 'TOP', col: 'Top', rowNum: 3}).click(); + await checkMiddleEmpty(); + await checkBottomEmpty(); + + // Double-check: make all sections show some data again, + // and then make both the middle and bottom empty in one click instead of one at a time. + await gu.getCell({section: 'TOP', col: 'Top', rowNum: 2}).click(); + await checkMiddleCells([ + 'B', 'B1', // selected initially + 'B', 'B2', + ]); + await checkBottomCells([ + 'B', 'B1', '5', + 'B', 'B1', '6', + ]); + + await gu.getCell({section: 'TOP', col: 'Top', rowNum: 3}).click(); + await checkMiddleEmpty(); + await checkBottomEmpty(); + }); +});