Skip to content

Commit 7f0cb80

Browse files
authored
Merge pull request #294 from rvsia/wizardRealtimePredicting
feat(pf4): wizard predicts nextSteps in realtime
2 parents abd059c + d71e4db commit 7f0cb80

File tree

18 files changed

+3582
-1675
lines changed

18 files changed

+3582
-1675
lines changed

packages/mui-component-mapper/src/form-fields/form-fields.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ const FieldInterface = ({
208208
componentType,
209209
initialKey,
210210
FieldArrayProvider, // eslint-disable-line react/prop-types
211+
FormSpyProvider, // eslint-disable-line react/prop-types
211212
...props
212213
}) => (
213214
<Grid xs={ 12 } item style={{ marginBottom: 16, padding: 0 }}>

packages/mui-component-mapper/src/form-fields/sub-form.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const SubForm = ({
99
title,
1010
description,
1111
FieldProvider: _FieldProvider,
12+
FormSpyProvider: _FormSpyProvider,
1213
validate: _validate,
1314
...rest
1415
}) => (
@@ -30,6 +31,7 @@ SubForm.propTypes = {
3031
title: PropTypes.string,
3132
description: PropTypes.string,
3233
FieldProvider: PropTypes.any,
34+
FormSpyProvider: PropTypes.any,
3335
validate: PropTypes.any,
3436
};
3537

packages/pf3-component-mapper/src/form-fields/form-fields.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ const FieldInterface = ({
139139
componentType,
140140
initialKey,
141141
FieldArrayProvider, // eslint-disable-line
142+
FormSpyProvider, // eslint-disable-line react/prop-types
142143
...props
143144
}) => (
144145
fieldMapper(componentType)({

packages/pf3-component-mapper/src/form-fields/sub-form.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const SubForm = ({
77
title,
88
description,
99
FieldProvider: _FieldProvider,
10+
FormSpyProvider: _FormSpyProvider,
1011
validate: _validate,
1112
...rest
1213
}) => (
@@ -26,6 +27,7 @@ SubForm.propTypes = {
2627
title: PropTypes.string,
2728
description: PropTypes.string,
2829
FieldProvider: PropTypes.any,
30+
FormSpyProvider: PropTypes.any,
2931
validate: PropTypes.any,
3032
};
3133

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const wizardSchema = {
3232
fields: [{
3333
component: componentTypes.WIZARD,
3434
name: 'wizzard',
35+
crossroads: ['source.source-type'],
3536
predictSteps: true,
3637
//inModal: true,
3738
title: 'Title',

packages/pf4-component-mapper/demo/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const fieldArrayState = { schema: arraySchemaDDF, additionalOptions: {
2121
class App extends React.Component {
2222
constructor(props) {
2323
super(props);
24-
this.state = fieldArrayState
24+
this.state = { schema: wizardSchema, additionalOptions: { showFormControls: false, wizard: true } }
2525
}
2626

2727
render() {

packages/pf4-component-mapper/src/form-fields/fieldArray/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ const DynamicArray = ({
8383
formOptions,
8484
meta,
8585
FieldArrayProvider,
86+
FormSpyProvider, // eslint-disable-line react/prop-types
8687
minItems,
8788
maxItems,
8889
noItemsMessage,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ const FieldInterface = ({
136136
componentType,
137137
initialKey,
138138
FieldArrayProvider, // catch it and don't send it to components
139+
FormSpyProvider, // eslint-disable-line react/prop-types
139140
...props
140141
}) => (
141142
fieldMapper(componentType)({
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React from 'react';
2+
import { WizardNavItem, WizardNav } from '@patternfly/react-core';
3+
import isEqual from 'lodash/isEqual';
4+
import get from 'lodash/get';
5+
import PropTypes from 'prop-types';
6+
7+
const memoValues = (initialValue) => {
8+
let valueCache = initialValue;
9+
10+
return (value) => {
11+
if (!isEqual(value, valueCache)){
12+
valueCache = value;
13+
return true;
14+
}
15+
16+
return false;
17+
};
18+
};
19+
20+
const WizardNavigationInternal = React.memo(({
21+
navSchema,
22+
activeStepIndex,
23+
formOptions,
24+
maxStepIndex,
25+
jumpToStep,
26+
}) => {
27+
return (navSchema
28+
.filter(field => field.primary)
29+
.map(step => {
30+
const substeps = step.substepOf && navSchema.filter(field => field.substepOf === step.substepOf);
31+
32+
return <WizardNavItem
33+
key={ step.substepOf || step.title }
34+
text={ step.substepOf || step.title }
35+
isCurrent={ substeps ? activeStepIndex >= step.index && activeStepIndex < step.index + substeps.length : activeStepIndex === step.index }
36+
isDisabled={ formOptions.valid ? maxStepIndex < step.index : step.index > activeStepIndex }
37+
onNavItemClick={ (ind) => jumpToStep(ind, formOptions.valid) }
38+
step={ step.index }
39+
>
40+
{ substeps && <WizardNav returnList>
41+
{ substeps.map(substep => <WizardNavItem
42+
key={ substep.title }
43+
text={ substep.title }
44+
isCurrent={ activeStepIndex === substep.index }
45+
isDisabled={ formOptions.valid ?
46+
maxStepIndex < substep.index
47+
: substep.index > activeStepIndex }
48+
onNavItemClick={ (ind) => jumpToStep(ind, formOptions.valid) }
49+
step={ substep.index }
50+
/>) }
51+
</WizardNav> }
52+
</WizardNavItem>;
53+
}));
54+
}, isEqual);
55+
56+
WizardNavigationInternal.propTypes = {
57+
activeStepIndex: PropTypes.number.isRequired,
58+
formOptions: PropTypes.shape({
59+
valid: PropTypes.bool.isRequired,
60+
}).isRequired,
61+
maxStepIndex: PropTypes.number.isRequired,
62+
jumpToStep: PropTypes.func.isRequired,
63+
navSchema: PropTypes.array.isRequired,
64+
};
65+
66+
class WizardNavigationClass extends React.Component {
67+
constructor(props) {
68+
super(props);
69+
70+
const { crossroads, values } = this.props;
71+
72+
this.state = {
73+
memoValue: memoValues(crossroads ? crossroads.reduce((acc, curr) => ({
74+
...acc,
75+
[curr]: get(values, curr),
76+
}), {}) : {}),
77+
maxStepIndex: undefined,
78+
};
79+
}
80+
81+
componentDidUpdate(prevProps) {
82+
if (this.props.crossroads) {
83+
const modifiedRoad = this.props.crossroads.reduce((acc, curr) => ({
84+
...acc,
85+
[curr]: get(this.props.values, curr),
86+
}), {});
87+
88+
if (this.state.memoValue(modifiedRoad)) {
89+
this.setState({
90+
maxStepIndex: this.props.activeStepIndex,
91+
});
92+
this.props.setPrevSteps();
93+
} else {
94+
if (prevProps.activeStepIndex !== this.props.activeStepIndex) {
95+
this.setState({ maxStepIndex: undefined });
96+
}
97+
}
98+
}
99+
}
100+
101+
render() {
102+
const {
103+
activeStepIndex,
104+
formOptions,
105+
maxStepIndex,
106+
jumpToStep,
107+
navSchema,
108+
} = this.props;
109+
110+
const { maxStepIndex: maxStepIndexState } = this.state;
111+
112+
const maxIndex = typeof maxStepIndexState === 'number' ? maxStepIndexState : maxStepIndex;
113+
114+
return (
115+
<WizardNavigationInternal
116+
navSchema={ navSchema }
117+
activeStepIndex={ activeStepIndex }
118+
formOptions={ formOptions }
119+
maxStepIndex={ maxIndex }
120+
jumpToStep={ jumpToStep }
121+
/>
122+
);
123+
}
124+
}
125+
126+
WizardNavigationClass.propTypes = {
127+
activeStepIndex: PropTypes.number.isRequired,
128+
formOptions: PropTypes.object.isRequired,
129+
maxStepIndex: PropTypes.number.isRequired,
130+
jumpToStep: PropTypes.func.isRequired,
131+
setPrevSteps: PropTypes.func.isRequired,
132+
navSchema: PropTypes.array.isRequired,
133+
values: PropTypes.object.isRequired,
134+
crossroads: PropTypes.arrayOf(PropTypes.string),
135+
};
136+
137+
export default WizardNavigationClass;

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

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import React, { cloneElement } from 'react';
22
import { createPortal } from 'react-dom';
33
import PropTypes from 'prop-types';
4-
import { WizardHeader, WizardNav, WizardNavItem, Backdrop, Bullseye } from '@patternfly/react-core';
4+
import { WizardHeader, Backdrop, Bullseye, WizardNav } from '@patternfly/react-core';
55
import WizardStep from './wizard-step';
66
import './wizard-styles.scss';
77
import get from 'lodash/get';
88
import set from 'lodash/set';
99
import flattenDeep from 'lodash/flattenDeep';
1010
import handleEnter from '@data-driven-forms/common/src/wizard/enter-handler';
11+
import WizardNavigation from './wizard-nav';
1112

1213
const DYNAMIC_WIZARD_TYPES = [ 'function', 'object' ];
1314

@@ -106,11 +107,12 @@ class Wizard extends React.Component {
106107
const currentStep = this.findCurrentStep(prevState.prevSteps[index]);
107108
const currentStepHasStepMapper = DYNAMIC_WIZARD_TYPES.includes(typeof currentStep.nextStep);
108109

110+
const hardcodedCrossroads = this.props.crossroads;
109111
const dynamicStepShouldDisableNav = prevState.isDynamic && (currentStepHasStepMapper || !this.props.predictSteps);
110112

111113
const invalidStepShouldDisableNav = valid === false;
112114

113-
if (dynamicStepShouldDisableNav) {
115+
if (dynamicStepShouldDisableNav && !hardcodedCrossroads) {
114116
newState = {
115117
navSchema: this.props.predictSteps ? this.createSchema({ currentIndex: index }) : prevState.navSchema.slice(0, index + INDEXING_BY_ZERO),
116118
prevSteps: prevState.prevSteps.slice(0, index),
@@ -181,73 +183,62 @@ class Wizard extends React.Component {
181183
return schema;
182184
};
183185

186+
handleSubmitFinal = () => this.props.formOptions.onSubmit(
187+
this.handleSubmit(
188+
this.props.formOptions.getState().values,
189+
[ ...this.state.prevSteps, this.state.activeStep ],
190+
this.props.formOptions.getRegisteredFields,
191+
),
192+
this.props.formOptions
193+
);
194+
195+
setPrevSteps = () => this.setState((prevState) => ({
196+
navSchema: this.createSchema({ currentIndex: this.state.activeStepIndex }),
197+
prevSteps: prevState.prevSteps.slice(0, this.state.activeStepIndex),
198+
maxStepIndex: this.state.activeStepIndex,
199+
}))
200+
184201
render() {
185202
if (this.state.loading) {
186203
return null;
187204
}
188205

189206
const {
190-
title, description, FieldProvider, formOptions, buttonLabels, buttonsClassName, inModal, setFullWidth, setFullHeight, isCompactNav, showTitles,
207+
title,
208+
description,
209+
FieldProvider,
210+
formOptions,
211+
buttonLabels,
212+
buttonsClassName,
213+
inModal,
214+
setFullWidth,
215+
setFullHeight,
216+
isCompactNav,
217+
showTitles,
218+
FormSpyProvider,
219+
crossroads,
191220
} = this.props;
192-
const { activeStepIndex, navSchema, maxStepIndex } = this.state;
193-
194-
const handleSubmit = () =>
195-
formOptions.onSubmit(
196-
this.handleSubmit(
197-
formOptions.getState().values,
198-
[ ...this.state.prevSteps, this.state.activeStep ],
199-
formOptions.getRegisteredFields,
200-
),
201-
formOptions
202-
);
221+
const { activeStepIndex, navSchema, maxStepIndex, isDynamic } = this.state;
203222

204223
const currentStep = (
205224
<WizardStep
206225
{ ...this.findCurrentStep(this.state.activeStep) }
207226
formOptions={{
208227
...formOptions,
209-
handleSubmit,
228+
handleSubmit: this.handleSubmitFinal,
210229
}}
211230
buttonLabels={ buttonLabels }
212231
FieldProvider={ FieldProvider }
213232
buttonsClassName={ buttonsClassName }
214233
showTitles={ showTitles }
215234
/>);
216235

217-
const createStepsMap = () => navSchema
218-
.filter(field => field.primary)
219-
.map(step => {
220-
const substeps = step.substepOf && navSchema.filter(field => field.substepOf === step.substepOf);
221-
222-
return <WizardNavItem
223-
key={ step.substepOf || step.title }
224-
text={ step.substepOf || step.title }
225-
isCurrent={ substeps ? activeStepIndex >= step.index && activeStepIndex < step.index + substeps.length : activeStepIndex === step.index }
226-
isDisabled={ formOptions.valid ? maxStepIndex < step.index : step.index > activeStepIndex }
227-
onNavItemClick={ (ind) => this.jumpToStep(ind, formOptions.valid) }
228-
step={ step.index }
229-
>
230-
{ substeps && <WizardNav returnList>
231-
{ substeps.map(substep => <WizardNavItem
232-
key={ substep.title }
233-
text={ substep.title }
234-
isCurrent={ activeStepIndex === substep.index }
235-
isDisabled={ formOptions.valid ?
236-
maxStepIndex < substep.index
237-
: substep.index > activeStepIndex }
238-
onNavItemClick={ (ind) => this.jumpToStep(ind, formOptions.valid) }
239-
step={ substep.index }
240-
/>) }
241-
</WizardNav> }
242-
</WizardNavItem>;
243-
});
244-
245236
return (
246237
<Modal inModal={ inModal } container={ this.container }>
247238
<div className={ `pf-c-wizard ${inModal ? '' : 'no-shadow'} ${isCompactNav ? 'pf-m-compact-nav' : ''} ${setFullWidth ? 'pf-m-full-width' : ''} ${setFullHeight ? 'pf-m-full-height' : ''}` }
248239
role="dialog"
249240
aria-modal={ inModal ? 'true' : undefined }
250-
onKeyDown={ e => handleEnter(e, formOptions, this.state.activeStep, this.findCurrentStep, this.handleNext, handleSubmit) }
241+
onKeyDown={ e => handleEnter(e, formOptions, this.state.activeStep, this.findCurrentStep, this.handleNext, this.handleSubmitFinal) }
251242
>
252243
{ title && <WizardHeader
253244
title={ title }
@@ -256,7 +247,21 @@ class Wizard extends React.Component {
256247
/> }
257248
<div className="pf-c-wizard__outer-wrap">
258249
<WizardNav>
259-
{ createStepsMap() }
250+
<FormSpyProvider>
251+
{ ({ values }) => (
252+
<WizardNavigation
253+
navSchema={ navSchema }
254+
activeStepIndex={ activeStepIndex }
255+
formOptions={ formOptions }
256+
maxStepIndex={ maxStepIndex }
257+
jumpToStep={ this.jumpToStep }
258+
crossroads={ crossroads }
259+
isDynamic={ isDynamic }
260+
values={ values }
261+
setPrevSteps={ this.setPrevSteps }
262+
/>
263+
) }
264+
</FormSpyProvider>
260265
</WizardNav>
261266
{ cloneElement(currentStep, {
262267
handleNext: (nextStep) => this.handleNext(nextStep, formOptions.getRegisteredFields),
@@ -281,6 +286,7 @@ Wizard.propTypes = {
281286
title: PropTypes.any,
282287
description: PropTypes.any,
283288
FieldProvider: PropTypes.PropTypes.oneOfType([ PropTypes.object, PropTypes.func ]).isRequired,
289+
FormSpyProvider: PropTypes.PropTypes.oneOfType([ PropTypes.object, PropTypes.func ]).isRequired,
284290
formOptions: PropTypes.shape({
285291
getState: PropTypes.func.isRequired,
286292
onSubmit: PropTypes.func.isRequired,
@@ -298,6 +304,7 @@ Wizard.propTypes = {
298304
isDynamic: PropTypes.bool,
299305
showTitles: PropTypes.bool,
300306
predictSteps: PropTypes.bool,
307+
crossroads: PropTypes.arrayOf(PropTypes.string),
301308
};
302309

303310
const defaultLabels = {

0 commit comments

Comments
 (0)