Skip to content

Commit e8a930c

Browse files
committed
feat(pf4): wizard predicts nextSteps in realtime
1 parent abd059c commit e8a930c

File tree

12 files changed

+196
-35
lines changed

12 files changed

+196
-35
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/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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
6+
const memoValues = (initialValue) => {
7+
let valueCache = initialValue;
8+
9+
return (value) => {
10+
if (!isEqual(value, valueCache)){
11+
valueCache = value;
12+
return true;
13+
}
14+
15+
return false;
16+
};
17+
};
18+
19+
const WizardNavigationInternal = React.memo(({
20+
navSchema,
21+
activeStepIndex,
22+
formOptions,
23+
maxStepIndex,
24+
jumpToStep,
25+
}) => {
26+
return (navSchema
27+
.filter(field => field.primary)
28+
.map(step => {
29+
const substeps = step.substepOf && navSchema.filter(field => field.substepOf === step.substepOf);
30+
31+
return <WizardNavItem
32+
key={ step.substepOf || step.title }
33+
text={ step.substepOf || step.title }
34+
isCurrent={ substeps ? activeStepIndex >= step.index && activeStepIndex < step.index + substeps.length : activeStepIndex === step.index }
35+
isDisabled={ formOptions.valid ? maxStepIndex < step.index : step.index > activeStepIndex }
36+
onNavItemClick={ (ind) => jumpToStep(ind, formOptions.valid) }
37+
step={ step.index }
38+
>
39+
{ substeps && <WizardNav returnList>
40+
{ substeps.map(substep => <WizardNavItem
41+
key={ substep.title }
42+
text={ substep.title }
43+
isCurrent={ activeStepIndex === substep.index }
44+
isDisabled={ formOptions.valid ?
45+
maxStepIndex < substep.index
46+
: substep.index > activeStepIndex }
47+
onNavItemClick={ (ind) => jumpToStep(ind, formOptions.valid) }
48+
step={ substep.index }
49+
/>) }
50+
</WizardNav> }
51+
</WizardNavItem>;
52+
}));
53+
}, isEqual);
54+
55+
class WizardNavigationClass extends React.Component {
56+
constructor(props) {
57+
super(props);
58+
59+
const { crossroads, values } = this.props;
60+
61+
this.state = {
62+
memoValue: memoValues(crossroads ? crossroads.reduce((acc, curr) => ({
63+
...acc,
64+
[curr]: get(values, curr),
65+
}), {}) : {}),
66+
maxStepIndex: undefined,
67+
};
68+
}
69+
70+
componentDidUpdate(prevProps) {
71+
if (this.props.crossroads) {
72+
const modifiedRoad = this.props.crossroads.reduce((acc, curr) => ({
73+
...acc,
74+
[curr]: get(this.props.values, curr),
75+
}), {});
76+
77+
if (this.state.memoValue(modifiedRoad)) {
78+
this.setState({
79+
maxStepIndex: this.props.activeStepIndex,
80+
});
81+
this.props.setPrevSteps();
82+
} else {
83+
if (prevProps.activeStepIndex !== this.props.activeStepIndex) {
84+
this.setState({ maxStepIndex: undefined });
85+
}
86+
}
87+
}
88+
}
89+
90+
render() {
91+
const {
92+
activeStepIndex,
93+
formOptions,
94+
maxStepIndex,
95+
jumpToStep,
96+
navSchema,
97+
} = this.props;
98+
99+
const { maxStepIndex: maxStepIndexState } = this.state;
100+
101+
const maxIndex = typeof maxStepIndexState === 'number' ? maxStepIndexState : maxStepIndex;
102+
103+
return (
104+
<WizardNavigationInternal
105+
navSchema={ navSchema }
106+
activeStepIndex={ activeStepIndex }
107+
formOptions={ formOptions }
108+
maxStepIndex={ maxIndex }
109+
jumpToStep={ jumpToStep }
110+
/>
111+
);
112+
}
113+
}
114+
115+
export default WizardNavigationClass;

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

Lines changed: 39 additions & 33 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),
@@ -187,9 +189,21 @@ class Wizard extends React.Component {
187189
}
188190

189191
const {
190-
title, description, FieldProvider, formOptions, buttonLabels, buttonsClassName, inModal, setFullWidth, setFullHeight, isCompactNav, showTitles,
192+
title,
193+
description,
194+
FieldProvider,
195+
formOptions,
196+
buttonLabels,
197+
buttonsClassName,
198+
inModal,
199+
setFullWidth,
200+
setFullHeight,
201+
isCompactNav,
202+
showTitles,
203+
FormSpyProvider,
204+
crossroads,
191205
} = this.props;
192-
const { activeStepIndex, navSchema, maxStepIndex } = this.state;
206+
const { activeStepIndex, navSchema, maxStepIndex, isDynamic } = this.state;
193207

194208
const handleSubmit = () =>
195209
formOptions.onSubmit(
@@ -214,34 +228,6 @@ class Wizard extends React.Component {
214228
showTitles={ showTitles }
215229
/>);
216230

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-
245231
return (
246232
<Modal inModal={ inModal } container={ this.container }>
247233
<div className={ `pf-c-wizard ${inModal ? '' : 'no-shadow'} ${isCompactNav ? 'pf-m-compact-nav' : ''} ${setFullWidth ? 'pf-m-full-width' : ''} ${setFullHeight ? 'pf-m-full-height' : ''}` }
@@ -256,7 +242,25 @@ class Wizard extends React.Component {
256242
/> }
257243
<div className="pf-c-wizard__outer-wrap">
258244
<WizardNav>
259-
{ createStepsMap() }
245+
<FormSpyProvider>
246+
{ ({ values }) => (
247+
<WizardNavigation
248+
navSchema={ navSchema }
249+
activeStepIndex={ activeStepIndex }
250+
formOptions={ formOptions }
251+
maxStepIndex={ maxStepIndex }
252+
jumpToStep={ this.jumpToStep }
253+
crossroads={ crossroads }
254+
isDynamic={ isDynamic }
255+
values={ values }
256+
setPrevSteps={ () => this.setState((prevState) => ({
257+
navSchema: this.createSchema({ currentIndex: activeStepIndex }),
258+
prevSteps: prevState.prevSteps.slice(0, activeStepIndex),
259+
maxStepIndex: activeStepIndex,
260+
})) }
261+
/>
262+
) }
263+
</FormSpyProvider>
260264
</WizardNav>
261265
{ cloneElement(currentStep, {
262266
handleNext: (nextStep) => this.handleNext(nextStep, formOptions.getRegisteredFields),
@@ -281,6 +285,7 @@ Wizard.propTypes = {
281285
title: PropTypes.any,
282286
description: PropTypes.any,
283287
FieldProvider: PropTypes.PropTypes.oneOfType([ PropTypes.object, PropTypes.func ]).isRequired,
288+
FormSpyProvider: PropTypes.PropTypes.oneOfType([ PropTypes.object, PropTypes.func ]).isRequired,
284289
formOptions: PropTypes.shape({
285290
getState: PropTypes.func.isRequired,
286291
onSubmit: PropTypes.func.isRequired,
@@ -298,6 +303,7 @@ Wizard.propTypes = {
298303
isDynamic: PropTypes.bool,
299304
showTitles: PropTypes.bool,
300305
predictSteps: PropTypes.bool,
306+
crossroads: PropTypes.arrayOf(PropTypes.string),
301307
};
302308

303309
const defaultLabels = {

packages/react-form-renderer/src/form-renderer/field-wrapper.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3+
import { FormSpy } from 'react-final-form';
34

45
import { components } from '../constants';
56
import FieldProvider from './field-provider';
@@ -38,7 +39,7 @@ const FieldWrapper = ({ componentType, validate, component, ...rest }) => {
3839
const Component = component;
3940
return shouldWrapInField(componentType)
4041
? <FieldProvider { ...componentProps } />
41-
: <Component validate={ composeValidators(validate) } { ...rest } FieldProvider={ FieldProvider } />;
42+
: <Component validate={ composeValidators(validate) } { ...rest } FieldProvider={ FieldProvider } FormSpyProvider={ FormSpy } />;
4243
};
4344

4445
FieldWrapper.propTypes = {

0 commit comments

Comments
 (0)