(core) Replace questionnaire for new users with a popup asking for just their primary use-case.

Summary:
- WelcomeQuestions implements the new popup.
- Popup shows up on any doc-list page, the first time the user visits one after
  signing up and setting their name.
- Submits responses to the same "New User Questions" doc, which has been
  changed to accept two new columns (ChoiceList of use_cases, and Text for
  use_other).
- Improve modals on mobile along the way.

Test Plan: Added browser tests and tested manually

Reviewers: alexmojaki

Reviewed By: alexmojaki

Subscribers: jarek

Differential Revision: https://phab.getgrist.com/D3213
This commit is contained in:
Dmitry S
2022-01-13 21:55:55 -05:00
parent ba6ecc5e9e
commit 215bb90e68
19 changed files with 335 additions and 47 deletions

View File

@@ -7,7 +7,9 @@ import {Features} from 'app/common/Features';
import {GristLoadConfig} from 'app/common/gristUrls';
import {FullUser} from 'app/common/LoginSessionAPI';
import {LocalPlugin} from 'app/common/plugin';
import {UserPrefs} from 'app/common/Prefs';
import {getOrgName, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
import {getUserPrefsObs} from 'app/client/models/UserPrefs';
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
export {reportError} from 'app/client/models/errors';
@@ -58,6 +60,7 @@ export interface AppModel {
orgError?: OrgError; // If currentOrg is null, the error that caused it.
currentFeatures: Features; // features of the current org's product.
userPrefsObs: Observable<UserPrefs>;
pageType: Observable<PageType>;
@@ -177,6 +180,8 @@ export class AppModelImpl extends Disposable implements AppModel {
public readonly currentFeatures = (this.currentOrg && this.currentOrg.billingAccount) ?
this.currentOrg.billingAccount.product.features : {};
public readonly userPrefsObs = getUserPrefsObs(this);
// Get the current PageType from the URL.
public readonly pageType: Observable<PageType> = Computed.create(this, urlState().state,
(use, state) => (state.doc ? "doc" : (state.billing ? "billing" : (state.welcome ? "welcome" : "home"))));

View File

@@ -1,40 +1,60 @@
import {localStorageObs} from 'app/client/lib/localStorageObs';
import {AppModel} from 'app/client/models/AppModel';
import {UserOrgPrefs} from 'app/common/Prefs';
import {UserOrgPrefs, UserPrefs} from 'app/common/Prefs';
import {Computed, Observable} from 'grainjs';
/**
* Creates an observable that returns UserOrgPrefs, and which stores them when set.
*
* For anon user, the prefs live in localStorage. Note that the observable isn't actually watching
* for changes on the server, it will only change when set.
*/
export function getUserOrgPrefsObs(appModel: AppModel): Observable<UserOrgPrefs> {
const savedPrefs = appModel.currentValidUser ? appModel.currentOrg?.userOrgPrefs : undefined;
if (savedPrefs) {
const prefsObs = Observable.create<UserOrgPrefs>(null, savedPrefs);
return Computed.create(null, (use) => use(prefsObs))
.onWrite(userOrgPrefs => {
prefsObs.set(userOrgPrefs);
return appModel.api.updateOrg('current', {userOrgPrefs});
});
} else {
const userId = appModel.currentUser?.id || 0;
const jsonPrefsObs = localStorageObs(`userOrgPrefs:u=${userId}`);
return Computed.create(null, jsonPrefsObs, (use, p) => (p && JSON.parse(p) || {}) as UserOrgPrefs)
.onWrite(userOrgPrefs => {
jsonPrefsObs.set(JSON.stringify(userOrgPrefs));
});
}
interface PrefsTypes {
userOrgPrefs: UserOrgPrefs;
userPrefs: UserPrefs;
}
/**
* Creates an observable that returns a particular preference value from `prefsObs`, and which
* stores it when set.
*/
export function getUserOrgPrefObs<Name extends keyof UserOrgPrefs>(
prefsObs: Observable<UserOrgPrefs>, prefName: Name
): Observable<UserOrgPrefs[Name]> {
return Computed.create(null, (use) => use(prefsObs)[prefName])
.onWrite(value => prefsObs.set({...prefsObs.get(), [prefName]: value}));
function makePrefFunctions<P extends keyof PrefsTypes>(prefsTypeName: P) {
type PrefsType = PrefsTypes[P];
/**
* Creates an observable that returns UserOrgPrefs, and which stores them when set.
*
* For anon user, the prefs live in localStorage. Note that the observable isn't actually watching
* for changes on the server, it will only change when set.
*/
function getPrefsObs(appModel: AppModel): Observable<PrefsType> {
const savedPrefs = appModel.currentValidUser ? appModel.currentOrg?.[prefsTypeName] : undefined;
if (savedPrefs) {
const prefsObs = Observable.create<PrefsType>(null, savedPrefs!);
return Computed.create(null, (use) => use(prefsObs))
.onWrite(prefs => {
prefsObs.set(prefs);
return appModel.api.updateOrg('current', {[prefsTypeName]: prefs});
});
} else {
const userId = appModel.currentUser?.id || 0;
const jsonPrefsObs = localStorageObs(`${prefsTypeName}:u=${userId}`);
return Computed.create(null, jsonPrefsObs, (use, p) => (p && JSON.parse(p) || {}) as PrefsType)
.onWrite(prefs => {
jsonPrefsObs.set(JSON.stringify(prefs));
});
}
}
/**
* Creates an observable that returns a particular preference value from `prefsObs`, and which
* stores it when set.
*/
function getPrefObs<Name extends keyof PrefsType>(
prefsObs: Observable<PrefsType>, prefName: Name
): Observable<PrefsType[Name]> {
return Computed.create(null, (use) => use(prefsObs)[prefName])
.onWrite(value => prefsObs.set({...prefsObs.get(), [prefName]: value}));
}
return {getPrefsObs, getPrefObs};
}
// Functions actually exported are:
// - getUserOrgPrefsObs(appModel): Observsble<UserOrgPrefs>
// - getUserOrgPrefObs(userOrgPrefsObs, prefName): Observsble<PrefType[prefName]>
// - getUserPrefsObs(appModel): Observsble<UserPrefs>
// - getUserPrefObs(userPrefsObs, prefName): Observsble<PrefType[prefName]>
export const {getPrefsObs: getUserOrgPrefsObs, getPrefObs: getUserOrgPrefObs} = makePrefFunctions('userOrgPrefs');
export const {getPrefsObs: getUserPrefsObs, getPrefObs: getUserPrefObs} = makePrefFunctions('userPrefs');