Skip to content

Commit a0506ee

Browse files
authored
Merge pull request #398 from rvsia/wizardCommon
[V2] Wizard common
2 parents 999d4c0 + 2d0944d commit a0506ee

File tree

10 files changed

+4869
-2600
lines changed

10 files changed

+4869
-2600
lines changed

packages/common/src/wizard/reducer.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import get from 'lodash/get';
2+
3+
export const DYNAMIC_WIZARD_TYPES = ['function', 'object'];
4+
5+
const createSchema = ({ formOptions, fields }) => {
6+
const { values } = formOptions.getState();
7+
let schema = [];
8+
let field = fields[0]; // find first wizard step
9+
let index = -1;
10+
11+
while (field) {
12+
index += 1;
13+
schema = [
14+
...schema,
15+
{
16+
title: field.title,
17+
substepOf: field.substepOf,
18+
index,
19+
primary: !schema[schema.length - 1] || !field.substepOf || field.substepOf !== schema[schema.length - 1].substepOf
20+
}
21+
];
22+
23+
let nextStep = field.nextStep;
24+
25+
if (typeof field.nextStep === 'object') {
26+
nextStep = nextStep.stepMapper[get(values, nextStep.when)];
27+
}
28+
29+
if (typeof field.nextStep === 'function') {
30+
nextStep = field.nextStep({ values });
31+
}
32+
33+
if (nextStep) {
34+
field = fields.find(({ name }) => name === nextStep);
35+
} else {
36+
field = undefined;
37+
}
38+
}
39+
40+
return schema;
41+
};
42+
43+
const handleNext = (state, nextStep, formOptions, fields) => {
44+
const newActiveIndex = state.activeStepIndex + 1;
45+
const shouldInsertStepIntoHistory = state.prevSteps.includes(state.activeStep);
46+
47+
return {
48+
...state,
49+
registeredFieldsHistory: { ...state.registeredFieldsHistory, [state.activeStep]: formOptions.getRegisteredFields() },
50+
activeStep: nextStep,
51+
prevSteps: shouldInsertStepIntoHistory ? state.prevSteps : [...state.prevSteps, state.activeStep],
52+
activeStepIndex: newActiveIndex,
53+
maxStepIndex: newActiveIndex > state.maxStepIndex ? newActiveIndex : state.maxStepIndex,
54+
navSchema: state.isDynamic
55+
? createSchema({
56+
...state,
57+
fields,
58+
formOptions,
59+
currentIndex: newActiveIndex
60+
})
61+
: state.navSchema
62+
};
63+
};
64+
65+
export const findCurrentStep = (activeStep, fields) => fields.find(({ name }) => name === activeStep);
66+
67+
const jumpToStep = (state, index, valid, fields, crossroads, formOptions) => {
68+
const clickOnPreviousStep = state.prevSteps[index];
69+
70+
if (clickOnPreviousStep) {
71+
let originalActiveStep;
72+
73+
const includeActiveStep = state.prevSteps.includes(state.activeStep, fields);
74+
originalActiveStep = state.activeStep;
75+
76+
const newState = {
77+
...state,
78+
activeStep: state.prevSteps[index],
79+
prevSteps: includeActiveStep ? state.prevSteps : [...state.prevSteps, state.activeStep],
80+
activeStepIndex: index
81+
};
82+
83+
const INDEXING_BY_ZERO = 1;
84+
85+
const currentStep = findCurrentStep(newState.prevSteps[index], fields);
86+
87+
const currentStepHasStepMapper = DYNAMIC_WIZARD_TYPES.includes(typeof currentStep.nextStep);
88+
89+
const hardcodedCrossroads = crossroads;
90+
const dynamicStepShouldDisableNav = newState.isDynamic && currentStepHasStepMapper;
91+
92+
const invalidStepShouldDisableNav = valid === false;
93+
94+
let updatedState = {
95+
...newState
96+
};
97+
98+
if (dynamicStepShouldDisableNav && !hardcodedCrossroads) {
99+
updatedState = {
100+
...updatedState,
101+
navSchema: createSchema({
102+
...updatedState,
103+
formOptions,
104+
fields,
105+
currentIndex: index
106+
}),
107+
prevSteps: newState.prevSteps.slice(0, index),
108+
maxStepIndex: index
109+
};
110+
} else if (currentStep.disableForwardJumping) {
111+
updatedState = {
112+
...updatedState,
113+
prevSteps: newState.prevSteps.slice(0, index),
114+
maxStepIndex: index
115+
};
116+
} else if (invalidStepShouldDisableNav) {
117+
const indexOfCurrentStep = newState.prevSteps.indexOf(originalActiveStep);
118+
119+
updatedState = {
120+
...updatedState,
121+
prevSteps: newState.prevSteps.slice(0, indexOfCurrentStep + INDEXING_BY_ZERO),
122+
maxStepIndex: newState.prevSteps.slice(0, indexOfCurrentStep + INDEXING_BY_ZERO).length - INDEXING_BY_ZERO
123+
};
124+
}
125+
126+
return updatedState;
127+
}
128+
};
129+
130+
const reducer = (state, { type, payload }) => {
131+
switch (type) {
132+
case 'finishLoading':
133+
return {
134+
...state,
135+
loading: false,
136+
navSchema: createSchema({
137+
...state,
138+
fields: payload.fields,
139+
formOptions: payload.formOptions,
140+
currentIndex: 0
141+
})
142+
};
143+
case 'handleNext':
144+
return handleNext(state, payload.nextStep, payload.formOptions, payload.fields);
145+
case 'setPrevSteps':
146+
return {
147+
...state,
148+
prevSteps: state.prevSteps.slice(0, state.activeStepIndex),
149+
maxStepIndex: state.activeStepIndex,
150+
navSchema: createSchema({
151+
...state,
152+
fields: payload.fields,
153+
formOptions: payload.formOptions,
154+
currentIndex: state.activeStepIndex
155+
})
156+
};
157+
case 'jumpToStep':
158+
return jumpToStep(state, payload.index, payload.valid, payload.fields, payload.crossroads, payload.formOptions);
159+
default:
160+
return state;
161+
}
162+
};
163+
164+
export default reducer;

packages/common/src/wizard/wizard.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import React, { useReducer, useEffect } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { useFormApi } from '@data-driven-forms/react-form-renderer';
4+
5+
import get from 'lodash/get';
6+
import set from 'lodash/set';
7+
import flattenDeep from 'lodash/flattenDeep';
8+
import handleEnter from './enter-handler';
9+
import reducer, { DYNAMIC_WIZARD_TYPES, findCurrentStep } from './reducer';
10+
11+
const Wizard = ({ fields, isDynamic, crossroads, Wizard, ...props }) => {
12+
const formOptions = useFormApi();
13+
14+
const [state, dispatch] = useReducer(reducer, {
15+
activeStep: fields[0].name,
16+
prevSteps: [],
17+
activeStepIndex: 0,
18+
maxStepIndex: 0,
19+
isDynamic: isDynamic || fields.some(({ nextStep }) => DYNAMIC_WIZARD_TYPES.includes(typeof nextStep)),
20+
loading: true
21+
});
22+
23+
useEffect(() => {
24+
dispatch({ type: 'finishLoading', payload: { formOptions, fields } });
25+
}, [fields]);
26+
27+
if (state.loading) {
28+
return null;
29+
}
30+
31+
const prepareValues = (values, visitedSteps, getRegisteredFields) => {
32+
// Add the final step fields to history
33+
const finalRegisteredFieldsHistory = {
34+
...state.registeredFieldsHistory,
35+
[state.activeStep]: getRegisteredFields()
36+
};
37+
38+
const finalObject = {};
39+
40+
// Find only visited fields
41+
flattenDeep(
42+
Object.values([...visitedSteps, state.activeStep].reduce((obj, key) => ({ ...obj, [key]: finalRegisteredFieldsHistory[key] }), {}))
43+
).forEach((key) => set(finalObject, key, get(values, key)));
44+
45+
return finalObject;
46+
};
47+
48+
const handleSubmit = () =>
49+
formOptions.onSubmit(
50+
prepareValues(formOptions.getState().values, [...state.prevSteps, state.activeStep], formOptions.getRegisteredFields),
51+
formOptions
52+
);
53+
54+
const jumpToStep = (index, valid) => dispatch({ type: 'jumpToStep', payload: { index, valid, fields, crossroads, formOptions } });
55+
56+
const handlePrev = () => jumpToStep(state.activeStepIndex - 1);
57+
58+
const handleNext = (nextStep) => dispatch({ type: 'handleNext', payload: { nextStep, formOptions, fields } });
59+
60+
const setPrevSteps = () => dispatch({ type: 'setPrevSteps', payload: { formOptions, fields } });
61+
62+
const findCurrentStepWrapped = (step) => findCurrentStep(step, fields);
63+
64+
const onKeyDown = (e) => handleEnter(e, formOptions, state.activeStep, findCurrentStepWrapped, handleNext, handleSubmit);
65+
66+
return (
67+
<Wizard
68+
{...props}
69+
handleNext={handleNext}
70+
onKeyDown={onKeyDown}
71+
setPrevSteps={setPrevSteps}
72+
currentStep={findCurrentStep(state.activeStep, fields)}
73+
jumpToStep={jumpToStep}
74+
handlePrev={handlePrev}
75+
formOptions={{
76+
...formOptions,
77+
handleSubmit
78+
}}
79+
navSchema={state.navSchema}
80+
activeStepIndex={state.activeStepIndex}
81+
maxStepIndex={state.maxStepIndex}
82+
isDynamic={state.isDynamic}
83+
crossroads={crossroads}
84+
prevSteps={state.prevSteps}
85+
/>
86+
);
87+
};
88+
89+
Wizard.propTypes = {
90+
fields: PropTypes.arrayOf(
91+
PropTypes.shape({
92+
name: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
93+
})
94+
).isRequired,
95+
isDynamic: PropTypes.bool,
96+
crossroads: PropTypes.arrayOf(PropTypes.string),
97+
Wizard: PropTypes.oneOfType([PropTypes.node, PropTypes.func])
98+
};
99+
100+
export default Wizard;
101+
102+
export const wizardProps = {
103+
currentStep: PropTypes.object,
104+
handlePrev: PropTypes.func,
105+
onKeyDown: PropTypes.func,
106+
jumpToStep: PropTypes.func,
107+
setPrevSteps: PropTypes.func,
108+
handleNext: PropTypes.func,
109+
navSchema: PropTypes.array,
110+
activeStepIndex: PropTypes.number,
111+
maxStepIndex: PropTypes.number,
112+
formOptions: PropTypes.shape({
113+
onCancel: PropTypes.func
114+
}),
115+
prevSteps: PropTypes.array
116+
};
Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,11 @@
1-
import React, { useState, cloneElement } from 'react';
1+
import React, { cloneElement } from 'react';
22
import PropTypes from 'prop-types';
33
import WizardStep from './wizard/wizard-step';
44
import { Grid, Typography } from '@material-ui/core';
5-
import { useFormApi } from '@data-driven-forms/react-form-renderer';
5+
import Wizard, { wizardProps } from '@data-driven-forms/common/src/wizard/wizard';
66

7-
const Wizard = ({ fields, title, description }) => {
8-
const [activeStep, setActiveStep] = useState(fields[0].name);
9-
const [prevSteps, setPrevSteps] = useState([]);
10-
11-
const formOptions = useFormApi();
12-
13-
const handleNext = (nextStep) => {
14-
setPrevSteps([...prevSteps, activeStep]);
15-
setActiveStep(nextStep);
16-
};
17-
18-
const handlePrev = () => {
19-
setActiveStep(prevSteps[prevSteps.length - 1]);
20-
21-
const newSteps = prevSteps;
22-
newSteps.pop();
23-
setPrevSteps(newSteps);
24-
};
25-
26-
const findCurrentStep = (activeStep) => fields.find(({ name }) => name === activeStep);
27-
28-
const findActiveFields = (visitedSteps) =>
29-
visitedSteps.map((key) => findCurrentStep(key).fields.map(({ name }) => name)).reduce((acc, curr) => curr.concat(acc.map((item) => item)), []);
30-
31-
const getValues = (values, visitedSteps) =>
32-
Object.keys(values)
33-
.filter((key) => findActiveFields(visitedSteps).includes(key))
34-
.reduce((acc, curr) => ({ ...acc, [curr]: values[curr] }), {});
35-
36-
const handleSubmit = () => formOptions.onSubmit(getValues(formOptions.getState().values, [...prevSteps, activeStep]));
37-
38-
const currentStep = (
39-
<WizardStep
40-
{...findCurrentStep(activeStep)}
41-
formOptions={{
42-
...formOptions,
43-
handleSubmit
44-
}}
45-
/>
46-
);
7+
const WizardInternal = ({ title, description, currentStep, formOptions, prevSteps, handleNext, handlePrev }) => {
8+
const step = <WizardStep {...currentStep} formOptions={formOptions} />;
479

4810
return (
4911
<Grid container spacing={6}>
@@ -53,7 +15,7 @@ const Wizard = ({ fields, title, description }) => {
5315
<Typography component="h5">{`Step ${prevSteps.length + 1}`}</Typography>
5416
</Grid>
5517
<Grid item xs={12}>
56-
{cloneElement(currentStep, {
18+
{cloneElement(step, {
5719
handleNext,
5820
handlePrev,
5921
disableBack: prevSteps.length === 0
@@ -63,14 +25,12 @@ const Wizard = ({ fields, title, description }) => {
6325
);
6426
};
6527

66-
Wizard.propTypes = {
28+
WizardInternal.propTypes = {
6729
title: PropTypes.node,
6830
description: PropTypes.node,
69-
fields: PropTypes.arrayOf(
70-
PropTypes.shape({
71-
name: PropTypes.string
72-
})
73-
).isRequired
31+
...wizardProps
7432
};
7533

76-
export default Wizard;
34+
const MuiWizard = (props) => <Wizard Wizard={WizardInternal} {...props} />;
35+
36+
export default MuiWizard;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const selectSchema = {
104104
}
105105

106106
const App = () => {
107-
const [schema, setSchema] = useState(sandbox)
107+
const [schema, setSchema] = useState(wizardSchema)
108108

109109
return (
110110
<div>

0 commit comments

Comments
 (0)