Skip to content

Commit 7912123

Browse files
committed
add support for skipping prompter backward and forward direction
1 parent 016e478 commit 7912123

File tree

9 files changed

+98
-29
lines changed

9 files changed

+98
-29
lines changed

packages/core/src/dev/activation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ async function openStorageFromInput() {
411411
title: 'Enter a key',
412412
})
413413
} else if (target === 'globalsView') {
414-
return new SkipPrompter('')
414+
return new SkipPrompter()
415415
} else if (target === 'globals') {
416416
// List all globalState keys in the quickpick menu.
417417
const items = globalState
@@ -483,7 +483,7 @@ async function resetState() {
483483

484484
this.form.key.bindPrompter(({ target }) => {
485485
if (target && resettableFeatures.some((f) => f.name === target)) {
486-
return new SkipPrompter('')
486+
return new SkipPrompter()
487487
}
488488
throw new Error('invalid feature target')
489489
})

packages/core/src/shared/ui/common/skipPrompter.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,17 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { StepEstimator } from '../../wizards/wizard'
6+
import { StepEstimator, WIZARD_SKIP } from '../../wizards/wizard'
77
import { Prompter, PromptResult } from '../prompter'
88

9-
/** Pseudo-prompter that immediately returns a value (and thus "skips" a step). */
9+
/** Prompter that return SKIP control signal to parent wizard */
1010
export class SkipPrompter<T> extends Prompter<T> {
11-
/**
12-
* @param val Value immediately returned by the prompter.
13-
*/
14-
constructor(public readonly val: T) {
11+
constructor() {
1512
super()
1613
}
1714

1815
protected async promptUser(): Promise<PromptResult<T>> {
19-
const promptPromise = new Promise<PromptResult<T>>((resolve) => {
20-
resolve(this.val)
21-
})
22-
23-
return await promptPromise
16+
return WIZARD_SKIP
2417
}
2518

2619
public get recentItem(): any {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Wizard, WizardOptions } from '../wizards/wizard'
7+
import { WizardPrompter } from './wizardPrompter'
8+
import { createHash } from 'crypto'
9+
10+
export abstract class NestedWizard<T> extends Wizard<T> {
11+
// Map to store wizard instances
12+
private wizardInstances: Map<string, any> = new Map()
13+
14+
protected constructor(options: WizardOptions<T>) {
15+
super(options)
16+
}
17+
18+
protected createWizardPrompter<T>(constructor: new (...args: any[]) => T, ...args: any[]): WizardPrompter<T> {
19+
const memoizeKey = createHash('sha256')
20+
.update(constructor.name + JSON.stringify(args))
21+
.digest('hex')
22+
if (!this.wizardInstances.get(memoizeKey) as T) {
23+
this.wizardInstances.set(memoizeKey, new constructor(...args))
24+
}
25+
return new WizardPrompter(this.wizardInstances.get(memoizeKey))
26+
}
27+
}

packages/core/src/shared/ui/wizardPrompter.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,22 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { StepEstimator, Wizard } from '../wizards/wizard'
6+
import _ from 'lodash'
7+
import { StepEstimator, Wizard, WIZARD_BACK, WIZARD_SKIP } from '../wizards/wizard'
78
import { Prompter, PromptResult } from './prompter'
89

910
/**
1011
* Wraps {@link Wizard} object into its own {@link Prompter}, allowing wizards to use other
1112
* wizards in their flows.
1213
*/
14+
15+
// eslint-disable-next-line @typescript-eslint/naming-convention
16+
export const WIZARD_PROMPTER = 'WIZARD_PROMPTER'
1317
export class WizardPrompter<T> extends Prompter<T> {
1418
public get recentItem(): any {
1519
return undefined
1620
}
1721
public set recentItem(response: any) {}
18-
1922
private stepOffset: number = 0
2023
private response: T | undefined
2124

@@ -49,8 +52,18 @@ export class WizardPrompter<T> extends Prompter<T> {
4952
}
5053
}
5154

55+
// eslint-disable-next-line @typescript-eslint/naming-convention
5256
protected async promptUser(): Promise<PromptResult<T>> {
5357
this.response = await this.wizard.run()
54-
return this.response
58+
59+
if (this.response === undefined) {
60+
return WIZARD_BACK as PromptResult<T>
61+
} else if (_.isEmpty(this.response)) {
62+
return WIZARD_SKIP as PromptResult<T>
63+
}
64+
65+
return {
66+
...this.response,
67+
} as PromptResult<T>
5568
}
5669
}

packages/core/src/shared/wizards/stateController.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,16 @@ export enum ControlSignal {
99
Retry,
1010
Exit,
1111
Back,
12-
Continue,
12+
Skip,
13+
}
14+
15+
/**
16+
* Value for indicating current direction of the wizard
17+
* Created mainly to support skipping prompters
18+
*/
19+
export enum DIRECTON {
20+
Forward,
21+
Backward,
1322
}
1423

1524
export interface StepResult<TState> {
@@ -39,9 +48,11 @@ export class StateMachineController<TState> {
3948
private extraSteps = new Map<number, Branch<TState>>()
4049
private steps: Branch<TState> = []
4150
private internalStep: number = 0
51+
private direction: DIRECTON
4252

4353
public constructor(private state: TState = {} as TState) {
4454
this.previousStates = [_.cloneDeep(state)]
55+
this.direction = DIRECTON.Forward
4556
}
4657

4758
public addStep(step: StepFunction<TState>): void {
@@ -71,12 +82,14 @@ export class StateMachineController<TState> {
7182

7283
this.state = this.previousStates.pop()!
7384
this.internalStep -= 1
85+
this.direction = DIRECTON.Backward
7486
}
7587

7688
protected advanceState(nextState: TState): void {
7789
this.previousStates.push(this.state)
7890
this.state = nextState
7991
this.internalStep += 1
92+
this.direction = DIRECTON.Forward
8093
}
8194

8295
protected detectCycle(step: StepFunction<TState>): TState | undefined {
@@ -105,6 +118,12 @@ export class StateMachineController<TState> {
105118
}
106119

107120
if (isMachineResult(result)) {
121+
if (result.controlSignal === ControlSignal.Skip) {
122+
/**
123+
* Depending on current wizard direction, skip signal get converted to forward or backward control signal
124+
*/
125+
result.controlSignal = this.direction === DIRECTON.Forward ? undefined : ControlSignal.Back
126+
}
108127
return result
109128
} else {
110129
return { nextState: result }

packages/core/src/shared/wizards/wizard.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ export const WIZARD_RETRY = {
4646
// eslint-disable-next-line @typescript-eslint/naming-convention
4747
export const WIZARD_BACK = { id: WIZARD_CONTROL, type: ControlSignal.Back, toString: () => makeControlString('Back') }
4848
// eslint-disable-next-line @typescript-eslint/naming-convention
49+
export const WIZARD_SKIP = { id: WIZARD_CONTROL, type: ControlSignal.Skip, toString: () => makeControlString('Skip') }
50+
// eslint-disable-next-line @typescript-eslint/naming-convention
4951
export const WIZARD_EXIT = { id: WIZARD_CONTROL, type: ControlSignal.Exit, toString: () => makeControlString('Exit') }
5052

5153
/** Control signals allow for alterations of the normal wizard flow */
52-
export type WizardControl = typeof WIZARD_RETRY | typeof WIZARD_BACK | typeof WIZARD_EXIT
54+
export type WizardControl = typeof WIZARD_RETRY | typeof WIZARD_BACK | typeof WIZARD_EXIT | typeof WIZARD_SKIP
5355

5456
export function isWizardControl(obj: any): obj is WizardControl {
5557
return obj !== undefined && obj.id === WIZARD_CONTROL
@@ -269,9 +271,7 @@ export class Wizard<TState extends Partial<Record<keyof TState, unknown>>> {
269271

270272
if (isValidResponse(answer)) {
271273
state.stepCache.picked = prompter.recentItem
272-
}
273-
274-
if (!isValidResponse(answer)) {
274+
} else {
275275
delete state.stepCache.stepOffset
276276
}
277277

packages/core/src/shared/wizards/wizardForm.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface ContextOptions<TState, TProp> {
2525
* in a single resolution step then they will be added in the order in which they were
2626
* bound.
2727
*/
28-
showWhen?: (state: WizardState<TState>) => boolean
28+
showWhen?: (state: WizardState<TState>) => boolean | Promise<boolean>
2929
/**
3030
* Sets a default value to the target property. This default is applied to the current state
3131
* as long as the property has not been set.
@@ -135,16 +135,30 @@ export class WizardForm<TState extends Partial<Record<keyof TState, unknown>>> {
135135
this.formData.set(key, { ...this.formData.get(key), ...element })
136136
}
137137

138-
public canShowProperty(prop: string, state: TState, defaultState: TState = this.applyDefaults(state)): boolean {
138+
public canShowProperty(
139+
prop: string,
140+
state: TState,
141+
defaultState: TState = this.applyDefaults(state)
142+
): boolean | Promise<boolean> {
139143
const current = _.get(state, prop)
140144
const options = this.formData.get(prop) ?? {}
141145

142146
if (isAssigned(current) || checkParent(prop, state, options)) {
143147
return false
144148
}
145149

146-
if (options.showWhen !== undefined && !options.showWhen(defaultState as WizardState<TState>)) {
147-
return false
150+
if (options.showWhen !== undefined) {
151+
const showStatus = options.showWhen(defaultState as WizardState<TState>)
152+
if (showStatus instanceof Promise) {
153+
return showStatus
154+
.then((result) => {
155+
return result
156+
})
157+
.catch(() => {
158+
return false // Default to not showing if there's an error
159+
})
160+
}
161+
return showStatus
148162
}
149163

150164
return options.provider !== undefined

packages/core/src/test/shared/wizards/wizard.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,11 @@ describe('Wizard', function () {
211211

212212
it('binds prompter to (sync AND async) property', async function () {
213213
wizard.form.prop1.bindPrompter(() => helloPrompter)
214-
wizard.form.prop3.bindPrompter(async () => new SkipPrompter('helloooo (async)'))
214+
wizard.form.prop3.bindPrompter(async () => new SkipPrompter())
215215

216216
const result = await wizard.run()
217217
assert.strictEqual(result?.prop1, 'hello')
218-
assert.strictEqual(result?.prop3, 'helloooo (async)')
218+
assert(!result?.prop3)
219219
})
220220

221221
it('initializes state to empty object if not provided', async function () {

packages/core/src/test/shared/wizards/wizardTestUtils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,11 @@ export async function createWizardTester<T extends Partial<T>>(wizard: Wizard<T>
156156
`No properties of "${propPath}" would be shown`
157157
)
158158
case NOT_ASSERT_SHOW:
159-
return () =>
160-
failIf(form.canShowProperty(propPath, state), `Property "${propPath}" would be shown`)
159+
return async () =>
160+
failIf(
161+
await form.canShowProperty(propPath, state),
162+
`Property "${propPath}" would be shown`
163+
)
161164
case NOT_ASSERT_SHOW_ANY:
162165
return assertShowNone(propPath)
163166
case ASSERT_VALUE:

0 commit comments

Comments
 (0)