(core) Enable Record Cards

Summary:
Adds remaining functionality, fixes, and polish to Record Cards and
removes their feature flag, enabling them by default.

Test Plan: Tests deferred; will be included in a follow-up diff.

Reviewers: jarek, paulfitz

Reviewed By: jarek

Subscribers: paulfitz, jarek

Differential Revision: https://phab.getgrist.com/D4121
pull/774/head
George Gevoian 6 months ago
parent 84329404a4
commit 707a8c7b32

@ -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;
});
}

@ -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', `

@ -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));
});

@ -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);
},
};

@ -209,6 +209,8 @@ export class GristDoc extends DisposableWithEvents {
private _showBackgroundVideoPlayer: Observable<boolean> = Observable.create(this, false);
private _backgroundVideoPlayerHolder: Holder<YouTubePlayer> = 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
*/

@ -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', `

@ -84,7 +84,6 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent {
public viewModel: ViewRec;
public layoutSpec: ko.Computed<BoxSpec>;
public maximized: Observable<number|null>;
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();

@ -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)
)

@ -33,7 +33,3 @@ export function PERMITTED_CUSTOM_WIDGETS(): Observable<string[]> {
}
return G.window.PERMITTED_CUSTOM_WIDGETS;
}
export function RECORD_CARDS() {
return Boolean(getGristConfig().experimentalPlugins);
}

@ -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,

@ -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<TableRec | null>;
protected _refTable: Computed<TableRec | null>;
private _visibleColRef: Computed<number>;
private _validCols: Computed<Array<IOptionFull<number>>>;
@ -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();
}),

@ -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;
`);

@ -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);

@ -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);
}

@ -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.

Loading…
Cancel
Save