From 166143557a43b29af12f20622a1bda81e102e096 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Wed, 9 Sep 2020 22:48:11 -0400 Subject: [PATCH] (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 --- app/client/ui/transitions.ts | 33 ++++++++++++++++++----------- app/common/Prefs.ts | 5 +++++ app/gen-server/lib/HomeDBManager.ts | 15 +++++++++++++ 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/app/client/ui/transitions.ts b/app/client/ui/transitions.ts index 541cf8d5..df15cad6 100644 --- a/app/client/ui/transitions.ts +++ b/app/client/ui/transitions.ts @@ -57,22 +57,31 @@ export function transition(obs: BindableValue, trans: ITransitionLogic) }); // 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(obs: BindableValue, trans: ITransitionLogic) * 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; diff --git a/app/common/Prefs.ts b/app/common/Prefs.ts index 74584033..1405f6f9 100644 --- a/app/common/Prefs.ts +++ b/app/common/Prefs.ts @@ -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; diff --git a/app/gen-server/lib/HomeDBManager.ts b/app/gen-server/lib/HomeDBManager.ts index 7f711718..c29fec23 100644 --- a/app/gen-server/lib/HomeDBManager.ts +++ b/app/gen-server/lib/HomeDBManager.ts @@ -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 { return User.findOne({apiKey}); }