(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. // Call prepare() with transitions turned off.
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; const prior = elem.style.transitionProperty;
elem.style.transitionProperty = 'none'; elem.style.transitionProperty = 'none';
prepare(elem, val); prepare();
// Recompute styles while transitions are off. See https://stackoverflow.com/a/16575811/328565 // 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 // for explanation and https://stackoverflow.com/a/31862081/328565 for the recommendation used
// here to trigger a style computation without a reflow. // here to trigger a style computation without a reflow.
window.getComputedStyle(elem).opacity; // tslint:disable-line:no-unused-expression window.getComputedStyle(elem).opacity; // tslint:disable-line:no-unused-expression
// Restore transitions before run(). // Restore transitions.
elem.style.transitionProperty = prior; elem.style.transitionProperty = prior;
}
run(elem, val);
});
} }
/** /**
* Helper for waiting for an active transition to end. Beyond listening to 'transitionend', it * Helper for waiting for an active transition to end. Beyond listening to 'transitionend', it
* does a few things: * 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 * When the transition ends, TransitionWatcher disposes itself. Its onDispose() method allows
* registering callbacks. * registering callbacks.
*/ */
class TransitionWatcher extends Disposable { export class TransitionWatcher extends Disposable {
private _propertyName: string; private _propertyName: string;
private _durationMs: number; private _durationMs: number;
private _timer: ReturnType<typeof setTimeout>; private _timer: ReturnType<typeof setTimeout>;

View File

@ -18,6 +18,11 @@ export type UserPrefs = Prefs;
export interface UserOrgPrefs extends Prefs { export interface UserOrgPrefs extends Prefs {
docMenuSort?: SortPref; docMenuSort?: SortPref;
docMenuView?: ViewPref; 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; export type OrgPrefs = Prefs;

View File

@ -336,6 +336,21 @@ export class HomeDBManager extends EventEmitter {
throw new Error(`Cannot testGetId(${name})`); 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> { public getUserByKey(apiKey: string): Promise<User|undefined> {
return User.findOne({apiKey}); return User.findOne({apiKey});
} }