From 3fa5125cf7a16f2f75088151dcfd13368dd3efd3 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Wed, 21 Jun 2023 10:43:22 -0400 Subject: [PATCH] (core) Highlight rows used as a selector in linking, but do not show 'inactive' cursors. Summary: 1. Introduces another highlight for link-selector rows, with the same color as regular selection, and allowing to overlap with regular selection. 2. Don't show "secondary" cursors (those in inactive sections), to keep a single cursor on the screen, since having multiple (which different in color) could cause confusion. 3. An unrelated improvement (prompted by a new fixture doc) is to default the active section to the top-left one (rather than the one with smallest rowId). 4. Another unrelated improvement (prompted by a test affected by the previous unrelated improvement) is to skip chart widgets when searching (previously search would step through those with an invisible "cursor"). Includes also tweaks for better testing on Arm-based Macs: - Add support for TEST_CHROME_BINARY_PATH environment variable (helpful for a Mac arm64 architecture workaround) - Remove unsetting of SELENIUM_REMOTE_URL when running headless (unlikely to affect anyone, and can be done outside the script, but interferes with the Mac workaround) Test Plan: Added a new test case that cursor and linking-selector CSS classes are present or absent appropriately. Fixed test affected by the fix to default active section. Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3891 --- app/client/components/BaseView.js | 2 + app/client/components/GridView.js | 2 + app/client/components/viewCommon.css | 21 +++- app/client/models/SearchModel.ts | 8 +- app/client/models/entities/ViewRec.ts | 15 ++- app/client/models/entities/ViewSectionRec.ts | 4 + app/client/ui/RightPanel.ts | 4 +- test/fixtures/docs/Class Enrollment.grist | Bin 0 -> 188416 bytes test/nbrowser/LinkingSelector.ts | 96 +++++++++++++++++++ test/nbrowser/gristUtils.ts | 2 +- test/nbrowser/testUtils.ts | 4 + 11 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 test/fixtures/docs/Class Enrollment.grist create mode 100644 test/nbrowser/LinkingSelector.ts diff --git a/app/client/components/BaseView.js b/app/client/components/BaseView.js index a90e38d7..391eb3ed 100644 --- a/app/client/components/BaseView.js +++ b/app/client/components/BaseView.js @@ -147,6 +147,8 @@ function BaseView(gristDoc, viewSectionModel, options) { } })); + this.isLinkSource = this.autoDispose(ko.pureComputed(() => this.viewSection.linkedSections().all().length > 0)); + // Indicated whether editing the section should be disabled given the current linking state. this.disableEditing = this.autoDispose(ko.computed(() => { const linking = this.viewSection.linkingState(); diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index ec7e906e..0ad54d9c 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -1226,6 +1226,8 @@ GridView.prototype.buildDom = function() { dom.autoDispose(fontUnderline), dom.autoDispose(fontStrikethrough), + kd.toggleClass('link_selector_row', () => self.isLinkSource() && isRowActive()), + // rowid dom dom('div.gridview_data_row_num', kd.style("width", ROW_NUMBER_WIDTH + 'px'), diff --git a/app/client/components/viewCommon.css b/app/client/components/viewCommon.css index f37e5d63..3f1077c9 100644 --- a/app/client/components/viewCommon.css +++ b/app/client/components/viewCommon.css @@ -172,15 +172,14 @@ top: 0px; width: 100%; height: 100%; - /* one pixel outline around the cell, and one inside the cell */ - outline: 1px solid var(--grist-theme-cursor-inactive, var(--grist-color-inactive-cursor)); - box-shadow: inset 0 0 0 1px var(--grist-theme-cursor-inactive, var(--grist-color-inactive-cursor)); pointer-events: none; } .active_cursor { + /* one pixel outline around the cell, and one inside the cell */ outline: 1px solid var(--grist-theme-cursor, var(--grist-color-cursor)); box-shadow: inset 0 0 0 1px var(--grist-theme-cursor, var(--grist-color-cursor)); + z-index: 1; } } @@ -207,6 +206,22 @@ background-color: var(--grist-theme-table-header-selected-bg, var(--grist-color-medium-grey-opaque)); } +.link_selector_row > .gridview_data_row_num { + color: var(--grist-theme-left-panel-active-page-fg, white); + background-color: var(--grist-theme-left-panel-active-page-bg, var(--grist-color-dark-bg)); +} + +.link_selector_row > .record::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background-color: var(--grist-theme-selection, var(--grist-color-selection)); + /* z-index should be higher than '.record .field.frozen' (10) to show for frozen columns, + * but lower than '.gridview_stick-top' (20) to stay under column headers. */ + z-index: 15; +} + .gridview_data_row_info.linked_dst::before { position: absolute; content: '\25B8'; diff --git a/app/client/models/SearchModel.ts b/app/client/models/SearchModel.ts index a5163313..f3525c67 100644 --- a/app/client/models/SearchModel.ts +++ b/app/client/models/SearchModel.ts @@ -314,14 +314,18 @@ class FinderImpl implements IFinder { private _initNewSectionShown() { this._initNewSectionCommon(); const viewInstance = this._sectionStepper.value.viewInstance.peek()!; - this._rowStepper.array = viewInstance.sortedRows.getKoArray().peek() as number[]; + const skip = ['chart'].includes(this._sectionStepper.value.parentKey.peek()); + this._rowStepper.array = skip ? [] : viewInstance.sortedRows.getKoArray().peek() as number[]; } private async _initNewSectionAny() { const tableModel = this._initNewSectionCommon(); const viewInstance = this._sectionStepper.value.viewInstance.peek(); - if (viewInstance) { + const skip = ['chart'].includes(this._sectionStepper.value.parentKey.peek()); + if (skip) { + this._rowStepper.array = []; + } else if (viewInstance) { this._rowStepper.array = viewInstance.sortedRows.getKoArray().peek() as number[]; } else { // If we are searching through another page (not currently loaded), we will NOT have a diff --git a/app/client/models/entities/ViewRec.ts b/app/client/models/entities/ViewRec.ts index b4f48d9c..29f5cff4 100644 --- a/app/client/models/entities/ViewRec.ts +++ b/app/client/models/entities/ViewRec.ts @@ -59,13 +59,15 @@ export function createViewRec(this: ViewRec, docModel: DocModel): void { const collapsed = new Set(this.activeCollapsedSections()); const visible = all.filter(x => !collapsed.has(x.id())); - return visible.length > 0 ? visible[0].getRowId() : 0; + // Default to the first leaf from layoutSpec (which corresponds to the top-left section), or + // fall back to the first item in the list if anything goes wrong (previous behavior). + const firstLeaf = getFirstLeaf(this.layoutSpecObj.peek()); + return visible.find(s => s.getRowId() === firstLeaf) ? firstLeaf as number : + (visible[0]?.getRowId() || 0); }); this.activeSection = refRecord(docModel.viewSections, this.activeSectionId); - - // If the active section is removed, set the next active section to be the default. this._isActiveSectionGone = this.autoDispose(ko.computed(() => this.activeSection()._isDeleted())); this.autoDispose(this._isActiveSectionGone.subscribe(gone => { @@ -74,3 +76,10 @@ export function createViewRec(this: ViewRec, docModel: DocModel): void { } })); } + +function getFirstLeaf(layoutSpec: BoxSpec|undefined): BoxSpec['leaf'] { + while (layoutSpec?.children?.length) { + layoutSpec = layoutSpec.children[0]; + } + return layoutSpec?.leaf; +} diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index 6c0956b4..cfe0bca4 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -35,6 +35,9 @@ import defaults = require('lodash/defaults'); export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleOwner { viewFields: ko.Computed>; + // List of sections linked from this one, i.e. for whom this one is the selector or link source. + linkedSections: ko.Computed>; + // All table columns associated with this view section, excluding hidden helper columns. columns: ko.Computed; @@ -273,6 +276,7 @@ export interface Filter { export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): void { this.viewFields = recordSet(this, docModel.viewFields, 'parentId', {sortBy: 'parentPos'}); + this.linkedSections = recordSet(this, docModel.viewSections, 'linkSrcSectionRef'); // All table columns associated with this view section, excluding any hidden helper columns. this.columns = this.autoDispose(ko.pureComputed(() => this.table().columns().all().filter(c => !c.isHiddenCol()))); diff --git a/app/client/ui/RightPanel.ts b/app/client/ui/RightPanel.ts index 3524ea45..48695f2d 100644 --- a/app/client/ui/RightPanel.ts +++ b/app/client/ui/RightPanel.ts @@ -549,9 +549,7 @@ export class RightPanel extends Disposable { ]), domComputed((use) => { - const activeSectionRef = activeSection.getRowId(); - const allViewSections = use(use(viewModel.viewSections).getObservable()); - const selectorFor = allViewSections.filter((sec) => use(sec.linkSrcSectionRef) === activeSectionRef); + const selectorFor = use(use(activeSection.linkedSections).getObservable()); // TODO: sections should be listed following the order of appearance in the view layout (ie: // left/right - top/bottom); return selectorFor.length ? [ diff --git a/test/fixtures/docs/Class Enrollment.grist b/test/fixtures/docs/Class Enrollment.grist new file mode 100644 index 0000000000000000000000000000000000000000..153bbc45d24de3e04791deb50850ad75350897bd GIT binary patch literal 188416 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 F`F{xbDz5+l literal 0 HcmV?d00001 diff --git a/test/nbrowser/LinkingSelector.ts b/test/nbrowser/LinkingSelector.ts new file mode 100644 index 00000000..a3761f73 --- /dev/null +++ b/test/nbrowser/LinkingSelector.ts @@ -0,0 +1,96 @@ +import {assert, WebElement} from 'mocha-webdriver'; +import * as gu from 'test/nbrowser/gristUtils'; +import {setupTestSuite} from 'test/nbrowser/testUtils'; + +describe('LinkingSelector', function() { + this.timeout(20000); + + const cleanup = setupTestSuite({team: true}); + let session: gu.Session; + + 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`); + }); + + interface CursorSelectorInfo { + linkSelector: false | number; + cursor: false | {rowNum: number, col: number}; + } + + async function getCursorSelectorInfo(section: WebElement): Promise { + 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), + }; + } + + it('should mark selected row used for linking', async function() { + const families = gu.getSection('FAMILIES'); + const students = gu.getSection('STUDENTS'); + const enrollments = gu.getSection('ENROLLMENTS'); + + // Initially FAMILIES first row should be selected and marked as selector. + assert.deepEqual(await getCursorSelectorInfo(families), {linkSelector: 1, cursor: {rowNum: 1, col: 0}}); + assert.deepEqual(await gu.getActiveCell().getText(), 'Fin'); + + // STUDENTS shows appropriate records. + assert.deepEqual(await gu.getVisibleGridCells({section: students, col: 'First_Name', rowNums: [1, 2, 3, 4]}), + ['Brockie', 'Care', 'Alfonso', '']); + + // STUDENTS also has a selector row, but no active cursor. + assert.deepEqual(await getCursorSelectorInfo(students), {linkSelector: 1, cursor: false}); + assert.deepEqual(await getCursorSelectorInfo(enrollments), {linkSelector: false, cursor: false}); + + // Select a different Family + await gu.getCell({section: families, rowNum: 3, col: 'First_Name'}).click(); + assert.deepEqual(await gu.getActiveCell().getText(), 'Pat'); + assert.deepEqual(await getCursorSelectorInfo(families), {linkSelector: 3, cursor: {rowNum: 3, col: 0}}); + + // STUDENTS shows new values, has a new selector row + assert.deepEqual(await gu.getVisibleGridCells({section: students, col: 'First_Name', rowNums: [1, 2, 3]}), + ['Mordy', 'Noam', '']); + assert.deepEqual(await getCursorSelectorInfo(students), {linkSelector: 1, cursor: false}); + + // STUDENTS Card shows appropriate value + assert.deepEqual(await gu.getVisibleDetailCells( + {section: 'STUDENTS Card', cols: ['First_Name', 'Policy_Number'], rowNums: [1]}), + ['Mordy', '468617']); + + // Select another student + await gu.getCell({section: students, rowNum: 2, col: 'Last_Name'}).click(); + assert.deepEqual(await getCursorSelectorInfo(students), {linkSelector: 2, cursor: {rowNum: 2, col: 1}}); + assert.deepEqual(await gu.getVisibleDetailCells( + {section: 'STUDENTS Card', cols: ['First_Name', 'Policy_Number'], rowNums: [1]}), + ['Noam', '663208']); + + // There is no longer a cursor in FAMILIES, but still a link-selector. + assert.deepEqual(await getCursorSelectorInfo(families), {linkSelector: 3, cursor: false}); + + // Enrollments is linked to the selected student, but still shows no cursor or selector. + assert.deepEqual(await getCursorSelectorInfo(enrollments), {linkSelector: false, cursor: false}); + assert.deepEqual(await gu.getVisibleGridCells({section: enrollments, col: 'Class', rowNums: [1, 2, 3]}), + ['2019F-Yoga', '2019S-Yoga', '']); + + // Click into an enrollment; it will become the only section with a cursor. + await gu.getCell({section: enrollments, rowNum: 2, col: 'Status'}).click(); + assert.deepEqual(await getCursorSelectorInfo(enrollments), {linkSelector: false, cursor: {rowNum: 2, col: 2}}); + assert.deepEqual(await getCursorSelectorInfo(students), {linkSelector: 2, cursor: false}); + assert.deepEqual(await getCursorSelectorInfo(families), {linkSelector: 3, cursor: false}); + }); + + it('should show correct state on reload after cursors are positioned', async function() { + await gu.reloadDoc(); + const families = gu.getSection('FAMILIES'); + const students = gu.getSection('STUDENTS'); + const enrollments = gu.getSection('ENROLLMENTS'); + assert.deepEqual(await getCursorSelectorInfo(enrollments), {linkSelector: false, cursor: {rowNum: 2, col: 2}}); + assert.deepEqual(await getCursorSelectorInfo(students), {linkSelector: 2, cursor: false}); + assert.deepEqual(await getCursorSelectorInfo(families), {linkSelector: 3, cursor: false}); + }); +}); diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index ed44cb4e..d8c8423b 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -358,7 +358,7 @@ export async function getVisibleGridCellsFast(colOrOptions: any, rowNums?: numbe * If rowNums are not shown (for single-card view), use rowNum of 1. */ export async function getVisibleDetailCells(col: number|string, rows: number[], section?: string): Promise; -export async function getVisibleDetailCells(options: IColSelect): Promise; +export async function getVisibleDetailCells(options: IColSelect|IColsSelect): Promise; export async function getVisibleDetailCells( colOrOptions: number|string|IColSelect|IColsSelect, _rowNums?: number[], _section?: string ): Promise { diff --git a/test/nbrowser/testUtils.ts b/test/nbrowser/testUtils.ts index 0483d2d3..020f253f 100644 --- a/test/nbrowser/testUtils.ts +++ b/test/nbrowser/testUtils.ts @@ -25,6 +25,10 @@ import {server} from 'test/nbrowser/testServer'; export {server}; setOptionsModifyFunc(({chromeOpts, firefoxOpts}) => { + if (process.env.TEST_CHROME_BINARY_PATH) { + chromeOpts.setChromeBinaryPath(process.env.TEST_CHROME_BINARY_PATH); + } + // Set "kiosk" printing that saves to PDF without offering any dialogs. This applies to regular // (non-headless) Chrome. On headless Chrome, no dialog or output occurs regardless. chromeOpts.addArguments("--kiosk-printing");