From b1f7ca353aa23bc3671745ef7a9f55a4e1b1c265 Mon Sep 17 00:00:00 2001 From: George Gevoian Date: Tue, 30 Jan 2024 09:49:00 -0500 Subject: [PATCH] (core) Polish Record Cards Summary: Improvements - Widget and column descriptions are now copied when duplicating a table. - A Grist Plugin API command to open a Record Card is now available. - New Card widgets set initial settings based on those used by their table's Record Card. Fixes - Opening a reference in a Record Card from a Raw Data popup now opens the correct reference. Test Plan: Browser and python tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4164 --- app/client/components/BaseView.js | 13 ++ app/client/components/CustomView.ts | 11 ++ app/client/components/GridView.js | 19 +-- app/client/components/GristDoc.ts | 166 ++++++++++--------- app/client/components/LayoutEditor.ts | 2 +- app/client/components/LayoutTray.ts | 2 +- app/client/components/RawDataPage.ts | 8 +- app/client/components/RecordCardPopup.ts | 1 + app/client/components/WidgetFrame.ts | 1 + app/client/declarations.d.ts | 2 + app/client/models/entities/ViewSectionRec.ts | 8 +- app/client/ui/ViewLayoutMenu.ts | 8 +- app/client/ui2018/cssVars.ts | 1 + app/client/ui2018/search.ts | 2 +- sandbox/grist/test_useractions.py | 70 +++++++- sandbox/grist/testscript.json | 38 +++-- sandbox/grist/useractions.py | 102 ++++++++++-- test/nbrowser/RawData.ts | 4 +- test/nbrowser/RecordCards.ts | 59 ++++++- 19 files changed, 363 insertions(+), 154 deletions(-) diff --git a/app/client/components/BaseView.js b/app/client/components/BaseView.js index 5539a765..6f8190db 100644 --- a/app/client/components/BaseView.js +++ b/app/client/components/BaseView.js @@ -816,5 +816,18 @@ BaseView.prototype._duplicateRows = async function() { return result; } +BaseView.prototype.viewSelectedRecordAsCard = function() { + if (this.isRecordCardDisabled()) { return; } + + const colRef = this.viewSection.viewFields().at(this.cursor.fieldIndex()).column().id(); + const rowId = this.viewData.getRowId(this.cursor.rowIndex()); + const sectionId = this.viewSection.tableRecordCard().id(); + const anchorUrlState = {hash: {colRef, rowId, sectionId, recordCard: true}}; + urlState().pushUrl(anchorUrlState, {replace: true}).catch(reportError); +} + +BaseView.prototype.isRecordCardDisabled = function() { + return this.viewSection.isTableRecordCardDisabled(); +} module.exports = BaseView; diff --git a/app/client/components/CustomView.ts b/app/client/components/CustomView.ts index befe36ba..7cfe0aa7 100644 --- a/app/client/components/CustomView.ts +++ b/app/client/components/CustomView.ts @@ -70,6 +70,17 @@ export class CustomView extends Disposable { } } }, + async viewAsCard(event: Event) { + if (event instanceof KeyboardEvent) { + // Ignore the keyboard shortcut if pressed; it's disabled at this time for custom widgets. + return; + } + + (this as unknown as BaseView).viewSelectedRecordAsCard(); + + // Move focus back to the app, so that keyboard shortcuts work in the popup. + document.querySelector('textarea.copypaste.mousetrap')?.focus(); + }, }; /** * The HTMLElement embedding the content. diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index 633cbb8a..ab527c19 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -55,8 +55,6 @@ const {NEW_FILTER_JSON} = require('app/client/models/ColumnFilter'); 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 {urlState} = require('app/client/models/gristUrlState'); const t = makeT('GridView'); @@ -374,16 +372,10 @@ GridView.gridCommands = { this.viewSection.rawNumFrozen.setAndSave(action.numFrozen); }, viewAsCard() { - 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: {colRef, rowId, sectionId, recordCard: true}}; - urlState().pushUrl(anchorUrlState, {replace: true}).catch(reportError); + this.viewSelectedRecordAsCard(); }, }; @@ -1924,14 +1916,13 @@ GridView.prototype.rowContextMenu = function() { GridView.prototype._getRowContextMenuOptions = function() { return { ...this._getCellContextMenuOptions(), - disableShowRecordCard: this._isRecordCardDisabled(), + disableShowRecordCard: this.isRecordCardDisabled(), }; }; -GridView.prototype._isRecordCardDisabled = function() { - return this.getSelection().onlyAddRowSelected() || - this.viewSection.isTableRecordCardDisabled() || - this.viewSection.table().summarySourceTable() !== 0; +GridView.prototype.isRecordCardDisabled = function() { + return BaseView.prototype.isRecordCardDisabled.call(this) || + this.getSelection().onlyAddRowSelected(); } GridView.prototype.cellContextMenu = function() { diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index af9f6b7d..8fff29a4 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -206,7 +206,7 @@ export class GristDoc extends DisposableWithEvents { private _showGristTour = getUserOrgPrefObs(this.userOrgPrefs, 'showGristTour'); private _seenDocTours = getUserOrgPrefObs(this.userOrgPrefs, 'seenDocTours'); private _popupSectionOptions: Observable = Observable.create(this, null); - private _activeContent: Computed; + private _activeContent: Computed; private _docTutorialHolder = Holder.create(this); private _isRickRowing: Observable = Observable.create(this, false); private _showBackgroundVideoPlayer: Observable = Observable.create(this, false); @@ -261,7 +261,7 @@ export class GristDoc extends DisposableWithEvents { const viewId = this.docModel.views.tableData.findRow(docPage === 'GristDocTour' ? 'name' : 'id', docPage); return viewId || use(defaultViewId); }); - this._activeContent = Computed.create(this, use => use(this._popupSectionOptions) ?? use(this.activeViewId)); + this._activeContent = Computed.create(this, use => use(this.activeViewId)); this.externalSectionId = Computed.create(this, use => { const externalContent = use(this._popupSectionOptions); return externalContent ? use(externalContent.viewSection.id) : null; @@ -308,7 +308,7 @@ export class GristDoc extends DisposableWithEvents { try { if (state.hash.popup || state.hash.recordCard) { - await this.openPopup(state.hash); + await this._openPopup(state.hash); } else { // Navigate to an anchor if one is present in the url hash. const cursorPos = this._getCursorPosFromHash(state.hash); @@ -343,7 +343,7 @@ export class GristDoc extends DisposableWithEvents { } this.behavioralPromptsManager.showTip(cursor, 'rickRow', { - onDispose: () => this.playRickRollVideo(), + onDispose: () => this._playRickRollVideo(), }); }) .catch(reportError); @@ -602,7 +602,7 @@ export class GristDoc extends DisposableWithEvents { const isPopup = Computed.create(this, use => { return ['data', 'settings'].includes(use(this.activeViewId) as any) // On Raw data or doc settings pages || use(isMaximized) // Layout has a maximized section visible - || typeof use(this._activeContent) === 'object'; // We are on show raw data popup + || Boolean(use(this._popupSectionOptions)); // Layout has a popup section visible }); return cssViewContentPane( testId('gristdoc'), @@ -623,43 +623,48 @@ export class GristDoc extends DisposableWithEvents { content === 'settings' ? dom.create(DocSettingsPage, this) : content === 'webhook' ? dom.create(WebhookPage, this) : content === 'GristDocTour' ? null : - (typeof content === 'object') ? dom.create(owner => { - // In case user changes a page, close the popup. - owner.autoDispose(this.activeViewId.addListener(content.close)); - // In case the section is removed, close the popup. - content.viewSection.autoDispose({dispose: content.close}); - - 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, + [ + dom.create((owner) => { + this.viewLayout = ViewLayout.create(owner, this, content); + 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(); + } }); - } else { - return dom.create(RawDataPopup, this, content.viewSection, content.close); - } - }) : - dom.create((owner) => { - this.viewLayout = ViewLayout.create(owner, this, content); - 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; - }) + owner.onDispose(() => this.viewLayout = null); + return this.viewLayout; + }), + dom.maybe(this._popupSectionOptions, (popupOptions) => { + return dom.create((owner) => { + // In case user changes a page, close the popup. + owner.autoDispose(this.activeViewId.addListener(popupOptions.close)); + + // In case the section is removed, close the popup. + popupOptions.viewSection.autoDispose({dispose: popupOptions.close}); + + const {recordCard, rowId} = popupOptions.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: popupOptions.viewSection, + onClose: popupOptions.close, + }); + } else { + return dom.create(RawDataPopup, this, popupOptions.viewSection, popupOptions.close); + } + }); + }), + ] ); }), dom.maybe(this._showBackgroundVideoPlayer, () => [ @@ -1371,10 +1376,36 @@ export class GristDoc extends DisposableWithEvents { await tableRec.tableName.saveOnly(newTableName); } + /** + * Creates computed with all the data for the given column. + */ + public columnObserver(owner: IDisposableOwner, tableId: Observable, columnId: Observable) { + const tableModel = Computed.create(owner, (use) => this.docModel.dataTables[use(tableId)]); + const refreshed = Observable.create(owner, 0); + const toggle = () => !refreshed.isDisposed() && refreshed.set(refreshed.get() + 1); + const holder = Holder.create(owner); + const listener = (tab: TableModel) => { + // Now subscribe to any data change in that table. + const subs = MultiHolder.create(holder); + subs.autoDispose(tab.tableData.dataLoadedEmitter.addListener(toggle)); + subs.autoDispose(tab.tableData.tableActionEmitter.addListener(toggle)); + tab.fetch().catch(reportError); + }; + owner.autoDispose(tableModel.addListener(listener)); + listener(tableModel.get()); + const values = Computed.create(owner, refreshed, (use) => { + const rows = use(tableModel).getAllRows(); + const colValues = use(tableModel).tableData.getColValues(use(columnId)); + if (!colValues) { return []; } + return rows.map((row, i) => [row, colValues[i]]); + }); + return values; + } + /** * Opens popup with a section data (used by Raw Data view). */ - public async openPopup(hash: HashLink) { + private async _openPopup(hash: HashLink) { // We can only open a popup for a section. if (!hash.sectionId) { return; @@ -1386,13 +1417,17 @@ export class GristDoc extends DisposableWithEvents { if (this.viewModel.viewSections.peek().peek().some(s => s.id.peek() === hash.sectionId)) { this.viewModel.activeSectionId(hash.sectionId); // If the anchor link is valid, set the cursor. - if (hash.colRef && hash.rowId) { + if (hash.colRef || hash.rowId) { const activeSection = this.viewModel.activeSection.peek(); - const fieldIndex = activeSection.viewFields.peek().all().findIndex(f => f.colRef.peek() === hash.colRef); - if (fieldIndex >= 0) { - const view = await this._waitForView(activeSection); - view?.setCursorPos({rowId: hash.rowId, fieldIndex}); + const {rowId} = hash; + let fieldIndex = undefined; + if (hash.colRef) { + const maybeFieldIndex = activeSection.viewFields.peek().all() + .findIndex(f => f.colRef.peek() === hash.colRef); + if (maybeFieldIndex !== -1) { fieldIndex = maybeFieldIndex; } } + const view = await this._waitForView(activeSection); + view?.setCursorPos({rowId, fieldIndex}); } this.viewLayout?.maximized.set(hash.sectionId); return; @@ -1451,7 +1486,7 @@ export class GristDoc extends DisposableWithEvents { /** * Starts playing the music video for Never Gonna Give You Up in the background. */ - public async playRickRollVideo() { + private async _playRickRollVideo() { const backgroundVideoPlayer = this._backgroundVideoPlayerHolder.get(); if (!backgroundVideoPlayer) { return; @@ -1487,32 +1522,6 @@ export class GristDoc extends DisposableWithEvents { this._showBackgroundVideoPlayer.set(false); } - /** - * Creates computed with all the data for the given column. - */ - public columnObserver(owner: IDisposableOwner, tableId: Observable, columnId: Observable) { - const tableModel = Computed.create(owner, (use) => this.docModel.dataTables[use(tableId)]); - const refreshed = Observable.create(owner, 0); - const toggle = () => !refreshed.isDisposed() && refreshed.set(refreshed.get() + 1); - const holder = Holder.create(owner); - const listener = (tab: TableModel) => { - // Now subscribe to any data change in that table. - const subs = MultiHolder.create(holder); - subs.autoDispose(tab.tableData.dataLoadedEmitter.addListener(toggle)); - subs.autoDispose(tab.tableData.tableActionEmitter.addListener(toggle)); - tab.fetch().catch(reportError); - }; - owner.autoDispose(tableModel.addListener(listener)); - listener(tableModel.get()); - const values = Computed.create(owner, refreshed, (use) => { - const rows = use(tableModel).getAllRows(); - const colValues = use(tableModel).tableData.getColValues(use(columnId)); - if (!colValues) { return []; } - return rows.map((row, i) => [row, colValues[i]]); - }); - return values; - } - private _focusPreviousSection() { const prevSectionId = this._prevSectionId; if (!prevSectionId) { return; } @@ -1890,26 +1899,25 @@ async function finalizeAnchor() { } const cssViewContentPane = styled('div', ` - --view-content-page-margin: 12px; + --view-content-page-padding: 12px; flex: auto; display: flex; flex-direction: column; overflow: visible; position: relative; min-width: 240px; - margin: var(--view-content-page-margin, 12px); + padding: var(--view-content-page-padding, 12px); @media ${mediaSmall} { & { - margin: 4px; + padding: 4px; } } @media print { & { - margin: 0px; + padding: 0px; } } &-contents { - margin: 0px; overflow: hidden; } `); diff --git a/app/client/components/LayoutEditor.ts b/app/client/components/LayoutEditor.ts index 0c9eb4b9..52aca944 100644 --- a/app/client/components/LayoutEditor.ts +++ b/app/client/components/LayoutEditor.ts @@ -501,7 +501,7 @@ export class LayoutEditor extends Disposable { handles: isWidth ? 'e' : 's', start: this.onResizeStart.bind(this, helperObj, isWidth), resize: this.onResizeMove.bind(this, helperObj, isWidth), - stop: this.triggerUserEditStop.bind(this) + stop: this.triggerUserEditStop.bind(this), }); } public unmakeResizable(box: LayoutBox) { diff --git a/app/client/components/LayoutTray.ts b/app/client/components/LayoutTray.ts index d0657395..99c92941 100644 --- a/app/client/components/LayoutTray.ts +++ b/app/client/components/LayoutTray.ts @@ -1179,7 +1179,7 @@ const cssCollapsedTray = styled('div.collapsed_layout', ` overflow: hidden; transition: height 0.2s; position: relative; - margin: calc(-1 * var(--view-content-page-margin, 12px)); + margin: calc(-1 * var(--view-content-page-padding, 12px)); margin-bottom: 0; user-select: none; background-color: ${theme.pageBg}; diff --git a/app/client/components/RawDataPage.ts b/app/client/components/RawDataPage.ts index 703ed75b..45a73969 100644 --- a/app/client/components/RawDataPage.ts +++ b/app/client/components/RawDataPage.ts @@ -4,7 +4,7 @@ import {DocumentUsage} from 'app/client/components/DocumentUsage'; import {GristDoc} from 'app/client/components/GristDoc'; import {printViewSection} from 'app/client/components/Printing'; import {ViewSectionHelper} from 'app/client/components/ViewLayout'; -import {mediaSmall, theme} from 'app/client/ui2018/cssVars'; +import {mediaSmall, theme, vars} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; import {Computed, Disposable, dom, fromKo, makeTestId, Observable, styled} from 'grainjs'; import {reportError} from 'app/client/models/errors'; @@ -115,7 +115,8 @@ export class RawDataPopup extends Disposable { const cssContainer = styled('div', ` height: 100%; overflow: hidden; - position: relative; + inset: 0px; + position: absolute; `); const cssPage = styled('div', ` @@ -132,10 +133,9 @@ const cssPage = styled('div', ` export const cssOverlay = styled('div', ` background-color: ${theme.modalBackdrop}; inset: 0px; - height: 100%; - width: 100%; padding: 20px 56px 20px 56px; position: absolute; + z-index: ${vars.popupSectionBackdropZIndex}; @media ${mediaSmall} { & { padding: 22px; diff --git a/app/client/components/RecordCardPopup.ts b/app/client/components/RecordCardPopup.ts index b4abe574..7fc1efac 100644 --- a/app/client/components/RecordCardPopup.ts +++ b/app/client/components/RecordCardPopup.ts @@ -50,6 +50,7 @@ export class RecordCardPopup extends DisposableWithEvents { focusable: false, renamable: false, }), + testId('wrapper'), ), cssCloseButton('CrossBig', dom.on('click', () => this._handleClose()), diff --git a/app/client/components/WidgetFrame.ts b/app/client/components/WidgetFrame.ts index c89dac5d..0730ed6f 100644 --- a/app/client/components/WidgetFrame.ts +++ b/app/client/components/WidgetFrame.ts @@ -544,6 +544,7 @@ export class WidgetAPIImpl implements WidgetAPI { const COMMAND_MINIMUM_ACCESS_LEVELS: Map = new Map([ ['undo', AccessLevel.full], ['redo', AccessLevel.full], + ['viewAsCard', AccessLevel.read_table], ]); export class CommandAPI { diff --git a/app/client/declarations.d.ts b/app/client/declarations.d.ts index d7127317..056869f6 100644 --- a/app/client/declarations.d.ts +++ b/app/client/declarations.d.ts @@ -77,6 +77,8 @@ declare module "app/client/components/BaseView" { public moveEditRowToCursor(): DataRowModel; public scrollToCursor(sync: boolean): Promise; public getAnchorLinkForSection(sectionId: number): IGristUrlState; + public viewSelectedRecordAsCard(): void; + public isRecordCardDisabled(): boolean; } export = BaseView; } diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index 7bee81cc..729f79de 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -99,10 +99,7 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleO /** True if this section is disabled. Currently only used by Record Card sections. */ disabled: modelUtil.KoSaveableObservable; - /** - * True if the Record Card section of this section's table is disabled. Shortcut for - * `this.tableRecordCard().disabled()`. - */ + /** True if the Record Card section of this section's table is disabled. */ isTableRecordCardDisabled: ko.Computed; isVirtual: ko.Computed; @@ -485,7 +482,8 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): this.isRecordCard = this.autoDispose(ko.pureComputed(() => this.table().recordCardViewSectionRef() === this.id())); this.disabled = modelUtil.fieldWithDefault(this.optionsObj.prop('disabled'), false); - this.isTableRecordCardDisabled = ko.pureComputed(() => this.tableRecordCard().disabled()); + this.isTableRecordCardDisabled = this.autoDispose(ko.pureComputed(() => this.tableRecordCard().disabled() || + this.table().summarySourceTable() !== 0)); this.isVirtual = this.autoDispose(ko.pureComputed(() => typeof this.id() === 'string')); diff --git a/app/client/ui/ViewLayoutMenu.ts b/app/client/ui/ViewLayoutMenu.ts index e7714f3f..301b81b1 100644 --- a/app/client/ui/ViewLayoutMenu.ts +++ b/app/client/ui/ViewLayoutMenu.ts @@ -72,11 +72,9 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool dom.maybe(showRawData, () => menuItemLink( { href: rawUrl}, t("Show raw data"), testId('show-raw-data'), - dom.on('click', (ev) => { + dom.on('click', () => { // Replace the current URL so that the back button works as expected (it navigates back from // the current page). - ev.stopImmediatePropagation(); - ev.preventDefault(); urlState().pushUrl(anchorUrlState, { replace: true }).catch(reportError); }) ) @@ -128,11 +126,9 @@ export function makeCollapsedLayoutMenu(viewSection: ViewSectionRec, gristDoc: G dom.maybe((use) => !use(viewSection.isRaw) && !isSinglePage && !use(gristDoc.maximizedSectionId), () => menuItemLink( { href: rawUrl}, t("Show raw data"), testId('show-raw-data'), - dom.on('click', (ev) => { + dom.on('click', () => { // Replace the current URL so that the back button works as expected (it navigates back from // the current page). - ev.stopImmediatePropagation(); - ev.preventDefault(); urlState().pushUrl(anchorUrlState, { replace: true }).catch(reportError); }) ) diff --git a/app/client/ui2018/cssVars.ts b/app/client/ui2018/cssVars.ts index d315f592..fe1e26b2 100644 --- a/app/client/ui2018/cssVars.ts +++ b/app/client/ui2018/cssVars.ts @@ -137,6 +137,7 @@ export const vars = { /* Z indexes */ insertColumnLineZIndex: new CustomProp('insert-column-line-z-index', '20'), + popupSectionBackdropZIndex: new CustomProp('popup-section-backdrop-z-index', '100'), menuZIndex: new CustomProp('menu-z-index', '999'), modalZIndex: new CustomProp('modal-z-index', '999'), onboardingBackdropZIndex: new CustomProp('onboarding-backdrop-z-index', '999'), diff --git a/app/client/ui2018/search.ts b/app/client/ui2018/search.ts index 37a53340..ada9ab12 100644 --- a/app/client/ui2018/search.ts +++ b/app/client/ui2018/search.ts @@ -119,7 +119,7 @@ const cssOptions = styled('div', ` position: absolute; right: 0; top: 48px; - z-index: 1; + z-index: ${vars.menuZIndex}; padding: 2px 4px; overflow: hidden; white-space: nowrap; diff --git a/sandbox/grist/test_useractions.py b/sandbox/grist/test_useractions.py index 7ed2ee5a..2238028a 100644 --- a/sandbox/grist/test_useractions.py +++ b/sandbox/grist/test_useractions.py @@ -1599,6 +1599,14 @@ class TestUserActions(test_engine.EngineTestCase): 'formula': '$A == "Foo"', }]) + # Add a column and widget description. + self.apply_user_action(['UpdateRecord', '_grist_Tables_column', 23, { + 'description': 'A column description.', + }]) + self.apply_user_action(['UpdateRecord', '_grist_Views_section', 2, { + 'description': 'A widget description.', + }]) + # Duplicate Table1 as Foo without including any of its data. self.apply_user_action(['DuplicateTable', 'Table1', 'Foo', False]) @@ -1642,6 +1650,16 @@ class TestUserActions(test_engine.EngineTestCase): ["id", "A", "B", "C", "D", "E", "F", "G", "H", "gristHelper_ConditionalRule", "gristHelper_RowConditionalRule", "manualSort"], ]) + self.assertTableData('_grist_Tables_column', rows='subset', cols='subset', data=[ + ['id', 'description'], + [23, 'A column description.'], + [34, 'A column description.'], + ]) + self.assertTableData('_grist_Views_section', rows='subset', cols='subset', data=[ + ['id', 'description'], + [2, 'A widget description.'], + [4, 'A widget description.'], + ]) # Duplicate Table1 as FooData and include all of its data. self.apply_user_action(['DuplicateTable', 'Table1', 'FooData', True]) @@ -1686,7 +1704,7 @@ class TestUserActions(test_engine.EngineTestCase): duplicated_times = self.engine.fetch_table('FooData').columns['E'] self.assertEqual(existing_times, duplicated_times) - def test_duplicate_table2(self): + def test_duplicate_table_untie_col_id_bug(self): # This test case verifies a bug fix: when a column doesn't match its label despite # untieColIdFromLabel being False (which is possible), ensure that duplicating still works. @@ -1703,3 +1721,53 @@ class TestUserActions(test_engine.EngineTestCase): self.apply_user_action(['DuplicateTable', 'Table1', 'Foo', True]) self.assertTableData('Table1', data=[["id", "State2", 'manualSort'], [1, 'NY', 1.0]]) self.assertTableData('Foo', data=[["id", "State2", 'manualSort'], [1, 'NY', 1.0]]) + + def test_duplicate_table_record_card(self): + self.load_sample(self.sample) + self.apply_user_action(['AddEmptyTable', None]) + self.apply_user_action(['AddColumn', 'Table1', None, { + 'type': 'Ref:Table1', + 'visibleCol': 23, + }]) + self.apply_user_action(['AddColumn', 'Table1', None, { + 'type': 'RefList:Table1', + 'visibleCol': 24, + }]) + self.apply_user_action(['BulkUpdateRecord', '_grist_Views_section_field', [11, 13], { + 'visibleCol': [23, 24], + }]) + self.apply_user_action(['UpdateRecord', '_grist_Views_section', 3, { + 'layoutSpec': '{"children":[{"children":[{"leaf":7},{"leaf":8}]},{"leaf":9},{"leaf":11}]}', + 'options': '{"verticalGridlines":true,"horizontalGridlines":true,"zebraStripes":false,' + + '"customView":"","numFrozen":0,"disabled":true}', + 'theme': 'compact', + }]) + self.apply_user_action(['DuplicateTable', 'Table1', 'Foo', False]) + + self.assertTableData('_grist_Views_section', rows="subset", cols="subset", data=[ + ["id", "parentId", "tableRef", "layoutSpec", "options", "theme"], + # The original record card section. + [3, 0, 2, '{"children":[{"children":[{"leaf":7},{"leaf":8}]},{"leaf":9},{"leaf":11}]}', + '{"verticalGridlines":true,"horizontalGridlines":true,"zebraStripes":false,' + + '"customView":"","numFrozen":0,"disabled":true}', 'compact'], + # The duplicated record card section. + [5, 0, 3, + '{"children": [{"children": [{"leaf": 19}, {"leaf": 20}]}, {"leaf": 21}, ' + + '{"leaf": 22}]}', + '{"verticalGridlines":true,"horizontalGridlines":true,"zebraStripes":false,' + + '"customView":"","numFrozen":0,"disabled":true}', 'compact'], + ]) + self.assertTableData('_grist_Views_section_field', rows="subset", cols="subset", data=[ + ["id", "parentId", "parentPos", "visibleCol"], + # The original record card fields. + [7, 3, 7.0, 0], + [8, 3, 8.0, 0], + [9, 3, 9.0, 0], + [11, 3, 11.0, 23], + [13, 3, 13.0, 24], + [19, 5, 6.5, 0], + [20, 5, 7.5, 0], + [21, 5, 8.5, 0], + [22, 5, 10.5, 29], + [23, 5, 12.5, 30], + ]) diff --git a/sandbox/grist/testscript.json b/sandbox/grist/testscript.json index f911001a..14a34def 100644 --- a/sandbox/grist/testscript.json +++ b/sandbox/grist/testscript.json @@ -921,9 +921,10 @@ // Record card widget ["AddRecord", "_grist_Views_section", 3, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "single", "tableRef": 4, "title": ""}], + ["UpdateRecord", "_grist_Tables", 4, {"recordCardViewSectionRef": 3}], ["AddRecord", "_grist_Views_section_field", 3, {"colRef": 34, "parentId": 3, "parentPos": 3.0}], - ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2, "recordCardViewSectionRef": 3}], + ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}], // Actions generated from AddColumn. ["AddColumn", "Bar", "world", @@ -937,7 +938,7 @@ ], "direct": [true, true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true], + true, true, true, true], "undo": [ ["RemoveTable", "Bar"], ["RemoveRecord", "_grist_Tables", 4], @@ -950,8 +951,9 @@ ["RemoveRecord", "_grist_Views_section", 2], ["RemoveRecord", "_grist_Views_section_field", 2], ["RemoveRecord", "_grist_Views_section", 3], + ["UpdateRecord", "_grist_Tables", 4, {"recordCardViewSectionRef": 0}], ["RemoveRecord", "_grist_Views_section_field", 3], - ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 0, "rawViewSectionRef": 0, "recordCardViewSectionRef": 0}], + ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 0, "rawViewSectionRef": 0}], ["RemoveColumn", "Bar", "world"], ["RemoveRecord", "_grist_Tables_column", 35], ["RemoveRecord", "_grist_Views_section_field", 4], @@ -1267,15 +1269,16 @@ ["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4, "title": ""}], ["BulkAddRecord", "_grist_Views_section_field", [3, 4], {"colRef": [31, 32], "parentId": [2, 2], "parentPos": [3.0, 4.0]}], ["AddRecord", "_grist_Views_section", 3, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "single", "tableRef": 4, "title": ""}], + ["UpdateRecord", "_grist_Tables", 4, {"recordCardViewSectionRef": 3}], ["BulkAddRecord", "_grist_Views_section_field", [5, 6], {"colRef": [31, 32], "parentId": [3, 3], "parentPos": [5.0, 6.0]}], - ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2, "recordCardViewSectionRef": 3}], + ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}], ["BulkRemoveRecord", "_grist_Views_section_field", [1, 3, 5]], ["RemoveRecord", "_grist_Tables_column", 31], ["RemoveColumn", "ViewTest", "hello"] ], "direct": [true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true], + true, true, true, true, true, true, true, true], "undo": [ ["RemoveTable", "ViewTest"], ["RemoveRecord", "_grist_Tables", 4], @@ -1288,8 +1291,9 @@ ["RemoveRecord", "_grist_Views_section", 2], ["BulkRemoveRecord", "_grist_Views_section_field", [3, 4]], ["RemoveRecord", "_grist_Views_section", 3], + ["UpdateRecord", "_grist_Tables", 4, {"recordCardViewSectionRef": 0}], ["BulkRemoveRecord", "_grist_Views_section_field", [5, 6]], - ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 0, "rawViewSectionRef": 0, "recordCardViewSectionRef": 0}], + ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 0, "rawViewSectionRef": 0}], ["BulkAddRecord", "_grist_Views_section_field", [1, 3, 5], {"colRef": [31, 31, 31], "parentId": [1, 2, 3], "parentPos": [1.0, 3.0, 5.0]}], ["AddRecord", "_grist_Tables_column", 31, @@ -2213,7 +2217,8 @@ "parentId": 1, "parentKey": "record", "sortColRefs": "[]", "title": ""}], ["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4, "title": ""}], ["AddRecord", "_grist_Views_section", 3, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "single", "tableRef": 4, "title": ""}], - ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2, "recordCardViewSectionRef": 3}], + ["UpdateRecord", "_grist_Tables", 4, {"recordCardViewSectionRef": 3}], + ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}], ["AddTable", "Bar", [ {"id": "manualSort", "formula": "", "isFormula": false, "type": "ManualSortPos"}, {"isFormula": false, "formula": "", "type": "Text", "id": "hello"}, @@ -2245,15 +2250,16 @@ ["AddRecord", "_grist_Views_section", 5, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 5, "title": ""}], ["BulkAddRecord", "_grist_Views_section_field", [4, 5, 6], {"colRef": [32, 33, 34], "parentId": [5, 5, 5], "parentPos": [4.0, 5.0, 6.0]}], ["AddRecord", "_grist_Views_section", 6, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "single", "tableRef": 5, "title": ""}], + ["UpdateRecord", "_grist_Tables", 5, {"recordCardViewSectionRef": 6}], ["BulkAddRecord", "_grist_Views_section_field", [7, 8, 9], {"colRef": [32, 33, 34], "parentId": [6, 6, 6], "parentPos": [7.0, 8.0, 9.0]}], - ["UpdateRecord", "_grist_Tables", 5, {"primaryViewId": 2, "rawViewSectionRef": 5, "recordCardViewSectionRef": 6}], + ["UpdateRecord", "_grist_Tables", 5, {"primaryViewId": 2, "rawViewSectionRef": 5}], ["AddRecord", "Bar", 1, {"foo": 0, "hello": "a", "manualSort": 1.0}], ["AddRecord", "Bar", 2, {"foo": 1, "hello": "b", "manualSort": 2.0}], ["AddRecord", "Bar", 3, {"foo": 1, "hello": "c", "manualSort": 3.0}], ["BulkUpdateRecord", "Bar", [1, 2, 3], {"world": ["A", "B", "C"]}] ], "direct": [true, true, true, true, true, true, true, true, - true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false], "undo": [ @@ -2266,7 +2272,8 @@ ["RemoveRecord", "_grist_Views_section", 1], ["RemoveRecord", "_grist_Views_section", 2], ["RemoveRecord", "_grist_Views_section", 3], - ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 0, "rawViewSectionRef": 0, "recordCardViewSectionRef": 0}], + ["UpdateRecord", "_grist_Tables", 4, {"recordCardViewSectionRef": 0}], + ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 0, "rawViewSectionRef": 0}], ["RemoveTable", "Bar"], ["RemoveRecord", "_grist_Tables", 5], ["BulkRemoveRecord", "_grist_Tables_column", [31,32,33,34]], @@ -2278,8 +2285,9 @@ ["RemoveRecord", "_grist_Views_section", 5], ["BulkRemoveRecord", "_grist_Views_section_field", [4, 5, 6]], ["RemoveRecord", "_grist_Views_section", 6], + ["UpdateRecord", "_grist_Tables", 5, {"recordCardViewSectionRef": 0}], ["BulkRemoveRecord", "_grist_Views_section_field", [7, 8, 9]], - ["UpdateRecord", "_grist_Tables", 5, {"primaryViewId": 0, "rawViewSectionRef": 0, "recordCardViewSectionRef": 0}], + ["UpdateRecord", "_grist_Tables", 5, {"primaryViewId": 0, "rawViewSectionRef": 0}], ["RemoveRecord", "Bar", 1], ["RemoveRecord", "Bar", 2], ["RemoveRecord", "Bar", 3] @@ -2355,9 +2363,10 @@ // Record card widget ["AddRecord", "_grist_Views_section", 3, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "single", "tableRef": 4, "title": ""}], // As part of adding a table, we also set the primaryViewId. - ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2, "recordCardViewSectionRef": 3}] + ["UpdateRecord", "_grist_Tables", 4, {"recordCardViewSectionRef": 3}], + ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}] ], - "direct": [true, true, true, true, true, true, true, true, true, true], + "direct": [true, true, true, true, true, true, true, true, true, true, true], "undo": [ ["RemoveTable", "Foo"], ["RemoveRecord", "_grist_Tables", 4], @@ -2368,7 +2377,8 @@ ["RemoveRecord", "_grist_Views_section", 1], ["RemoveRecord", "_grist_Views_section", 2], ["RemoveRecord", "_grist_Views_section", 3], - ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 0, "rawViewSectionRef": 0, "recordCardViewSectionRef": 0}] + ["UpdateRecord", "_grist_Tables", 4, {"recordCardViewSectionRef": 0}], + ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 0, "rawViewSectionRef": 0}] ] } }], diff --git a/sandbox/grist/useractions.py b/sandbox/grist/useractions.py index 6aecc88a..3c13e60d 100644 --- a/sandbox/grist/useractions.py +++ b/sandbox/grist/useractions.py @@ -1930,19 +1930,16 @@ class UserActions(object): ) if record_card_section: - record_card_section = self.create_plain_view_section( + record_card_section = self._create_record_card_view_section( result["id"], table_id, - self._docmodel.view_sections, - "single", - "" + self._docmodel.view_sections ) - if primary_view or raw_section or record_card_section: + if primary_view or raw_section: self.UpdateRecord('_grist_Tables', result["id"], { 'primaryViewId': primary_view["id"] if primary_view else 0, 'rawViewSectionRef': raw_section.id if raw_section else 0, - 'recordCardViewSectionRef': record_card_section.id if record_card_section else 0, }) return result @@ -1994,10 +1991,10 @@ class UserActions(object): new_raw_section = new_table.rawViewSectionRef new_record_card_section = new_table.recordCardViewSectionRef - # Copy view section options to the new raw and record card view sections. - self._docmodel.update( - [new_raw_section, new_record_card_section], - options=[raw_section.options, record_card_section.options] + # Copy view section description and options to the new raw view section. + self._docmodel.update([new_raw_section], + description=raw_section.description, + options=raw_section.options, ) old_to_new_col_refs = {} @@ -2034,6 +2031,7 @@ class UserActions(object): recalcWhen=existing_column.recalcWhen, recalcDeps=new_recalc_deps, formula=formula_updates.get(new_column, existing_column.formula), + description=existing_column.description, ) self.maybe_copy_display_formula(existing_column, new_column) @@ -2049,6 +2047,8 @@ class UserActions(object): for rule in existing_column.rules: self.doAddRule(new_table_id, None, new_column.id, rule.formula) + self._copy_record_card_settings(record_card_section, new_record_card_section) + # Copy all row conditional styles to the new table. for rule in raw_section.rules: self.doAddRule(new_table_id, None, None, rule.formula) @@ -2064,6 +2064,55 @@ class UserActions(object): 'raw_section_id': new_raw_section.id, } + def _copy_record_card_settings(self, src_record_card_section, dst_record_card_section): + """ + Helper that copies settings from `src_record_card_section` to `dst_record_card_section`. + """ + old_to_new_col_refs = {} + old_to_new_field_refs = {} + for existing_field, new_field in zip(src_record_card_section.fields, + dst_record_card_section.fields): + old_to_new_col_refs[existing_field.colRef.id] = new_field.colRef + old_to_new_field_refs[existing_field.id] = new_field.id + + for existing_field, new_field in zip(src_record_card_section.fields, + dst_record_card_section.fields): + # Copy field settings to the new fields. + self._docmodel.update( + [new_field], + displayCol=old_to_new_col_refs.get(existing_field.displayCol.id, 0), + parentPos=existing_field.parentPos, + visibleCol=old_to_new_col_refs.get(existing_field.visibleCol.id, 0), + widgetOptions=existing_field.widgetOptions, + ) + + if existing_field.rules: + # Copy all field conditional styles to the new section. + for rule in existing_field.rules: + self.doAddRule(dst_record_card_section.tableRef.tableId, new_field.id, None, rule.formula) + + def patch_layout_spec(layout_spec): + if isinstance(layout_spec, (dict, list)): + for k, v in (layout_spec.items() + if isinstance(layout_spec, dict) + else enumerate(layout_spec)): + if k == 'leaf' and v in old_to_new_field_refs: + layout_spec[k] = old_to_new_field_refs[v] + patch_layout_spec(v) + + try: + new_layout_spec = json.loads(src_record_card_section.layoutSpec) + patch_layout_spec(new_layout_spec) + new_layout_spec = json.dumps(new_layout_spec) + except ValueError: + new_layout_spec = '' + + # Copy options, theme, and layout to the new record card view section. + self._docmodel.update([dst_record_card_section], + options=src_record_card_section.options, + layoutSpec=new_layout_spec, + theme=src_record_card_section.theme, + ) def _fetch_table_col_recs(self, table_ref, col_refs): """Helper that converts col_refs from table table_ref into column Records.""" @@ -2123,6 +2172,15 @@ class UserActions(object): limit=limit) return section + def _create_record_card_view_section(self, tableRef, tableId, view_sections): + section = self._docmodel.add(view_sections, tableRef=tableRef, parentKey='single', + title='', borderWidth=1, defaultWidth=100)[0] + self.UpdateRecord('_grist_Tables', tableRef, { + 'recordCardViewSectionRef': section.id, + }) + self._RebuildViewFields(tableId, section.id) + return section + @useraction def UpdateSummaryViewSection(self, section_ref, groupby_colrefs): """ @@ -2247,14 +2305,22 @@ class UserActions(object): if section_rec.fields: self._docmodel.remove(section_rec.fields) - # Include all table columns that are intended to be visible to the user. - cols = [c for c in table_rec.columns if column.is_visible_column(c.colId) - # TODO: hack to avoid auto-adding the 'group' column when detaching summary tables. - and c.colId != 'group'] - cols.sort(key=lambda c: c.parentPos) - if limit is not None: - cols = cols[:limit] - self._docmodel.add(section_rec.fields, colRef=[c.id for c in cols]) + is_card = section_rec.parentKey in ('single', 'detail') + is_record_card = section_rec == table_rec.recordCardViewSectionRef + if is_card and not is_record_card: + # Copy settings from the table's record card section to the new section. + record_card_section = table_rec.recordCardViewSectionRef + self._docmodel.add(section_rec.fields, colRef=[f.colRef for f in record_card_section.fields]) + self._copy_record_card_settings(record_card_section, section_rec) + else : + # Include all table columns that are intended to be visible to the user. + cols = [c for c in table_rec.columns if column.is_visible_column(c.colId) + # TODO: hack to avoid auto-adding the 'group' column when detaching summary tables. + and c.colId != 'group'] + cols.sort(key=lambda c: c.parentPos) + if limit is not None: + cols = cols[:limit] + self._docmodel.add(section_rec.fields, colRef=[c.id for c in cols]) #---------------------------------------------------------------------- diff --git a/test/nbrowser/RawData.ts b/test/nbrowser/RawData.ts index a7cfc51e..d3bb3258 100644 --- a/test/nbrowser/RawData.ts +++ b/test/nbrowser/RawData.ts @@ -515,8 +515,8 @@ describe('RawData', function () { await gu.checkTextEditor("abc"); await gu.sendKeys(Key.ESCAPE); // Click on another cell, check page hasn't changed (there was a bug about that) - await gu.getCell({rowNum: 10, col: 1}).click(); - assert.deepEqual(await gu.getCursorPosition(), {rowNum: 10, col: 1}); + await gu.getCell({rowNum: 21, col: 1}).click(); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 21, col: 1}); assert.equal(await gu.getCurrentPageName(), 'City'); // Close by hitting escape. diff --git a/test/nbrowser/RecordCards.ts b/test/nbrowser/RecordCards.ts index 102ac82b..9145e1b8 100644 --- a/test/nbrowser/RecordCards.ts +++ b/test/nbrowser/RecordCards.ts @@ -23,7 +23,10 @@ describe('RecordCards', function() { it('opens popup when keyboard shortcut is pressed', async function() { await gu.sendKeys(Key.SPACE); assert.isTrue(await driver.findWait('.test-record-card-popup-overlay', 100).isDisplayed()); - assert.equal(await driver.find('.test-widget-title-text').getText(), 'COUNTRY Card'); + assert.equal( + await driver.find('.test-record-card-popup-wrapper .test-widget-title-text').getText(), + 'COUNTRY Card' + ); assert.equal(await gu.getCardCell('Code').getText(), 'ALB'); assert.isFalse(await driver.find('.grist-single-record__menu').isPresent()); await gu.sendKeys(Key.ESCAPE); @@ -32,7 +35,10 @@ describe('RecordCards', function() { it('opens popup when menu item is clicked', async function() { await (await gu.openRowMenu(2)).findContent('li', /View as card/).click(); assert.isTrue(await driver.findWait('.test-record-card-popup-overlay', 100).isDisplayed()); - assert.equal(await driver.find('.test-widget-title-text').getText(), 'COUNTRY Card'); + assert.equal( + await driver.find('.test-record-card-popup-wrapper .test-widget-title-text').getText(), + 'COUNTRY Card' + ); assert.equal(await gu.getCardCell('Code').getText(), 'AND'); await gu.sendKeys(Key.ESCAPE); }); @@ -76,7 +82,10 @@ describe('RecordCards', function() { it('opens popup when reference icon is clicked', async function() { await gu.getCell(0, 4).find('.test-ref-link-icon').click(); assert.isTrue(await driver.findWait('.test-record-card-popup-overlay', 100).isDisplayed()); - assert.equal(await driver.find('.test-widget-title-text').getText(), 'COUNTRY Card'); + assert.equal( + await driver.find('.test-record-card-popup-wrapper .test-widget-title-text').getText(), + 'COUNTRY Card' + ); assert.equal(await gu.getCardCell('Code').getText(), 'AFG'); assert.isFalse(await driver.find('.grist-single-record__menu').isPresent()); await gu.sendKeys(Key.ESCAPE); @@ -86,10 +95,16 @@ describe('RecordCards', function() { await gu.getCell(0, 4).find('.test-ref-text').click(); await gu.sendKeys(Key.SPACE); assert.isTrue(await driver.findWait('.test-record-card-popup-overlay', 100).isDisplayed()); - assert.equal(await driver.find('.test-widget-title-text').getText(), 'COUNTRYLANGUAGE Card'); + assert.equal( + await driver.find('.test-record-card-popup-wrapper .test-widget-title-text').getText(), + 'COUNTRYLANGUAGE Card' + ); assert.equal(await gu.getCardCell('Country').getText(), 'AFG'); await gu.getCardCell('Country').find('.test-ref-link-icon').click(); - assert.equal(await driver.find('.test-widget-title-text').getText(), 'COUNTRY Card'); + assert.equal( + await driver.find('.test-record-card-popup-wrapper .test-widget-title-text').getText(), + 'COUNTRY Card' + ); assert.equal(await gu.getCardCell('Code').getText(), 'AFG'); await gu.sendKeys(Key.ESCAPE); }); @@ -119,7 +134,10 @@ describe('RecordCards', function() { it('opens popup when reference icon is clicked', async function() { await gu.getCell(0, 4).find('.test-ref-list-link-icon').click(); assert.isTrue(await driver.findWait('.test-record-card-popup-overlay', 100).isDisplayed()); - assert.equal(await driver.find('.test-widget-title-text').getText(), 'COUNTRY Card'); + assert.equal( + await driver.find('.test-record-card-popup-wrapper .test-widget-title-text').getText(), + 'COUNTRY Card' + ); assert.equal(await gu.getCardCell('Code').getText(), 'AFG'); assert.isFalse(await driver.find('.grist-single-record__menu').isPresent()); await gu.sendKeys(Key.ESCAPE); @@ -129,12 +147,37 @@ describe('RecordCards', function() { await gu.getCell(0, 4).click(); await gu.sendKeys(Key.SPACE); assert.isTrue(await driver.findWait('.test-record-card-popup-overlay', 100).isDisplayed()); - assert.equal(await driver.find('.test-widget-title-text').getText(), 'COUNTRYLANGUAGE Card'); + assert.equal( + await driver.find('.test-record-card-popup-wrapper .test-widget-title-text').getText(), + 'COUNTRYLANGUAGE Card' + ); assert.equal(await gu.getCardCell('Country').getText(), 'AFG'); await gu.getCardCell('Country').find('.test-ref-list-link-icon').click(); - assert.equal(await driver.find('.test-widget-title-text').getText(), 'COUNTRY Card'); + assert.equal( + await driver.find('.test-record-card-popup-wrapper .test-widget-title-text').getText(), + 'COUNTRY Card' + ); assert.equal(await gu.getCardCell('Code').getText(), 'AFG'); await gu.sendKeys(Key.ESCAPE); }); }); + + describe('RawData', function() { + before(async function() { + await driver.find('.test-tools-raw').click(); + await driver.findWait('.test-raw-data-list', 2000); + await gu.waitForServer(); + }); + + it('opens popup when reference icon is clicked', async function() { + await driver.findContent('.test-raw-data-table-title', 'City').click(); + await gu.waitForServer(); + await gu.getCell(1, 5).find('.test-ref-link-icon').click(); + assert.equal( + await driver.find('.test-raw-data-overlay .test-widget-title-text').getText(), + 'COUNTRY Card' + ); + assert.equal(await gu.getCardCell('Code').getText(), 'NLD'); + }); + }); });