Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions grails-app/conf/runtime.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ openboxes {
defaultLabel: "Stock Movement",
menuItems: [
[label: "outbound.create.label", defaultLabel: "Create Outbound Movements", href: "/stockMovement/createOutbound?direction=OUTBOUND"],
[label: "outbound.import.label", defaultLabel: "Import Completed Outbound", href: "/stockMovement/importOutboundStockMovement"],
[label: "outbound.list.label", defaultLabel: "List Outbound Movements", href: "/stockMovement/list?direction=OUTBOUND"],
[label: "requests.list.label", defaultLabel: "List Requests", href: "/stockMovement/list?direction=OUTBOUND&sourceType=ELECTRONIC"],
[label: "outboundReturns.create.label", defaultLabel: "Create Outbound Return", href: "/stockTransfer/createOutboundReturn"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class StockMovementController {
render(view: "/common/react", params: params)
}

def importOutboundStockMovement() {
render(view: "/common/react", params: params)
}

def createInbound() {
render(view: "/common/react", params: params)
}
Expand Down
1 change: 1 addition & 0 deletions grails-app/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3777,6 +3777,7 @@ inbound.create.label=Create Inbound Movement
stockRequest.create.label=Create Stock Request
inbound.list.label=List Inbound Movements
outbound.create.label=Create Outbound Movement
outbound.import.label=Import Completed Outbound
outbound.list.label=List Outbound Movements
requests.list.label=List Requests
outboundReturns.create.label=Create Outbound Return
Expand Down
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@fortawesome/free-solid-svg-icons": "5.14.0",
"@fortawesome/react-fontawesome": "0.1.8",
"@hookform/resolvers": "3.3.4",
"@testing-library/react-hooks": "8.0.1",
"axios": "1.6.8",
"bootstrap": "4.6.2",
"chart.js": "2.9.4",
Expand Down
6 changes: 6 additions & 0 deletions src/js/components/Router.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ const AsyncProductSupplierCreatePage = Loadable({
loading: Loading,
});

const AsyncOutboundImport = Loadable({
loader: () => import('components/stock-movement-wizard/outboundImport/OutboundImport'),
loading: Loading,
});

const StockMovementList = (props) => {
const parsedSearchQuery = queryString.parse(props?.location?.search);
const direction = parsedSearchQuery?.direction?.toUpperCase();
Expand Down Expand Up @@ -208,6 +213,7 @@ const Router = (props) => {
<MainLayoutRoute path="**/putAway/create/:putAwayId?" component={AsyncPutAwayMainPage} />
<MainLayoutRoute path="**/stockMovement/list" component={StockMovementList} />
<MainLayoutRoute path="**/stockMovement/createOutbound/:stockMovementId?" component={AsyncStockMovement} />
<MainLayoutRoute path="**/stockMovement/importOutboundStockMovement" component={AsyncOutboundImport} />
<MainLayoutRoute path="**/stockMovement/createInbound/:stockMovementId?" component={AsyncStockMovementInbound} />
<MainLayoutRoute path="**/stockMovement/createCombinedShipments/:stockMovementId?" component={AsyncStockMovementCombinedShipments} />
<MainLayoutRoute path="**/stockMovement/createRequest/:stockMovementId?" component={AsyncStockMovementRequest} />
Expand Down
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;
48 changes: 48 additions & 0 deletions src/js/components/wizard/v2/WizardStepsV2.jsx
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 }) => {
Copy link
Collaborator

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const isCurrentStep = (iteratedStep) => iteratedStep.key === currentStepKey;
return (
<div className="steps-main-box">
<div className="steps-inside-wrapper">
Copy link
Collaborator

Choose a reason for hiding this comment

The 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"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are the class names used in the current WizardSteps.jsx. I think we want to keep the same styling for now, so the only difference in this component and the current WizardSteps is that I eventually call the step.title at the end, not the step. I didn't want to do some workarounds in the existing WizardSteps, this is why I created a brand new compoment, as it will be more maintainable if we decide to change anything there and not break the existing one.

{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,
};
75 changes: 75 additions & 0 deletions src/js/hooks/useWizard.js
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
Copy link
Collaborator

@drodzewicz drodzewicz May 29, 2024

Choose a reason for hiding this comment

The 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?
I don't care if they stay or they go, I am just not familiar with any use case where we will want to jump to first and last steps.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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".

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but I am not talking about function set but last and first

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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:
Let me know if I should remove them in your opinion guys.


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,
};
86 changes: 86 additions & 0 deletions src/js/tests/hooks/useWizard.test.jsx
Copy link
Collaborator

Choose a reason for hiding this comment

The 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);
});
});