(core) Update onboarding flow

Summary:
A new onboarding page is now shown to all new users visiting the doc
menu for the first time. Tutorial cards on the doc menu have been
replaced with a new version that tracks completion progress, alongside
a new card that opens the orientation video.

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D4296
This commit is contained in:
George Gevoian
2024-07-22 11:10:57 -04:00
parent 3fd8719d8a
commit 4740f1f933
40 changed files with 1462 additions and 706 deletions

View File

@@ -1124,7 +1124,7 @@ export class DocWorkerApi {
const scope = getDocScope(req);
const tutorialTrunkId = options.sourceDocId;
await this._dbManager.connection.transaction(async (manager) => {
// Fetch the tutorial trunk doc so we can replace the tutorial doc's name.
// Fetch the tutorial trunk so we can replace the tutorial fork's name.
const tutorialTrunk = await this._dbManager.getDoc({...scope, urlId: tutorialTrunkId}, manager);
await this._dbManager.updateDocument(
scope,
@@ -1132,9 +1132,8 @@ export class DocWorkerApi {
name: tutorialTrunk.name,
options: {
tutorial: {
...tutorialTrunk.options?.tutorial,
// For now, the only state we need to reset is the slide position.
lastSlideIndex: 0,
percentComplete: 0,
},
},
},

View File

@@ -1055,7 +1055,7 @@ export class FlexServer implements GristServer {
// Reset isFirstTimeUser flag.
await this._dbManager.updateUser(user.id, {isFirstTimeUser: false});
// This is a good time to set some other flags, for showing a popup with welcome question(s)
// This is a good time to set some other flags, for showing a page with welcome question(s)
// to this new user and recording their sign-up with Google Tag Manager. These flags are also
// scoped to the user, but isFirstTimeUser has a dedicated DB field because it predates userPrefs.
// Note that the updateOrg() method handles all levels of prefs (for user, user+org, or org).
@@ -1586,20 +1586,25 @@ export class FlexServer implements GristServer {
this.app.post('/welcome/info', ...middleware, expressWrap(async (req, resp, next) => {
const userId = getUserId(req);
const user = getUser(req);
const orgName = stringParam(req.body.org_name, 'org_name');
const orgRole = stringParam(req.body.org_role, 'org_role');
const useCases = stringArrayParam(req.body.use_cases, 'use_cases');
const useOther = stringParam(req.body.use_other, 'use_other');
const row = {
UserID: userId,
Name: user.name,
Email: user.loginEmail,
org_name: orgName,
org_role: orgRole,
use_cases: ['L', ...useCases],
use_other: useOther,
};
this._recordNewUserInfo(row)
.catch(e => {
try {
await this._recordNewUserInfo(row);
} catch (e) {
// If we failed to record, at least log the data, so we could potentially recover it.
log.rawWarn(`Failed to record new user info: ${e.message}`, {newUserQuestions: row});
});
}
const nonOtherUseCases = useCases.filter(useCase => useCase !== 'Other');
for (const useCase of [...nonOtherUseCases, ...(useOther ? [`Other - ${useOther}`] : [])]) {
this.getTelemetry().logEvent(req as RequestWithLogin, 'answeredUseCaseQuestion', {

View File

@@ -11,3 +11,9 @@ export function getTemplateOrg() {
}
return org;
}
export function getOnboardingTutorialDocId() {
return appSettings.section('tutorials').flag('onboardingTutorialDocId').readString({
envVar: 'GRIST_ONBOARDING_TUTORIAL_DOC_ID',
});
}

View File

@@ -16,7 +16,7 @@ import {SUPPORT_EMAIL} from 'app/gen-server/lib/homedb/HomeDBManager';
import {isAnonymousUser, isSingleUserMode, RequestWithLogin} from 'app/server/lib/Authorizer';
import {RequestWithOrg} from 'app/server/lib/extractOrg';
import {GristServer} from 'app/server/lib/GristServer';
import {getTemplateOrg} from 'app/server/lib/gristSettings';
import {getOnboardingTutorialDocId, getTemplateOrg} from 'app/server/lib/gristSettings';
import {getSupportedEngineChoices} from 'app/server/lib/serverUtils';
import {readLoadedLngs, readLoadedNamespaces} from 'app/server/localization';
import * as express from 'express';
@@ -97,6 +97,7 @@ export function makeGristConfig(options: MakeGristConfigOptions): GristLoadConfi
telemetry: server?.getTelemetry().getTelemetryConfig(req as RequestWithLogin | undefined),
deploymentType: server?.getDeploymentType(),
templateOrg: getTemplateOrg(),
onboardingTutorialDocId: getOnboardingTutorialDocId(),
canCloseAccount: isAffirmative(process.env.GRIST_ACCOUNT_CLOSE),
experimentalPlugins: isAffirmative(process.env.GRIST_EXPERIMENTAL_PLUGINS),
notifierEnabled: server?.hasNotifier(),