Skip to content

Commit 91bdf11

Browse files
authored
Merge pull request #167 from rvsia/dynamic-wizard-pf4-future
feat(pf4): predict the future in the wizard nav
2 parents 9733d38 + 4ef5241 commit 91bdf11

File tree

4 files changed

+227
-48
lines changed

4 files changed

+227
-48
lines changed

packages/pf4-component-mapper/demo/demo-schemas/wizard-schema.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const ValidateButtons = ({ disableBack, handlePrev, buttonLabels: { back, cancel
77

88
const setValidating = () => {
99
setState('validating');
10-
setTimeout(() => setState('done'), 2000);
10+
setTimeout(() => setState('done'), 0);
1111
};
1212

1313
return (
@@ -32,6 +32,7 @@ export const wizardSchema = {
3232
fields: [{
3333
component: componentTypes.WIZARD,
3434
name: 'wizzard',
35+
predictSteps: true,
3536
//inModal: true,
3637
title: 'Title',
3738
showTitles: true,
@@ -97,6 +98,9 @@ export const wizardSchema = {
9798
component: componentTypes.TEXT_FIELD,
9899
name: 'google.google-field',
99100
label: 'Google field part',
101+
validate: [{
102+
type: validatorTypes.REQUIRED,
103+
}],
100104
}],
101105
}, {
102106
fields: [{
@@ -201,10 +205,12 @@ export const wizardSchemaSubsteps = {
201205
export const wizardSchemaMoreSubsteps = {
202206
fields: [{
203207
component: componentTypes.WIZARD,
208+
isDynamic: true,
204209
name: 'wizzard',
205-
title: 'Title',
210+
title: 'Dynamic with steps predicting',
206211
description: 'Description',
207212
buttonsPosition: 'left',
213+
predictSteps: true,
208214
fields: [{
209215
title: 'Get started with adding source',
210216
name: 'step-1',
@@ -228,15 +234,16 @@ export const wizardSchemaMoreSubsteps = {
228234
label: 'Aws field part',
229235
}],
230236
}, {
231-
title: 'Configure AWS part 2',
237+
title: 'Configure AWS part 2 - disabled jumping',
232238
name: 'step-88',
239+
disableForwardJumping: true,
233240
stepKey: 'aws2',
234241
nextStep: 'summary',
235242
substepOf: 'Summary',
236243
fields: [{
237244
component: componentTypes.TEXT_FIELD,
238-
name: 'aws-field',
239-
label: 'Aws field part',
245+
name: 'aws-field-1',
246+
label: 'Aws field part 1',
240247
}],
241248
},
242249
{

packages/pf4-component-mapper/src/form-fields/wizard/wizard.js

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,13 @@ class Wizard extends React.Component {
2121
// find if wizard contains any dynamic steps (nextStep is mapper object)
2222
const isDynamic = this.props.isDynamic ? true : this.props.fields.find(({ nextStep }) => typeof nextStep === 'object') ? true : false;
2323

24-
// insert into navigation schema first step if dynamic, otherwise create the whole schema
25-
// if the wizard is dynamic, the navigation is build progressively
26-
const firstStep = this.props.fields.find(({ stepKey }) => stepKey === 1 || stepKey === '1');
27-
28-
const navSchema = isDynamic ?
29-
[{ title: firstStep.title, index: 0, primary: true, substepOf: firstStep.substepOf }]
30-
: this.createSchema();
31-
3224
this.state = {
3325
activeStep: this.props.fields[0].stepKey,
3426
prevSteps: [],
3527
activeStepIndex: 0,
3628
maxStepIndex: 0,
3729
isDynamic, // wizard contains nextStep mapper
38-
navSchema, // schema of navigation
30+
navSchema: this.createSchema({ currentIndex: 0, isDynamic }),
3931
loading: true,
4032
};
4133
}
@@ -55,38 +47,18 @@ class Wizard extends React.Component {
5547
}
5648
}
5749

58-
insertDynamicStep = (nextStep, navSchema) => {
59-
const { title, substepOf } = this.props.fields.find(({ stepKey }) => stepKey === nextStep);
60-
const lastStep = navSchema[navSchema.length - 1];
61-
62-
return [
63-
...navSchema,
64-
{
65-
title,
66-
substepOf,
67-
index: lastStep.index + 1,
68-
primary: (!substepOf) || (substepOf && substepOf !== lastStep.substepOf),
69-
},
70-
];
71-
}
72-
7350
handleNext = (nextStep, getRegisteredFields) =>
74-
this.setState(prevState =>
75-
({
76-
registeredFieldsHistory: { ...prevState.registeredFieldsHistory, [prevState.activeStep]: getRegisteredFields() },
77-
activeStep: nextStep,
78-
prevSteps: prevState.prevSteps.includes(prevState.activeStep) ? prevState.prevSteps : [ ...prevState.prevSteps, prevState.activeStep ],
79-
activeStepIndex: prevState.activeStepIndex + 1,
80-
maxStepIndex: (prevState.activeStepIndex + 1) > prevState.maxStepIndex ? prevState.maxStepIndex + 1 : prevState.maxStepIndex,
81-
navSchema: this.state.isDynamic ? this.insertDynamicStep(nextStep, prevState.navSchema) : prevState.navSchema,
82-
}));
51+
this.setState(prevState => ({
52+
registeredFieldsHistory: { ...prevState.registeredFieldsHistory, [prevState.activeStep]: getRegisteredFields() },
53+
activeStep: nextStep,
54+
prevSteps: prevState.prevSteps.includes(prevState.activeStep) ? prevState.prevSteps : [ ...prevState.prevSteps, prevState.activeStep ],
55+
activeStepIndex: prevState.activeStepIndex + 1,
56+
maxStepIndex: (prevState.activeStepIndex + 1) > prevState.maxStepIndex ? prevState.maxStepIndex + 1 : prevState.maxStepIndex,
57+
navSchema: this.state.isDynamic ? this.createSchema({ currentIndex: prevState.activeStepIndex + 1 }) : prevState.navSchema,
58+
}));
8359

8460
handlePrev = () => this.jumpToStep(this.state.activeStepIndex - 1);
8561

86-
findActiveFields = visitedSteps =>
87-
visitedSteps.map(key =>this.findCurrentStep(key).fields.map(({ name }) => name))
88-
.reduce((acc, curr) => curr.concat(acc.map(item => item)), []);
89-
9062
handleSubmit = (values, visitedSteps, getRegisteredFields) => {
9163
// Add the final step fields to history
9264
const finalRegisteredFieldsHistory = {
@@ -116,11 +88,21 @@ class Wizard extends React.Component {
11688
activeStepIndex: index,
11789
}));
11890

119-
// jumping in dynamic form disables returning to back (!)
120-
if (this.state.isDynamic) {
91+
const currentStep = this.findCurrentStep(this.state.prevSteps[index]);
92+
const currentStepHasStepMapper = typeof currentStep.nextStep === 'object';
93+
94+
if (this.state.isDynamic && (currentStepHasStepMapper || !this.props.predictSteps)) {
12195
this.setState((prevState) => ({
12296
navSchema: prevState.navSchema.slice(0, index + 1),
12397
prevSteps: prevState.prevSteps.slice(0, index + 1),
98+
maxStepIndex: prevState.prevSteps.slice(0, index + 1).length,
99+
}));
100+
}
101+
102+
if (currentStep.disableForwardJumping) {
103+
this.setState((prevState) => ({
104+
prevSteps: prevState.prevSteps.slice(0, index + 1),
105+
maxStepIndex: prevState.prevSteps.slice(0, index).length,
124106
}));
125107
}
126108

@@ -135,21 +117,42 @@ class Wizard extends React.Component {
135117
};
136118

137119
// builds schema used for generating of the navigation links
138-
createSchema = () => {
120+
createSchema = ({ currentIndex, isDynamic }) => {
121+
if (typeof isDynamic === 'undefined'){
122+
isDynamic = this.state.isDynamic;
123+
}
124+
125+
const { formOptions, predictSteps } = this.props;
126+
const { values } = formOptions.getState();
139127
let schema = [];
140128
let field = this.props.fields.find(({ stepKey }) => stepKey === 1 || stepKey === '1'); // find first wizard step
141-
let index = 0;
129+
let index = -1;
142130

143131
while (field){
132+
index += 1;
144133
schema = [
145134
...schema,
146135
{ title: field.title,
147136
substepOf: field.substepOf,
148-
index: index++,
137+
index,
149138
primary: (!schema[schema.length - 1] || !field.substepOf || field.substepOf !== schema[schema.length - 1].substepOf) },
150139
];
151140

152-
field = this.props.fields.find(({ stepKey }) => stepKey === field.nextStep);
141+
if (isDynamic && !predictSteps && currentIndex === index) {
142+
break;
143+
}
144+
145+
let nextStep = field.nextStep;
146+
147+
if (typeof field.nextStep === 'object') {
148+
nextStep = nextStep.stepMapper[get(values, nextStep.when)];
149+
}
150+
151+
if (nextStep) {
152+
field = this.props.fields.find(({ stepKey }) => stepKey === nextStep);
153+
} else {
154+
field = undefined;
155+
}
153156
}
154157

155158
return schema;
@@ -268,6 +271,7 @@ Wizard.propTypes = {
268271
setFullHeight: PropTypes.bool,
269272
isDynamic: PropTypes.bool,
270273
showTitles: PropTypes.bool,
274+
predictSteps: PropTypes.bool,
271275
};
272276

273277
const defaultLabels = {

packages/pf4-component-mapper/src/tests/wizard/wizard.test.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,4 +480,168 @@ describe('<Wizard />', () => {
480480

481481
expect(wrapper.find('.pf-c-wizard__nav-item').last().childAt(0).prop('aria-disabled')).toEqual(false);
482482
});
483+
484+
describe('predicting steps', () => {
485+
const FIRST_TITLE = 'Get started with adding source';
486+
const SECOND_TITLE_AWS = 'Configure AWS';
487+
const SECOND_TITLE_GOOLE = 'Configure google';
488+
const THIRD_TITLE = 'Summary';
489+
490+
const wizardSchema = {
491+
fields: [{
492+
component: componentTypes.WIZARD,
493+
name: 'wizard',
494+
predictSteps: true,
495+
fields: [{
496+
title: FIRST_TITLE,
497+
stepKey: 1,
498+
nextStep: {
499+
when: 'source.source-type',
500+
stepMapper: {
501+
aws: 'aws',
502+
google: 'google',
503+
},
504+
},
505+
fields: [{
506+
name: 'source.source-type',
507+
label: 'Source type',
508+
component: componentTypes.TEXT_FIELD,
509+
}],
510+
}, {
511+
title: SECOND_TITLE_AWS,
512+
stepKey: 'aws',
513+
nextStep: 'summary',
514+
fields: [{
515+
component: componentTypes.TEXT_FIELD,
516+
name: 'aws-field',
517+
label: 'Aws field part',
518+
}],
519+
}, {
520+
title: SECOND_TITLE_GOOLE,
521+
stepKey: 'google',
522+
nextStep: 'summary',
523+
fields: [{
524+
component: componentTypes.TEXT_FIELD,
525+
name: 'google.google-field',
526+
label: 'Google field part',
527+
}],
528+
}, {
529+
title: THIRD_TITLE,
530+
fields: [],
531+
stepKey: 'summary',
532+
}],
533+
}],
534+
};
535+
536+
const nextButtonClick = (wrapper) => {
537+
wrapper.find('button').at(0).simulate('click');
538+
wrapper.update();
539+
};
540+
541+
const backButtonClick = (wrapper) => {
542+
wrapper.find('button').at(1).simulate('click');
543+
wrapper.update();
544+
};
545+
546+
const changeValue = (wrapper, value) => {
547+
wrapper.find('input').instance().value = value;
548+
wrapper.find('input').simulate('change');
549+
wrapper.update();
550+
};
551+
552+
it('predict steps with dynamic wizard', () => {
553+
const wrapper = mount(<FormRenderer
554+
schema={ wizardSchema }
555+
formFieldsMapper={ formFieldsMapper }
556+
layoutMapper={ layoutMapper }
557+
onSubmit={ jest.fn() }
558+
onCancel={ jest.fn() }
559+
showFormControls={ false }
560+
/>);
561+
562+
expect(wrapper.find('WizardNavItem')).toHaveLength(1);
563+
expect(wrapper.find('WizardNavItem').at(0).text()).toEqual(FIRST_TITLE);
564+
565+
changeValue(wrapper, 'aws');
566+
nextButtonClick(wrapper);
567+
568+
expect(wrapper.find('WizardNavItem')).toHaveLength(3);
569+
expect(wrapper.find('WizardNavItem').at(0).text()).toEqual(FIRST_TITLE);
570+
expect(wrapper.find('WizardNavItem').at(1).text()).toEqual(SECOND_TITLE_AWS);
571+
expect(wrapper.find('WizardNavItem').at(2).text()).toEqual(THIRD_TITLE);
572+
});
573+
574+
it('reset nav when jumped into compileMapper step', () => {
575+
const wrapper = mount(<FormRenderer
576+
schema={ wizardSchema }
577+
formFieldsMapper={ formFieldsMapper }
578+
layoutMapper={ layoutMapper }
579+
onSubmit={ jest.fn() }
580+
onCancel={ jest.fn() }
581+
showFormControls={ false }
582+
/>);
583+
584+
changeValue(wrapper, 'aws');
585+
nextButtonClick(wrapper);
586+
587+
expect(wrapper.find('WizardNavItem')).toHaveLength(3);
588+
589+
backButtonClick(wrapper);
590+
591+
expect(wrapper.find('WizardNavItem')).toHaveLength(1);
592+
expect(wrapper.find('WizardNavItem').at(0).text()).toEqual(FIRST_TITLE);
593+
});
594+
595+
it('disable nav when jumped into disableForwardJumping step', () => {
596+
const wizardSchema = {
597+
fields: [{
598+
component: componentTypes.WIZARD,
599+
name: 'wizard',
600+
predictSteps: true,
601+
fields: [{
602+
title: FIRST_TITLE,
603+
stepKey: 1,
604+
nextStep: 'aws',
605+
disableForwardJumping: true,
606+
fields: [{
607+
name: 'source.source-type',
608+
label: 'Source type',
609+
component: componentTypes.TEXT_FIELD,
610+
}],
611+
}, {
612+
title: SECOND_TITLE_AWS,
613+
stepKey: 'aws',
614+
nextStep: 'summary',
615+
fields: [{
616+
component: componentTypes.TEXT_FIELD,
617+
name: 'aws-field',
618+
label: 'Aws field part',
619+
}],
620+
}],
621+
}],
622+
};
623+
624+
const wrapper = mount(<FormRenderer
625+
schema={ wizardSchema }
626+
formFieldsMapper={ formFieldsMapper }
627+
layoutMapper={ layoutMapper }
628+
onSubmit={ jest.fn() }
629+
onCancel={ jest.fn() }
630+
showFormControls={ false }
631+
/>);
632+
633+
changeValue(wrapper, 'aws');
634+
nextButtonClick(wrapper);
635+
636+
expect(wrapper.find('WizardNavItem')).toHaveLength(2);
637+
expect(wrapper.find('WizardNavItem').at(0).props().isDisabled).toEqual(false);
638+
expect(wrapper.find('WizardNavItem').at(1).props().isDisabled).toEqual(false);
639+
640+
backButtonClick(wrapper);
641+
642+
expect(wrapper.find('WizardNavItem')).toHaveLength(2);
643+
expect(wrapper.find('WizardNavItem').at(0).props().isDisabled).toEqual(false);
644+
expect(wrapper.find('WizardNavItem').at(1).props().isDisabled).toEqual(true);
645+
});
646+
});
483647
});

0 commit comments

Comments
 (0)