(core) Automatically or explicitly show document tours in the same way as example cards.

Summary:
Extracts code from showExampleCard into a generic function which is reused for document tours.

It handles reading and writing to user preferences for automatic showing and explicitly reopening.

Test Plan:
Manually tested that it automatically shows a tour just once and clicking to reopen works.

There's not much new functionality so there's little that needs testing. This is an initial version that's mostly internal and is likely to be polished for users in the future.

If I should still add tests, I'd like confirmation that the current behaviour is as desired.

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2944
pull/9/head
Alex Hall 3 years ago
parent 15f1ef96fa
commit cd0c6de53e

@ -63,7 +63,7 @@ import { Drafts } from "app/client/components/Drafts";
const G = getBrowserGlobals('document', 'window');
// Re-export some tools to move them from main webpack bundle to the one with GristDoc.
export {DocComm, showDocSettingsModal};
export {DocComm, showDocSettingsModal, startDocTour};
export interface TabContent {
showObs?: any;

@ -1,44 +1,24 @@
import {AppModel} from 'app/client/models/AppModel';
import {getUserOrgPrefObs} from 'app/client/models/UserPrefs';
import {IExampleInfo} from 'app/client/ui/ExampleInfo';
import {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions';
import {colors, mediaXSmall, testId, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {dom, Observable, styled} from 'grainjs';
import {dom, styled} from 'grainjs';
import {AutomaticHelpToolInfo} from "app/client/ui/Tools";
let prevCardClose: (() => void)|null = null;
// Open a popup with a card introducing this example, if the user hasn't dismissed it in the past.
export function showExampleCard(
example: IExampleInfo, appModel: AppModel, btnElem: HTMLElement, reopen: boolean = false
example: IExampleInfo, toolInfo: AutomaticHelpToolInfo
) {
const prefObs: Observable<number[]|undefined> = getUserOrgPrefObs(appModel, 'seenExamples');
const seenExamples = prefObs.get() || [];
// If this example was previously dismissed, don't show the card, unless the user is reopening it.
if (!reopen && seenExamples.includes(example.id)) {
return;
}
// When an example card is closed, if it's the first time it's dismissed, save this fact, to avoid
// opening the card again in the future.
async function markAsSeen() {
if (!seenExamples.includes(example.id)) {
const seen = new Set(seenExamples);
seen.add(example.id);
prefObs.set([...seen].sort());
}
}
const {elem: btnElem, markAsSeen, reopen} = toolInfo;
// Close the example card.
function close() {
prevCardClose = null;
collapseAndRemoveCard(cardElem, btnElem.getBoundingClientRect());
// If we fail to save this preference, it's probably not worth alerting the user about,
// so just log to console.
// tslint:disable-next-line:no-console
markAsSeen().catch((err) => console.warn("Failed to save userPref", err));
markAsSeen();
}
const card = example.welcomeCard;

@ -11,6 +11,8 @@ import { cssLink } from 'app/client/ui2018/links';
import { menuAnnotate } from 'app/client/ui2018/menus';
import { userOverrideParams } from 'app/common/gristUrls';
import { Disposable, dom, makeTestId, Observable, observable, styled } from "grainjs";
import {getUserOrgPrefObs} from "app/client/models/UserPrefs";
import {loadGristDoc} from "app/client/lib/imports";
const testId = makeTestId('test-tools-');
@ -67,29 +69,121 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
if (!doc.workspace.isSupportWorkspace) { return null; }
const ex = examples.find((e) => e.matcher.test(doc.name));
if (!ex || !ex.tutorialUrl) { return null; }
const appModel = gristDoc.docPageModel.appModel;
return cssPageEntry(
cssPageLink(cssPageIcon('Page'), cssLinkText('How-to Tutorial'), testId('tutorial'),
{href: ex.tutorialUrl, target: '_blank'},
cssExampleCardOpener(
icon('TypeDetails'),
dom.on('click', (ev, elem) => {
ev.preventDefault();
showExampleCard(ex, appModel, elem, true);
}),
testId('welcome-opener'),
(elem) => {
// Once the trigger element is attached to DOM, show the card.
setTimeout(() => showExampleCard(ex, appModel, elem), 0);
},
automaticHelpTool(
(info) => showExampleCard(ex, info),
gristDoc,
"seenExamples",
ex.id
),
),
),
);
}),
// Shows the 'Tour of this Document' button if a GristDocTour table exists
// at the time of running. Currently doesn't observe the set of existing tables
gristDoc.docData.getTable('GristDocTour') &&
cssPageEntry(
cssPageLink(
cssPageIcon('Page'),
cssLinkText('Tour of this Document'),
testId('doctour'),
automaticHelpTool(
async ({markAsSeen}) => {
const gristDocModule = await loadGristDoc();
await gristDocModule.startDocTour(gristDoc.docData, markAsSeen);
},
gristDoc,
"seenDocTours",
gristDoc.docId()
),
),
),
createHelpTools(gristDoc.docPageModel.appModel, false)
);
}
/**
* Helper for showing users some kind of help (example cards or document tours)
* automatically if they haven't seen it before, or if they click
* on some element to explicitly show it again. Put this in said dom element,
* and it will provide the onclick handler and a handler which automatically
* shows when the dom element is attached, both by calling showFunc.
*
* prefKey is a key for a list of identifiers saved in user preferences.
* itemId should be a single identifier that fits in that list.
* If itemId is already present then the help will not be shown automatically,
* otherwise it will be added to the list and saved under prefKey
* when info.markAsSeen() is called.
*/
function automaticHelpTool(
showFunc: (info: AutomaticHelpToolInfo) => void,
gristDoc: GristDoc,
prefKey: 'seenExamples' | 'seenDocTours',
itemId: number | string
) {
function show(elem: HTMLElement, reopen: boolean) {
const appModel = gristDoc.docPageModel.appModel;
const prefObs: Observable<typeof itemId[] | undefined> = getUserOrgPrefObs(appModel, prefKey);
const seenIds = prefObs.get() || [];
// If this help was previously dismissed, don't show it again, unless the user is reopening it.
if (!reopen && seenIds.includes(itemId)) {
return;
}
// When the help is closed, if it's the first time it's dismissed, save this fact, to avoid
// showing it automatically again in the future.
function markAsSeen() {
try {
if (!seenIds.includes(itemId)) {
const seen = new Set(seenIds);
seen.add(itemId);
prefObs.set([...seen].sort());
}
} catch (e) {
// If we fail to save this preference, it's probably not worth alerting the user about,
// so just log to console.
// tslint:disable-next-line:no-console
console.warn("Failed to save userPref " + prefKey, e);
}
}
showFunc({elem, reopen, markAsSeen});
}
return [
dom.on('click', (ev, elem) => {
ev.preventDefault();
show(elem as HTMLElement, true);
}),
(elem: HTMLElement) => {
// Once the trigger element is attached to DOM, show the help
setTimeout(() => show(elem, false), 0);
}
];
}
/** Values which may be useful when showing an automatic help tool */
export interface AutomaticHelpToolInfo {
// Element where automaticHelpTool is attached, typically a button,
// which shows the help when clicked
elem: HTMLElement;
// true if the help was shown explicitly by clicking elem,
// false if it's being shown automatically to new users
reopen: boolean;
// Call this when the user explicitly dismisses the help to
// remember this in user preferences and not show it automatically on next load
markAsSeen: () => void;
}
// When viewing a page as another user, the "Access Rules" page link includes a button to revert
// the user and open the page, and a click on the page link shows a tooltip to revert.
function addRevertViewAsUI() {

@ -23,6 +23,9 @@ export interface UserOrgPrefs extends Prefs {
// The numbers are the `id` from IExampleInfo in app/client/ui/ExampleInfo.
// By living in UserOrgPrefs, this applies only to the examples-containing org.
seenExamples?: number[];
// List of document IDs where the user has seen and dismissed the document tour.
seenDocTours?: string[];
}
export type OrgPrefs = Prefs;

Loading…
Cancel
Save