(core) Show a welcome card when a user opens an example for the first time.

Summary:
- The card includes an image, a brief description, and a link to the tutorial.
- The left panel includes a link to the tutorial, and a button to reopen card.
- Card is collapsed and expanded with a little animation.
- Add a seenExamples pref for whether an example has been seen.
- Store the pref in localStorage for anon user.

Separately, added clearing of prefs of test users between tests, to avoid tests
affecting unrelated tests.

Test Plan: Added a browser test.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2602
This commit is contained in:
Dmitry S 2020-09-09 22:48:11 -04:00
parent 526fda4eba
commit 166143557a
3 changed files with 41 additions and 12 deletions

View File

@ -57,22 +57,31 @@ export function transition<T>(obs: BindableValue<T>, trans: ITransitionLogic<T>)
});
// Call prepare() with transitions turned off.
const prior = elem.style.transitionProperty;
elem.style.transitionProperty = 'none';
prepare(elem, val);
// Recompute styles while transitions are off. See https://stackoverflow.com/a/16575811/328565
// for explanation and https://stackoverflow.com/a/31862081/328565 for the recommendation used
// here to trigger a style computation without a reflow.
window.getComputedStyle(elem).opacity; // tslint:disable-line:no-unused-expression
// Restore transitions before run().
elem.style.transitionProperty = prior;
prepareForTransition(elem, () => prepare(elem, val));
}
run(elem, val);
});
}
/**
* Call prepare() with transitions turned off. This allows preparing an element before another
* change to properties actually gets animated using the element's transition settings.
*/
export function prepareForTransition(elem: HTMLElement, prepare: () => void) {
const prior = elem.style.transitionProperty;
elem.style.transitionProperty = 'none';
prepare();
// Recompute styles while transitions are off. See https://stackoverflow.com/a/16575811/328565
// for explanation and https://stackoverflow.com/a/31862081/328565 for the recommendation used
// here to trigger a style computation without a reflow.
window.getComputedStyle(elem).opacity; // tslint:disable-line:no-unused-expression
// Restore transitions.
elem.style.transitionProperty = prior;
}
/**
* Helper for waiting for an active transition to end. Beyond listening to 'transitionend', it
* does a few things:
@ -86,7 +95,7 @@ export function transition<T>(obs: BindableValue<T>, trans: ITransitionLogic<T>)
* When the transition ends, TransitionWatcher disposes itself. Its onDispose() method allows
* registering callbacks.
*/
class TransitionWatcher extends Disposable {
export class TransitionWatcher extends Disposable {
private _propertyName: string;
private _durationMs: number;
private _timer: ReturnType<typeof setTimeout>;

View File

@ -18,6 +18,11 @@ export type UserPrefs = Prefs;
export interface UserOrgPrefs extends Prefs {
docMenuSort?: SortPref;
docMenuView?: ViewPref;
// List of example docs that the user has seen and dismissed the welcome card for.
// 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[];
}
export type OrgPrefs = Prefs;

View File

@ -336,6 +336,21 @@ export class HomeDBManager extends EventEmitter {
throw new Error(`Cannot testGetId(${name})`);
}
/**
* Clear all user preferences associated with the given email addresses.
* For use in tests.
*/
public async testClearUserPrefs(emails: string[]) {
return await this._connection.transaction(async manager => {
for (const email of emails) {
const user = await this.getUserByLogin(email, undefined, manager);
if (user) {
await manager.delete(Pref, {userId: user.id});
}
}
});
}
public getUserByKey(apiKey: string): Promise<User|undefined> {
return User.findOne({apiKey});
}