diff --git a/app/client/components/BaseView.js b/app/client/components/BaseView.js index 585c73f5..8134fcce 100644 --- a/app/client/components/BaseView.js +++ b/app/client/components/BaseView.js @@ -274,7 +274,9 @@ BaseView.prototype.deleteRecords = function(source) { buildConfirmDelete(selectedCell, onSave, rowIds.length <= 1); } else { onSave().then(() => { - reportUndo(this.gristDoc, `You deleted ${rowIds.length} row${rowIds.length > 1 ? 's' : ''}.`); + if (!this.isDisposed()) { + reportUndo(this.gristDoc, `You deleted ${rowIds.length} row${rowIds.length > 1 ? 's' : ''}.`); + } return true; }); } diff --git a/app/client/components/DataTables.ts b/app/client/components/DataTables.ts index c75cca09..46598113 100644 --- a/app/client/components/DataTables.ts +++ b/app/client/components/DataTables.ts @@ -3,7 +3,6 @@ import {GristDoc} from 'app/client/components/GristDoc'; import {copyToClipboard} from 'app/client/lib/clipboardUtils'; import {setTestState} from 'app/client/lib/testState'; import {TableRec} from 'app/client/models/DocModel'; -import {RECORD_CARDS} from 'app/client/models/features'; import {docListHeader, docMenuTrigger} from 'app/client/ui/DocMenuCss'; import {duplicateTable, DuplicateTableResponse} from 'app/client/ui/DuplicateTable'; import {hoverTooltip, showTransientTooltip} from 'app/client/ui/tooltips'; @@ -99,13 +98,14 @@ export class DataTables extends Disposable { hoverTooltip( dom.domComputed(use => use(use(tableRec.recordCardViewSection).disabled) ? t('Record Card Disabled') - : t('Record Card')), + : t('Edit Record Card')), {key: DATA_TABLES_TOOLTIP_KEY, closeOnClick: false} ), - dom.hide(!RECORD_CARDS()), + dom.hide(this._gristDoc.isReadonly), // Make the button invisible to maintain consistent alignment with non-summary tables. dom.style('visibility', u => u(tableRec.summarySourceTable) === 0 ? 'visible' : 'hidden'), cssRecordCardButton.cls('-disabled', use => use(use(tableRec.recordCardViewSection).disabled)), + testId('table-record-card'), ), cssDotsButton( testId('table-menu'), @@ -120,7 +120,8 @@ export class DataTables extends Disposable { throw new Error(`Table ${tableRec.tableId.peek()} doesn't have a raw view section.`); } this._gristDoc.viewModel.activeSectionId(sectionId); - }) + }), + cssTable.cls('-readonly', this._gristDoc.isReadonly), ); }) ), @@ -132,7 +133,8 @@ export class DataTables extends Disposable { return dom.domComputed((use) => { const rawViewSectionRef = use(fromKo(table.rawViewSectionRef)); const isSummaryTable = use(table.summarySourceTable) !== 0; - if (!rawViewSectionRef || isSummaryTable) { + const isReadonly = use(this._gristDoc.isReadonly); + if (!rawViewSectionRef || isSummaryTable || isReadonly) { // Some very old documents might not have a rawViewSection, and raw summary // tables can't currently be renamed. const tableName = [ @@ -185,7 +187,7 @@ export class DataTables extends Disposable { )), testId('menu-remove-table'), ), - dom.maybe(use => RECORD_CARDS() && use(table.summarySourceTable) === 0, () => [ + dom.maybe(use => use(table.summarySourceTable) === 0, () => [ menuDivider(), menuItem( () => this._editRecordCard(table), @@ -308,6 +310,10 @@ const cssTable = styled('div', ` &:hover { border-color: ${css.theme.rawDataTableBorderHover}; } + &-readonly { + /* Row count column is hidden when document is read-only. */ + grid-template-columns: 16px auto 56px; + } `); const cssTableIcon = styled('div', ` diff --git a/app/client/components/DetailView.js b/app/client/components/DetailView.js index 9e7e4f7a..138a6517 100644 --- a/app/client/components/DetailView.js +++ b/app/client/components/DetailView.js @@ -33,6 +33,7 @@ function DetailView(gristDoc, viewSectionModel) { this.viewFields = gristDoc.docModel.viewFields; this._isSingle = (this.viewSection.parentKey.peek() === 'single'); + this._isExternalSectionPopup = gristDoc.externalSectionId.get() === this.viewSection.id(); //-------------------------------------------------- // Create and attach the DOM for the view. @@ -191,7 +192,9 @@ DetailView.prototype.deleteRows = async function(rowIds) { try { await BaseView.prototype.deleteRows.call(this, rowIds); } finally { - this.cursor.rowIndex(index); + if (!this.isDisposed()) { + this.cursor.rowIndex(index); + } } }; @@ -365,7 +368,13 @@ DetailView.prototype.buildTitleControls = function() { // the controls can be confusing in this case. // Note that the controls should still be visible with a filter link. const showControls = ko.computed(() => { - if (!this._isSingle || this.recordLayout.layoutEditor()) { return false; } + if ( + !this._isSingle|| + this.recordLayout.layoutEditor() || + this._isExternalSectionPopup + ) { + return false; + } const linkingState = this.viewSection.linkingState(); return !(linkingState && Boolean(linkingState.cursorPos)); }); diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index e32cf73e..f188ef19 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -56,7 +56,6 @@ const {CombinedStyle} = require("app/client/models/Styles"); const {buildRenameColumn} = require('app/client/ui/ColumnTitle'); const {makeT} = require('app/client/lib/localization'); const {reportError} = require('app/client/models/AppModel'); -const {RECORD_CARDS} = require('app/client/models/features'); const {urlState} = require('app/client/models/gristUrlState'); const t = makeT('GridView'); @@ -375,13 +374,15 @@ GridView.gridCommands = { this.viewSection.rawNumFrozen.setAndSave(action.numFrozen); }, viewAsCard() { - if (!RECORD_CARDS()) { return; } if (this._isRecordCardDisabled()) { return; } const selectedRows = this.selectedRows(); + if (selectedRows.length !== 1) { return; } + + const colRef = this.viewSection.viewFields().at(this.cursor.fieldIndex()).column().id(); const rowId = selectedRows[0]; const sectionId = this.viewSection.tableRecordCard().id(); - const anchorUrlState = {hash: {rowId, sectionId, recordCard: true}}; + const anchorUrlState = {hash: {colRef, rowId, sectionId, recordCard: true}}; urlState().pushUrl(anchorUrlState, {replace: true}).catch(reportError); }, }; diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index f2fc59cc..d652eb35 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -209,6 +209,8 @@ export class GristDoc extends DisposableWithEvents { private _showBackgroundVideoPlayer: Observable = Observable.create(this, false); private _backgroundVideoPlayerHolder: Holder = Holder.create(this); private _disableAutoStartingTours: boolean = false; + private _isShowingPopupSection = false; + private _prevSectionId: number | null = null; constructor( public readonly app: App, @@ -565,6 +567,13 @@ export class GristDoc extends DisposableWithEvents { commands.allCommands.viewTabFocus.run(); } })); + + this.autoDispose(this._popupSectionOptions.addListener((popupOptions) => { + if (!popupOptions) { + this._isShowingPopupSection = false; + this._prevSectionId = null; + } + })); } /** @@ -616,10 +625,16 @@ export class GristDoc extends DisposableWithEvents { // In case the section is removed, close the popup. content.viewSection.autoDispose({dispose: content.close}); - const {recordCard} = content.hash; + const {recordCard, rowId} = content.hash; if (recordCard) { + if (!rowId || rowId === 'new') { + // Should be unreachable, but just to be sure (and to satisfy type checking)... + throw new Error('Unable to open Record Card: undefined row id'); + } + return dom.create(RecordCardPopup, { gristDoc: this, + rowId, viewSection: content.viewSection, onClose: content.close, }); @@ -629,7 +644,15 @@ export class GristDoc extends DisposableWithEvents { }) : dom.create((owner) => { this.viewLayout = ViewLayout.create(owner, this, content); - this.viewLayout.maximized.addListener(n => this.maximizedSectionId.set(n)); + this.viewLayout.maximized.addListener(sectionId => { + this.maximizedSectionId.set(sectionId); + + if (sectionId === null && !this._isShowingPopupSection) { + // If we didn't navigate to another section in the popup, move focus + // back to the previous section. + this._focusPreviousSection(); + } + }); owner.onDispose(() => this.viewLayout = null); return this.viewLayout; }) @@ -1290,11 +1313,11 @@ export class GristDoc extends DisposableWithEvents { if (!hash.sectionId) { return; } + if (!this._prevSectionId) { + this._prevSectionId = this.viewModel.activeSection.peek().id(); + } // We might open popup either for a section in this view or some other section (like Raw Data Page). if (this.viewModel.viewSections.peek().peek().some(s => s.id.peek() === hash.sectionId)) { - if (this.viewLayout) { - this.viewLayout.previousSectionId = this.viewModel.activeSectionId.peek(); - } this.viewModel.activeSectionId(hash.sectionId); // If the anchor link is valid, set the cursor. if (hash.colRef && hash.rowId) { @@ -1308,10 +1331,10 @@ export class GristDoc extends DisposableWithEvents { this.viewLayout?.maximized.set(hash.sectionId); return; } + this._isShowingPopupSection = true; // We will borrow active viewModel and will trick him into believing that // the section from the link is his viewSection and it is active. Fortunately // he doesn't care. After popup is closed, we will restore the original. - const prevSection = this.viewModel.activeSection.peek(); this.viewModel.activeSectionId(hash.sectionId); // Now we have view section we want to show in the popup. const popupSection = this.viewModel.activeSection.peek(); @@ -1329,20 +1352,17 @@ export class GristDoc extends DisposableWithEvents { if (!this._popupSectionOptions.get()) { return; } - if (popupSection !== prevSection) { + if (popupSection.id() !== this._prevSectionId) { // We need to blur the popup section. Otherwise it will automatically be opened // on raw data view. Note: raw data and record card sections don't have parent views; // they use the empty row model as a parent (which feels like a hack). if (!popupSection.isDisposed()) { popupSection.hasFocus(false); } - // We need to restore active viewSection for a view that we borrowed. // When this popup was opened we tricked active view by setting its activeViewSection - // to our viewSection (which might be a completely diffrent section or a raw data section) not - // connected to this view. - if (!prevSection.isDisposed()) { - prevSection.hasFocus(true); - } + // to our viewSection (which might be a completely different section or a raw data section) not + // connected to this view. We need to return focus back to the previous section. + this._focusPreviousSection(); } // Clearing popup section data will close this popup. this._popupSectionOptions.set(null); @@ -1401,6 +1421,19 @@ export class GristDoc extends DisposableWithEvents { this._showBackgroundVideoPlayer.set(false); } + private _focusPreviousSection() { + const prevSectionId = this._prevSectionId; + if (!prevSectionId) { return; } + + if ( + this.viewModel.viewSections.peek().all().some(s => + !s.isDisposed() && s.id.peek() === prevSectionId) + ) { + this.viewModel.activeSectionId(prevSectionId); + } + this._prevSectionId = null; + } + /** * Waits for a view to be ready */ diff --git a/app/client/components/RecordCardPopup.ts b/app/client/components/RecordCardPopup.ts index bdfe9967..b4abe574 100644 --- a/app/client/components/RecordCardPopup.ts +++ b/app/client/components/RecordCardPopup.ts @@ -2,22 +2,27 @@ import {buildViewSectionDom} from 'app/client/components/buildViewSectionDom'; import * as commands from 'app/client/components/commands'; import {GristDoc} from 'app/client/components/GristDoc'; import {cssCloseButton, cssOverlay} from 'app/client/components/RawDataPage'; -import {ViewSectionRec} from 'app/client/models/DocModel'; import {ViewSectionHelper} from 'app/client/components/ViewLayout'; +import {ViewSectionRec} from 'app/client/models/DocModel'; +import {ChangeType, RowList} from 'app/client/models/rowset'; import {theme} from 'app/client/ui2018/cssVars'; -import {Disposable, dom, makeTestId, styled} from 'grainjs'; +import {DisposableWithEvents} from 'app/common/DisposableWithEvents'; +import {dom, makeTestId, styled} from 'grainjs'; const testId = makeTestId('test-record-card-popup-'); interface RecordCardPopupOptions { gristDoc: GristDoc; + rowId: number; viewSection: ViewSectionRec; onClose(): void; } -export class RecordCardPopup extends Disposable { +export class RecordCardPopup extends DisposableWithEvents { private _gristDoc = this._options.gristDoc; + private _rowId = this._options.rowId; private _viewSection = this._options.viewSection; + private _tableModel = this._gristDoc.getTableModel(this._viewSection.table().tableId()); private _handleClose = this._options.onClose; constructor(private _options: RecordCardPopupOptions) { @@ -26,6 +31,11 @@ export class RecordCardPopup extends Disposable { cancel: () => { this._handleClose(); }, }; this.autoDispose(commands.createGroup(commandGroup, this, true)); + + // Close the popup if the underlying row is removed. + const onRowChange = this._onRowChange.bind(this); + this._tableModel.on('rowChange', onRowChange); + this.onDispose(() => this._tableModel.off('rowChange', onRowChange)); } public buildDom() { @@ -39,7 +49,6 @@ export class RecordCardPopup extends Disposable { draggable: false, focusable: false, renamable: false, - hideTitleControls: true, }), ), cssCloseButton('CrossBig', @@ -49,6 +58,12 @@ export class RecordCardPopup extends Disposable { dom.on('click', (ev, elem) => void (ev.target === elem ? this._handleClose() : null)), ); } + + private _onRowChange(type: ChangeType, rows: RowList) { + if (type === 'remove' && [...rows].includes(this._rowId)) { + this._handleClose(); + } + } } const cssSectionWrapper = styled('div', ` diff --git a/app/client/components/ViewLayout.ts b/app/client/components/ViewLayout.ts index b1ed218e..244efd24 100644 --- a/app/client/components/ViewLayout.ts +++ b/app/client/components/ViewLayout.ts @@ -84,7 +84,6 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent { public viewModel: ViewRec; public layoutSpec: ko.Computed; public maximized: Observable; - public previousSectionId = 0; // Used to restore focus after a maximized section is closed. public isResizing = Observable.create(this, false); public layout: Layout; public layoutEditor: LayoutEditor; @@ -203,16 +202,6 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent { // If we are closing popup, resize all sections. if (!sectionId) { this._onResize(); - // Reset active section to the first one if the section is popup is collapsed. - if (prev - && this.viewModel.activeCollapsedSections.peek().includes(prev) - && this.previousSectionId) { - // Make sure that previous section exists still. - if (this.viewModel.viewSections.peek().all() - .some(s => !s.isDisposed() && s.id.peek() === this.previousSectionId)) { - this.viewModel.activeSectionId(this.previousSectionId); - } - } } else { // Otherwise resize only active one (the one in popup). const section = this.viewModel.activeSection.peek(); diff --git a/app/client/components/buildViewSectionDom.ts b/app/client/components/buildViewSectionDom.ts index fc45cf7d..5e92d193 100644 --- a/app/client/components/buildViewSectionDom.ts +++ b/app/client/components/buildViewSectionDom.ts @@ -78,7 +78,6 @@ export function buildViewSectionDom(options: { tableNameHidden, widgetNameHidden, renamable = true, - hideTitleControls = false, } = options; // Creating normal section dom @@ -110,7 +109,7 @@ export function buildViewSectionDom(options: { testId('viewsection-title'), cssTestClick(testId("viewsection-blank")), ), - hideTitleControls ? null : viewInstance.buildTitleControls(), + viewInstance.buildTitleControls(), dom('div.viewsection_buttons', dom.create(viewSectionMenu, gristDoc, vs) ) diff --git a/app/client/models/features.ts b/app/client/models/features.ts index a1bcc03b..353fe0a3 100644 --- a/app/client/models/features.ts +++ b/app/client/models/features.ts @@ -33,7 +33,3 @@ export function PERMITTED_CUSTOM_WIDGETS(): Observable { } return G.window.PERMITTED_CUSTOM_WIDGETS; } - -export function RECORD_CARDS() { - return Boolean(getGristConfig().experimentalPlugins); -} diff --git a/app/client/ui/RowContextMenu.ts b/app/client/ui/RowContextMenu.ts index 87b75e17..365387ce 100644 --- a/app/client/ui/RowContextMenu.ts +++ b/app/client/ui/RowContextMenu.ts @@ -1,6 +1,5 @@ import { allCommands } from 'app/client/components/commands'; import { makeT } from 'app/client/lib/localization'; -import { RECORD_CARDS } from 'app/client/models/features'; import { menuDivider, menuIcon, menuItemCmd, menuItemCmdLabel } from 'app/client/ui2018/menus'; import { dom } from 'grainjs'; @@ -22,7 +21,7 @@ export function RowContextMenu({ numRows }: IRowContextMenu) { const result: Element[] = []; - if (RECORD_CARDS() && numRows === 1) { + if (numRows === 1) { result.push( menuItemCmd( allCommands.viewAsCard, diff --git a/app/client/widgets/Reference.ts b/app/client/widgets/Reference.ts index 296b39a7..f1d94446 100644 --- a/app/client/widgets/Reference.ts +++ b/app/client/widgets/Reference.ts @@ -2,7 +2,6 @@ import {makeT} from 'app/client/lib/localization'; import {DataRowModel} from 'app/client/models/DataRowModel'; import {TableRec} from 'app/client/models/DocModel'; import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; -import {RECORD_CARDS} from 'app/client/models/features'; import {urlState} from 'app/client/models/gristUrlState'; import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles'; import {hideInPrintView, testId, theme} from 'app/client/ui2018/cssVars'; @@ -20,7 +19,7 @@ const t = makeT('Reference'); * Reference - The widget for displaying references to another table's records. */ export class Reference extends NTextBox { - private _refTable: Computed; + protected _refTable: Computed; private _visibleColRef: Computed; private _validCols: Computed>>; @@ -120,23 +119,20 @@ export class Reference extends NTextBox { dom.cls('text_wrapping', this.wrapping), cssRefIcon('FieldReference', cssRefIcon.cls('-view-as-card', use => - RECORD_CARDS() && use(referenceId) !== 0 && use(formattedValue).hasRecordCard), + use(referenceId) !== 0 && use(formattedValue).hasRecordCard), dom.on('click', async () => { - if (!RECORD_CARDS()) { return; } if (referenceId.get() === 0 || !formattedValue.get().hasRecordCard) { return; } const rowId = referenceId.get() as UIRowId; const sectionId = this._refTable.get()?.recordCardViewSectionRef(); if (sectionId === undefined) { - throw new Error('Unable to find Record Card section'); + throw new Error('Unable to open Record Card: undefined section id'); } const anchorUrlState = {hash: {rowId, sectionId, recordCard: true}}; await urlState().pushUrl(anchorUrlState, {replace: true}); }), dom.on('mousedown', (ev) => { - if (!RECORD_CARDS()) { return; } - ev.stopPropagation(); ev.preventDefault(); }), diff --git a/app/client/widgets/ReferenceList.ts b/app/client/widgets/ReferenceList.ts index 90f84456..e4a74b81 100644 --- a/app/client/widgets/ReferenceList.ts +++ b/app/client/widgets/ReferenceList.ts @@ -1,7 +1,9 @@ import {DataRowModel} from 'app/client/models/DataRowModel'; -import {testId} from 'app/client/ui2018/cssVars'; +import {urlState} from 'app/client/models/gristUrlState'; +import {testId, theme} from 'app/client/ui2018/cssVars'; +import {icon} from 'app/client/ui2018/icons'; import {isList} from 'app/common/gristTypes'; -import {dom} from 'grainjs'; +import {Computed, dom, styled} from 'grainjs'; import {cssChoiceList, cssToken} from "app/client/widgets/ChoiceListCell"; import {Reference} from "app/client/widgets/Reference"; import {choiceToken} from "app/client/widgets/ChoiceToken"; @@ -10,24 +12,33 @@ import {choiceToken} from "app/client/widgets/ChoiceToken"; * ReferenceList - The widget for displaying lists of references to another table's records. */ export class ReferenceList extends Reference { + private _hasRecordCard = Computed.create(this, (use) => { + const table = use(this._refTable); + if (!table) { return false; } + + return !use(use(table.recordCardViewSection).disabled); + }); + public buildDom(row: DataRowModel) { return cssChoiceList( dom.cls('field_clip'), cssChoiceList.cls('-wrap', this.wrapping), dom.style('justify-content', use => use(this.alignment) === 'right' ? 'flex-end' : use(this.alignment)), dom.domComputed((use) => { - if (use(row._isAddRow) || this.isDisposed() || use(this.field.displayColModel).isDisposed()) { // Work around JS errors during certain changes (noticed when visibleCol field gets removed // for a column using per-field settings). return null; } - const value = row.cells[use(use(this.field.displayColModel).colId)]; - if (!value) { - return null; - } - const content = use(value); - if (!content) { return null; } + + const valueObs = row.cells[use(this.field.colId)]; + const value = valueObs && use(valueObs); + if (!value) { return null; } + + const displayValueObs = row.cells[use(use(this.field.displayColModel).colId)]; + const displayValue = displayValueObs && use(displayValueObs); + if (!displayValue) { return null; } + // TODO: Figure out what the implications of this block are for ReferenceList. // if (isVersions(content)) { // // We can arrive here if the reference value is unchanged (viewed as a foreign key) @@ -36,20 +47,51 @@ export class ReferenceList extends Reference { // // just showing one version of the cell. TODO: elaborate. // return use(this._formatValue)(content[1].local || content[1].parent); // } - const items = isList(content) ? content.slice(1) : [content]; + const values = isList(value) ? value.slice(1) : [value]; + const displayValues = isList(displayValue) ? displayValue.slice(1) : [displayValue]; // Use field.visibleColFormatter instead of field.formatter // because we're formatting each list element to render tokens, not the whole list. const formatter = use(this.field.visibleColFormatter); - return items.map(item => formatter.formatAny(item)); + return values.map((referenceId, i) => { + return { + referenceId, + formattedValue: formatter.formatAny(displayValues[i]), + }; + }); }, - (input) => { - if (!input) { + (values) => { + if (!values) { return null; } - return input.map(token => { - const isBlankReference = token.trim() === ''; + return values.map(({referenceId, formattedValue}) => { + const isBlankReference = formattedValue.trim() === ''; return choiceToken( - isBlankReference ? '[Blank]' : token, + [ + cssRefIcon('FieldReference', + cssRefIcon.cls('-view-as-card', use => + referenceId !== 0 && use(this._hasRecordCard)), + dom.on('click', async () => { + if (referenceId === 0 || !this._hasRecordCard.get()) { return; } + + const rowId = referenceId as number; + const sectionId = this._refTable.get()?.recordCardViewSectionRef(); + if (sectionId === undefined) { + throw new Error('Unable to open Record Card: undefined section id'); + } + + const anchorUrlState = {hash: {rowId, sectionId, recordCard: true}}; + await urlState().pushUrl(anchorUrlState, {replace: true}); + }), + dom.on('mousedown', (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + }), + ), + cssLabel(isBlankReference ? '[Blank]' : formattedValue, + testId('ref-list-cell-token-label'), + ), + dom.cls(cssRefIconAndLabel.className), + ], { blank: isBlankReference, }, @@ -61,3 +103,26 @@ export class ReferenceList extends Reference { ); } } + +const cssRefIcon = styled(icon, ` + --icon-color: ${theme.lightText}; + flex-shrink: 0; + + &-view-as-card { + cursor: pointer; + } + &-view-as-card:hover { + --icon-color: ${theme.controlFg}; + } +`); + +const cssRefIconAndLabel = styled('div', ` + display: flex; + align-items: center; +`); + +const cssLabel = styled('div', ` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`); diff --git a/test/nbrowser/ReferenceList.ts b/test/nbrowser/ReferenceList.ts index 6abffb01..c3342d03 100644 --- a/test/nbrowser/ReferenceList.ts +++ b/test/nbrowser/ReferenceList.ts @@ -5,7 +5,6 @@ import {Session} from 'test/nbrowser/gristUtils'; describe('ReferenceList', function() { this.timeout(60000); - setupTestSuite(); let session: Session; const cleanup = setupTestSuite({team: true}); @@ -79,6 +78,7 @@ describe('ReferenceList', function() { await gu.sendKeys(Key.ARROW_DOWN, Key.ENTER, 'The Avengers', Key.ENTER, Key.ENTER); // Check that the cells are rendered correctly. + await gu.resizeColumn({col: 'Favorite Film'}, 100); assert.deepEqual(await gu.getVisibleGridCells('Favorite Film', [1, 2, 3, 4, 5, 6]), [ 'Forrest Gump\nAlien', @@ -273,6 +273,7 @@ describe('ReferenceList', function() { await driver.find('.test-fbuilder-ref-col-select').click(); await driver.findContent('.test-select-row', /Name/).click(); await gu.waitForServer(); + await gu.resizeColumn({col: 'A'}, 100); assert.deepEqual(await gu.getVisibleGridCells(3, [1, 2, 3, 4, 5, 6]), ['Roger\nTom', 'Tom', 'Sydney\nBill\nEvan', '', '', '']); @@ -311,7 +312,7 @@ describe('ReferenceList', function() { const cell = gu.getCell({col: 'A', rowNum: 1}); await server.pauseUntil(async () => { assert.equal(await cell.getText(), 'Friends[1]\nFriends[2]'); - await cell.click(); + await gu.clickReferenceListCell(cell); await gu.sendKeys('5'); // Check that the autocomplete has no items yet. assert.isEmpty(await driver.findAll('.test-autocomplete .test-ref-editor-new-item')); @@ -324,7 +325,7 @@ describe('ReferenceList', function() { assert.equal(await cell.getText(), 'Friends[1]\nFriends[2]'); // Once server is responsive, a valid value should not offer a "new item". - await cell.click(); + await gu.clickReferenceListCell(cell); await gu.sendKeys('5'); await driver.findWait('.test-ref-editor-item', 500); assert.isFalse(await driver.find('.test-ref-editor-new-item').isPresent()); @@ -750,7 +751,8 @@ describe('ReferenceList', function() { }); it('should update choices as user types into textbox', async function() { - let cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 1}).doClick(); + let cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 1}); + await gu.clickReferenceListCell(cell); assert.equal(await cell.getText(), 'TECHNOLOGY, ARTS AND SCIENCES STUDIO'); await driver.sendKeys('TECHNOLOGY, ARTS AND SCIENCES STUDIO'); assert.deepEqual(await getACOptions(3), [ @@ -759,7 +761,8 @@ describe('ReferenceList', function() { 'SCHOOL OF SCIENCE AND TECHNOLOGY', ]); await driver.sendKeys(Key.ESCAPE); - cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 2}).doClick(); + cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 2}); + await gu.clickReferenceListCell(cell); await driver.sendKeys('stuy'); assert.deepEqual(await getACOptions(3), [ 'STUYVESANT HIGH SCHOOL', @@ -790,7 +793,8 @@ describe('ReferenceList', function() { it('should highlight matching parts of items', async function() { await driver.sendKeys(Key.HOME); - let cell = await gu.getCell({section: 'References', col: 'Colors', rowNum: 2}).doClick(); + let cell = await gu.getCell({section: 'References', col: 'Colors', rowNum: 2}); + await gu.clickReferenceListCell(cell); assert.equal(await cell.getText(), 'Red'); await driver.sendKeys(Key.ENTER, 'Red'); await driver.findWait('.test-ref-editor-item', 1000); @@ -802,7 +806,8 @@ describe('ReferenceList', function() { ['Re']); await driver.sendKeys(Key.ESCAPE); - cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 1}).doClick(); + cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 1}); + await gu.clickReferenceListCell(cell); await driver.sendKeys('br tech'); assert.deepEqual( await driver.findContentWait('.test-ref-editor-item', /BROOKLYN TECH/, 1000).findAll('span', e => e.getText()), @@ -819,19 +824,20 @@ describe('ReferenceList', function() { it('should reflect changes to the target column', async function() { await driver.sendKeys(Key.HOME); - const cell = await gu.getCell({section: 'References', col: 'Colors', rowNum: 4}).doClick(); + const cell = await gu.getCell({section: 'References', col: 'Colors', rowNum: 4}); + await gu.clickReferenceListCell(cell); assert.equal(await cell.getText(), ''); await driver.sendKeys(Key.ENTER); assert.deepEqual(await getACOptions(2), ['Alice Blue', 'Añil']); await driver.sendKeys(Key.ESCAPE); // Change a color - await gu.getCell({section: 'Colors', col: 'Color Name', rowNum: 1}).doClick(); - await driver.sendKeys('HAZELNUT', Key.ENTER); + await gu.clickReferenceListCell(await gu.getCell({section: 'Colors', col: 'Color Name', rowNum: 1})); + await driver.sendKeys('HAZELNUT', Key.ENTER, Key.ENTER); await gu.waitForServer(); // See that the old value is gone from the autocomplete, and the new one is present. - await cell.click(); + await gu.clickReferenceListCell(cell); await driver.sendKeys(Key.ENTER); assert.deepEqual(await getACOptions(2), ['Añil', 'Aqua']); await driver.sendKeys('H'); @@ -839,11 +845,11 @@ describe('ReferenceList', function() { await driver.sendKeys(Key.ESCAPE); // Delete a row. - await gu.getCell({section: 'Colors', col: 'Color Name', rowNum: 1}).doClick(); + await gu.clickReferenceListCell(await gu.getCell({section: 'Colors', col: 'Color Name', rowNum: 1})); await gu.removeRow(1); // See that the value is gone from the autocomplete. - await cell.click(); + await gu.clickReferenceListCell(cell); await driver.sendKeys('H'); assert.deepEqual(await getACOptions(2), ['Honeydew', 'Hot Pink']); await driver.sendKeys(Key.ESCAPE); @@ -856,7 +862,7 @@ describe('ReferenceList', function() { await gu.waitForServer(); // See that the new value is visible in the autocomplete. - await cell.click(); + await gu.clickReferenceListCell(cell); await driver.sendKeys('H'); assert.deepEqual(await getACOptions(2), ['HELIOTROPE', 'Honeydew']); await driver.sendKeys(Key.BACK_SPACE); @@ -866,7 +872,7 @@ describe('ReferenceList', function() { // Undo all the changes. await gu.undo(4); - await cell.click(); + await gu.clickReferenceListCell(cell); await driver.sendKeys('H'); assert.deepEqual(await getACOptions(2), ['Honeydew', 'Hot Pink']); await driver.sendKeys(Key.BACK_SPACE); diff --git a/test/nbrowser/SelectByRefList.ts b/test/nbrowser/SelectByRefList.ts index 7d6c384d..a80a8610 100644 --- a/test/nbrowser/SelectByRefList.ts +++ b/test/nbrowser/SelectByRefList.ts @@ -183,7 +183,12 @@ async function checkSelectingRecords(selectBy: string, sourceData: string[][], n await gu.waitForServer(); const selectByTable = selectBy.split(' ')[0]; - await gu.getCell({section: selectByTable, col: 0, rowNum: 3}).click(); + const cell = await gu.getCell({section: selectByTable, col: 0, rowNum: 3}); + if (selectByTable === 'REFLISTS') { + await gu.clickReferenceListCell(cell); + } else { + await cell.click(); + } let numSourceRows = 0; @@ -207,7 +212,12 @@ async function checkSelectingRecords(selectBy: string, sourceData: string[][], n } for (let i = 0; i < sourceData.length; i++) { - await gu.getCell({section: selectByTable, col: 0, rowNum: i + 1}).click(); + const cell = await gu.getCell({section: selectByTable, col: 0, rowNum: i + 1}); + if (selectByTable === 'REFLISTS') { + await gu.clickReferenceListCell(cell); + } else { + await cell.click(); + } await checkSourceGroup(i); } diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index cfc574a3..1e114b22 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -572,6 +572,19 @@ export async function rightClick(cell: WebElement) { await driver.withActions((actions) => actions.contextClick(cell)); } +/** + * Clicks a Reference List cell, taking care not to click the icon (which can + * cause an unexpected Record Card popup to appear). + */ +export async function clickReferenceListCell(cell: WebElement) { + const tokens = await cell.findAll('.test-ref-list-cell-token-label'); + if (tokens.length > 0) { + await tokens[0].click(); + } else { + await cell.click(); + } +} + /** * Gets the selector position in the Grid view section (or null if not present). * Selector is the black box around the row number.