mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) Add tip for "Add New" button
Summary: Adds a new tip for the doc menu's Add New button. The tip is shown only when the current user is an editor or owner, and the site is non-empty. The presence of welcome videos or popups will also cause the tip to not be shown; it will instead be shown the next time the doc menu is visited. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3757
This commit is contained in:
		
							parent
							
								
									b7f65ff408
								
							
						
					
					
						commit
						db64dfeef0
					
				@ -335,7 +335,7 @@ export class AccessRules extends Disposable {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  public buildDom() {
 | 
					  public buildDom() {
 | 
				
			||||||
    return cssOuter(
 | 
					    return cssOuter(
 | 
				
			||||||
      dom('div', this._gristDoc.behavioralPrompts.attachTip('accessRules', {
 | 
					      dom('div', this._gristDoc.behavioralPromptsManager.attachTip('accessRules', {
 | 
				
			||||||
        hideArrow: true,
 | 
					        hideArrow: true,
 | 
				
			||||||
      })),
 | 
					      })),
 | 
				
			||||||
      cssAddTableRow(
 | 
					      cssAddTableRow(
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,11 @@
 | 
				
			|||||||
import {showBehavioralPrompt} from 'app/client/components/modals';
 | 
					import {showBehavioralPrompt} from 'app/client/components/modals';
 | 
				
			||||||
import {AppModel} from 'app/client/models/AppModel';
 | 
					import {AppModel} from 'app/client/models/AppModel';
 | 
				
			||||||
 | 
					import {getUserPrefObs} from 'app/client/models/UserPrefs';
 | 
				
			||||||
import {GristBehavioralPrompts} from 'app/client/ui/GristTooltips';
 | 
					import {GristBehavioralPrompts} from 'app/client/ui/GristTooltips';
 | 
				
			||||||
import {isNarrowScreen} from 'app/client/ui2018/cssVars';
 | 
					import {isNarrowScreen} from 'app/client/ui2018/cssVars';
 | 
				
			||||||
import {BehavioralPrompt} from 'app/common/Prefs';
 | 
					import {BehavioralPrompt, BehavioralPromptPrefs} from 'app/common/Prefs';
 | 
				
			||||||
import {getGristConfig} from 'app/common/urlUtils';
 | 
					import {getGristConfig} from 'app/common/urlUtils';
 | 
				
			||||||
import {Computed, Disposable, dom} from 'grainjs';
 | 
					import {Computed, Disposable, dom, Observable} from 'grainjs';
 | 
				
			||||||
import {IPopupOptions} from 'popweasel';
 | 
					import {IPopupOptions} from 'popweasel';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface AttachOptions {
 | 
					export interface AttachOptions {
 | 
				
			||||||
@ -25,12 +26,15 @@ interface QueuedTip {
 | 
				
			|||||||
 *
 | 
					 *
 | 
				
			||||||
 * Tips are shown in the order that they are attached.
 | 
					 * Tips are shown in the order that they are attached.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export class BehavioralPrompts extends Disposable {
 | 
					export class BehavioralPromptsManager extends Disposable {
 | 
				
			||||||
  private _prefs = this._appModel.behavioralPrompts;
 | 
					  private readonly _prefs = getUserPrefObs(this._appModel.userPrefsObs, 'behavioralPrompts',
 | 
				
			||||||
 | 
					    { defaultValue: { dontShowTips: false, dismissedTips: [] } }) as Observable<BehavioralPromptPrefs>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _dismissedTips: Computed<Set<BehavioralPrompt>> = Computed.create(this, use => {
 | 
					  private _dismissedTips: Computed<Set<BehavioralPrompt>> = Computed.create(this, use => {
 | 
				
			||||||
    const {dismissedTips} = use(this._prefs);
 | 
					    const {dismissedTips} = use(this._prefs);
 | 
				
			||||||
    return new Set(dismissedTips.filter(BehavioralPrompt.guard));
 | 
					    return new Set(dismissedTips.filter(BehavioralPrompt.guard));
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _queuedTips: QueuedTip[] = [];
 | 
					  private _queuedTips: QueuedTip[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(private _appModel: AppModel) {
 | 
					  constructor(private _appModel: AppModel) {
 | 
				
			||||||
@ -6,7 +6,6 @@
 | 
				
			|||||||
import {AccessRules} from 'app/client/aclui/AccessRules';
 | 
					import {AccessRules} from 'app/client/aclui/AccessRules';
 | 
				
			||||||
import {ActionLog} from 'app/client/components/ActionLog';
 | 
					import {ActionLog} from 'app/client/components/ActionLog';
 | 
				
			||||||
import BaseView from 'app/client/components/BaseView';
 | 
					import BaseView from 'app/client/components/BaseView';
 | 
				
			||||||
import {BehavioralPrompts} from 'app/client/components/BehavioralPrompts';
 | 
					 | 
				
			||||||
import {isNumericLike, isNumericOnly} from 'app/client/components/ChartView';
 | 
					import {isNumericLike, isNumericOnly} from 'app/client/components/ChartView';
 | 
				
			||||||
import {CodeEditorPanel} from 'app/client/components/CodeEditorPanel';
 | 
					import {CodeEditorPanel} from 'app/client/components/CodeEditorPanel';
 | 
				
			||||||
import * as commands from 'app/client/components/commands';
 | 
					import * as commands from 'app/client/components/commands';
 | 
				
			||||||
@ -166,7 +165,7 @@ export class GristDoc extends DisposableWithEvents {
 | 
				
			|||||||
  // If the doc has a docTour. Used also to enable the UI button to restart the tour.
 | 
					  // If the doc has a docTour. Used also to enable the UI button to restart the tour.
 | 
				
			||||||
  public readonly hasDocTour: Computed<boolean>;
 | 
					  public readonly hasDocTour: Computed<boolean>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public readonly behavioralPrompts = BehavioralPrompts.create(this, this.docPageModel.appModel);
 | 
					  public readonly behavioralPromptsManager = this.docPageModel.appModel.behavioralPromptsManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _actionLog: ActionLog;
 | 
					  private _actionLog: ActionLog;
 | 
				
			||||||
  private _undoStack: UndoStack;
 | 
					  private _undoStack: UndoStack;
 | 
				
			||||||
@ -1100,7 +1099,7 @@ export class GristDoc extends DisposableWithEvents {
 | 
				
			|||||||
      // 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've already seen it.
 | 
				
			||||||
      this.behavioralPrompts.hasSeenTip('editCardLayout')
 | 
					      this.behavioralPromptsManager.hasSeenTip('editCardLayout')
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -1114,7 +1113,7 @@ export class GristDoc extends DisposableWithEvents {
 | 
				
			|||||||
    const editLayoutButton = document.querySelector('.behavioral-prompt-edit-card-layout');
 | 
					    const editLayoutButton = document.querySelector('.behavioral-prompt-edit-card-layout');
 | 
				
			||||||
    if (!editLayoutButton) { throw new Error('GristDoc failed to find edit card layout button'); }
 | 
					    if (!editLayoutButton) { throw new Error('GristDoc failed to find edit card layout button'); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.behavioralPrompts.showTip(editLayoutButton, 'editCardLayout', {
 | 
					    this.behavioralPromptsManager.showTip(editLayoutButton, 'editCardLayout', {
 | 
				
			||||||
      popupOptions: {
 | 
					      popupOptions: {
 | 
				
			||||||
        placement: 'left-start',
 | 
					        placement: 'left-start',
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
				
			|||||||
@ -42,7 +42,7 @@ export class RawDataPage extends Disposable {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  public buildDom() {
 | 
					  public buildDom() {
 | 
				
			||||||
    return cssContainer(
 | 
					    return cssContainer(
 | 
				
			||||||
      dom('div', this._gristDoc.behavioralPrompts.attachTip('rawDataPage', {hideArrow: true})),
 | 
					      dom('div', this._gristDoc.behavioralPromptsManager.attachTip('rawDataPage', {hideArrow: true})),
 | 
				
			||||||
      dom('div',
 | 
					      dom('div',
 | 
				
			||||||
        dom.create(DataTables, this._gristDoc),
 | 
					        dom.create(DataTables, this._gristDoc),
 | 
				
			||||||
        dom.create(DocumentUsage, this._gristDoc.docPageModel),
 | 
					        dom.create(DocumentUsage, this._gristDoc.docPageModel),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import {BehavioralPromptsManager} from 'app/client/components/BehavioralPromptsManager';
 | 
				
			||||||
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
 | 
					import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
 | 
				
			||||||
import {makeT} from 'app/client/lib/localization';
 | 
					import {makeT} from 'app/client/lib/localization';
 | 
				
			||||||
import {error} from 'app/client/lib/log';
 | 
					import {error} from 'app/client/lib/log';
 | 
				
			||||||
@ -12,9 +13,8 @@ import {Features, isLegacyPlan, Product} from 'app/common/Features';
 | 
				
			|||||||
import {GristLoadConfig} from 'app/common/gristUrls';
 | 
					import {GristLoadConfig} from 'app/common/gristUrls';
 | 
				
			||||||
import {FullUser} from 'app/common/LoginSessionAPI';
 | 
					import {FullUser} from 'app/common/LoginSessionAPI';
 | 
				
			||||||
import {LocalPlugin} from 'app/common/plugin';
 | 
					import {LocalPlugin} from 'app/common/plugin';
 | 
				
			||||||
import {BehavioralPromptPrefs, DeprecationWarning, DismissedPopup, DismissedReminder,
 | 
					import {DeprecationWarning, DismissedPopup, DismissedReminder, UserPrefs} from 'app/common/Prefs';
 | 
				
			||||||
        UserPrefs} from 'app/common/Prefs';
 | 
					import {isOwner, isOwnerOrEditor} from 'app/common/roles';
 | 
				
			||||||
import {isOwner} from 'app/common/roles';
 | 
					 | 
				
			||||||
import {getTagManagerScript} from 'app/common/tagManager';
 | 
					import {getTagManagerScript} from 'app/common/tagManager';
 | 
				
			||||||
import {getDefaultThemePrefs, Theme, ThemeAppearance, ThemeColors, ThemePrefs,
 | 
					import {getDefaultThemePrefs, Theme, ThemeAppearance, ThemeColors, ThemePrefs,
 | 
				
			||||||
        ThemePrefsChecker} from 'app/common/ThemePrefs';
 | 
					        ThemePrefsChecker} from 'app/common/ThemePrefs';
 | 
				
			||||||
@ -99,19 +99,21 @@ export interface AppModel {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  deprecatedWarnings: Observable<DeprecationWarning[]>;
 | 
					  deprecatedWarnings: Observable<DeprecationWarning[]>;
 | 
				
			||||||
  dismissedWelcomePopups: Observable<DismissedReminder[]>;
 | 
					  dismissedWelcomePopups: Observable<DismissedReminder[]>;
 | 
				
			||||||
  behavioralPrompts: Observable<BehavioralPromptPrefs>;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pageType: Observable<PageType>;
 | 
					  pageType: Observable<PageType>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  notifier: Notifier;
 | 
					  notifier: Notifier;
 | 
				
			||||||
  planName: string|null;
 | 
					  planName: string|null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  behavioralPromptsManager: BehavioralPromptsManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  refreshOrgUsage(): Promise<void>;
 | 
					  refreshOrgUsage(): Promise<void>;
 | 
				
			||||||
  showUpgradeModal(): void;
 | 
					  showUpgradeModal(): void;
 | 
				
			||||||
  showNewSiteModal(): void;
 | 
					  showNewSiteModal(): void;
 | 
				
			||||||
  isBillingManager(): boolean;          // If user is a billing manager for this org
 | 
					  isBillingManager(): boolean;          // If user is a billing manager for this org
 | 
				
			||||||
  isSupport(): boolean;                 // If user is a Support user
 | 
					  isSupport(): boolean;                 // If user is a Support user
 | 
				
			||||||
  isOwner(): boolean;                   // If user is an owner of this org
 | 
					  isOwner(): boolean;                   // If user is an owner of this org
 | 
				
			||||||
 | 
					  isOwnerOrEditor(): boolean;           // If user is an owner or editor of this org
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class TopAppModelImpl extends Disposable implements TopAppModel {
 | 
					export class TopAppModelImpl extends Disposable implements TopAppModel {
 | 
				
			||||||
@ -246,8 +248,6 @@ export class AppModelImpl extends Disposable implements AppModel {
 | 
				
			|||||||
    { defaultValue: [] }) as Observable<DeprecationWarning[]>;
 | 
					    { defaultValue: [] }) as Observable<DeprecationWarning[]>;
 | 
				
			||||||
  public readonly dismissedWelcomePopups = getUserPrefObs(this.userPrefsObs, 'dismissedWelcomePopups',
 | 
					  public readonly dismissedWelcomePopups = getUserPrefObs(this.userPrefsObs, 'dismissedWelcomePopups',
 | 
				
			||||||
    { defaultValue: [] }) as Observable<DismissedReminder[]>;
 | 
					    { defaultValue: [] }) as Observable<DismissedReminder[]>;
 | 
				
			||||||
  public readonly behavioralPrompts = getUserPrefObs(this.userPrefsObs, 'behavioralPrompts',
 | 
					 | 
				
			||||||
    { defaultValue: { dontShowTips: false, dismissedTips: [] } }) as Observable<BehavioralPromptPrefs>;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Get the current PageType from the URL.
 | 
					  // Get the current PageType from the URL.
 | 
				
			||||||
  public readonly pageType: Observable<PageType> = Computed.create(this, urlState().state,
 | 
					  public readonly pageType: Observable<PageType> = Computed.create(this, urlState().state,
 | 
				
			||||||
@ -255,6 +255,9 @@ export class AppModelImpl extends Disposable implements AppModel {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  public readonly notifier = this.topAppModel.notifier;
 | 
					  public readonly notifier = this.topAppModel.notifier;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public readonly behavioralPromptsManager: BehavioralPromptsManager =
 | 
				
			||||||
 | 
					    BehavioralPromptsManager.create(this, this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    public readonly topAppModel: TopAppModel,
 | 
					    public readonly topAppModel: TopAppModel,
 | 
				
			||||||
    public readonly currentUser: FullUser|null,
 | 
					    public readonly currentUser: FullUser|null,
 | 
				
			||||||
@ -314,6 +317,10 @@ export class AppModelImpl extends Disposable implements AppModel {
 | 
				
			|||||||
    return Boolean(this.currentOrg && isOwner(this.currentOrg));
 | 
					    return Boolean(this.currentOrg && isOwner(this.currentOrg));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public isOwnerOrEditor() {
 | 
				
			||||||
 | 
					    return Boolean(this.currentOrg && isOwnerOrEditor(this.currentOrg));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Fetch and update the current org's usage.
 | 
					   * Fetch and update the current org's usage.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
 | 
				
			|||||||
@ -75,6 +75,8 @@ export interface HomeModel {
 | 
				
			|||||||
  // user isn't allowed to create a doc.
 | 
					  // user isn't allowed to create a doc.
 | 
				
			||||||
  newDocWorkspace: Observable<Workspace|null|"unsaved">;
 | 
					  newDocWorkspace: Observable<Workspace|null|"unsaved">;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  shouldShowAddNewTip: Observable<boolean>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createWorkspace(name: string): Promise<void>;
 | 
					  createWorkspace(name: string): Promise<void>;
 | 
				
			||||||
  renameWorkspace(id: number, name: string): Promise<void>;
 | 
					  renameWorkspace(id: number, name: string): Promise<void>;
 | 
				
			||||||
  deleteWorkspace(id: number, forever: boolean): Promise<void>;
 | 
					  deleteWorkspace(id: number, forever: boolean): Promise<void>;
 | 
				
			||||||
@ -154,6 +156,9 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
 | 
				
			|||||||
  public readonly showIntro = Computed.create(this, this.workspaces, (use, wss) => (
 | 
					  public readonly showIntro = Computed.create(this, this.workspaces, (use, wss) => (
 | 
				
			||||||
    wss.every((ws) => ws.isSupportWorkspace || ws.docs.length === 0)));
 | 
					    wss.every((ws) => ws.isSupportWorkspace || ws.docs.length === 0)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public readonly shouldShowAddNewTip = Observable.create(this,
 | 
				
			||||||
 | 
					    !this._app.behavioralPromptsManager.hasSeenTip('addNew'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _userOrgPrefs = Observable.create<UserOrgPrefs|undefined>(this, this._app.currentOrg?.userOrgPrefs);
 | 
					  private _userOrgPrefs = Observable.create<UserOrgPrefs|undefined>(this, this._app.currentOrg?.userOrgPrefs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(private _app: AppModel, clientScope: ClientScope) {
 | 
					  constructor(private _app: AppModel, clientScope: ClientScope) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										53
									
								
								app/client/ui/AddNewTip.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								app/client/ui/AddNewTip.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					import {HomeModel} from 'app/client/models/HomeModel';
 | 
				
			||||||
 | 
					import {shouldShowWelcomeQuestions} from 'app/client/ui/WelcomeQuestions';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function attachAddNewTip(home: HomeModel): (el: Element) => void {
 | 
				
			||||||
 | 
					  return () => {
 | 
				
			||||||
 | 
					    const {app: {userPrefsObs}} = home;
 | 
				
			||||||
 | 
					    if (shouldShowWelcomeQuestions(userPrefsObs)) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (shouldShowAddNewTip(home)) {
 | 
				
			||||||
 | 
					      showAddNewTip(home);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function shouldShowAddNewTip(home: HomeModel): boolean {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    // Only show if the user is an owner or editor.
 | 
				
			||||||
 | 
					    home.app.isOwnerOrEditor() &&
 | 
				
			||||||
 | 
					    // And the tip hasn't been shown before.
 | 
				
			||||||
 | 
					    home.shouldShowAddNewTip.get() &&
 | 
				
			||||||
 | 
					    // And the intro isn't being shown.
 | 
				
			||||||
 | 
					    !home.showIntro.get() &&
 | 
				
			||||||
 | 
					    // And the workspace loaded correctly.
 | 
				
			||||||
 | 
					    home.available.get() &&
 | 
				
			||||||
 | 
					    // And the current page isn't /p/trash; the Add New button is limited there.
 | 
				
			||||||
 | 
					    home.currentPage.get() !== 'trash'
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function showAddNewTip(home: HomeModel): void {
 | 
				
			||||||
 | 
					  const addNewButton = document.querySelector('.behavioral-prompt-add-new');
 | 
				
			||||||
 | 
					  if (!addNewButton) {
 | 
				
			||||||
 | 
					    console.warn('AddNewTip failed to find Add New button');
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!isVisible(addNewButton as HTMLElement)) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  home.app.behavioralPromptsManager.showTip(addNewButton, 'addNew', {
 | 
				
			||||||
 | 
					    popupOptions: {
 | 
				
			||||||
 | 
					      placement: 'right-start',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onDispose: () => home.shouldShowAddNewTip.set(false),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isVisible(element: HTMLElement): boolean {
 | 
				
			||||||
 | 
					  // From https://github.com/jquery/jquery/blob/c66d4700dcf98efccb04061d575e242d28741223/src/css/hiddenVisibleSelectors.js.
 | 
				
			||||||
 | 
					  return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -352,7 +352,7 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
 | 
				
			|||||||
              icon('PinTilted'),
 | 
					              icon('PinTilted'),
 | 
				
			||||||
              cssPinButton.cls('-pinned', model.filterInfo.isPinned),
 | 
					              cssPinButton.cls('-pinned', model.filterInfo.isPinned),
 | 
				
			||||||
              dom.on('click', () => filterInfo.pinned(!filterInfo.pinned())),
 | 
					              dom.on('click', () => filterInfo.pinned(!filterInfo.pinned())),
 | 
				
			||||||
              gristDoc.behavioralPrompts.attachTip('filterButtons', {
 | 
					              gristDoc.behavioralPromptsManager.attachTip('filterButtons', {
 | 
				
			||||||
                popupOptions: {
 | 
					                popupOptions: {
 | 
				
			||||||
                  attach: null,
 | 
					                  attach: null,
 | 
				
			||||||
                  modifiers: {
 | 
					                  modifiers: {
 | 
				
			||||||
 | 
				
			|||||||
@ -4,18 +4,19 @@
 | 
				
			|||||||
 * Orgs, workspaces and docs are fetched asynchronously on build via the passed in API.
 | 
					 * Orgs, workspaces and docs are fetched asynchronously on build via the passed in API.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
import {loadUserManager} from 'app/client/lib/imports';
 | 
					import {loadUserManager} from 'app/client/lib/imports';
 | 
				
			||||||
import {AppModel, reportError} from 'app/client/models/AppModel';
 | 
					import {reportError} from 'app/client/models/AppModel';
 | 
				
			||||||
import {docUrl, urlState} from 'app/client/models/gristUrlState';
 | 
					import {docUrl, urlState} from 'app/client/models/gristUrlState';
 | 
				
			||||||
import {getTimeFromNow, HomeModel, makeLocalViewSettings, ViewSettings} from 'app/client/models/HomeModel';
 | 
					import {getTimeFromNow, HomeModel, makeLocalViewSettings, ViewSettings} from 'app/client/models/HomeModel';
 | 
				
			||||||
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
 | 
					import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
 | 
				
			||||||
 | 
					import {attachAddNewTip} from 'app/client/ui/AddNewTip';
 | 
				
			||||||
import * as css from 'app/client/ui/DocMenuCss';
 | 
					import * as css from 'app/client/ui/DocMenuCss';
 | 
				
			||||||
import {buildHomeIntro, buildWorkspaceIntro} from 'app/client/ui/HomeIntro';
 | 
					import {buildHomeIntro, buildWorkspaceIntro} from 'app/client/ui/HomeIntro';
 | 
				
			||||||
import {buildUpgradeButton} from 'app/client/ui/ProductUpgrades';
 | 
					import {buildUpgradeButton} from 'app/client/ui/ProductUpgrades';
 | 
				
			||||||
import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs';
 | 
					import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs';
 | 
				
			||||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
 | 
					import {shadowScroll} from 'app/client/ui/shadowScroll';
 | 
				
			||||||
import {transition} from 'app/client/ui/transitions';
 | 
					import {transition} from 'app/client/ui/transitions';
 | 
				
			||||||
import {showWelcomeCoachingCall} from 'app/client/ui/WelcomeCoachingCall';
 | 
					import {shouldShowWelcomeCoachingCall, showWelcomeCoachingCall} from 'app/client/ui/WelcomeCoachingCall';
 | 
				
			||||||
import {showWelcomeQuestions} from 'app/client/ui/WelcomeQuestions';
 | 
					import {shouldShowWelcomeQuestions, showWelcomeQuestions} from 'app/client/ui/WelcomeQuestions';
 | 
				
			||||||
import {createVideoTourTextButton} from 'app/client/ui/OpenVideoTour';
 | 
					import {createVideoTourTextButton} from 'app/client/ui/OpenVideoTour';
 | 
				
			||||||
import {buttonSelect, cssButtonSelect} from 'app/client/ui2018/buttonSelect';
 | 
					import {buttonSelect, cssButtonSelect} from 'app/client/ui2018/buttonSelect';
 | 
				
			||||||
import {isNarrowScreenObs, theme} from 'app/client/ui2018/cssVars';
 | 
					import {isNarrowScreenObs, theme} from 'app/client/ui2018/cssVars';
 | 
				
			||||||
@ -47,7 +48,7 @@ const testId = makeTestId('test-dm-');
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export function createDocMenu(home: HomeModel): DomElementArg[] {
 | 
					export function createDocMenu(home: HomeModel): DomElementArg[] {
 | 
				
			||||||
  return [
 | 
					  return [
 | 
				
			||||||
    attachWelcomePopups(home.app),
 | 
					    attachWelcomePopups(home),
 | 
				
			||||||
    dom.domComputed(home.loading, loading => (
 | 
					    dom.domComputed(home.loading, loading => (
 | 
				
			||||||
      loading === 'slow' ? css.spinner(loadingSpinner()) :
 | 
					      loading === 'slow' ? css.spinner(loadingSpinner()) :
 | 
				
			||||||
      loading ? null :
 | 
					      loading ? null :
 | 
				
			||||||
@ -56,12 +57,14 @@ export function createDocMenu(home: HomeModel): DomElementArg[] {
 | 
				
			|||||||
  ];
 | 
					  ];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function attachWelcomePopups(app: AppModel): (el: Element) => void {
 | 
					function attachWelcomePopups(home: HomeModel): (el: Element) => void {
 | 
				
			||||||
  return (element: Element) => {
 | 
					  return (element: Element) => {
 | 
				
			||||||
    const isShowingPopup = showWelcomeQuestions(app.userPrefsObs);
 | 
					    const {app, app: {userPrefsObs}} = home;
 | 
				
			||||||
    if (isShowingPopup) { return; }
 | 
					    if (shouldShowWelcomeQuestions(userPrefsObs)) {
 | 
				
			||||||
 | 
					      showWelcomeQuestions(userPrefsObs);
 | 
				
			||||||
    showWelcomeCoachingCall(element, app);
 | 
					    } else if (shouldShowWelcomeCoachingCall(app)) {
 | 
				
			||||||
 | 
					      showWelcomeCoachingCall(element, app);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -70,6 +73,8 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
 | 
				
			|||||||
  const upgradeButton = buildUpgradeButton(owner, home.app);
 | 
					  const upgradeButton = buildUpgradeButton(owner, home.app);
 | 
				
			||||||
  return css.docList(
 | 
					  return css.docList(
 | 
				
			||||||
    css.docMenu(
 | 
					    css.docMenu(
 | 
				
			||||||
 | 
					      attachAddNewTip(home),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      dom.maybe(!home.app.currentFeatures.workspaces, () => [
 | 
					      dom.maybe(!home.app.currentFeatures.workspaces, () => [
 | 
				
			||||||
        css.docListHeader(t("This service is not available right now")),
 | 
					        css.docListHeader(t("This service is not available right now")),
 | 
				
			||||||
        dom('span', t("(The organization needs a paid plan)")),
 | 
					        dom('span', t("(The organization needs a paid plan)")),
 | 
				
			||||||
 | 
				
			|||||||
@ -24,7 +24,7 @@ export function filterBar(
 | 
				
			|||||||
    dom.forEach(viewSection.activeFilters, (filterInfo) => makeFilterField(filterInfo, popupControls)),
 | 
					    dom.forEach(viewSection.activeFilters, (filterInfo) => makeFilterField(filterInfo, popupControls)),
 | 
				
			||||||
    dom.maybe(viewSection.showNestedFilteringPopup, () => {
 | 
					    dom.maybe(viewSection.showNestedFilteringPopup, () => {
 | 
				
			||||||
      return dom('div',
 | 
					      return dom('div',
 | 
				
			||||||
        gristDoc.behavioralPrompts.attachTip('nestedFiltering', {
 | 
					        gristDoc.behavioralPromptsManager.attachTip('nestedFiltering', {
 | 
				
			||||||
          onDispose: () => viewSection.showNestedFilteringPopup.set(false),
 | 
					          onDispose: () => viewSection.showNestedFilteringPopup.set(false),
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
				
			|||||||
@ -188,4 +188,12 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
 | 
				
			|||||||
      ...args,
 | 
					      ...args,
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  addNew: {
 | 
				
			||||||
 | 
					    title: 'Add New',
 | 
				
			||||||
 | 
					    content: (...args: DomElementArg[]) => cssTooltipContent(
 | 
				
			||||||
 | 
					      dom('div', 'Click the Add New button to create new documents or workspaces, '
 | 
				
			||||||
 | 
					       + 'or import data.'),
 | 
				
			||||||
 | 
					      ...args,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -38,7 +38,8 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
 | 
				
			|||||||
        // "Add New" menu should have the same width as the "Add New" button that opens it.
 | 
					        // "Add New" menu should have the same width as the "Add New" button that opens it.
 | 
				
			||||||
        stretchToSelector: `.${cssAddNewButton.className}`
 | 
					        stretchToSelector: `.${cssAddNewButton.className}`
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
      testId('dm-add-new')
 | 
					      dom.cls('behavioral-prompt-add-new'),
 | 
				
			||||||
 | 
					      testId('dm-add-new'),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    cssScrollPane(
 | 
					    cssScrollPane(
 | 
				
			||||||
      cssPageEntry(
 | 
					      cssPageEntry(
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { BehavioralPrompts } from 'app/client/components/BehavioralPrompts';
 | 
					import { BehavioralPromptsManager } from 'app/client/components/BehavioralPromptsManager';
 | 
				
			||||||
import { GristDoc } from 'app/client/components/GristDoc';
 | 
					import { GristDoc } from 'app/client/components/GristDoc';
 | 
				
			||||||
import { makeT } from 'app/client/lib/localization';
 | 
					import { makeT } from 'app/client/lib/localization';
 | 
				
			||||||
import { reportError } from 'app/client/models/AppModel';
 | 
					import { reportError } from 'app/client/models/AppModel';
 | 
				
			||||||
@ -138,7 +138,7 @@ export function buildPageWidgetPicker(
 | 
				
			|||||||
  onSave: ISaveFunc,
 | 
					  onSave: ISaveFunc,
 | 
				
			||||||
  options: IOptions = {}
 | 
					  options: IOptions = {}
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const {behavioralPrompts, docModel} = gristDoc;
 | 
					  const {behavioralPromptsManager, docModel} = gristDoc;
 | 
				
			||||||
  const tables = fromKo(docModel.visibleTables.getObservable());
 | 
					  const tables = fromKo(docModel.visibleTables.getObservable());
 | 
				
			||||||
  const columns = fromKo(docModel.columns.createAllRowsModel('parentPos').getObservable());
 | 
					  const columns = fromKo(docModel.columns.createAllRowsModel('parentPos').getObservable());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -207,7 +207,8 @@ export function buildPageWidgetPicker(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // dom
 | 
					  // dom
 | 
				
			||||||
  return cssPopupWrapper(
 | 
					  return cssPopupWrapper(
 | 
				
			||||||
    dom.create(PageWidgetSelect, value, tables, columns, onSaveCB, behavioralPrompts, options),
 | 
					    dom.create(PageWidgetSelect,
 | 
				
			||||||
 | 
					      value, tables, columns, onSaveCB, behavioralPromptsManager, options),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // gives focus and binds keydown events
 | 
					    // gives focus and binds keydown events
 | 
				
			||||||
    (elem: any) => { setTimeout(() => elem.focus(), 0); },
 | 
					    (elem: any) => { setTimeout(() => elem.focus(), 0); },
 | 
				
			||||||
@ -276,7 +277,7 @@ export class PageWidgetSelect extends Disposable {
 | 
				
			|||||||
    private _tables: Observable<TableRec[]>,
 | 
					    private _tables: Observable<TableRec[]>,
 | 
				
			||||||
    private _columns: Observable<ColumnRec[]>,
 | 
					    private _columns: Observable<ColumnRec[]>,
 | 
				
			||||||
    private _onSave: () => Promise<void>,
 | 
					    private _onSave: () => Promise<void>,
 | 
				
			||||||
    private _behavioralPrompts: BehavioralPrompts,
 | 
					    private _behavioralPromptsManager: BehavioralPromptsManager,
 | 
				
			||||||
    private _options: ISelectOptions = {}
 | 
					    private _options: ISelectOptions = {}
 | 
				
			||||||
  ) { super(); }
 | 
					  ) { super(); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -307,7 +308,7 @@ export class PageWidgetSelect extends Disposable {
 | 
				
			|||||||
            cssIcon('TypeTable'), 'New Table',
 | 
					            cssIcon('TypeTable'), 'New Table',
 | 
				
			||||||
            // prevent the selection of 'New Table' if it is disabled
 | 
					            // prevent the selection of 'New Table' if it is disabled
 | 
				
			||||||
            dom.on('click', (ev) => !this._isNewTableDisabled.get() && this._selectTable('New Table')),
 | 
					            dom.on('click', (ev) => !this._isNewTableDisabled.get() && this._selectTable('New Table')),
 | 
				
			||||||
            this._behavioralPrompts.attachTip('pageWidgetPicker', {
 | 
					            this._behavioralPromptsManager.attachTip('pageWidgetPicker', {
 | 
				
			||||||
              popupOptions: {
 | 
					              popupOptions: {
 | 
				
			||||||
                attach: null,
 | 
					                attach: null,
 | 
				
			||||||
                placement: 'right-start',
 | 
					                placement: 'right-start',
 | 
				
			||||||
@ -365,7 +366,7 @@ export class PageWidgetSelect extends Disposable {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
              GristTooltips.selectBy(),
 | 
					              GristTooltips.selectBy(),
 | 
				
			||||||
              {tooltipMenuOptions: {attach: null}, domArgs: [
 | 
					              {tooltipMenuOptions: {attach: null}, domArgs: [
 | 
				
			||||||
                this._behavioralPrompts.attachTip('pageWidgetPickerSelectBy', {
 | 
					                this._behavioralPromptsManager.attachTip('pageWidgetPickerSelectBy', {
 | 
				
			||||||
                  popupOptions: {
 | 
					                  popupOptions: {
 | 
				
			||||||
                    attach: null,
 | 
					                    attach: null,
 | 
				
			||||||
                    placement: 'bottom',
 | 
					                    placement: 'bottom',
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,9 @@
 | 
				
			|||||||
import {AppModel} from 'app/client/models/AppModel';
 | 
					import {AppModel} from 'app/client/models/AppModel';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function showWelcomeCoachingCall(_triggerElement: Element, _app: AppModel): boolean {
 | 
					export function shouldShowWelcomeCoachingCall(_app: AppModel) {
 | 
				
			||||||
  return false;
 | 
					  return false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function showWelcomeCoachingCall(_triggerElement: Element, _app: AppModel) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,17 +12,15 @@ import {dom, input, Observable, styled, subscribeElem} from 'grainjs';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const t = makeT('WelcomeQuestions');
 | 
					const t = makeT('WelcomeQuestions');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function shouldShowWelcomeQuestions(userPrefsObs: Observable<UserPrefs>): boolean {
 | 
				
			||||||
 | 
					  return Boolean(getGristConfig().survey && userPrefsObs.get()?.showNewUserQuestions);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Shows a modal with welcome questions if surveying is enabled and the user hasn't
 | 
					 * Shows a modal with welcome questions if surveying is enabled and the user hasn't
 | 
				
			||||||
 * dismissed the modal before.
 | 
					 * dismissed the modal before.
 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Returns a boolean indicating whether the modal was shown or not.
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>): boolean {
 | 
					export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>) {
 | 
				
			||||||
  if (!(getGristConfig().survey && userPrefsObs.get()?.showNewUserQuestions)) {
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  saveModal((ctl, owner): ISaveModalOptions => {
 | 
					  saveModal((ctl, owner): ISaveModalOptions => {
 | 
				
			||||||
    const selection = choices.map(c => Observable.create(owner, false));
 | 
					    const selection = choices.map(c => Observable.create(owner, false));
 | 
				
			||||||
    const otherText = Observable.create(owner, '');
 | 
					    const otherText = Observable.create(owner, '');
 | 
				
			||||||
@ -60,8 +58,6 @@ export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>): boole
 | 
				
			|||||||
      modalArgs: cssModalCentered.cls(''),
 | 
					      modalArgs: cssModalCentered.cls(''),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  return true;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const choices: Array<{icon: IconName, color: string, textKey: string}> = [
 | 
					const choices: Array<{icon: IconName, color: string, textKey: string}> = [
 | 
				
			||||||
 | 
				
			|||||||
@ -294,7 +294,7 @@ export class FieldBuilder extends Disposable {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (op.label === 'Reference') {
 | 
					            if (op.label === 'Reference') {
 | 
				
			||||||
              return this.gristDoc.behavioralPrompts.attachTip('referenceColumns', {
 | 
					              return this.gristDoc.behavioralPromptsManager.attachTip('referenceColumns', {
 | 
				
			||||||
                popupOptions: {
 | 
					                popupOptions: {
 | 
				
			||||||
                  attach: `.${cssTypeSelectMenu.className}`,
 | 
					                  attach: `.${cssTypeSelectMenu.className}`,
 | 
				
			||||||
                  placement: 'left-start',
 | 
					                  placement: 'left-start',
 | 
				
			||||||
@ -370,7 +370,7 @@ export class FieldBuilder extends Disposable {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
      cssLabel('DATA FROM TABLE',
 | 
					      cssLabel('DATA FROM TABLE',
 | 
				
			||||||
        !this._showRefConfigPopup.peek() ? null : this.gristDoc.behavioralPrompts.attachTip(
 | 
					        !this._showRefConfigPopup.peek() ? null : this.gristDoc.behavioralPromptsManager.attachTip(
 | 
				
			||||||
          'referenceColumnsConfig',
 | 
					          'referenceColumnsConfig',
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            onDispose: () => this._showRefConfigPopup(false),
 | 
					            onDispose: () => this._showRefConfigPopup(false),
 | 
				
			||||||
 | 
				
			|||||||
@ -77,6 +77,7 @@ export const BehavioralPrompt = StringUnion(
 | 
				
			|||||||
  'pageWidgetPicker',
 | 
					  'pageWidgetPicker',
 | 
				
			||||||
  'pageWidgetPickerSelectBy',
 | 
					  'pageWidgetPickerSelectBy',
 | 
				
			||||||
  'editCardLayout',
 | 
					  'editCardLayout',
 | 
				
			||||||
 | 
					  'addNew',
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
export type BehavioralPrompt = typeof BehavioralPrompt.type;
 | 
					export type BehavioralPrompt = typeof BehavioralPrompt.type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -45,6 +45,10 @@ export function isOwner(resource: {access: Role}|null): resource is {access: Rol
 | 
				
			|||||||
  return resource?.access === OWNER;
 | 
					  return resource?.access === OWNER;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isOwnerOrEditor(resource: {access: Role}|null): resource is {access: Role} {
 | 
				
			||||||
 | 
					  return canEdit(resource?.access ?? null);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function canUpgradeOrg(org: Organization|null): org is Organization {
 | 
					export function canUpgradeOrg(org: Organization|null): org is Organization {
 | 
				
			||||||
  // TODO: Need to consider billing managers and support user.
 | 
					  // TODO: Need to consider billing managers and support user.
 | 
				
			||||||
  return isOwner(org);
 | 
					  return isOwner(org);
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user