-
-
Notifications
You must be signed in to change notification settings - Fork 471
OBPIH-6395 Add reusable useWizard hook #4642
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
19808f1
65d7485
d854d21
1be7055
e3b97d4
ffdfb68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import React, { useMemo } from 'react'; | ||
|
|
||
| import OutboundImportConfirm from 'components/stock-movement-wizard/outboundImport/OutboundImportConfirm'; | ||
| import OutboundImportDetails from 'components/stock-movement-wizard/outboundImport/OutboundImportDetails'; | ||
| import WizardStepsV2 from 'components/wizard/v2/WizardStepsV2'; | ||
| import useWizard from 'hooks/useWizard'; | ||
|
|
||
| const OutboundImport = () => { | ||
| const OutboundImportStep = { | ||
| DETAILS: { | ||
| key: 'DETAILS', | ||
| // TODO: Make titles translatable in OBPIH-6329 | ||
| title: 'Create', | ||
| }, | ||
| CONFIRM: { | ||
| key: 'CONFIRM', | ||
| title: 'Confirm', | ||
| }, | ||
| }; | ||
| const steps = useMemo(() => [ | ||
| { | ||
| key: OutboundImportStep.DETAILS.key, | ||
| title: OutboundImportStep.DETAILS.title, | ||
| Component: () => (<OutboundImportDetails />), | ||
| }, | ||
| { | ||
| key: OutboundImportStep.CONFIRM.key, | ||
| title: OutboundImportStep.CONFIRM.title, | ||
| Component: () => (<OutboundImportConfirm />), | ||
| }, | ||
| ], []); | ||
|
|
||
| const stepsTitles = steps.map((step) => ({ | ||
| title: step.title, | ||
| key: step.key, | ||
| })); | ||
|
|
||
| const [ | ||
| Step, | ||
| { | ||
| next, | ||
| previous, | ||
| }, | ||
| ] = useWizard({ initialKey: OutboundImportStep.DETAILS.key, steps }); | ||
|
|
||
| return ( | ||
| <div> | ||
| <WizardStepsV2 steps={stepsTitles} currentStepKey={Step.key} /> | ||
| <Step.Component /> | ||
| <button type="button" onClick={() => previous()}>previous</button> | ||
| <button type="button" onClick={() => next()}>next</button> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default OutboundImport; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import React from 'react'; | ||
|
|
||
| const OutboundImportConfirm = () => { | ||
| return ( | ||
| <div> | ||
| Confirm | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default OutboundImportConfirm; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import React from 'react'; | ||
|
|
||
| const OutboundImportDetails = () => ( | ||
| <div> | ||
| Details | ||
| </div> | ||
| ); | ||
|
|
||
| export default OutboundImportDetails; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import React from 'react'; | ||
|
|
||
| import PropTypes from 'prop-types'; | ||
|
|
||
| import 'components/wizard/WizardSteps.scss'; | ||
|
|
||
| const WizardStepsV2 = ({ steps, currentStepKey }) => { | ||
| const isCurrentStep = (iteratedStep) => iteratedStep.key === currentStepKey; | ||
| return ( | ||
| <div className="steps-main-box"> | ||
| <div className="steps-inside-wrapper"> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this className sounds slightly different to me: Can it be just "steps" or "steps-container"?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Those are the class names used in the current |
||
| {steps.map((step) => ( | ||
| <div | ||
| key={step.key} | ||
| className={`step-container ${isCurrentStep(step) ? 'active' : ''}`} | ||
| data-testid="wizard-step" | ||
| data-stepstate={isCurrentStep(step) ? 'active' : 'inactive'} | ||
| > | ||
| <div | ||
| className="circle" | ||
| /> | ||
| <div className="step-name"> | ||
| {step.title} | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default WizardStepsV2; | ||
|
|
||
| WizardStepsV2.propTypes = { | ||
| steps: PropTypes.arrayOf( | ||
| PropTypes.shape({ | ||
| title: PropTypes.string.isRequired, | ||
| key: PropTypes.oneOfType([ | ||
| PropTypes.number, | ||
| PropTypes.string, | ||
| ]).isRequired, | ||
| }), | ||
| ).isRequired, | ||
| currentStepKey: PropTypes.oneOfType([ | ||
| PropTypes.number, | ||
| PropTypes.string, | ||
| ]).isRequired, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { useMemo, useState } from 'react'; | ||
|
|
||
| import PropTypes from 'prop-types'; | ||
|
|
||
| const useWizard = ({ initialKey, steps }) => { | ||
| const [key, setKey] = useState(initialKey); | ||
|
|
||
| const stepProperties = useMemo(() => { | ||
| const foundStepIdx = steps.findIndex((s) => s.key === key); | ||
| // findIndex returns -1 if the index is not found for given predicate | ||
| if (foundStepIdx === -1) { | ||
| throw new Error('Wizard step has not been found!'); | ||
| } | ||
| return { | ||
| Step: steps[foundStepIdx], | ||
| currentStepIdx: foundStepIdx, | ||
| }; | ||
| }, [key, initialKey]); | ||
|
|
||
| const first = () => { | ||
| setKey(steps[0]?.key); | ||
| }; | ||
|
|
||
| const last = () => { | ||
| const lastIdx = steps.length - 1; | ||
| setKey(steps[lastIdx]?.key); | ||
| }; | ||
|
Comment on lines
+20
to
+27
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's cool that you added these but are we ever going to use them?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already have such use cases - e.g. if we have an SM in progress and it is supposed to be on "X" step, we go to step "X".
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, but I am not talking about function
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe now we don't have the use case, but they felt cute and I also wanted to have a complete hook, that would cover any of the future cases, so I kept them :sadsmile: |
||
|
|
||
| const next = () => { | ||
| const nextStepIdx = stepProperties.currentStepIdx + 1; | ||
| const nextStep = steps[nextStepIdx]; | ||
| if (nextStep) { | ||
| setKey(nextStep.key); | ||
| } | ||
| }; | ||
|
|
||
| const previous = () => { | ||
| const previousStepIdx = stepProperties.currentStepIdx - 1; | ||
| if (previousStepIdx >= 0) { | ||
| setKey(steps[previousStepIdx]?.key); | ||
| } | ||
| }; | ||
|
|
||
| const { Step } = stepProperties; | ||
|
|
||
| return [ | ||
| Step, | ||
| { | ||
| set: setKey, | ||
| first, | ||
| next, | ||
| previous, | ||
| last, | ||
| }, | ||
| ]; | ||
| }; | ||
|
|
||
| export default useWizard; | ||
|
|
||
| useWizard.propTypes = { | ||
| initialKey: PropTypes.oneOfType([ | ||
| PropTypes.number, | ||
| PropTypes.string, | ||
| ]).isRequired, | ||
| steps: PropTypes.arrayOf( | ||
| PropTypes.shape({ | ||
| title: PropTypes.string.isRequired, | ||
| key: PropTypes.oneOfType([ | ||
| PropTypes.number, | ||
| PropTypes.string, | ||
| ]).isRequired, | ||
| Component: PropTypes.node.isRequired, | ||
| }), | ||
| ).isRequired, | ||
| }; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great work on tests! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import React from 'react'; | ||
|
|
||
| import { act, renderHook } from '@testing-library/react-hooks'; | ||
|
|
||
| import useWizard from 'hooks/useWizard'; | ||
|
|
||
| describe('Changing steps', () => { | ||
| const steps = [ | ||
| { | ||
| key: 1, | ||
| Component: () => (<div>First compoment</div>), | ||
| title: 'First', | ||
| }, | ||
| { | ||
| key: 2, | ||
| Component: () => (<div>Second compoment</div>), | ||
| title: 'Second', | ||
| }, | ||
| { | ||
| key: 3, | ||
| Component: () => (<div>Third compoment</div>), | ||
| title: 'Third', | ||
| }, | ||
| { | ||
| key: 4, | ||
| Component: () => (<div>Fourth compoment</div>), | ||
| title: 'Fourth', | ||
| }, | ||
| ]; | ||
|
|
||
| it('should start with the initial key', () => { | ||
| const { result } = renderHook(() => useWizard({ initialKey: 3, steps })); | ||
|
|
||
| expect(result.current[0].key).toEqual(3); | ||
| }); | ||
|
|
||
| it('should go to next step', () => { | ||
| const { result } = renderHook(() => useWizard({ initialKey: 2, steps })); | ||
|
|
||
| act(() => { | ||
| result.current[1].next(); | ||
| }); | ||
|
|
||
| expect(result.current[0].key).toEqual(3); | ||
| }); | ||
|
|
||
| it('should go to previous step', () => { | ||
| const { result } = renderHook(() => useWizard({ initialKey: 4, steps })); | ||
|
|
||
| act(() => { | ||
| result.current[1].previous(); | ||
| }); | ||
|
|
||
| expect(result.current[0].key).toEqual(3); | ||
| }); | ||
|
|
||
| it('should go to specific step', () => { | ||
| const { result } = renderHook(() => useWizard({ initialKey: 1, steps })); | ||
|
|
||
| act(() => { | ||
| result.current[1].set(4); | ||
| }); | ||
|
|
||
| expect(result.current[0].key).toEqual(4); | ||
| }); | ||
|
|
||
| it('should go to last step', () => { | ||
| const { result } = renderHook(() => useWizard({ initialKey: 1, steps })); | ||
|
|
||
| act(() => { | ||
| result.current[1].last(); | ||
| }); | ||
|
|
||
| expect(result.current[0].key).toEqual(4); | ||
| }); | ||
|
|
||
| it('should go to first step', () => { | ||
| const { result } = renderHook(() => useWizard({ initialKey: 3, steps })); | ||
|
|
||
| act(() => { | ||
| result.current[1].first(); | ||
| }); | ||
|
|
||
| expect(result.current[0].key).toEqual(1); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What has changed here compared to
WizardSteps?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I answered that question in one of the comments below: https://github.com/openboxes/openboxes/pull/4642/files#r1617300455