mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +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);
|
||||||
|
} else if (shouldShowWelcomeCoachingCall(app)) {
|
||||||
showWelcomeCoachingCall(element, 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