Skip to content

Commit ad8ad67

Browse files
authored
Merge pull request desktop#8320 from desktop/ku-welcome-and-youre-done-panes
[Onboarding tutorial] Add "Welcome" and "You're done!" panes
2 parents 72f5d85 + 127e49f commit ad8ad67

27 files changed

+665
-79
lines changed

app/src/lib/stores/app-store.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,16 @@ export class AppStore extends TypedBaseStore<IAppState> {
442442
}
443443
}
444444

445+
public async _resumeTutorial(repository: Repository) {
446+
this.tutorialAssessor.resumeTutorial()
447+
await this.updateCurrentTutorialStep(repository)
448+
}
449+
450+
public async _pauseTutorial(repository: Repository) {
451+
this.tutorialAssessor.pauseTutorial()
452+
await this.updateCurrentTutorialStep(repository)
453+
}
454+
445455
/** Call via `Dispatcher` when the user opts to skip the pick editor step of the onboarding tutorial */
446456
public async _skipPickEditorTutorialStep(repository: Repository) {
447457
this.tutorialAssessor.skipPickEditor()
@@ -4888,6 +4898,18 @@ export class AppStore extends TypedBaseStore<IAppState> {
48884898
return Promise.resolve()
48894899
}
48904900

4901+
public async _showGitHubExplore(repository: Repository): Promise<void> {
4902+
const { gitHubRepository } = repository
4903+
if (!gitHubRepository || gitHubRepository.htmlURL === null) {
4904+
return
4905+
}
4906+
4907+
const url = new URL(gitHubRepository.htmlURL)
4908+
url.pathname = '/explore'
4909+
4910+
await this._openInBrowser(url.toString())
4911+
}
4912+
48914913
public async _createPullRequest(repository: Repository): Promise<void> {
48924914
const gitHubRepository = repository.gitHubRepository
48934915
if (!gitHubRepository) {

app/src/lib/stores/helpers/tutorial-assessor.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { setBoolean, getBoolean } from '../../local-storage'
66

77
const skipInstallEditorKey = 'tutorial-install-editor-skipped'
88
const pullRequestStepCompleteKey = 'tutorial-pull-request-step-complete'
9+
const tutorialPausedKey = 'tutorial-paused'
910

1011
/**
1112
* Used to determine which step of the onboarding
@@ -26,6 +27,8 @@ export class OnboardingTutorialAssessor {
2627
pullRequestStepCompleteKey,
2728
false
2829
)
30+
/** Is the tutorial currently paused? */
31+
private tutorialPaused: boolean = getBoolean(tutorialPausedKey, false)
2932

3033
public constructor(
3134
/** Method to call when we need to get the current editor */
@@ -39,6 +42,8 @@ export class OnboardingTutorialAssessor {
3942
): Promise<TutorialStep> {
4043
if (!isTutorialRepo) {
4144
return TutorialStep.NotApplicable
45+
} else if (this.tutorialPaused) {
46+
return TutorialStep.Paused
4247
} else if (!(await this.isEditorInstalled())) {
4348
return TutorialStep.PickEditor
4449
} else if (!this.isBranchCheckedOut(repositoryState)) {
@@ -143,5 +148,19 @@ export class OnboardingTutorialAssessor {
143148
localStorage.removeItem(skipInstallEditorKey)
144149
this.prStepComplete = false
145150
localStorage.removeItem(pullRequestStepCompleteKey)
151+
this.tutorialPaused = false
152+
localStorage.removeItem(tutorialPausedKey)
153+
}
154+
155+
/** Call when the user pauses the tutorial */
156+
public pauseTutorial() {
157+
this.tutorialPaused = true
158+
setBoolean(tutorialPausedKey, this.tutorialPaused)
159+
}
160+
161+
/** Call when the user resumes the tutorial */
162+
public resumeTutorial() {
163+
this.tutorialPaused = false
164+
setBoolean(tutorialPausedKey, this.tutorialPaused)
146165
}
147166
}

app/src/models/popup.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export enum PopupType {
5151
ConfirmOverwriteStash,
5252
ConfirmDiscardStash,
5353
CreateTutorialRepository,
54+
ConfirmExitTutorial,
5455
}
5556

5657
export type Popup =
@@ -201,3 +202,6 @@ export type Popup =
201202
type: PopupType.CreateTutorialRepository
202203
account: Account
203204
}
205+
| {
206+
type: PopupType.ConfirmExitTutorial
207+
}

app/src/models/tutorial-step.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export enum TutorialStep {
77
PushBranch = 'PushBranch',
88
OpenPullRequest = 'OpenPullRequest',
99
AllDone = 'AllDone',
10+
Paused = 'Paused',
1011
}
1112

1213
export type ValidTutorialStep =
@@ -18,6 +19,12 @@ export type ValidTutorialStep =
1819
| TutorialStep.OpenPullRequest
1920
| TutorialStep.AllDone
2021

22+
export function isValidTutorialStep(
23+
step: TutorialStep
24+
): step is ValidTutorialStep {
25+
return step !== TutorialStep.NotApplicable && step !== TutorialStep.Paused
26+
}
27+
2128
export const orderedTutorialSteps: ReadonlyArray<ValidTutorialStep> = [
2229
TutorialStep.PickEditor,
2330
TutorialStep.CreateBranch,

app/src/ui/app.tsx

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ import { OverwriteStash } from './stash-changes/overwrite-stashed-changes-dialog
105105
import { ConfirmDiscardStashDialog } from './stashing/confirm-discard-stash'
106106
import { CreateTutorialRepositoryDialog } from './blank-slate/create-tutorial-repository-dialog'
107107
import { enableTutorial } from '../lib/feature-flag'
108+
import { ConfirmExitTutorial } from './tutorial'
109+
import { TutorialStep, isValidTutorialStep } from '../models/tutorial-step'
108110

109111
const MinuteInMilliseconds = 1000 * 60
110112
const HourInMilliseconds = MinuteInMilliseconds * 60
@@ -698,6 +700,30 @@ export class App extends React.Component<IAppProps, IAppState> {
698700
})
699701
}
700702

703+
private onResumeTutorialRepository = () => {
704+
const tutorialRepository = this.getSelectedTutorialRepository()
705+
if (!tutorialRepository) {
706+
return
707+
}
708+
709+
this.props.dispatcher.resumeTutorial(tutorialRepository)
710+
}
711+
712+
private getSelectedTutorialRepository() {
713+
const { selectedState } = this.state
714+
const selectedRepository =
715+
selectedState && selectedState.type === SelectionType.Repository
716+
? selectedState.repository
717+
: null
718+
719+
const isTutorialRepository =
720+
enableTutorial() &&
721+
selectedRepository &&
722+
selectedRepository.isTutorialRepository
723+
724+
return isTutorialRepository ? selectedRepository : null
725+
}
726+
701727
private showAbout() {
702728
this.props.dispatcher.showPopup({ type: PopupType.About })
703729
}
@@ -963,7 +989,7 @@ export class App extends React.Component<IAppProps, IAppState> {
963989
// it if needed.
964990
if (paths.length > 1) {
965991
const addedRepositories = await this.addRepositories(paths)
966-
if (addedRepositories.length) {
992+
if (addedRepositories.length > 0) {
967993
this.props.dispatcher.recordAddExistingRepository()
968994
}
969995
} else {
@@ -1029,7 +1055,7 @@ export class App extends React.Component<IAppProps, IAppState> {
10291055

10301056
private async addRepositories(paths: ReadonlyArray<string>) {
10311057
const repositories = await this.props.dispatcher.addRepositories(paths)
1032-
if (repositories.length) {
1058+
if (repositories.length > 0) {
10331059
this.props.dispatcher.selectRepository(repositories[0])
10341060
}
10351061

@@ -1177,7 +1203,7 @@ export class App extends React.Component<IAppProps, IAppState> {
11771203

11781204
const showAppIcon = __WIN32__ && !this.state.showWelcomeFlow
11791205
const inWelcomeFlow = this.state.showWelcomeFlow
1180-
const inNoRepositoriesView = this.state.repositories.length === 0
1206+
const inNoRepositoriesView = this.inNoRepositoriesBlankSlateState()
11811207

11821208
// The light title bar style should only be used while we're in
11831209
// the welcome flow as well as the no-repositories blank slate
@@ -1811,11 +1837,30 @@ export class App extends React.Component<IAppProps, IAppState> {
18111837
/>
18121838
)
18131839
}
1840+
case PopupType.ConfirmExitTutorial: {
1841+
return (
1842+
<ConfirmExitTutorial
1843+
key="confirm-exit-tutorial"
1844+
onDismissed={this.onPopupDismissed}
1845+
onContinue={this.onExitTutorialToHomeScreen}
1846+
/>
1847+
)
1848+
}
18141849
default:
18151850
return assertNever(popup, `Unknown popup type: ${popup}`)
18161851
}
18171852
}
18181853

1854+
private onExitTutorialToHomeScreen = () => {
1855+
const tutorialRepository = this.getSelectedTutorialRepository()
1856+
if (!tutorialRepository) {
1857+
return
1858+
}
1859+
1860+
this.props.dispatcher.pauseTutorial(tutorialRepository)
1861+
this.props.dispatcher.closePopup()
1862+
}
1863+
18191864
private onTutorialRepositoryError = (error: Error) => {
18201865
this.props.dispatcher.closePopup(PopupType.CreateTutorialRepository)
18211866
this.props.dispatcher.postError(error)
@@ -2071,6 +2116,22 @@ export class App extends React.Component<IAppProps, IAppState> {
20712116
}
20722117
}
20732118

2119+
private onExitTutorial = () => {
2120+
if (
2121+
this.state.repositories.length === 1 &&
2122+
isValidTutorialStep(this.state.currentOnboardingTutorialStep)
2123+
) {
2124+
// If the only repository present is the tutorial repo,
2125+
// prompt for confirmation and exit to the BlankSlateView
2126+
this.props.dispatcher.showPopup({
2127+
type: PopupType.ConfirmExitTutorial,
2128+
})
2129+
} else {
2130+
// Otherwise pop open repositories panel
2131+
this.onRepositoryDropdownStateChanged('open')
2132+
}
2133+
}
2134+
20742135
private renderRepositoryToolbarButton() {
20752136
const selection = this.state.selectedState
20762137

@@ -2265,7 +2326,7 @@ export class App extends React.Component<IAppProps, IAppState> {
22652326
// can't support banners at the moment. So for the
22662327
// no-repositories blank slate we'll have to live without
22672328
// them.
2268-
if (this.state.repositories.length === 0) {
2329+
if (this.inNoRepositoriesBlankSlateState()) {
22692330
return null
22702331
}
22712332

@@ -2310,7 +2371,7 @@ export class App extends React.Component<IAppProps, IAppState> {
23102371
/**
23112372
* No toolbar if we're in the blank slate view.
23122373
*/
2313-
if (this.state.repositories.length === 0) {
2374+
if (this.inNoRepositoriesBlankSlateState()) {
23142375
return null
23152376
}
23162377

@@ -2330,7 +2391,7 @@ export class App extends React.Component<IAppProps, IAppState> {
23302391

23312392
private renderRepository() {
23322393
const state = this.state
2333-
if (state.repositories.length < 1) {
2394+
if (this.inNoRepositoriesBlankSlateState()) {
23342395
return (
23352396
<BlankSlateView
23362397
dotComAccount={this.getDotComAccount()}
@@ -2339,7 +2400,9 @@ export class App extends React.Component<IAppProps, IAppState> {
23392400
onClone={this.showCloneRepo}
23402401
onAdd={this.showAddLocalRepo}
23412402
onCreateTutorialRepository={this.onCreateTutorialRepository}
2342-
apiRepositories={this.state.apiRepositories}
2403+
onResumeTutorialRepository={this.onResumeTutorialRepository}
2404+
tutorialPaused={this.isTutorialPaused()}
2405+
apiRepositories={state.apiRepositories}
23432406
onRefreshRepositories={this.onRefreshRepositories}
23442407
/>
23452408
)
@@ -2375,8 +2438,9 @@ export class App extends React.Component<IAppProps, IAppState> {
23752438
externalEditorLabel={externalEditorLabel}
23762439
resolvedExternalEditor={state.resolvedExternalEditor}
23772440
onOpenInExternalEditor={this.openFileInExternalEditor}
2378-
appMenu={this.state.appMenuState[0]}
2379-
currentTutorialStep={this.state.currentOnboardingTutorialStep}
2441+
appMenu={state.appMenuState[0]}
2442+
currentTutorialStep={state.currentOnboardingTutorialStep}
2443+
onExitTutorial={this.onExitTutorial}
23802444
/>
23812445
)
23822446
} else if (selectedState.type === SelectionType.CloningRepository) {
@@ -2469,6 +2533,14 @@ export class App extends React.Component<IAppProps, IAppState> {
24692533
kind: HistoryTabMode.History,
24702534
})
24712535
}
2536+
2537+
private inNoRepositoriesBlankSlateState() {
2538+
return this.state.repositories.length === 0 || this.isTutorialPaused()
2539+
}
2540+
2541+
private isTutorialPaused() {
2542+
return this.state.currentOnboardingTutorialStep === TutorialStep.Paused
2543+
}
24722544
}
24732545

24742546
function NoRepositorySelected() {

app/src/ui/blank-slate/blank-slate.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ interface IBlankSlateProps {
2828
/** Called when the user chooses to create a tutorial repository */
2929
readonly onCreateTutorialRepository: () => void
3030

31+
/** Called when the user chooses to resume a tutorial repository */
32+
readonly onResumeTutorialRepository: () => void
33+
34+
/** true if tutorial is in paused state. */
35+
readonly tutorialPaused: boolean
36+
3137
/** The logged in account for GitHub.com. */
3238
readonly dotComAccount: Account | null
3339

@@ -341,7 +347,7 @@ export class BlankSlateView extends React.Component<
341347
)
342348
}
343349

344-
private renderCreateTutorialRepositoryButton() {
350+
private renderTutorialRepositoryButton() {
345351
if (!enableTutorial()) {
346352
return null
347353
}
@@ -354,14 +360,25 @@ export class BlankSlateView extends React.Component<
354360
return null
355361
}
356362

357-
return this.renderButtonGroupButton(
358-
OcticonSymbol.mortarBoard,
359-
__DARWIN__
360-
? 'Create a Tutorial Repository…'
361-
: 'Create a tutorial repository…',
362-
this.props.onCreateTutorialRepository,
363-
'submit'
364-
)
363+
if (this.props.tutorialPaused) {
364+
return this.renderButtonGroupButton(
365+
OcticonSymbol.mortarBoard,
366+
__DARWIN__
367+
? 'Return to In Progress Tutorial'
368+
: 'Return to in progress tutorial',
369+
this.props.onResumeTutorialRepository,
370+
'submit'
371+
)
372+
} else {
373+
return this.renderButtonGroupButton(
374+
OcticonSymbol.mortarBoard,
375+
__DARWIN__
376+
? 'Create a Tutorial Repository…'
377+
: 'Create a tutorial repository…',
378+
this.props.onCreateTutorialRepository,
379+
'submit'
380+
)
381+
}
365382
}
366383

367384
private renderCloneButton() {
@@ -398,7 +415,7 @@ export class BlankSlateView extends React.Component<
398415
return (
399416
<div className="content-pane right">
400417
<ul className="button-group">
401-
{this.renderCreateTutorialRepositoryButton()}
418+
{this.renderTutorialRepositoryButton()}
402419
{this.renderCloneButton()}
403420
{this.renderCreateRepositoryButton()}
404421
{this.renderAddExistingRepositoryButton()}

app/src/ui/dispatcher/dispatcher.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,16 @@ export class Dispatcher {
145145
return this.appStore._addTutorialRepository(path, endpoint, apiRepository)
146146
}
147147

148+
/** Resume an already started onboarding tutorial */
149+
public resumeTutorial(repository: Repository) {
150+
return this.appStore._resumeTutorial(repository)
151+
}
152+
153+
/** Suspend the onboarding tutorial and go to the no repositories blank slate view */
154+
public pauseTutorial(repository: Repository) {
155+
return this.appStore._pauseTutorial(repository)
156+
}
157+
148158
/** Remove the repositories represented by the given IDs from local storage. */
149159
public removeRepositories(
150160
repositories: ReadonlyArray<Repository | CloningRepository>,
@@ -1693,6 +1703,13 @@ export class Dispatcher {
16931703
return this.appStore._changeBranchesTab(tab)
16941704
}
16951705

1706+
/**
1707+
* Open the Explore page at the GitHub instance of this repository
1708+
*/
1709+
public showGitHubExplore(repository: Repository): Promise<void> {
1710+
return this.appStore._showGitHubExplore(repository)
1711+
}
1712+
16961713
/**
16971714
* Open the Create Pull Request page on GitHub after verifying ahead/behind.
16981715
*

0 commit comments

Comments
 (0)