Skip to content

Commit

Permalink
refactor(inventoryTabs): sw-625 class to function, hooks (#1039)
Browse files Browse the repository at this point in the history
* inventoryTabs, class to function, hooks
* productView, productId from hook
  • Loading branch information
cdcabrera authored Jan 30, 2023
1 parent 6ddd995 commit 878184a
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ exports[`InventoryTab Component should have accessible props: component props 1`
]
`;

exports[`InventoryTab Component should output a non-connected component: non-connected 1`] = `
exports[`InventoryTab Component should output a basic component: basic 1`] = `
<div>
lorem ipsum
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`InventoryTabs Component should handle updating tabs for redux state: dispatch filter 1`] = `
exports[`InventoryTabs Component should handle updating through redux state with component: dispatch, component 1`] = `
[
[
{
"tabs": {
"undefined": 1,
},
"type": "SET_INVENTORY_TAB",
},
],
]
`;

exports[`InventoryTabs Component should handle updating through redux state with hook: dispatch, hook 1`] = `
[
[
{
Expand All @@ -13,7 +26,7 @@ exports[`InventoryTabs Component should handle updating tabs for redux state: di
]
`;

exports[`InventoryTabs Component should render a non-connected component: non-connected 1`] = `
exports[`InventoryTabs Component should render a basic component: basic 1`] = `
<Fragment>
<Title
className="sr-only"
Expand Down
4 changes: 2 additions & 2 deletions src/components/inventoryTabs/__tests__/inventoryTab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { mount, shallow } from 'enzyme';
import { InventoryTab } from '../inventoryTab';

describe('InventoryTab Component', () => {
it('should output a non-connected component', () => {
it('should output a basic component', () => {
const props = {
children: <div>lorem ipsum</div>,
title: 'lorem ipsum'
};

const component = shallow(<InventoryTab {...props} />);
expect(component).toMatchSnapshot('non-connected');
expect(component).toMatchSnapshot('basic');
});

it('should have accessible props', () => {
Expand Down
46 changes: 28 additions & 18 deletions src/components/inventoryTabs/__tests__/inventoryTabs.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { InventoryTabs, InventoryTab } from '../inventoryTabs';
import { shallow } from 'enzyme';
import { InventoryTabs, InventoryTab, useOnTab } from '../inventoryTabs';
import { store } from '../../../redux';

describe('InventoryTabs Component', () => {
Expand All @@ -14,7 +14,7 @@ describe('InventoryTabs Component', () => {
jest.clearAllMocks();
});

it('should render a non-connected component', () => {
it('should render a basic component', () => {
const GenericComponent = () => (
<div>
<strong>world</strong>
Expand All @@ -35,7 +35,7 @@ describe('InventoryTabs Component', () => {
};

const component = shallow(<InventoryTabs {...props} />);
expect(component).toMatchSnapshot('non-connected');
expect(component).toMatchSnapshot('basic');
});

it('should return an empty render when disabled', () => {
Expand All @@ -56,22 +56,32 @@ describe('InventoryTabs Component', () => {
expect(component).toMatchSnapshot('disabled component');
});

it('should handle updating tabs for redux state', () => {
it('should handle updating through redux state with component', async () => {
const props = {
productId: 'lorem',
children: [
<InventoryTab key="lorem" title="lorem">
ipsum
</InventoryTab>,
<InventoryTab key="sit" title="sit">
amet
</InventoryTab>
]
useProduct: () => ({ productId: 'lorem' })
};

const component = await mountHookComponent(
<InventoryTabs {...props}>
<InventoryTab title="loremIpsum">lorem ipsum</InventoryTab>
<InventoryTab title="dolorSit">dolor sit</InventoryTab>
</InventoryTabs>
);
component.find('button.pf-c-tabs__link').last().simulate('click');

expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch, component');
});

it('should handle updating through redux state with hook', () => {
const options = {
useProduct: () => ({ productId: 'lorem' })
};
const component = mount(<InventoryTabs {...props} />);
const componentInstance = component.instance();

componentInstance.onTab({ index: 1 });
expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch filter');
const onTab = useOnTab(options);
onTab({
index: 1
});

expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch, hook');
});
});
138 changes: 78 additions & 60 deletions src/components/inventoryTabs/inventoryTabs.js
Original file line number Diff line number Diff line change
@@ -1,105 +1,123 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Title } from '@patternfly/react-core';
import { connect, reduxTypes, store } from '../../redux';
import { reduxTypes, storeHooks } from '../../redux';
import { useProduct } from '../productView/productViewContext';
import { Tabs } from '../tabs/tabs';
import { helpers } from '../../common';
import { translate } from '../i18n/i18n';
import { InventoryTab } from './inventoryTab';

/**
* A system inventory tabs component.
* Update tab state.
*
* @augments React.Component
* @fires onTab
* @param {object} options
* @param {Function} options.useDispatch
* @param {Function} options.useProduct
* @returns {Function}
*/
class InventoryTabs extends React.Component {
/**
* On tab update state.
*
* @event onTab
* @param {object} params
* @param {string} params.index tab index
*/
onTab = ({ index }) => {
const { productId } = this.props;
const useOnTab = ({
useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch,
useProduct: useAliasProduct = useProduct
} = {}) => {
const { productId } = useAliasProduct();
const dispatch = useAliasDispatch();

store.dispatch({
return ({ index } = {}) => {
dispatch({
type: reduxTypes.inventory.SET_INVENTORY_TAB,
tabs: {
[productId]: index
}
});
};
};

/**
* Render inventory tabs using Inventory tab passed props only. A parallel outcome can be
* achieved by passing an array of objects through a prop.
*
* @returns {Node}
*/
render() {
const { activeTab, children, defaultActiveTab, isDisabled, t } = this.props;
/**
* An inventory tabs component.
* Render inventory tabs using Inventory tab passed props only.
*
* @fires onTab
* @param {object} props
* @param {number} props.activeTab
* @param {React.ReactNode} props.children
* @param {number} props.defaultActiveTab
* @param {boolean} props.isDisabled
* @param {Function} props.t
* @param {Function} props.useOnTab
* @param {Function} props.useProduct
* @param {Function} props.useSelector
* @returns {React.ReactNode|null}
*/
const InventoryTabs = ({
activeTab,
children,
defaultActiveTab,
isDisabled,
t,
useOnTab: useAliasOnTab,
useProduct: useAliasProduct,
useSelector: useAliasSelector
}) => {
const { productId } = useAliasProduct();
const updatedActiveTab = useAliasSelector(({ inventory }) => inventory.tabs?.[productId], activeTab);
const onTab = useAliasOnTab();

if (isDisabled) {
return null;
}
if (isDisabled) {
return null;
}

const updatedChildren = React.Children.toArray(children).map((child, index) => {
const { props = {} } = child;
const updatedChildren = React.Children.toArray(children).map((child, index) => {
const { props: childProps = {} } = child;

return {
active: props.active || false,
content: props.children || child,
title: props.title || t('curiosity-inventory.tabSubHeading', { count: index })
};
});
return {
active: childProps.active || false,
content: childProps.children || child,
title: childProps.title || t('curiosity-inventory.tabSubHeading', { count: index })
};
});

return (
<React.Fragment>
<Title headingLevel="h2" className="sr-only">
{t('curiosity-inventory.tabHeading', { count: updatedChildren.length })}
</Title>
<Tabs activeTab={activeTab} defaultActiveTab={defaultActiveTab} onTab={this.onTab} tabs={updatedChildren} />
</React.Fragment>
);
}
}
return (
<React.Fragment>
<Title headingLevel="h2" className="sr-only">
{t('curiosity-inventory.tabHeading', { count: updatedChildren.length })}
</Title>
<Tabs activeTab={updatedActiveTab} defaultActiveTab={defaultActiveTab} onTab={onTab} tabs={updatedChildren} />
</React.Fragment>
);
};

/**
* Prop types.
*
* @type {{productId: string, t: Function, children: Node, defaultActiveTab: number, isDisabled: boolean,
* activeTab: number}}
* @type {{useOnTab: Function, useProduct: Function, t: Function, children: React.ReactNode,
* useSelector: Function, defaultActiveTab: number, isDisabled: boolean, activeTab: number}}
*/
InventoryTabs.propTypes = {
activeTab: PropTypes.number,
children: PropTypes.node.isRequired,
defaultActiveTab: PropTypes.number,
isDisabled: PropTypes.bool,
productId: PropTypes.string.isRequired,
t: PropTypes.func
t: PropTypes.func,
useOnTab: PropTypes.func,
useProduct: PropTypes.func,
useSelector: PropTypes.func
};

/**
* Default props.
*
* @type {{t: translate, defaultActiveTab: number, isDisabled: boolean, activeTab: number}}
* @type {{useOnTab: Function, useProduct: Function, t: translate, useSelector: Function,
* defaultActiveTab: number, isDisabled: boolean, activeTab: number}}
*/
InventoryTabs.defaultProps = {
activeTab: 0,
defaultActiveTab: 0,
isDisabled: helpers.UI_DISABLED_TABLE,
t: translate
t: translate,
useOnTab,
useProduct,
useSelector: storeHooks.reactRedux.useSelector
};

/**
* Create a selector from applied state, props.
*
* @type {Function}
*/
const mapStateToProps = ({ inventory }, { productId }) => ({ activeTab: inventory.tabs?.[productId] });

const ConnectedInventoryTabs = connect(mapStateToProps)(InventoryTabs);

export { ConnectedInventoryTabs as default, ConnectedInventoryTabs, InventoryTabs, InventoryTab };
export { InventoryTabs as default, InventoryTabs, InventoryTab, useOnTab };
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,14 @@ exports[`ProductView Component should allow custom inventory displays via config
<PageSection
className=""
>
<Connect(InventoryTabs)
<InventoryTabs
activeTab={0}
defaultActiveTab={0}
isDisabled={false}
key="inventory_lorem"
productId="lorem"
t={[Function]}
useOnTab={[Function]}
useProduct={[Function]}
useSelector={[Function]}
>
<InventoryTab
active={false}
Expand All @@ -84,7 +88,7 @@ exports[`ProductView Component should allow custom inventory displays via config
useProductInventoryQuery={[Function]}
/>
</InventoryTab>
</Connect(InventoryTabs)>
</InventoryTabs>
</PageSection>
</ContextProvider>
</PageColumns>
Expand Down Expand Up @@ -155,10 +159,14 @@ exports[`ProductView Component should allow custom product views via props: cust
<PageSection
className=""
>
<Connect(InventoryTabs)
<InventoryTabs
activeTab={0}
defaultActiveTab={0}
isDisabled={true}
key="inventory_lorem"
productId="lorem"
t={[Function]}
useOnTab={[Function]}
useProduct={[Function]}
useSelector={[Function]}
/>
</PageSection>
</ContextProvider>
Expand Down Expand Up @@ -230,10 +238,14 @@ exports[`ProductView Component should allow custom product views via props: cust
<PageSection
className=""
>
<Connect(InventoryTabs)
<InventoryTabs
activeTab={0}
defaultActiveTab={0}
isDisabled={true}
key="inventory_lorem"
productId="lorem"
t={[Function]}
useOnTab={[Function]}
useProduct={[Function]}
useSelector={[Function]}
/>
</PageSection>
</ContextProvider>
Expand Down Expand Up @@ -305,10 +317,14 @@ exports[`ProductView Component should render a basic component: basic 1`] = `
<PageSection
className=""
>
<Connect(InventoryTabs)
<InventoryTabs
activeTab={0}
defaultActiveTab={0}
isDisabled={true}
key="inventory_lorem"
productId="lorem"
t={[Function]}
useOnTab={[Function]}
useProduct={[Function]}
useSelector={[Function]}
/>
</PageSection>
</ContextProvider>
Expand Down
Loading

0 comments on commit 878184a

Please sign in to comment.