mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) Fix calendar and card tip bug on mobile
Summary: On mobile, tips for calendar and card list aren't currently shown, but the creator panel was still automatically being opened in preparation for showing the tip. Test Plan: Manual and existing tests. Reviewers: jarek Reviewed By: jarek Subscribers: jarek Differential Revision: https://phab.getgrist.com/D4053
This commit is contained in:
		
							parent
							
								
									82c95ec074
								
							
						
					
					
						commit
						e033889b6a
					
				| @ -8,26 +8,32 @@ import {getGristConfig} from 'app/common/urlUtils'; | |||||||
| import {Computed, Disposable, dom, Observable} from 'grainjs'; | import {Computed, Disposable, dom, Observable} from 'grainjs'; | ||||||
| import {IPopupOptions} from 'popweasel'; | import {IPopupOptions} from 'popweasel'; | ||||||
| 
 | 
 | ||||||
| export interface AttachOptions { | /** | ||||||
|   /** Defaults to false. */ |  * Options for showing a tip. | ||||||
|   forceShow?: boolean; |  */ | ||||||
|   /** Defaults to false. */ | export interface ShowTipOptions { | ||||||
|  |   /** Defaults to `false`. */ | ||||||
|   hideArrow?: boolean; |   hideArrow?: boolean; | ||||||
|   /** Defaults to false. */ |  | ||||||
|   hideDontShowTips?: boolean; |  | ||||||
|   /** Defaults to true. */ |  | ||||||
|   markAsSeen?: boolean; |  | ||||||
|   /** Defaults to false. */ |  | ||||||
|   showOnMobile?: boolean; |  | ||||||
|   popupOptions?: IPopupOptions; |   popupOptions?: IPopupOptions; | ||||||
|   onDispose?(): void; |   onDispose?(): void; | ||||||
|   shouldShow?(): boolean; | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Options for attaching a tip to a DOM element. | ||||||
|  |  */ | ||||||
|  | export interface AttachTipOptions extends ShowTipOptions { | ||||||
|  |   /** | ||||||
|  |    * Optional callback that should return true if the tip should be disabled. | ||||||
|  |    * | ||||||
|  |    * If omitted, the tip is enabled. | ||||||
|  |    */ | ||||||
|  |   isDisabled?(): boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface QueuedTip { | interface QueuedTip { | ||||||
|   prompt: BehavioralPrompt; |   prompt: BehavioralPrompt; | ||||||
|   refElement: Element; |   refElement: Element; | ||||||
|   options: AttachOptions; |   options: ShowTipOptions; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -52,12 +58,14 @@ export class BehavioralPromptsManager extends Disposable { | |||||||
|     super(); |     super(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public showTip(refElement: Element, prompt: BehavioralPrompt, options: AttachOptions = {}) { |   public showTip(refElement: Element, prompt: BehavioralPrompt, options: ShowTipOptions = {}) { | ||||||
|     this._queueTip(refElement, prompt, options); |     this._queueTip(refElement, prompt, options); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public attachTip(prompt: BehavioralPrompt, options: AttachOptions = {}) { |   public attachTip(prompt: BehavioralPrompt, options: AttachTipOptions = {}) { | ||||||
|     return (element: Element) => { |     return (element: Element) => { | ||||||
|  |       if (options.isDisabled?.()) { return; } | ||||||
|  | 
 | ||||||
|       this._queueTip(element, prompt, options); |       this._queueTip(element, prompt, options); | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| @ -70,6 +78,29 @@ export class BehavioralPromptsManager extends Disposable { | |||||||
|     return !this._prefs.get().dontShowTips; |     return !this._prefs.get().dontShowTips; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public shouldShowTip(prompt: BehavioralPrompt): boolean { | ||||||
|  |     if (this._isDisabled) { return false; } | ||||||
|  | 
 | ||||||
|  |     const { | ||||||
|  |       showContext = 'desktop', | ||||||
|  |       showDeploymentTypes, | ||||||
|  |       forceShow = false, | ||||||
|  |     } = GristBehavioralPrompts[prompt]; | ||||||
|  | 
 | ||||||
|  |     const {deploymentType} = getGristConfig(); | ||||||
|  |     if ( | ||||||
|  |       showDeploymentTypes !== '*' && | ||||||
|  |       (!deploymentType || !showDeploymentTypes.includes(deploymentType)) | ||||||
|  |     ) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const context = isNarrowScreen() ? 'mobile' : 'desktop'; | ||||||
|  |     if (showContext !== '*' && showContext !== context) { return false; } | ||||||
|  | 
 | ||||||
|  |     return forceShow || (!this._prefs.get().dontShowTips && !this.hasSeenTip(prompt)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public enable() { |   public enable() { | ||||||
|     this._isDisabled = false; |     this._isDisabled = false; | ||||||
|   } |   } | ||||||
| @ -83,8 +114,8 @@ export class BehavioralPromptsManager extends Disposable { | |||||||
|     this.enable(); |     this.enable(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private _queueTip(refElement: Element, prompt: BehavioralPrompt, options: AttachOptions) { |   private _queueTip(refElement: Element, prompt: BehavioralPrompt, options: ShowTipOptions) { | ||||||
|     if (!this._shouldQueueTip(prompt, options)) { return; } |     if (!this.shouldShowTip(prompt)) { return; } | ||||||
| 
 | 
 | ||||||
|     this._queuedTips.push({prompt, refElement, options}); |     this._queuedTips.push({prompt, refElement, options}); | ||||||
|     if (this._queuedTips.length > 1) { |     if (this._queuedTips.length > 1) { | ||||||
| @ -96,15 +127,15 @@ export class BehavioralPromptsManager extends Disposable { | |||||||
|     this._showTip(refElement, prompt, options); |     this._showTip(refElement, prompt, options); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private _showTip(refElement: Element, prompt: BehavioralPrompt, options: AttachOptions) { |   private _showTip(refElement: Element, prompt: BehavioralPrompt, options: ShowTipOptions) { | ||||||
|     const close = () => { |     const close = () => { | ||||||
|       if (!ctl.isDisposed()) { |       if (!ctl.isDisposed()) { | ||||||
|         ctl.close(); |         ctl.close(); | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const {hideArrow, hideDontShowTips, markAsSeen = true, onDispose, popupOptions} = options; |     const {hideArrow, onDispose, popupOptions} = options; | ||||||
|     const {title, content} = GristBehavioralPrompts[prompt]; |     const {title, content, hideDontShowTips = false, markAsSeen = true} = GristBehavioralPrompts[prompt]; | ||||||
|     const ctl = showBehavioralPrompt(refElement, title(), content(), { |     const ctl = showBehavioralPrompt(refElement, title(), content(), { | ||||||
|       onClose: (dontShowTips) => { |       onClose: (dontShowTips) => { | ||||||
|         if (dontShowTips) { this._dontShowTips(); } |         if (dontShowTips) { this._dontShowTips(); } | ||||||
| @ -143,27 +174,4 @@ export class BehavioralPromptsManager extends Disposable { | |||||||
|     this._prefs.set({...this._prefs.get(), dontShowTips: true}); |     this._prefs.set({...this._prefs.get(), dontShowTips: true}); | ||||||
|     this._queuedTips = []; |     this._queuedTips = []; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   private _shouldQueueTip(prompt: BehavioralPrompt, options: AttachOptions) { |  | ||||||
|     if ( |  | ||||||
|       this._isDisabled || |  | ||||||
|       options.shouldShow?.() === false || |  | ||||||
|       (isNarrowScreen() && !options.showOnMobile) || |  | ||||||
|       (this._prefs.get().dontShowTips && !options.forceShow) || |  | ||||||
|       this.hasSeenTip(prompt) |  | ||||||
|     ) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const {deploymentType} = getGristConfig(); |  | ||||||
|     const {deploymentTypes} = GristBehavioralPrompts[prompt]; |  | ||||||
|     if ( |  | ||||||
|       deploymentTypes !== '*' && |  | ||||||
|       (!deploymentType || !deploymentTypes.includes(deploymentType)) |  | ||||||
|     ) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -336,10 +336,6 @@ export class GristDoc extends DisposableWithEvents { | |||||||
|               } |               } | ||||||
| 
 | 
 | ||||||
|               this.behavioralPromptsManager.showTip(cursor, 'rickRow', { |               this.behavioralPromptsManager.showTip(cursor, 'rickRow', { | ||||||
|                 forceShow: true, |  | ||||||
|                 hideDontShowTips: true, |  | ||||||
|                 markAsSeen: false, |  | ||||||
|                 showOnMobile: true, |  | ||||||
|                 onDispose: () => this.playRickRollVideo(), |                 onDispose: () => this.playRickRollVideo(), | ||||||
|               }); |               }); | ||||||
|             }) |             }) | ||||||
| @ -1422,8 +1418,8 @@ export class GristDoc extends DisposableWithEvents { | |||||||
|     if ( |     if ( | ||||||
|       // Don't show the tip if a non-card widget was selected.
 |       // Don't show the tip if a non-card widget was selected.
 | ||||||
|       !['single', 'detail'].includes(selectedWidgetType) || |       !['single', 'detail'].includes(selectedWidgetType) || | ||||||
|       // Or if we've already seen it.
 |       // Or if we shouldn't see the tip.
 | ||||||
|       this.behavioralPromptsManager.hasSeenTip('editCardLayout') |       !this.behavioralPromptsManager.shouldShowTip('editCardLayout') | ||||||
|     ) { |     ) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @ -1449,11 +1445,13 @@ export class GristDoc extends DisposableWithEvents { | |||||||
|   private async _handleNewAttachedCustomWidget(widget: IAttachedCustomWidget) { |   private async _handleNewAttachedCustomWidget(widget: IAttachedCustomWidget) { | ||||||
|     switch (widget) { |     switch (widget) { | ||||||
|       case 'custom.calendar': { |       case 'custom.calendar': { | ||||||
|         // Open the right panel to the calendar subtab.
 |         if (this.behavioralPromptsManager.shouldShowTip('calendarConfig')) { | ||||||
|         commands.allCommands.viewTabOpen.run(); |           // Open the right panel to the calendar subtab.
 | ||||||
|  |           commands.allCommands.viewTabOpen.run(); | ||||||
| 
 | 
 | ||||||
|         // Wait for the right panel to finish animation if it was collapsed before.
 |           // Wait for the right panel to finish animation if it was collapsed before.
 | ||||||
|         await commands.allCommands.rightPanelOpen.run(); |           await commands.allCommands.rightPanelOpen.run(); | ||||||
|  |         } | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -481,9 +481,9 @@ export class CustomSectionConfig extends Disposable { | |||||||
|             popupOptions: { |             popupOptions: { | ||||||
|               placement: 'left-start', |               placement: 'left-start', | ||||||
|             }, |             }, | ||||||
|             shouldShow: () => { |             isDisabled: () => { | ||||||
|               // Only show tip if a custom widget isn't already selected.
 |               // Disable tip if a custom widget is already selected.
 | ||||||
|               return !this._selectedId.get() || (isCustom.get() && this._url.get().trim() === ''); |               return Boolean(this._selectedId.get() && !(isCustom.get() && this._url.get().trim() === '')); | ||||||
|             }, |             }, | ||||||
|           }) |           }) | ||||||
|         ), |         ), | ||||||
|  | |||||||
| @ -103,7 +103,15 @@ see or edit which parts of your document.') | |||||||
| export interface BehavioralPromptContent { | export interface BehavioralPromptContent { | ||||||
|   title: () => string; |   title: () => string; | ||||||
|   content: (...domArgs: DomElementArg[]) => DomContents; |   content: (...domArgs: DomElementArg[]) => DomContents; | ||||||
|   deploymentTypes: GristDeploymentType[] | '*'; |   showDeploymentTypes: GristDeploymentType[] | '*'; | ||||||
|  |   /** Defaults to `desktop`. */ | ||||||
|  |   showContext?: 'mobile' | 'desktop' | '*'; | ||||||
|  |   /** Defaults to `false`. */ | ||||||
|  |   hideDontShowTips?: boolean; | ||||||
|  |   /** Defaults to `false`. */ | ||||||
|  |   forceShow?: boolean; | ||||||
|  |   /** Defaults to `true`. */ | ||||||
|  |   markAsSeen?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptContent> = { | export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptContent> = { | ||||||
| @ -119,7 +127,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo | |||||||
|       ), |       ), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
|   referenceColumnsConfig: { |   referenceColumnsConfig: { | ||||||
|     title: () => t('Reference Columns'), |     title: () => t('Reference Columns'), | ||||||
| @ -134,7 +142,7 @@ record in that table, but you may select which column from that record to show.' | |||||||
|       ), |       ), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
|   rawDataPage: { |   rawDataPage: { | ||||||
|     title: () => t('Raw Data page'), |     title: () => t('Raw Data page'), | ||||||
| @ -144,7 +152,7 @@ including summary tables and tables not included in page layouts.')), | |||||||
|       dom('div', cssLink({href: commonUrls.helpRawData, target: '_blank'}, t('Learn more.'))), |       dom('div', cssLink({href: commonUrls.helpRawData, target: '_blank'}, t('Learn more.'))), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
|   accessRules: { |   accessRules: { | ||||||
|     title: () => t('Access Rules'), |     title: () => t('Access Rules'), | ||||||
| @ -154,7 +162,7 @@ to determine who can see or edit which parts of your document.')), | |||||||
|       dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.'))), |       dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.'))), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
|   filterButtons: { |   filterButtons: { | ||||||
|     title: () => t('Pinning Filters'), |     title: () => t('Pinning Filters'), | ||||||
| @ -164,7 +172,7 @@ to determine who can see or edit which parts of your document.')), | |||||||
|       dom('div', cssLink({href: commonUrls.helpFilterButtons, target: '_blank'}, t('Learn more.'))), |       dom('div', cssLink({href: commonUrls.helpFilterButtons, target: '_blank'}, t('Learn more.'))), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
|   nestedFiltering: { |   nestedFiltering: { | ||||||
|     title: () => t('Nested Filtering'), |     title: () => t('Nested Filtering'), | ||||||
| @ -173,7 +181,7 @@ to determine who can see or edit which parts of your document.')), | |||||||
|       dom('div', t('Only those rows will appear which match all of the filters.')), |       dom('div', t('Only those rows will appear which match all of the filters.')), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
|   pageWidgetPicker: { |   pageWidgetPicker: { | ||||||
|     title: () => t('Selecting Data'), |     title: () => t('Selecting Data'), | ||||||
| @ -182,7 +190,7 @@ to determine who can see or edit which parts of your document.')), | |||||||
|       dom('div', t('Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.')), |       dom('div', t('Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.')), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
|   pageWidgetPickerSelectBy: { |   pageWidgetPickerSelectBy: { | ||||||
|     title: () => t('Linking Widgets'), |     title: () => t('Linking Widgets'), | ||||||
| @ -192,7 +200,7 @@ to determine who can see or edit which parts of your document.')), | |||||||
|       dom('div', cssLink({href: commonUrls.helpLinkingWidgets, target: '_blank'}, t('Learn more.'))), |       dom('div', cssLink({href: commonUrls.helpLinkingWidgets, target: '_blank'}, t('Learn more.'))), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
|   editCardLayout: { |   editCardLayout: { | ||||||
|     title: () => t('Editing Card Layout'), |     title: () => t('Editing Card Layout'), | ||||||
| @ -203,7 +211,7 @@ to determine who can see or edit which parts of your document.')), | |||||||
|       })), |       })), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
|   addNew: { |   addNew: { | ||||||
|     title: () => t('Add New'), |     title: () => t('Add New'), | ||||||
| @ -211,7 +219,7 @@ to determine who can see or edit which parts of your document.')), | |||||||
|       dom('div', t('Click the Add New button to create new documents or workspaces, or import data.')), |       dom('div', t('Click the Add New button to create new documents or workspaces, or import data.')), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
|   rickRow: { |   rickRow: { | ||||||
|     title: () => t('Anchor Links'), |     title: () => t('Anchor Links'), | ||||||
| @ -225,7 +233,11 @@ to determine who can see or edit which parts of your document.')), | |||||||
|       ), |       ), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: '*', |     showDeploymentTypes: '*', | ||||||
|  |     showContext: '*', | ||||||
|  |     hideDontShowTips: true, | ||||||
|  |     forceShow: true, | ||||||
|  |     markAsSeen: false, | ||||||
|   }, |   }, | ||||||
|   customURL: { |   customURL: { | ||||||
|     title: () => t('Custom Widgets'), |     title: () => t('Custom Widgets'), | ||||||
| @ -238,7 +250,7 @@ to determine who can see or edit which parts of your document.')), | |||||||
|       dom('div', cssLink({href: commonUrls.helpCustomWidgets, target: '_blank'}, t('Learn more.'))), |       dom('div', cssLink({href: commonUrls.helpCustomWidgets, target: '_blank'}, t('Learn more.'))), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
|   calendarConfig: { |   calendarConfig: { | ||||||
|     title: () => t('Calendar'), |     title: () => t('Calendar'), | ||||||
| @ -250,6 +262,6 @@ data.")), | |||||||
|       dom('div', cssLink({href: commonUrls.helpCalendarWidget, target: '_blank'}, t('Learn more.'))), |       dom('div', cssLink({href: commonUrls.helpCalendarWidget, target: '_blank'}, t('Learn more.'))), | ||||||
|       ...args, |       ...args, | ||||||
|     ), |     ), | ||||||
|     deploymentTypes: ['saas'], |     showDeploymentTypes: ['saas'], | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -84,7 +84,7 @@ describe('AttachedCustomWidget', function () { | |||||||
|   it('should not ask for permission', async () => { |   it('should not ask for permission', async () => { | ||||||
|     await gu.addNewSection(/Calendar/, /Table1/, {selectBy: /TABLE1/}); |     await gu.addNewSection(/Calendar/, /Table1/, {selectBy: /TABLE1/}); | ||||||
|     await gu.getSection('TABLE1 Calendar').click(); |     await gu.getSection('TABLE1 Calendar').click(); | ||||||
|     await gu.waitForSidePanel(); |     await gu.toggleSidePanel('right', 'open'); | ||||||
|     await driver.find('.test-right-tab-pagewidget').click(); |     await driver.find('.test-right-tab-pagewidget').click(); | ||||||
| 
 | 
 | ||||||
|     await gu.waitForServer(); |     await gu.waitForServer(); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user