mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +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:
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'),
|
||||
cssPinButton.cls('-pinned', model.filterInfo.isPinned),
|
||||
dom.on('click', () => filterInfo.pinned(!filterInfo.pinned())),
|
||||
gristDoc.behavioralPrompts.attachTip('filterButtons', {
|
||||
gristDoc.behavioralPromptsManager.attachTip('filterButtons', {
|
||||
popupOptions: {
|
||||
attach: null,
|
||||
modifiers: {
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
* Orgs, workspaces and docs are fetched asynchronously on build via the passed in API.
|
||||
*/
|
||||
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 {getTimeFromNow, HomeModel, makeLocalViewSettings, ViewSettings} from 'app/client/models/HomeModel';
|
||||
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||
import {attachAddNewTip} from 'app/client/ui/AddNewTip';
|
||||
import * as css from 'app/client/ui/DocMenuCss';
|
||||
import {buildHomeIntro, buildWorkspaceIntro} from 'app/client/ui/HomeIntro';
|
||||
import {buildUpgradeButton} from 'app/client/ui/ProductUpgrades';
|
||||
import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs';
|
||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||
import {transition} from 'app/client/ui/transitions';
|
||||
import {showWelcomeCoachingCall} from 'app/client/ui/WelcomeCoachingCall';
|
||||
import {showWelcomeQuestions} from 'app/client/ui/WelcomeQuestions';
|
||||
import {shouldShowWelcomeCoachingCall, showWelcomeCoachingCall} from 'app/client/ui/WelcomeCoachingCall';
|
||||
import {shouldShowWelcomeQuestions, showWelcomeQuestions} from 'app/client/ui/WelcomeQuestions';
|
||||
import {createVideoTourTextButton} from 'app/client/ui/OpenVideoTour';
|
||||
import {buttonSelect, cssButtonSelect} from 'app/client/ui2018/buttonSelect';
|
||||
import {isNarrowScreenObs, theme} from 'app/client/ui2018/cssVars';
|
||||
@@ -47,7 +48,7 @@ const testId = makeTestId('test-dm-');
|
||||
*/
|
||||
export function createDocMenu(home: HomeModel): DomElementArg[] {
|
||||
return [
|
||||
attachWelcomePopups(home.app),
|
||||
attachWelcomePopups(home),
|
||||
dom.domComputed(home.loading, loading => (
|
||||
loading === 'slow' ? css.spinner(loadingSpinner()) :
|
||||
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) => {
|
||||
const isShowingPopup = showWelcomeQuestions(app.userPrefsObs);
|
||||
if (isShowingPopup) { return; }
|
||||
|
||||
showWelcomeCoachingCall(element, app);
|
||||
const {app, app: {userPrefsObs}} = home;
|
||||
if (shouldShowWelcomeQuestions(userPrefsObs)) {
|
||||
showWelcomeQuestions(userPrefsObs);
|
||||
} else if (shouldShowWelcomeCoachingCall(app)) {
|
||||
showWelcomeCoachingCall(element, app);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,6 +73,8 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
|
||||
const upgradeButton = buildUpgradeButton(owner, home.app);
|
||||
return css.docList(
|
||||
css.docMenu(
|
||||
attachAddNewTip(home),
|
||||
|
||||
dom.maybe(!home.app.currentFeatures.workspaces, () => [
|
||||
css.docListHeader(t("This service is not available right now")),
|
||||
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.maybe(viewSection.showNestedFilteringPopup, () => {
|
||||
return dom('div',
|
||||
gristDoc.behavioralPrompts.attachTip('nestedFiltering', {
|
||||
gristDoc.behavioralPromptsManager.attachTip('nestedFiltering', {
|
||||
onDispose: () => viewSection.showNestedFilteringPopup.set(false),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -188,4 +188,12 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
|
||||
...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.
|
||||
stretchToSelector: `.${cssAddNewButton.className}`
|
||||
}),
|
||||
testId('dm-add-new')
|
||||
dom.cls('behavioral-prompt-add-new'),
|
||||
testId('dm-add-new'),
|
||||
),
|
||||
cssScrollPane(
|
||||
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 { makeT } from 'app/client/lib/localization';
|
||||
import { reportError } from 'app/client/models/AppModel';
|
||||
@@ -138,7 +138,7 @@ export function buildPageWidgetPicker(
|
||||
onSave: ISaveFunc,
|
||||
options: IOptions = {}
|
||||
) {
|
||||
const {behavioralPrompts, docModel} = gristDoc;
|
||||
const {behavioralPromptsManager, docModel} = gristDoc;
|
||||
const tables = fromKo(docModel.visibleTables.getObservable());
|
||||
const columns = fromKo(docModel.columns.createAllRowsModel('parentPos').getObservable());
|
||||
|
||||
@@ -207,7 +207,8 @@ export function buildPageWidgetPicker(
|
||||
|
||||
// dom
|
||||
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
|
||||
(elem: any) => { setTimeout(() => elem.focus(), 0); },
|
||||
@@ -276,7 +277,7 @@ export class PageWidgetSelect extends Disposable {
|
||||
private _tables: Observable<TableRec[]>,
|
||||
private _columns: Observable<ColumnRec[]>,
|
||||
private _onSave: () => Promise<void>,
|
||||
private _behavioralPrompts: BehavioralPrompts,
|
||||
private _behavioralPromptsManager: BehavioralPromptsManager,
|
||||
private _options: ISelectOptions = {}
|
||||
) { super(); }
|
||||
|
||||
@@ -307,7 +308,7 @@ export class PageWidgetSelect extends Disposable {
|
||||
cssIcon('TypeTable'), 'New Table',
|
||||
// prevent the selection of 'New Table' if it is disabled
|
||||
dom.on('click', (ev) => !this._isNewTableDisabled.get() && this._selectTable('New Table')),
|
||||
this._behavioralPrompts.attachTip('pageWidgetPicker', {
|
||||
this._behavioralPromptsManager.attachTip('pageWidgetPicker', {
|
||||
popupOptions: {
|
||||
attach: null,
|
||||
placement: 'right-start',
|
||||
@@ -365,7 +366,7 @@ export class PageWidgetSelect extends Disposable {
|
||||
),
|
||||
GristTooltips.selectBy(),
|
||||
{tooltipMenuOptions: {attach: null}, domArgs: [
|
||||
this._behavioralPrompts.attachTip('pageWidgetPickerSelectBy', {
|
||||
this._behavioralPromptsManager.attachTip('pageWidgetPickerSelectBy', {
|
||||
popupOptions: {
|
||||
attach: null,
|
||||
placement: 'bottom',
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
|
||||
export function showWelcomeCoachingCall(_triggerElement: Element, _app: AppModel): boolean {
|
||||
export function shouldShowWelcomeCoachingCall(_app: AppModel) {
|
||||
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');
|
||||
|
||||
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
|
||||
* dismissed the modal before.
|
||||
*
|
||||
* Returns a boolean indicating whether the modal was shown or not.
|
||||
*/
|
||||
export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>): boolean {
|
||||
if (!(getGristConfig().survey && userPrefsObs.get()?.showNewUserQuestions)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>) {
|
||||
saveModal((ctl, owner): ISaveModalOptions => {
|
||||
const selection = choices.map(c => Observable.create(owner, false));
|
||||
const otherText = Observable.create(owner, '');
|
||||
@@ -60,8 +58,6 @@ export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>): boole
|
||||
modalArgs: cssModalCentered.cls(''),
|
||||
};
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const choices: Array<{icon: IconName, color: string, textKey: string}> = [
|
||||
|
||||
Reference in New Issue
Block a user