Skip to content

Instantly share code, notes, and snippets.

@sp3ber
Last active September 27, 2017 11:22
Show Gist options
  • Select an option

  • Save sp3ber/66b7c7a7db985e3a791a70d14c77c126 to your computer and use it in GitHub Desktop.

Select an option

Save sp3ber/66b7c7a7db985e3a791a70d14c77c126 to your computer and use it in GitHub Desktop.
import React, { Component, PropTypes as types } from 'react';
import ReactGridLayout from 'react-grid-layout';
import SizeMe from 'react-sizeme';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import {
WIDGET_LAYOUT_COLUMNS_COUNT,
DASHBOARD_MOVE_BUTTON_CLASS,
WIDGET_LAYOUT_ROW_HEIGHT,
ORDER_DATE_COMPLETED,
ORDER_PROGRESS,
ORDER_PROGRESS_DIAGRAM
} from 'utils/constants/dashboard';
import DashBoardLayoutControls from 'containers/DashBoard/DashBoardLayoutContainer/DashBoardLayoutControlsContainer';
import OrderProgressDiagram from 'containers/DashBoard/WidgetsContainers/OrderProgressDiagramContainer/OrderProgressDiagramContainer';
import OrderProgress from 'containers/DashBoard/WidgetsContainers/OrderProgressContainer/OrderProgressContainer';
import OrderDateCompleted from 'containers/DashBoard/WidgetsContainers/OrderDateCompletedContainer/OrderDateCompletedContainer';
import { Element as ElementToScroll, scroller, Events } from 'react-scroll';
import cn from 'classnames';
import './style.scss';
const DASHBOARD_MOVE_BUTTON_SELECTOR = `.${DASHBOARD_MOVE_BUTTON_CLASS}`;
const TOP_FIXED_HEADER_HEIGHT_PX = 60;
const WIDGET_NAME_TO_COMPONENT_MAP = {
[ORDER_PROGRESS_DIAGRAM]: OrderProgressDiagram,
[ORDER_PROGRESS]: OrderProgress,
[ORDER_DATE_COMPLETED]: OrderDateCompleted
};
const SCROLL_OPTIONS = {
delay: 0,
offset: -1 * TOP_FIXED_HEADER_HEIGHT_PX,
duration: 50,
smooth: true
};
class DashBoardLayout extends Component {
state = {
widgetIdToEmphasize: null
};
componentWillMount() {
this.shouldHandleLayoutUpdate = _.isEmpty(this.props.widgetList);
}
componentDidUpdate(oldProps) {
const newWidgetId = this.getNewWidgetIdFromProps(oldProps, this.props);
newWidgetId && this.scrollToAndAnimateWidgetAppearance(newWidgetId);
}
getNewWidgetIdFromProps(oldProps, newProps) {
const { widgetList } = oldProps;
const { widgetList: nextWidgetList } = newProps;
const oldWidgetIds = widgetList.map(({ id }) => id);
const newWidgetIds = nextWidgetList.map(({ id }) => id);
return _.difference(newWidgetIds, oldWidgetIds)[0];
}
scrollToAndAnimateWidgetAppearance = newWidgetId => {
Events.scrollEvent.register('begin', () => {
this.setState({
widgetIdToEmphasize: newWidgetId
});
Events.scrollEvent.remove('begin');
});
Events.scrollEvent.register('end', () => {
this.setState({
widgetIdToEmphasize: null
});
Events.scrollEvent.remove('end');
});
scroller.scrollTo(newWidgetId, SCROLL_OPTIONS);
};
onLayoutChange = gridList =>
this.shouldHandleLayoutUpdate
? this.props.layoutUpdate(_.indexBy(gridList, 'i'))
: (this.shouldHandleLayoutUpdate = true);
renderWidgets() {
const { widgetList = [], size: { width } } = this.props;
return (
<div>
{!!widgetList.length && (
<ReactGridLayout
className="layout"
cols={WIDGET_LAYOUT_COLUMNS_COUNT}
rowHeight={WIDGET_LAYOUT_ROW_HEIGHT}
width={width}
onLayoutChange={this.onLayoutChange}
draggableHandle={DASHBOARD_MOVE_BUTTON_SELECTOR}
>
{widgetList.map(({ name, id, grid }) => {
const Widget = WIDGET_NAME_TO_COMPONENT_MAP[name];
const shouldEmphasizeWidget =
id === this.state.widgetIdToEmphasize;
return (
<div key={id} data-grid={{ ...grid }}>
<ElementToScroll
className={cn('dashboard-layout__item', {
'is-emphasized': shouldEmphasizeWidget
})}
name={id}
>
<Widget id={id} />
</ElementToScroll>
</div>
);
})}
</ReactGridLayout>
)}
</div>
);
}
render() {
return (
<div>
<DashBoardLayoutControls />
{this.renderWidgets()}
</div>
);
}
}
DashBoardLayout.propTypes = {
id: types.string.isRequired,
size: types.shape({ width: types.number.isRequired }).isRequired,
widgetList: types.arrayOf(
types.shape({
name: types.string,
grid: types.objectOf(types.any)
})
).isRequired,
layoutUpdate: types.func.isRequired
};
export default SizeMe()(DashBoardLayout);
import React, {PropTypes as types} from 'react';
import i18n from 'utils/i18n';
import {addPercentPostfix} from 'utils';
const OrderProgress = ({ orderName, progress}) =>
<div className="center">
<h4 className="brand-widget__header">
{i18n.t('Order Progress')}
</h4>
<h3 className="brand-widget__subtitle">
{orderName}
</h3>
<p className="brand-widget__text brand-widget__text--huge">
{_.isNumber(progress) && addPercentPostfix(progress.toFixed(1))}
</p>
</div>;
OrderProgress.propTypes = {
orderName: types.string,
progress: types.number
};
export default OrderProgress;
import { connect } from 'react-redux';
import { fetchOrderDetailedProgress } from 'actions';
import widgetHOCCreator from '../WidgetHOC/widgetHOCCreator';
import asyncComponent from 'decorators/asyncComponent';
import { orderWithCurrentQrmProgressSelector } from 'selectors/qrm/main';
import OrderProgress from 'components/DashBoard/Widgets/OrderProgress/OrderProgress';
import OrderProgressForm from './OrderProgressFormContainer';
import widgetWithOrderHOCCreator from '../WidgetHOC/widgetWithOrderHOCCreator';
import withActiveQrmSessionIdHOC from 'decorators/withActiveQrmSessionIdHOC';
import errorIfOrderIsNotInMainPlanHOCCreator from '../WidgetHOC/errorIfOrderIsNotInMainPlanHOCCreator';
const mapStateToProps = (state, ownProps) => {
const { orderId } = ownProps;
const progress = orderWithCurrentQrmProgressSelector(state, { orderId });
return {
...ownProps,
progress
};
};
const mapDispatchToProps = {
fetchOrderDetailedProgress
};
export default _.compose(
widgetWithOrderHOCCreator(),
withActiveQrmSessionIdHOC,
connect(mapStateToProps, mapDispatchToProps),
asyncComponent({
resolve: [
{
propsToObserve: ['sessionId'],
fn: props => props.fetchOrderDetailedProgress(props.activeSessionId)
}
]
}),
widgetHOCCreator({
form: OrderProgressForm
}),
errorIfOrderIsNotInMainPlanHOCCreator()
)(OrderProgress);
import { connect } from 'react-redux';
import { widgetRemove, saveWidgetSettings } from 'actions/dashBoard';
import WidgetWrapper from 'components/DashBoard/Widgets/WidgetWrapper';
import sizeMe from 'react-sizeme';
import {
widgetSettingsSelector,
widgetByIdSelector
} from 'selectors/dashboard';
import { AVAILABLE_WIDGET_LIST_BY_NAME } from 'utils/constants/dashboard';
import getDeep from 'lodash.get';
const widgetHocCreator = ({
isTuned = settings =>
!_.isEmpty(settings) &&
_.values(settings).every(
settingValue => !_.isUndefined(settingValue) && !_.isNull(settingValue)
)
}) => ComponentToDecorate => {
const mapStateToProps = (state, ownProps) => {
const id = ownProps.id;
const settings = widgetSettingsSelector(state, { id });
const { name: widgetName } = widgetByIdSelector(state, { id }) || {};
const widgetTitle = getDeep(
AVAILABLE_WIDGET_LIST_BY_NAME,
[widgetName, 'title'],
''
);
return {
id,
form,
widgetTitle,
contentComponent: ComponentToDecorate,
isTuned: isTuned(settings),
settings
};
};
const mapDispatchToProps = {
widgetRemove,
saveWidgetSettings
};
const mergeProps = (
{ id, ...etcStateProps },
{ widgetRemove, saveWidgetSettings },
ownProps
) => ({
...ownProps,
...etcStateProps,
id,
remove: () => widgetRemove(id),
saveSettings: settings => saveWidgetSettings(id, settings)
});
return _.compose(
connect(mapStateToProps, mapDispatchToProps, mergeProps),
sizeMe({
monitorHeight: true,
monitorWidth: true
})
)(WidgetWrapper);
};
export default widgetHocCreator;
import React, { Component, PropTypes as types } from 'react';
import { Label, ButtonGroup, Button, ButtonToolbar } from 'react-bootstrap';
import i18n from 'utils/i18n';
import cn from 'classnames';
import './style.scss';
import { DASHBOARD_MOVE_BUTTON_CLASS } from 'utils/constants/dashboard';
import {
TEST_REMOVE_BTN,
TEST_TOGGLE_BTN
} from '../../../utils/constants/test/elementSelectors';
const SETTINGS_TITLE = i18n.t('Settings');
const WIDGET_NOT_TUNED = i18n.t('Widget is not tuned');
const MOVE_TITLE = i18n.t('Move');
const REMOVE_TITLE = i18n.t('Delete');
const WIDGET_SETTINGS_TITLE = i18n.t('Widget settings');
export default class WidgetWrapper extends Component {
state = {
editMode: !this.props.isTuned
};
toggleEditMode = () =>
this.setState({
editMode: !this.state.editMode
});
onSubmitHandler = settings =>
Promise.resolve(this.props.saveSettings(settings)).then(
this.toggleEditMode
);
renderToolBar() {
const { remove, isTuned } = this.props;
const settingsTitle = isTuned ? SETTINGS_TITLE : WIDGET_NOT_TUNED;
return (
<ButtonToolbar
className={cn('brand-widget__toolbar', DASHBOARD_MOVE_BUTTON_CLASS)}
>
<ButtonGroup bsSize="xsmall">
<Button title={MOVE_TITLE} className="brand-widget__toolbar-drag">
<i className="fa fa-arrows" />
</Button>
<Button
title={settingsTitle}
className={TEST_TOGGLE_BTN}
disabled={!isTuned}
onClick={this.toggleEditMode}
>
<i className="fa fa-cog" />
</Button>
<Button
title={REMOVE_TITLE}
className={TEST_REMOVE_BTN}
onClick={remove}
>
<i className="fa fa-remove" />
</Button>
</ButtonGroup>
</ButtonToolbar>
);
}
renderForm() {
const { form: Form, settings, id, widgetTitle } = this.props;
return (
<div>
<Label className="brand-widget__form-label">
{WIDGET_SETTINGS_TITLE}: {widgetTitle}
</Label>
<div className="brand-widget__form">
<Form settings={settings} onSubmit={this.onSubmitHandler} id={id} />
</div>
</div>
);
}
renderWidgetContent() {
const {
contentComponent: ContentComponent,
settings,
id,
size,
...contentProps
} = this.props;
return (
<ContentComponent id={id} {...settings} size={size} {...contentProps} />
);
}
render() {
const { editMode } = this.state;
return (
<div className="brand-widget break-word">
{this.renderToolBar()}
<div className="brand-widget__content">
{editMode ? this.renderForm() : this.renderWidgetContent()}
</div>
</div>
);
}
}
WidgetWrapper.propTypes = {
saveSettings: types.func.isRequired,
remove: types.func.isRequired,
form: types.func,
widgetTitle: types.string,
contentComponent: types.func.isRequired,
isTuned: types.bool.isRequired,
settings: types.objectOf(types.any),
id: types.string.isRequired,
size: types.shape({
height: types.number.isRequired,
width: types.number.isRequired
})
};
import WidgetWrapper from '../../../app/components/DashBoard/Widgets/WidgetWrapper';
import React from 'react';
import { mount } from 'enzyme';
import getUniqId from 'uuid/v1';
import {TEST_REMOVE_BTN, TEST_TOGGLE_BTN} from '../../../app/utils/constants/test/elementSelectors';
describe('WidgetWrapper Component', () => {
describe('Form rendering', () => {
it('Form is rendered if widget is not tuned', () => {
const form = () => <div />;
const wrapper = mountWrapper({ form });
const renderedForm = wrapper.find(form);
expect(renderedForm.length).toBe(1);
});
it('Form is rendered with intitialValues prop whit consist of settings', () => {
const form = () => <div />;
const settings = createTestObject();
const wrapper = mountWrapper({ settings, form });
const renderedForm = wrapper.find(form);
expect(renderedForm.at(0).props().settings).toMatchObject(settings);
});
it(
'At first if isTuned === true form is not rendered ' +
'when we switch to edit mode',
() => {
const form = () => <div />;
const wrapper = mountWrapper({ isTuned: true, form });
const getForm = () => wrapper.find(form);
expect(getForm().length).toBe(0);
const toggleBtn = wrapper.find(`.${TEST_TOGGLE_BTN}`).at(0);
toggleBtn.simulate('click');
expect(getForm().length).toBe(1);
}
);
});
describe('Component content rendering', () => {
it('If widget is tuned main content is rendered', () => {
const contentComponent = () => <div />;
const wrapper = mountWrapper({ contentComponent, isTuned: true });
const renderedContent = wrapper.find(contentComponent);
expect(renderedContent.length).toBe(1);
});
it('Content is rendered with settings and other props', () => {
const contentComponent = () => <div />;
const settings = createTestObject();
const additionalProps = createTestObject();
const wrapper = mountWrapper({ contentComponent, isTuned: true, settings, ...additionalProps });
const renderedContent = wrapper.find(contentComponent);
expect(renderedContent.at(0).props()).toMatchObject({...settings, ...additionalProps});
});
it(
'At first is not rendered, but after setting isTuned to true and clicking on toggleBtn it is rendered',
() => {
const contentComponent = () => <div />;
const wrapper = mountWrapper({ contentComponent, isTuned: false});
const getContent = () => wrapper.find(contentComponent);
expect(getContent().length).toBe(0);
const toggleBtn = wrapper.find(`.${TEST_TOGGLE_BTN}`).at(0);
wrapper.setProps({isTuned: true});
expect(getContent().length).toBe(0); //still is not rendered
toggleBtn.simulate('click');
expect(getContent().length).toBe(1);
}
);
it(
'If trying switch to edit mode with isTuned === false content is not rendered',
() => {
const contentComponent = () => <div />;
const wrapper = mountWrapper({ contentComponent, isTuned: false});
const getContent = () => wrapper.find(contentComponent);
const toggleBtn = wrapper.find(`.${TEST_TOGGLE_BTN}`).at(0);
toggleBtn.simulate('click');
expect(getContent().length).toBe(0);
}
);
it(
'Content is rednered after saveSettings success resolving',
(done) => {
const saveSettings = () => Promise.resolve();
const contentComponent = () => <div />;
const preventDefault = jest.fn();
const submitBtnClassName = 'test-submit-btn';
const form = ({ onSubmit }) =>
<div>
<button
type="button"
onClick={onSubmit}
className={submitBtnClassName}
/>
</div>;
const wrapper = mountWrapper({ saveSettings, form, contentComponent });
const getContent = () => wrapper.find(contentComponent);
const submitBtn = wrapper.find('.test-submit-btn');
wrapper.setProps({isTuned: true});
submitBtn.simulate('click', { preventDefault });
//example from here https://github.com/pdhoopr/testing-async-react-methods-with-jest
setTimeout(() => {
try {
expect(getContent().length).toBe(1);
done();
} catch (error) {
done.fail(error);
}
});
}
);
});
it('Remove is called after clicking on remove btn', () => {
const remove = jest.fn();
const wrapper = mountWrapper({ remove });
const removeBtn = wrapper.find(`.${TEST_REMOVE_BTN}`).at(0);
removeBtn.simulate('click');
expect(remove).toHaveBeenCalledTimes(1);
});
it(
'saveSettings is called after clicking on save button',
() => {
const saveSettings = jest.fn(() => new Promise(resolve => resolve()));
const submitBtnClassName = 'test-submit-btn';
const form = ({ onSubmit }) =>
<button
onClick={() => {
onSubmit();
}}
className={submitBtnClassName}
/>;
const wrapper = mountWrapper({ saveSettings, form });
const submitBtn = wrapper.find('.test-submit-btn').at(0);
submitBtn.simulate('click');
expect(saveSettings).toHaveBeenCalledTimes(1);
}
);
it(
'saveSettings is called with transfered by form arguments',
() => {
const saveSettings = jest.fn(() => new Promise(resolve => resolve()));
const submitBtnClassName = 'test-submit-btn';
const settingsToSubmit = createTestObject();
const form = ({ onSubmit }) =>
<button
onClick={() => {
onSubmit(settingsToSubmit);
}}
className={submitBtnClassName}
/>;
const wrapper = mountWrapper({ saveSettings, form });
const submitBtn = wrapper.find('.test-submit-btn').at(0);
submitBtn.simulate('click');
expect(saveSettings).toHaveBeenLastCalledWith(settingsToSubmit);
}
);
});
const getDefaultProps = (overrides = {}) => {
return {
id: getUniqId(),
saveSettings: Function.prototype,
remove: Function.prototype,
contentComponent: () => <div />,
form: () => <form />,
isTuned: false,
settings: {},
...overrides
};
};
const renderWrapper = props => {
const currProps = getDefaultProps(props);
return <WidgetWrapper {...currProps} />;
};
const mountWrapper = props => mount(renderWrapper(props));
const createTestObject = () => ({
[getUniqId()]: getUniqId()
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment