mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(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
This commit is contained in:
		
							parent
							
								
									11afc08f65
								
							
						
					
					
						commit
						b1f7ca353a
					
				| @ -816,5 +816,18 @@ BaseView.prototype._duplicateRows = async function() { | |||||||
|   return result; |   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; | module.exports = BaseView; | ||||||
|  | |||||||
| @ -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<HTMLElement>('textarea.copypaste.mousetrap')?.focus(); | ||||||
|  |     }, | ||||||
|   }; |   }; | ||||||
|   /** |   /** | ||||||
|    * The HTMLElement embedding the content. |    * The HTMLElement embedding the content. | ||||||
|  | |||||||
| @ -55,8 +55,6 @@ const {NEW_FILTER_JSON} = require('app/client/models/ColumnFilter'); | |||||||
| const {CombinedStyle} = require("app/client/models/Styles"); | const {CombinedStyle} = require("app/client/models/Styles"); | ||||||
| const {buildRenameColumn} = require('app/client/ui/ColumnTitle'); | const {buildRenameColumn} = require('app/client/ui/ColumnTitle'); | ||||||
| const {makeT} = require('app/client/lib/localization'); | 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'); | const t = makeT('GridView'); | ||||||
| 
 | 
 | ||||||
| @ -374,16 +372,10 @@ GridView.gridCommands = { | |||||||
|     this.viewSection.rawNumFrozen.setAndSave(action.numFrozen); |     this.viewSection.rawNumFrozen.setAndSave(action.numFrozen); | ||||||
|   }, |   }, | ||||||
|   viewAsCard() { |   viewAsCard() { | ||||||
|     if (this._isRecordCardDisabled()) { return; } |  | ||||||
| 
 |  | ||||||
|     const selectedRows = this.selectedRows(); |     const selectedRows = this.selectedRows(); | ||||||
|     if (selectedRows.length !== 1) { return; } |     if (selectedRows.length !== 1) { return; } | ||||||
| 
 | 
 | ||||||
|     const colRef = this.viewSection.viewFields().at(this.cursor.fieldIndex()).column().id(); |     this.viewSelectedRecordAsCard(); | ||||||
|     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); |  | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -1924,14 +1916,13 @@ GridView.prototype.rowContextMenu = function() { | |||||||
| GridView.prototype._getRowContextMenuOptions = function() { | GridView.prototype._getRowContextMenuOptions = function() { | ||||||
|   return { |   return { | ||||||
|     ...this._getCellContextMenuOptions(), |     ...this._getCellContextMenuOptions(), | ||||||
|     disableShowRecordCard: this._isRecordCardDisabled(), |     disableShowRecordCard: this.isRecordCardDisabled(), | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| GridView.prototype._isRecordCardDisabled = function() { | GridView.prototype.isRecordCardDisabled = function() { | ||||||
|   return this.getSelection().onlyAddRowSelected() || |   return BaseView.prototype.isRecordCardDisabled.call(this) || | ||||||
|     this.viewSection.isTableRecordCardDisabled() || |     this.getSelection().onlyAddRowSelected(); | ||||||
|     this.viewSection.table().summarySourceTable() !== 0; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| GridView.prototype.cellContextMenu = function() { | GridView.prototype.cellContextMenu = function() { | ||||||
|  | |||||||
| @ -206,7 +206,7 @@ export class GristDoc extends DisposableWithEvents { | |||||||
|   private _showGristTour = getUserOrgPrefObs(this.userOrgPrefs, 'showGristTour'); |   private _showGristTour = getUserOrgPrefObs(this.userOrgPrefs, 'showGristTour'); | ||||||
|   private _seenDocTours = getUserOrgPrefObs(this.userOrgPrefs, 'seenDocTours'); |   private _seenDocTours = getUserOrgPrefObs(this.userOrgPrefs, 'seenDocTours'); | ||||||
|   private _popupSectionOptions: Observable<PopupSectionOptions | null> = Observable.create(this, null); |   private _popupSectionOptions: Observable<PopupSectionOptions | null> = Observable.create(this, null); | ||||||
|   private _activeContent: Computed<IDocPage | PopupSectionOptions>; |   private _activeContent: Computed<IDocPage>; | ||||||
|   private _docTutorialHolder = Holder.create<DocTutorial>(this); |   private _docTutorialHolder = Holder.create<DocTutorial>(this); | ||||||
|   private _isRickRowing: Observable<boolean> = Observable.create(this, false); |   private _isRickRowing: Observable<boolean> = Observable.create(this, false); | ||||||
|   private _showBackgroundVideoPlayer: Observable<boolean> = Observable.create(this, false); |   private _showBackgroundVideoPlayer: Observable<boolean> = 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); |       const viewId = this.docModel.views.tableData.findRow(docPage === 'GristDocTour' ? 'name' : 'id', docPage); | ||||||
|       return viewId || use(defaultViewId); |       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 => { |     this.externalSectionId = Computed.create(this, use => { | ||||||
|       const externalContent = use(this._popupSectionOptions); |       const externalContent = use(this._popupSectionOptions); | ||||||
|       return externalContent ? use(externalContent.viewSection.id) : null; |       return externalContent ? use(externalContent.viewSection.id) : null; | ||||||
| @ -308,7 +308,7 @@ export class GristDoc extends DisposableWithEvents { | |||||||
| 
 | 
 | ||||||
|       try { |       try { | ||||||
|         if (state.hash.popup || state.hash.recordCard) { |         if (state.hash.popup || state.hash.recordCard) { | ||||||
|           await this.openPopup(state.hash); |           await this._openPopup(state.hash); | ||||||
|         } else { |         } else { | ||||||
|           // Navigate to an anchor if one is present in the url hash.
 |           // Navigate to an anchor if one is present in the url hash.
 | ||||||
|           const cursorPos = this._getCursorPosFromHash(state.hash); |           const cursorPos = this._getCursorPosFromHash(state.hash); | ||||||
| @ -343,7 +343,7 @@ export class GristDoc extends DisposableWithEvents { | |||||||
|               } |               } | ||||||
| 
 | 
 | ||||||
|               this.behavioralPromptsManager.showTip(cursor, 'rickRow', { |               this.behavioralPromptsManager.showTip(cursor, 'rickRow', { | ||||||
|                 onDispose: () => this.playRickRollVideo(), |                 onDispose: () => this._playRickRollVideo(), | ||||||
|               }); |               }); | ||||||
|             }) |             }) | ||||||
|             .catch(reportError); |             .catch(reportError); | ||||||
| @ -602,7 +602,7 @@ export class GristDoc extends DisposableWithEvents { | |||||||
|     const isPopup = Computed.create(this, use => { |     const isPopup = Computed.create(this, use => { | ||||||
|       return ['data', 'settings'].includes(use(this.activeViewId) as any) // On Raw data or doc settings pages
 |       return ['data', 'settings'].includes(use(this.activeViewId) as any) // On Raw data or doc settings pages
 | ||||||
|         || use(isMaximized) // Layout has a maximized section visible
 |         || 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( |     return cssViewContentPane( | ||||||
|       testId('gristdoc'), |       testId('gristdoc'), | ||||||
| @ -623,43 +623,48 @@ export class GristDoc extends DisposableWithEvents { | |||||||
|           content === 'settings' ? dom.create(DocSettingsPage, this) : |           content === 'settings' ? dom.create(DocSettingsPage, this) : | ||||||
|           content === 'webhook' ? dom.create(WebhookPage, this) : |           content === 'webhook' ? dom.create(WebhookPage, this) : | ||||||
|           content === 'GristDocTour' ? null : |           content === 'GristDocTour' ? null : | ||||||
|           (typeof content === 'object') ? dom.create(owner => { |           [ | ||||||
|             // In case user changes a page, close the popup.
 |             dom.create((owner) => { | ||||||
|             owner.autoDispose(this.activeViewId.addListener(content.close)); |               this.viewLayout = ViewLayout.create(owner, this, content); | ||||||
|             // In case the section is removed, close the popup.
 |               this.viewLayout.maximized.addListener(sectionId => { | ||||||
|             content.viewSection.autoDispose({dispose: content.close}); |                 this.maximizedSectionId.set(sectionId); | ||||||
| 
 | 
 | ||||||
|             const {recordCard, rowId} = content.hash; |                 if (sectionId === null && !this._isShowingPopupSection) { | ||||||
|             if (recordCard) { |                   // If we didn't navigate to another section in the popup, move focus
 | ||||||
|               if (!rowId || rowId === 'new') { |                   // back to the previous section.
 | ||||||
|                 // Should be unreachable, but just to be sure (and to satisfy type checking)...
 |                   this._focusPreviousSection(); | ||||||
|                 throw new Error('Unable to open Record Card: undefined row id'); |                 } | ||||||
|               } |  | ||||||
| 
 |  | ||||||
|               return dom.create(RecordCardPopup, { |  | ||||||
|                 gristDoc: this, |  | ||||||
|                 rowId, |  | ||||||
|                 viewSection: content.viewSection, |  | ||||||
|                 onClose: content.close, |  | ||||||
|               }); |               }); | ||||||
|             } else { |               owner.onDispose(() => this.viewLayout = null); | ||||||
|               return dom.create(RawDataPopup, this, content.viewSection, content.close); |               return this.viewLayout; | ||||||
|             } |             }), | ||||||
|           }) : |             dom.maybe(this._popupSectionOptions, (popupOptions) => { | ||||||
|           dom.create((owner) => { |               return dom.create((owner) => { | ||||||
|             this.viewLayout = ViewLayout.create(owner, this, content); |                 // In case user changes a page, close the popup.
 | ||||||
|             this.viewLayout.maximized.addListener(sectionId => { |                 owner.autoDispose(this.activeViewId.addListener(popupOptions.close)); | ||||||
|               this.maximizedSectionId.set(sectionId); |  | ||||||
| 
 | 
 | ||||||
|               if (sectionId === null && !this._isShowingPopupSection) { |                 // In case the section is removed, close the popup.
 | ||||||
|                 // If we didn't navigate to another section in the popup, move focus
 |                 popupOptions.viewSection.autoDispose({dispose: popupOptions.close}); | ||||||
|                 // back to the previous section.
 | 
 | ||||||
|                 this._focusPreviousSection(); |                 const {recordCard, rowId} = popupOptions.hash; | ||||||
|               } |                 if (recordCard) { | ||||||
|             }); |                   if (!rowId || rowId === 'new') { | ||||||
|             owner.onDispose(() => this.viewLayout = null); |                     // Should be unreachable, but just to be sure (and to satisfy type checking)...
 | ||||||
|             return this.viewLayout; |                     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, () => [ |       dom.maybe(this._showBackgroundVideoPlayer, () => [ | ||||||
| @ -1371,10 +1376,36 @@ export class GristDoc extends DisposableWithEvents { | |||||||
|     await tableRec.tableName.saveOnly(newTableName); |     await tableRec.tableName.saveOnly(newTableName); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Creates computed with all the data for the given column. | ||||||
|  |    */ | ||||||
|  |   public columnObserver(owner: IDisposableOwner, tableId: Observable<string>, columnId: Observable<string>) { | ||||||
|  |     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). |    * 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.
 |     // We can only open a popup for a section.
 | ||||||
|     if (!hash.sectionId) { |     if (!hash.sectionId) { | ||||||
|       return; |       return; | ||||||
| @ -1386,13 +1417,17 @@ export class GristDoc extends DisposableWithEvents { | |||||||
|     if (this.viewModel.viewSections.peek().peek().some(s => s.id.peek() === hash.sectionId)) { |     if (this.viewModel.viewSections.peek().peek().some(s => s.id.peek() === hash.sectionId)) { | ||||||
|       this.viewModel.activeSectionId(hash.sectionId); |       this.viewModel.activeSectionId(hash.sectionId); | ||||||
|       // If the anchor link is valid, set the cursor.
 |       // 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 activeSection = this.viewModel.activeSection.peek(); | ||||||
|         const fieldIndex = activeSection.viewFields.peek().all().findIndex(f => f.colRef.peek() === hash.colRef); |         const {rowId} = hash; | ||||||
|         if (fieldIndex >= 0) { |         let fieldIndex = undefined; | ||||||
|           const view = await this._waitForView(activeSection); |         if (hash.colRef) { | ||||||
|           view?.setCursorPos({rowId: hash.rowId, fieldIndex}); |           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); |       this.viewLayout?.maximized.set(hash.sectionId); | ||||||
|       return; |       return; | ||||||
| @ -1451,7 +1486,7 @@ export class GristDoc extends DisposableWithEvents { | |||||||
|   /** |   /** | ||||||
|    * Starts playing the music video for Never Gonna Give You Up in the background. |    * 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(); |     const backgroundVideoPlayer = this._backgroundVideoPlayerHolder.get(); | ||||||
|     if (!backgroundVideoPlayer) { |     if (!backgroundVideoPlayer) { | ||||||
|       return; |       return; | ||||||
| @ -1487,32 +1522,6 @@ export class GristDoc extends DisposableWithEvents { | |||||||
|     this._showBackgroundVideoPlayer.set(false); |     this._showBackgroundVideoPlayer.set(false); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |  | ||||||
|    * Creates computed with all the data for the given column. |  | ||||||
|    */ |  | ||||||
|   public columnObserver(owner: IDisposableOwner, tableId: Observable<string>, columnId: Observable<string>) { |  | ||||||
|     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() { |   private _focusPreviousSection() { | ||||||
|     const prevSectionId = this._prevSectionId; |     const prevSectionId = this._prevSectionId; | ||||||
|     if (!prevSectionId) { return; } |     if (!prevSectionId) { return; } | ||||||
| @ -1890,26 +1899,25 @@ async function finalizeAnchor() { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const cssViewContentPane = styled('div', ` | const cssViewContentPane = styled('div', ` | ||||||
|   --view-content-page-margin: 12px; |   --view-content-page-padding: 12px; | ||||||
|   flex: auto; |   flex: auto; | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   overflow: visible; |   overflow: visible; | ||||||
|   position: relative; |   position: relative; | ||||||
|   min-width: 240px; |   min-width: 240px; | ||||||
|   margin: var(--view-content-page-margin, 12px); |   padding: var(--view-content-page-padding, 12px); | ||||||
|   @media ${mediaSmall} { |   @media ${mediaSmall} { | ||||||
|     & { |     & { | ||||||
|       margin: 4px; |       padding: 4px; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   @media print { |   @media print { | ||||||
|     & { |     & { | ||||||
|       margin: 0px; |       padding: 0px; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   &-contents { |   &-contents { | ||||||
|     margin: 0px; |  | ||||||
|     overflow: hidden; |     overflow: hidden; | ||||||
|   } |   } | ||||||
| `);
 | `);
 | ||||||
|  | |||||||
| @ -501,7 +501,7 @@ export class LayoutEditor extends Disposable { | |||||||
|       handles: isWidth ? 'e' : 's', |       handles: isWidth ? 'e' : 's', | ||||||
|       start: this.onResizeStart.bind(this, helperObj, isWidth), |       start: this.onResizeStart.bind(this, helperObj, isWidth), | ||||||
|       resize: this.onResizeMove.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) { |   public unmakeResizable(box: LayoutBox) { | ||||||
|  | |||||||
| @ -1179,7 +1179,7 @@ const cssCollapsedTray = styled('div.collapsed_layout', ` | |||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|   transition: height 0.2s; |   transition: height 0.2s; | ||||||
|   position: relative; |   position: relative; | ||||||
|   margin: calc(-1 * var(--view-content-page-margin, 12px)); |   margin: calc(-1 * var(--view-content-page-padding, 12px)); | ||||||
|   margin-bottom: 0; |   margin-bottom: 0; | ||||||
|   user-select: none; |   user-select: none; | ||||||
|   background-color: ${theme.pageBg}; |   background-color: ${theme.pageBg}; | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ import {DocumentUsage} from 'app/client/components/DocumentUsage'; | |||||||
| import {GristDoc} from 'app/client/components/GristDoc'; | import {GristDoc} from 'app/client/components/GristDoc'; | ||||||
| import {printViewSection} from 'app/client/components/Printing'; | import {printViewSection} from 'app/client/components/Printing'; | ||||||
| import {ViewSectionHelper} from 'app/client/components/ViewLayout'; | 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 {icon} from 'app/client/ui2018/icons'; | ||||||
| import {Computed, Disposable, dom, fromKo, makeTestId, Observable, styled} from 'grainjs'; | import {Computed, Disposable, dom, fromKo, makeTestId, Observable, styled} from 'grainjs'; | ||||||
| import {reportError} from 'app/client/models/errors'; | import {reportError} from 'app/client/models/errors'; | ||||||
| @ -115,7 +115,8 @@ export class RawDataPopup extends Disposable { | |||||||
| const cssContainer = styled('div', ` | const cssContainer = styled('div', ` | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|   position: relative; |   inset: 0px; | ||||||
|  |   position: absolute; | ||||||
| `);
 | `);
 | ||||||
| 
 | 
 | ||||||
| const cssPage = styled('div', ` | const cssPage = styled('div', ` | ||||||
| @ -132,10 +133,9 @@ const cssPage = styled('div', ` | |||||||
| export const cssOverlay = styled('div', ` | export const cssOverlay = styled('div', ` | ||||||
|   background-color: ${theme.modalBackdrop}; |   background-color: ${theme.modalBackdrop}; | ||||||
|   inset: 0px; |   inset: 0px; | ||||||
|   height: 100%; |  | ||||||
|   width: 100%; |  | ||||||
|   padding: 20px 56px 20px 56px; |   padding: 20px 56px 20px 56px; | ||||||
|   position: absolute; |   position: absolute; | ||||||
|  |   z-index: ${vars.popupSectionBackdropZIndex}; | ||||||
|   @media ${mediaSmall} { |   @media ${mediaSmall} { | ||||||
|     & { |     & { | ||||||
|       padding: 22px; |       padding: 22px; | ||||||
|  | |||||||
| @ -50,6 +50,7 @@ export class RecordCardPopup extends DisposableWithEvents { | |||||||
|           focusable: false, |           focusable: false, | ||||||
|           renamable: false, |           renamable: false, | ||||||
|         }), |         }), | ||||||
|  |         testId('wrapper'), | ||||||
|       ), |       ), | ||||||
|       cssCloseButton('CrossBig', |       cssCloseButton('CrossBig', | ||||||
|         dom.on('click', () => this._handleClose()), |         dom.on('click', () => this._handleClose()), | ||||||
|  | |||||||
| @ -544,6 +544,7 @@ export class WidgetAPIImpl implements WidgetAPI { | |||||||
| const COMMAND_MINIMUM_ACCESS_LEVELS: Map<CommandName, AccessLevel> = new Map([ | const COMMAND_MINIMUM_ACCESS_LEVELS: Map<CommandName, AccessLevel> = new Map([ | ||||||
|   ['undo', AccessLevel.full], |   ['undo', AccessLevel.full], | ||||||
|   ['redo', AccessLevel.full], |   ['redo', AccessLevel.full], | ||||||
|  |   ['viewAsCard', AccessLevel.read_table], | ||||||
| ]); | ]); | ||||||
| 
 | 
 | ||||||
| export class CommandAPI { | export class CommandAPI { | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								app/client/declarations.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								app/client/declarations.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -77,6 +77,8 @@ declare module "app/client/components/BaseView" { | |||||||
|     public moveEditRowToCursor(): DataRowModel; |     public moveEditRowToCursor(): DataRowModel; | ||||||
|     public scrollToCursor(sync: boolean): Promise<void>; |     public scrollToCursor(sync: boolean): Promise<void>; | ||||||
|     public getAnchorLinkForSection(sectionId: number): IGristUrlState; |     public getAnchorLinkForSection(sectionId: number): IGristUrlState; | ||||||
|  |     public viewSelectedRecordAsCard(): void; | ||||||
|  |     public isRecordCardDisabled(): boolean; | ||||||
|   } |   } | ||||||
|   export = BaseView; |   export = BaseView; | ||||||
| } | } | ||||||
|  | |||||||
| @ -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. */ |   /** True if this section is disabled. Currently only used by Record Card sections. */ | ||||||
|   disabled: modelUtil.KoSaveableObservable<boolean>; |   disabled: modelUtil.KoSaveableObservable<boolean>; | ||||||
| 
 | 
 | ||||||
|   /** |   /** True if the Record Card section of this section's table is disabled. */ | ||||||
|    * True if the Record Card section of this section's table is disabled. Shortcut for |  | ||||||
|    * `this.tableRecordCard().disabled()`. |  | ||||||
|    */ |  | ||||||
|   isTableRecordCardDisabled: ko.Computed<boolean>; |   isTableRecordCardDisabled: ko.Computed<boolean>; | ||||||
| 
 | 
 | ||||||
|   isVirtual: ko.Computed<boolean>; |   isVirtual: ko.Computed<boolean>; | ||||||
| @ -485,7 +482,8 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): | |||||||
|   this.isRecordCard = this.autoDispose(ko.pureComputed(() => |   this.isRecordCard = this.autoDispose(ko.pureComputed(() => | ||||||
|     this.table().recordCardViewSectionRef() === this.id())); |     this.table().recordCardViewSectionRef() === this.id())); | ||||||
|   this.disabled = modelUtil.fieldWithDefault(this.optionsObj.prop('disabled'), false); |   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')); |   this.isVirtual = this.autoDispose(ko.pureComputed(() => typeof this.id() === 'string')); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -72,11 +72,9 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool | |||||||
|     dom.maybe(showRawData, |     dom.maybe(showRawData, | ||||||
|       () => menuItemLink( |       () => menuItemLink( | ||||||
|         { href: rawUrl}, t("Show raw data"), testId('show-raw-data'), |         { 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
 |           // Replace the current URL so that the back button works as expected (it navigates back from
 | ||||||
|           // the current page).
 |           // the current page).
 | ||||||
|           ev.stopImmediatePropagation(); |  | ||||||
|           ev.preventDefault(); |  | ||||||
|           urlState().pushUrl(anchorUrlState, { replace: true }).catch(reportError); |           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), |     dom.maybe((use) => !use(viewSection.isRaw) && !isSinglePage && !use(gristDoc.maximizedSectionId), | ||||||
|       () => menuItemLink( |       () => menuItemLink( | ||||||
|         { href: rawUrl}, t("Show raw data"), testId('show-raw-data'), |         { 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
 |           // Replace the current URL so that the back button works as expected (it navigates back from
 | ||||||
|           // the current page).
 |           // the current page).
 | ||||||
|           ev.stopImmediatePropagation(); |  | ||||||
|           ev.preventDefault(); |  | ||||||
|           urlState().pushUrl(anchorUrlState, { replace: true }).catch(reportError); |           urlState().pushUrl(anchorUrlState, { replace: true }).catch(reportError); | ||||||
|         }) |         }) | ||||||
|       ) |       ) | ||||||
|  | |||||||
| @ -137,6 +137,7 @@ export const vars = { | |||||||
| 
 | 
 | ||||||
|   /* Z indexes */ |   /* Z indexes */ | ||||||
|   insertColumnLineZIndex: new CustomProp('insert-column-line-z-index', '20'), |   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'), |   menuZIndex: new CustomProp('menu-z-index', '999'), | ||||||
|   modalZIndex: new CustomProp('modal-z-index', '999'), |   modalZIndex: new CustomProp('modal-z-index', '999'), | ||||||
|   onboardingBackdropZIndex: new CustomProp('onboarding-backdrop-z-index', '999'), |   onboardingBackdropZIndex: new CustomProp('onboarding-backdrop-z-index', '999'), | ||||||
|  | |||||||
| @ -119,7 +119,7 @@ const cssOptions = styled('div', ` | |||||||
|   position: absolute; |   position: absolute; | ||||||
|   right: 0; |   right: 0; | ||||||
|   top: 48px; |   top: 48px; | ||||||
|   z-index: 1; |   z-index: ${vars.menuZIndex}; | ||||||
|   padding: 2px 4px; |   padding: 2px 4px; | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|   white-space: nowrap; |   white-space: nowrap; | ||||||
|  | |||||||
| @ -1599,6 +1599,14 @@ class TestUserActions(test_engine.EngineTestCase): | |||||||
|       'formula': '$A == "Foo"', |       '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. |     # Duplicate Table1 as Foo without including any of its data. | ||||||
|     self.apply_user_action(['DuplicateTable', 'Table1', 'Foo', False]) |     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", |       ["id", "A", "B", "C", "D", "E", "F", "G", "H", "gristHelper_ConditionalRule", | ||||||
|         "gristHelper_RowConditionalRule", "manualSort"], |         "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. |     # Duplicate Table1 as FooData and include all of its data. | ||||||
|     self.apply_user_action(['DuplicateTable', 'Table1', 'FooData', True]) |     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'] |     duplicated_times = self.engine.fetch_table('FooData').columns['E'] | ||||||
|     self.assertEqual(existing_times, duplicated_times) |     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 |     # 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. |     # 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.apply_user_action(['DuplicateTable', 'Table1', 'Foo', True]) | ||||||
|     self.assertTableData('Table1', data=[["id", "State2", 'manualSort'], [1, 'NY', 1.0]]) |     self.assertTableData('Table1', data=[["id", "State2", 'manualSort'], [1, 'NY', 1.0]]) | ||||||
|     self.assertTableData('Foo', 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], | ||||||
|  |     ]) | ||||||
|  | |||||||
| @ -921,9 +921,10 @@ | |||||||
| 
 | 
 | ||||||
|           // Record card widget |           // Record card widget | ||||||
|           ["AddRecord", "_grist_Views_section", 3, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "single", "tableRef": 4, "title": ""}], |           ["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}], |           ["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. |           // Actions generated from AddColumn. | ||||||
|           ["AddColumn", "Bar", "world", |           ["AddColumn", "Bar", "world", | ||||||
| @ -937,7 +938,7 @@ | |||||||
|         ], |         ], | ||||||
|         "direct": [true, true, true, true, true, true, true, |         "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], | ||||||
|         "undo": [ |         "undo": [ | ||||||
|           ["RemoveTable", "Bar"], |           ["RemoveTable", "Bar"], | ||||||
|           ["RemoveRecord", "_grist_Tables", 4], |           ["RemoveRecord", "_grist_Tables", 4], | ||||||
| @ -950,8 +951,9 @@ | |||||||
|           ["RemoveRecord", "_grist_Views_section", 2], |           ["RemoveRecord", "_grist_Views_section", 2], | ||||||
|           ["RemoveRecord", "_grist_Views_section_field", 2], |           ["RemoveRecord", "_grist_Views_section_field", 2], | ||||||
|           ["RemoveRecord", "_grist_Views_section", 3], |           ["RemoveRecord", "_grist_Views_section", 3], | ||||||
|  |           ["UpdateRecord", "_grist_Tables", 4, {"recordCardViewSectionRef": 0}], | ||||||
|           ["RemoveRecord", "_grist_Views_section_field", 3], |           ["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"], |           ["RemoveColumn", "Bar", "world"], | ||||||
|           ["RemoveRecord", "_grist_Tables_column", 35], |           ["RemoveRecord", "_grist_Tables_column", 35], | ||||||
|           ["RemoveRecord", "_grist_Views_section_field", 4], |           ["RemoveRecord", "_grist_Views_section_field", 4], | ||||||
| @ -1267,15 +1269,16 @@ | |||||||
|           ["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4, "title": ""}], |           ["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]}], |           ["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": ""}], |           ["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]}], |           ["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]], |           ["BulkRemoveRecord", "_grist_Views_section_field", [1, 3, 5]], | ||||||
|           ["RemoveRecord", "_grist_Tables_column", 31], |           ["RemoveRecord", "_grist_Tables_column", 31], | ||||||
|           ["RemoveColumn", "ViewTest", "hello"] |           ["RemoveColumn", "ViewTest", "hello"] | ||||||
| 
 | 
 | ||||||
|         ], |         ], | ||||||
|         "direct": [true, true, true, true, true, true, true, true, true, |         "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": [ |         "undo": [ | ||||||
|           ["RemoveTable", "ViewTest"], |           ["RemoveTable", "ViewTest"], | ||||||
|           ["RemoveRecord", "_grist_Tables", 4], |           ["RemoveRecord", "_grist_Tables", 4], | ||||||
| @ -1288,8 +1291,9 @@ | |||||||
|           ["RemoveRecord", "_grist_Views_section", 2], |           ["RemoveRecord", "_grist_Views_section", 2], | ||||||
|           ["BulkRemoveRecord", "_grist_Views_section_field", [3, 4]], |           ["BulkRemoveRecord", "_grist_Views_section_field", [3, 4]], | ||||||
|           ["RemoveRecord", "_grist_Views_section", 3], |           ["RemoveRecord", "_grist_Views_section", 3], | ||||||
|  |           ["UpdateRecord", "_grist_Tables", 4, {"recordCardViewSectionRef": 0}], | ||||||
|           ["BulkRemoveRecord", "_grist_Views_section_field", [5, 6]], |           ["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], |           ["BulkAddRecord", "_grist_Views_section_field", [1, 3, 5], | ||||||
|             {"colRef": [31, 31, 31], "parentId": [1, 2, 3], "parentPos": [1.0, 3.0, 5.0]}], |             {"colRef": [31, 31, 31], "parentId": [1, 2, 3], "parentPos": [1.0, 3.0, 5.0]}], | ||||||
|           ["AddRecord", "_grist_Tables_column", 31, |           ["AddRecord", "_grist_Tables_column", 31, | ||||||
| @ -2213,7 +2217,8 @@ | |||||||
|               "parentId": 1, "parentKey": "record", "sortColRefs": "[]", "title": ""}], |               "parentId": 1, "parentKey": "record", "sortColRefs": "[]", "title": ""}], | ||||||
|           ["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4, "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": ""}], |           ["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", [ |           ["AddTable", "Bar", [ | ||||||
|             {"id": "manualSort", "formula": "", "isFormula": false, "type": "ManualSortPos"}, |             {"id": "manualSort", "formula": "", "isFormula": false, "type": "ManualSortPos"}, | ||||||
|             {"isFormula": false, "formula": "", "type": "Text", "id": "hello"}, |             {"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": ""}], |           ["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]}], |           ["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": ""}], |           ["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]}], |           ["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", 1, {"foo": 0, "hello": "a", "manualSort": 1.0}], | ||||||
|           ["AddRecord", "Bar", 2, {"foo": 1, "hello": "b", "manualSort": 2.0}], |           ["AddRecord", "Bar", 2, {"foo": 1, "hello": "b", "manualSort": 2.0}], | ||||||
|           ["AddRecord", "Bar", 3, {"foo": 1, "hello": "c", "manualSort": 3.0}], |           ["AddRecord", "Bar", 3, {"foo": 1, "hello": "c", "manualSort": 3.0}], | ||||||
|           ["BulkUpdateRecord", "Bar", [1, 2, 3], {"world": ["A", "B", "C"]}] |           ["BulkUpdateRecord", "Bar", [1, 2, 3], {"world": ["A", "B", "C"]}] | ||||||
|         ], |         ], | ||||||
|         "direct": [true, true, true, true, true, true, true, true, |         "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, true, true, true, true, true, true, | ||||||
|                    true, true, true, false], |                    true, true, true, false], | ||||||
|         "undo": [ |         "undo": [ | ||||||
| @ -2266,7 +2272,8 @@ | |||||||
|           ["RemoveRecord", "_grist_Views_section", 1], |           ["RemoveRecord", "_grist_Views_section", 1], | ||||||
|           ["RemoveRecord", "_grist_Views_section", 2], |           ["RemoveRecord", "_grist_Views_section", 2], | ||||||
|           ["RemoveRecord", "_grist_Views_section", 3], |           ["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"], |           ["RemoveTable", "Bar"], | ||||||
|           ["RemoveRecord", "_grist_Tables", 5], |           ["RemoveRecord", "_grist_Tables", 5], | ||||||
|           ["BulkRemoveRecord", "_grist_Tables_column", [31,32,33,34]], |           ["BulkRemoveRecord", "_grist_Tables_column", [31,32,33,34]], | ||||||
| @ -2278,8 +2285,9 @@ | |||||||
|           ["RemoveRecord", "_grist_Views_section", 5], |           ["RemoveRecord", "_grist_Views_section", 5], | ||||||
|           ["BulkRemoveRecord", "_grist_Views_section_field", [4, 5, 6]], |           ["BulkRemoveRecord", "_grist_Views_section_field", [4, 5, 6]], | ||||||
|           ["RemoveRecord", "_grist_Views_section", 6], |           ["RemoveRecord", "_grist_Views_section", 6], | ||||||
|  |           ["UpdateRecord", "_grist_Tables", 5, {"recordCardViewSectionRef": 0}], | ||||||
|           ["BulkRemoveRecord", "_grist_Views_section_field", [7, 8, 9]], |           ["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", 1], | ||||||
|           ["RemoveRecord", "Bar", 2], |           ["RemoveRecord", "Bar", 2], | ||||||
|           ["RemoveRecord", "Bar", 3] |           ["RemoveRecord", "Bar", 3] | ||||||
| @ -2355,9 +2363,10 @@ | |||||||
|            // Record card widget |            // Record card widget | ||||||
|            ["AddRecord", "_grist_Views_section", 3, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "single", "tableRef": 4, "title": ""}], |            ["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. |            // 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": [ |          "undo": [ | ||||||
|            ["RemoveTable", "Foo"], |            ["RemoveTable", "Foo"], | ||||||
|            ["RemoveRecord", "_grist_Tables", 4], |            ["RemoveRecord", "_grist_Tables", 4], | ||||||
| @ -2368,7 +2377,8 @@ | |||||||
|            ["RemoveRecord", "_grist_Views_section", 1], |            ["RemoveRecord", "_grist_Views_section", 1], | ||||||
|            ["RemoveRecord", "_grist_Views_section", 2], |            ["RemoveRecord", "_grist_Views_section", 2], | ||||||
|            ["RemoveRecord", "_grist_Views_section", 3], |            ["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}] | ||||||
|          ] |          ] | ||||||
|        } |        } | ||||||
|      }], |      }], | ||||||
|  | |||||||
| @ -1930,19 +1930,16 @@ class UserActions(object): | |||||||
|       ) |       ) | ||||||
| 
 | 
 | ||||||
|     if record_card_section: |     if record_card_section: | ||||||
|       record_card_section = self.create_plain_view_section( |       record_card_section = self._create_record_card_view_section( | ||||||
|         result["id"], |         result["id"], | ||||||
|         table_id, |         table_id, | ||||||
|         self._docmodel.view_sections, |         self._docmodel.view_sections | ||||||
|         "single", |  | ||||||
|         "" |  | ||||||
|       ) |       ) | ||||||
| 
 | 
 | ||||||
|     if primary_view or raw_section or record_card_section: |     if primary_view or raw_section: | ||||||
|       self.UpdateRecord('_grist_Tables', result["id"], { |       self.UpdateRecord('_grist_Tables', result["id"], { | ||||||
|         'primaryViewId': primary_view["id"] if primary_view else 0, |         'primaryViewId': primary_view["id"] if primary_view else 0, | ||||||
|         'rawViewSectionRef': raw_section.id if raw_section else 0, |         'rawViewSectionRef': raw_section.id if raw_section else 0, | ||||||
|         'recordCardViewSectionRef': record_card_section.id if record_card_section else 0, |  | ||||||
|       }) |       }) | ||||||
| 
 | 
 | ||||||
|     return result |     return result | ||||||
| @ -1994,10 +1991,10 @@ class UserActions(object): | |||||||
|     new_raw_section = new_table.rawViewSectionRef |     new_raw_section = new_table.rawViewSectionRef | ||||||
|     new_record_card_section = new_table.recordCardViewSectionRef |     new_record_card_section = new_table.recordCardViewSectionRef | ||||||
| 
 | 
 | ||||||
|     # Copy view section options to the new raw and record card view sections. |     # Copy view section description and options to the new raw view section. | ||||||
|     self._docmodel.update( |     self._docmodel.update([new_raw_section], | ||||||
|       [new_raw_section, new_record_card_section], |       description=raw_section.description, | ||||||
|       options=[raw_section.options, record_card_section.options] |       options=raw_section.options, | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     old_to_new_col_refs = {} |     old_to_new_col_refs = {} | ||||||
| @ -2034,6 +2031,7 @@ class UserActions(object): | |||||||
|         recalcWhen=existing_column.recalcWhen, |         recalcWhen=existing_column.recalcWhen, | ||||||
|         recalcDeps=new_recalc_deps, |         recalcDeps=new_recalc_deps, | ||||||
|         formula=formula_updates.get(new_column, existing_column.formula), |         formula=formula_updates.get(new_column, existing_column.formula), | ||||||
|  |         description=existing_column.description, | ||||||
|       ) |       ) | ||||||
|       self.maybe_copy_display_formula(existing_column, new_column) |       self.maybe_copy_display_formula(existing_column, new_column) | ||||||
| 
 | 
 | ||||||
| @ -2049,6 +2047,8 @@ class UserActions(object): | |||||||
|         for rule in existing_column.rules: |         for rule in existing_column.rules: | ||||||
|           self.doAddRule(new_table_id, None, new_column.id, rule.formula) |           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. |     # Copy all row conditional styles to the new table. | ||||||
|     for rule in raw_section.rules: |     for rule in raw_section.rules: | ||||||
|       self.doAddRule(new_table_id, None, None, rule.formula) |       self.doAddRule(new_table_id, None, None, rule.formula) | ||||||
| @ -2064,6 +2064,55 @@ class UserActions(object): | |||||||
|       'raw_section_id': new_raw_section.id, |       '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): |   def _fetch_table_col_recs(self, table_ref, col_refs): | ||||||
|     """Helper that converts col_refs from table table_ref into column Records.""" |     """Helper that converts col_refs from table table_ref into column Records.""" | ||||||
| @ -2123,6 +2172,15 @@ class UserActions(object): | |||||||
|                             limit=limit) |                             limit=limit) | ||||||
|     return section |     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 |   @useraction | ||||||
|   def UpdateSummaryViewSection(self, section_ref, groupby_colrefs): |   def UpdateSummaryViewSection(self, section_ref, groupby_colrefs): | ||||||
|     """ |     """ | ||||||
| @ -2247,14 +2305,22 @@ class UserActions(object): | |||||||
|     if section_rec.fields: |     if section_rec.fields: | ||||||
|       self._docmodel.remove(section_rec.fields) |       self._docmodel.remove(section_rec.fields) | ||||||
| 
 | 
 | ||||||
|     # Include all table columns that are intended to be visible to the user. |     is_card = section_rec.parentKey in ('single', 'detail') | ||||||
|     cols = [c for c in table_rec.columns if column.is_visible_column(c.colId) |     is_record_card = section_rec == table_rec.recordCardViewSectionRef | ||||||
|             # TODO: hack to avoid auto-adding the 'group' column when detaching summary tables. |     if is_card and not is_record_card: | ||||||
|             and c.colId != 'group'] |       # Copy settings from the table's record card section to the new section. | ||||||
|     cols.sort(key=lambda c: c.parentPos) |       record_card_section = table_rec.recordCardViewSectionRef | ||||||
|     if limit is not None: |       self._docmodel.add(section_rec.fields, colRef=[f.colRef for f in record_card_section.fields]) | ||||||
|       cols = cols[:limit] |       self._copy_record_card_settings(record_card_section, section_rec) | ||||||
|     self._docmodel.add(section_rec.fields, colRef=[c.id for c in cols]) |     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]) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|   #---------------------------------------------------------------------- |   #---------------------------------------------------------------------- | ||||||
|  | |||||||
| @ -515,8 +515,8 @@ describe('RawData', function () { | |||||||
|     await gu.checkTextEditor("abc"); |     await gu.checkTextEditor("abc"); | ||||||
|     await gu.sendKeys(Key.ESCAPE); |     await gu.sendKeys(Key.ESCAPE); | ||||||
|     // Click on another cell, check page hasn't changed (there was a bug about that)
 |     // Click on another cell, check page hasn't changed (there was a bug about that)
 | ||||||
|     await gu.getCell({rowNum: 10, col: 1}).click(); |     await gu.getCell({rowNum: 21, col: 1}).click(); | ||||||
|     assert.deepEqual(await gu.getCursorPosition(), {rowNum: 10, col: 1}); |     assert.deepEqual(await gu.getCursorPosition(), {rowNum: 21, col: 1}); | ||||||
|     assert.equal(await gu.getCurrentPageName(), 'City'); |     assert.equal(await gu.getCurrentPageName(), 'City'); | ||||||
| 
 | 
 | ||||||
|     // Close by hitting escape.
 |     // Close by hitting escape.
 | ||||||
|  | |||||||
| @ -23,7 +23,10 @@ describe('RecordCards', function() { | |||||||
|     it('opens popup when keyboard shortcut is pressed', async function() { |     it('opens popup when keyboard shortcut is pressed', async function() { | ||||||
|       await gu.sendKeys(Key.SPACE); |       await gu.sendKeys(Key.SPACE); | ||||||
|       assert.isTrue(await driver.findWait('.test-record-card-popup-overlay', 100).isDisplayed()); |       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.equal(await gu.getCardCell('Code').getText(), 'ALB'); | ||||||
|       assert.isFalse(await driver.find('.grist-single-record__menu').isPresent()); |       assert.isFalse(await driver.find('.grist-single-record__menu').isPresent()); | ||||||
|       await gu.sendKeys(Key.ESCAPE); |       await gu.sendKeys(Key.ESCAPE); | ||||||
| @ -32,7 +35,10 @@ describe('RecordCards', function() { | |||||||
|     it('opens popup when menu item is clicked', async function() { |     it('opens popup when menu item is clicked', async function() { | ||||||
|       await (await gu.openRowMenu(2)).findContent('li', /View as card/).click(); |       await (await gu.openRowMenu(2)).findContent('li', /View as card/).click(); | ||||||
|       assert.isTrue(await driver.findWait('.test-record-card-popup-overlay', 100).isDisplayed()); |       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'); |       assert.equal(await gu.getCardCell('Code').getText(), 'AND'); | ||||||
|       await gu.sendKeys(Key.ESCAPE); |       await gu.sendKeys(Key.ESCAPE); | ||||||
|     }); |     }); | ||||||
| @ -76,7 +82,10 @@ describe('RecordCards', function() { | |||||||
|     it('opens popup when reference icon is clicked', async function() { |     it('opens popup when reference icon is clicked', async function() { | ||||||
|       await gu.getCell(0, 4).find('.test-ref-link-icon').click(); |       await gu.getCell(0, 4).find('.test-ref-link-icon').click(); | ||||||
|       assert.isTrue(await driver.findWait('.test-record-card-popup-overlay', 100).isDisplayed()); |       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.equal(await gu.getCardCell('Code').getText(), 'AFG'); | ||||||
|       assert.isFalse(await driver.find('.grist-single-record__menu').isPresent()); |       assert.isFalse(await driver.find('.grist-single-record__menu').isPresent()); | ||||||
|       await gu.sendKeys(Key.ESCAPE); |       await gu.sendKeys(Key.ESCAPE); | ||||||
| @ -86,10 +95,16 @@ describe('RecordCards', function() { | |||||||
|       await gu.getCell(0, 4).find('.test-ref-text').click(); |       await gu.getCell(0, 4).find('.test-ref-text').click(); | ||||||
|       await gu.sendKeys(Key.SPACE); |       await gu.sendKeys(Key.SPACE); | ||||||
|       assert.isTrue(await driver.findWait('.test-record-card-popup-overlay', 100).isDisplayed()); |       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'); |       assert.equal(await gu.getCardCell('Country').getText(), 'AFG'); | ||||||
|       await gu.getCardCell('Country').find('.test-ref-link-icon').click(); |       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'); |       assert.equal(await gu.getCardCell('Code').getText(), 'AFG'); | ||||||
|       await gu.sendKeys(Key.ESCAPE); |       await gu.sendKeys(Key.ESCAPE); | ||||||
|     }); |     }); | ||||||
| @ -119,7 +134,10 @@ describe('RecordCards', function() { | |||||||
|     it('opens popup when reference icon is clicked', async function() { |     it('opens popup when reference icon is clicked', async function() { | ||||||
|       await gu.getCell(0, 4).find('.test-ref-list-link-icon').click(); |       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.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.equal(await gu.getCardCell('Code').getText(), 'AFG'); | ||||||
|       assert.isFalse(await driver.find('.grist-single-record__menu').isPresent()); |       assert.isFalse(await driver.find('.grist-single-record__menu').isPresent()); | ||||||
|       await gu.sendKeys(Key.ESCAPE); |       await gu.sendKeys(Key.ESCAPE); | ||||||
| @ -129,12 +147,37 @@ describe('RecordCards', function() { | |||||||
|       await gu.getCell(0, 4).click(); |       await gu.getCell(0, 4).click(); | ||||||
|       await gu.sendKeys(Key.SPACE); |       await gu.sendKeys(Key.SPACE); | ||||||
|       assert.isTrue(await driver.findWait('.test-record-card-popup-overlay', 100).isDisplayed()); |       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'); |       assert.equal(await gu.getCardCell('Country').getText(), 'AFG'); | ||||||
|       await gu.getCardCell('Country').find('.test-ref-list-link-icon').click(); |       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'); |       assert.equal(await gu.getCardCell('Code').getText(), 'AFG'); | ||||||
|       await gu.sendKeys(Key.ESCAPE); |       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'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user