Last active
September 27, 2017 11:22
-
-
Save sp3ber/66b7c7a7db985e3a791a70d14c77c126 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }) | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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