diff --git a/output/cmf.eslint.txt b/output/cmf.eslint.txt index 436ba482861..6d67fa30350 100644 --- a/output/cmf.eslint.txt +++ b/output/cmf.eslint.txt @@ -4,6 +4,9 @@ The react/require-extension rule is deprecated. Please use the import/extensions rule from eslint-plugin-import instead. +/home/travis/build/Talend/ui/packages/cmf/src/App.js + 20:1 error 'history' should be listed in the project's dependencies. Run 'npm i -S history' to add it import/no-extraneous-dependencies + /home/travis/build/Talend/ui/packages/cmf/src/componentState.js 88:3 warning Unexpected console statement no-console @@ -21,5 +24,8 @@ The react/require-extension rule is deprecated. Please use the import/extensions /home/travis/build/Talend/ui/packages/cmf/src/sagas/collection.js 10:1 error Prefer default export import/prefer-default-export -✖ 7 problems (3 errors, 4 warnings) +/home/travis/build/Talend/ui/packages/cmf/src/store.js + 10:1 error 'history' should be listed in the project's dependencies. Run 'npm i -S history' to add it import/no-extraneous-dependencies + +✖ 9 problems (5 errors, 4 warnings) diff --git a/output/components.eslint.txt b/output/components.eslint.txt index 9c10a3b1283..3f086efefaf 100644 --- a/output/components.eslint.txt +++ b/output/components.eslint.txt @@ -29,5 +29,8 @@ The react/require-extension rule is deprecated. Please use the import/extensions /home/travis/build/Talend/ui/packages/components/src/VirtualizedList/utils/tablerow.js 33:3 warning Unexpected console statement no-console -✖ 11 problems (10 errors, 1 warning) +/home/travis/build/Talend/ui/packages/components/src/WithDrawer/withDrawer.test.js + 5:8 error 'Drawer' is defined but never used no-unused-vars + +✖ 12 problems (11 errors, 1 warning) diff --git a/output/components.sasslint.txt b/output/components.sasslint.txt index 063112a6307..02a5f8f473b 100644 --- a/output/components.sasslint.txt +++ b/output/components.sasslint.txt @@ -4,7 +4,7 @@ src/Drawer/Drawer.scss - 14:4 warning Statement must begin on a new line brace-style + 12:4 warning Statement must begin on a new line brace-style src/FilterBar/FilterBar.scss 47:6 warning Vendor prefixes should not be used no-vendor-prefixes diff --git a/packages/cmf-cqrs/package.json b/packages/cmf-cqrs/package.json index 19b98487e5b..61355cbf6c9 100644 --- a/packages/cmf-cqrs/package.json +++ b/packages/cmf-cqrs/package.json @@ -66,8 +66,8 @@ "react": "^15.6.2", "react-dom": "^15.6.2", "react-redux": "5.0.5", - "react-router": "3.2.0", - "react-router-redux": "4.0.8", + "react-router": "4.2.0", + "react-router-redux": "5.0.0-alpha.9", "react-test-renderer": "^15.6.2", "redux": "3.6.0", "redux-batched-actions": "0.2.0", @@ -90,8 +90,8 @@ "react": "^15.6.2", "react-dom": "^15.6.2", "react-redux": "5.0.5", - "react-router": "3.2.0", - "react-router-redux": "4.0.8", + "react-router": "4.2.0", + "react-router-redux": "5.0.0-alpha.9", "redux": "3.6.0", "redux-batched-actions": "0.2.0", "redux-storage": "^4.1.2", diff --git a/packages/cmf/__tests__/App.test.js b/packages/cmf/__tests__/App.test.js index 53581358f79..971f1f720d5 100644 --- a/packages/cmf/__tests__/App.test.js +++ b/packages/cmf/__tests__/App.test.js @@ -4,7 +4,7 @@ import { Provider } from 'react-redux'; import App from '../src/App'; import RegistryProvider from '../src/RegistryProvider'; -import UIRouter from '../src/UIRouter'; +import CMFRouter from '../src/route/CMFRouter'; describe('CMF App', () => { it('App should init stuff', () => { @@ -20,7 +20,7 @@ describe('CMF App', () => { expect(wrapper.contains( - + ) ).toEqual(true); diff --git a/packages/cmf/__tests__/history.test.js b/packages/cmf/__tests__/history.test.js deleted file mode 100644 index 85f336ba464..00000000000 --- a/packages/cmf/__tests__/history.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import { hashHistory } from 'react-router'; -import history from '../src/history'; - -jest.mock('react-router'); -jest.mock('react-router-redux', () => ({ - syncHistoryWithStore(module, store) { - return { module, store }; - }, -})); - -describe('uiAbstraction history', () => { - it('shoud expose a get method', () => { - expect(typeof history.get).toBe('function'); - }); - it('shoud return an history object for the router', () => { - const store = {}; - const routerHistory = history.get(store); - expect(typeof routerHistory).toBe('object'); - expect(routerHistory.module).toBe(hashHistory); - expect(routerHistory.store).toBe(store); - }); -}); diff --git a/packages/cmf/__tests__/route.test.js b/packages/cmf/__tests__/route.test.js index 8daf678c273..be37c2fdaa8 100644 --- a/packages/cmf/__tests__/route.test.js +++ b/packages/cmf/__tests__/route.test.js @@ -1,9 +1,6 @@ /* eslint no-underscore-dangle: ["error", {"allow": ["_registry", "_isLocked"] }] */ -import React from 'react'; -import { shallow } from 'enzyme'; import route from '../src/route'; import registry from '../src/registry'; -import mock from '../src/mock'; describe('CMF route', () => { it('registerComponent should be an alias to component.get', () => { @@ -14,76 +11,3 @@ describe('CMF route', () => { expect(emptyRegistry['_.route.component:C1']).toBe(C1); }); }); - -describe('loadComponent behavior', () => { - it('should inject dispatch into component properties from context.store', () => { - const mockItem = { - component: 'component', - view: 'something', - }; - route.loadComponents(mock.context(), mockItem); - const wrapper = shallow(React.createElement(mockItem.component), { context: mock.context() }); - expect(wrapper.props().dispatch()).toBe('dispatch'); - }); - - it('should replace component by regitry one', () => { - const mockItem = { - component: 'TestContainer', - view: 'appmenu', - }; - const obj = { fn: jest.fn() }; - const component = obj.fn; - component.CMFContainer = true; - const mockContext = mock.context(); - mockContext.registry = { - '_.route.component:TestContainer': component, - }; - route.loadComponents(mockContext, mockItem); - component(); - expect(obj.fn).toHaveBeenCalled(); - expect(mockItem.component.displayName).toBe('WithView'); - }); - - it('should replace onEnter/onLeave hooks', () => { - // given - const mockItem = { - component: 'TestContainer', - view: 'appmenu', - onEnter: 'onEnterId', - onLeave: 'onLeaveId', - }; - const dispatch = jest.fn(); - const component = jest.fn(); - component.CMFContainer = true; - const onEnter = jest.fn(); - const onLeave = jest.fn(); - const nextState = { params: {} }; - const replace = jest.fn(); - - const mockContext = mock.context(); - mockContext.registry = { - '_.route.hook:onEnterId': onEnter, - '_.route.hook:onLeaveId': onLeave, - '_.route.component:TestContainer': component, - }; - - // when - route.loadComponents(mockContext, mockItem, dispatch); - - - // then - expect(onEnter).not.toBeCalled(); - mockItem.onEnter(nextState, replace); - expect(onEnter).toBeCalledWith({ - router: { nextState, replace }, - dispatch, - }); - - expect(onLeave).not.toBeCalled(); - mockItem.onLeave(nextState, replace); - expect(onLeave).toBeCalledWith({ - router: { nextState, replace }, - dispatch, - }); - }); -}); diff --git a/packages/cmf/__tests__/route/CMFRoute.test.js b/packages/cmf/__tests__/route/CMFRoute.test.js new file mode 100644 index 00000000000..19ef4821200 --- /dev/null +++ b/packages/cmf/__tests__/route/CMFRoute.test.js @@ -0,0 +1,87 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { shallow } from 'enzyme'; +import CMFRoute from '../../src/route/CMFRoute'; + +const routes = { + path: '/', + component: 'App', + indexRoute: { + component: 'Redirect', + view: 'indexRouteRedirect', + }, + childRoutes: [ + { + path: 'preparations/:folderId?', + component: 'HomeListView', + view: 'preparations', + onEnter: 'preparation:fetch', + }, + { + path: 'datasets', + component: 'HomeListView', + view: 'datasets', + }, + { + path: 'datastores', + component: 'HomeListView', + view: 'datastores', + }, + ], +}; + +const App = props => (
{props.children}
); +App.propTypes = { children: PropTypes.element }; +const HomeListView = props => (
{props.children}
); +HomeListView.propTypes = { children: PropTypes.element }; +const Redirect = () => (
); +function createContext() { + return { + registry: { + '_.route.component:App': App, + '_.route.component:HomeListView': HomeListView, + '_.route.component:Redirect': Redirect, + }, + }; +} + +describe('CMFRoute', () => { + it('should instantiate Route component', () => { + // given + const context = createContext(); + + // when + const wrapper = shallow( + ( + +
+ + ), + { context }, + ); + + // then + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + it('should instantiate nested Route component', () => { + // given + const context = createContext(); + const match = { params: {}, url: '/', path: '/' }; + + // when + const wrapper = shallow( + ( + +
+ + ), + { context }, + ); + const Component = wrapper.props().component; + const componentWrapper = shallow(); + + // then + expect(componentWrapper.getElement()).toMatchSnapshot(); + }); +}); diff --git a/packages/cmf/__tests__/route/CMFRouteHook.test.js b/packages/cmf/__tests__/route/CMFRouteHook.test.js new file mode 100644 index 00000000000..f6ecb762af8 --- /dev/null +++ b/packages/cmf/__tests__/route/CMFRouteHook.test.js @@ -0,0 +1,119 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { CMFRouteHookComponent } from '../../src/route/CMFRouteHook'; + +const onEnter = 'myComponent:onEnter'; +const onLeave = 'myComponent:onLeave'; +function createContext(onEnterFn, onLeaveFn) { + return { + registry: { + [`_.route.hook:${onEnter}`]: onEnterFn, + [`_.route.hook:${onLeave}`]: onLeaveFn, + }, + }; +} + +describe('CMFRouteHook', () => { + it('should render its children', () => { + // given + const MyComponent = () => (
My component
); + + // when + const wrapper = shallow( + + + + ); + + // then + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + it('should create onEnter/onLeave function', () => { + // given + const onEnterFn = jest.fn(); + const onLeaveFn = jest.fn(); + const context = createContext(onEnterFn, onLeaveFn); + + // when + const wrapper = shallow( + ( + +
+ + ), + { context }, + ); + + // then + expect(wrapper.instance().onEnter).toBe(onEnterFn); + expect(wrapper.instance().onLeave).toBe(onLeaveFn); + }); + + it('should call onEnter when componentDidMount', () => { + // given + const onEnterFn = jest.fn(); + const onLeaveFn = jest.fn(); + const context = createContext(onEnterFn, onLeaveFn); + const dispatch = jest.fn(); + const match = { params: {} }; + + // when + shallow( + ( + +
+ + ), + { context }, + ); + + // then + expect(onEnterFn).toBeCalledWith({ + router: { match }, + dispatch, + }); + }); + + it('should call onLeave when componentWillUnmount', () => { + // given + const onEnterFn = jest.fn(); + const onLeaveFn = jest.fn(); + const context = createContext(onEnterFn, onLeaveFn); + const dispatch = jest.fn(); + const match = { params: {} }; + + const wrapper = shallow( + ( + +
+ + ), + { context }, + ); + expect(onLeaveFn).not.toBeCalled(); + + // when + wrapper.unmount(); + + // then + expect(onLeaveFn).toBeCalledWith({ + router: { match }, + dispatch, + }); + }); +}); diff --git a/packages/cmf/__tests__/route/CMFRouter.test.js b/packages/cmf/__tests__/route/CMFRouter.test.js new file mode 100644 index 00000000000..78c644a70c9 --- /dev/null +++ b/packages/cmf/__tests__/route/CMFRouter.test.js @@ -0,0 +1,52 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { CMFRouterComponent } from '../../src/route/CMFRouter'; + +const routes = { + path: '/', + component: 'App', + indexRoute: { + component: 'Redirect', + view: 'indexRouteRedirect', + }, + childRoutes: [ + { + path: 'preparations/:folderId?', + component: 'HomeListView', + view: 'preparations', + onEnter: 'preparation:fetch', + }, + { + path: 'datasets', + component: 'HomeListView', + view: 'datasets', + }, + { + path: 'datastores', + component: 'HomeListView', + view: 'datastores', + }, + ], +}; + +describe('CMFRouter', () => { + it('should render loading when the routes configuration is not complete', () => { + // when + const wrapper = shallow( + + ); + + // then + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + it('should render Route components', () => { + // when + const wrapper = shallow( + + ); + + // then + expect(wrapper.getElement()).toMatchSnapshot(); + }); +}); diff --git a/packages/cmf/__tests__/route/__snapshots__/CMFRoute.test.js.snap b/packages/cmf/__tests__/route/__snapshots__/CMFRoute.test.js.snap new file mode 100644 index 00000000000..4ff40353eeb --- /dev/null +++ b/packages/cmf/__tests__/route/__snapshots__/CMFRoute.test.js.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CMFRoute should instantiate Route component 1`] = ` + +`; + +exports[`CMFRoute should instantiate nested Route component 1`] = ` + + + + + + +`; diff --git a/packages/cmf/__tests__/route/__snapshots__/CMFRouteHook.test.js.snap b/packages/cmf/__tests__/route/__snapshots__/CMFRouteHook.test.js.snap new file mode 100644 index 00000000000..1982e1d54cb --- /dev/null +++ b/packages/cmf/__tests__/route/__snapshots__/CMFRouteHook.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CMFRouteHook should render its children 1`] = ``; diff --git a/packages/cmf/__tests__/route/__snapshots__/CMFRouter.test.js.snap b/packages/cmf/__tests__/route/__snapshots__/CMFRouter.test.js.snap new file mode 100644 index 00000000000..3c3b4f21cf5 --- /dev/null +++ b/packages/cmf/__tests__/route/__snapshots__/CMFRouter.test.js.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CMFRouter should render Route components 1`] = ` + + + +`; + +exports[`CMFRouter should render loading when the routes configuration is not complete 1`] = ` +
+ loading +
+`; diff --git a/packages/cmf/__tests__/sagaRouter/router.test.js b/packages/cmf/__tests__/sagaRouter/router.test.js index fb001389ad7..69b28b445d6 100644 --- a/packages/cmf/__tests__/sagaRouter/router.test.js +++ b/packages/cmf/__tests__/sagaRouter/router.test.js @@ -11,14 +11,11 @@ describe('sagaRouter import', () => { }); describe('sagaRouter RouteChange', () => { - it(`start the configured saga if route equals current location, additionnaly add a second param - to the started saga set to 'true'`, () => { - const mockHistory = { - getCurrentLocation() { - return { - pathname: '/matchingroute', - }; - }, + it('start the configured saga if route equals current location,' + + " additionnaly add a second param to the started saga set to 'true'", () => { + // given + const mockedHistory = { + location: { pathname: '/matchingroute' }, }; const routes = { '/matchingroute': function* matchingSaga(notUsed, isExact) { @@ -27,7 +24,11 @@ describe('sagaRouter RouteChange', () => { } }, }; - const gen = sagaRouter(mockHistory, routes); + + // when + const gen = sagaRouter(mockedHistory, routes); + + // then expect(gen.next().value).toEqual(take('@@router/LOCATION_CHANGE')); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( spawn(routes['/matchingroute'], {}, true), @@ -36,12 +37,9 @@ describe('sagaRouter RouteChange', () => { it(`start the configured saga if route is a fragment of current location additionnaly add a second param to the started saga set to 'false'`, () => { - const mockHistory = { - getCurrentLocation() { - return { - pathname: '/matchingroute/childroute', - }; - }, + // given + const mockedHistory = { + location: { pathname: '/matchingroute/childroute' }, }; const routes = { '/matchingroute': function* matchingSaga(notused, isExact) { @@ -51,7 +49,11 @@ describe('sagaRouter RouteChange', () => { yield take('SOMETHING'); }, }; - const gen = sagaRouter(mockHistory, routes); + + // when + const gen = sagaRouter(mockedHistory, routes); + + // then expect(gen.next().value).toEqual(take('@@router/LOCATION_CHANGE')); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( spawn(routes['/matchingroute'], {}, false), @@ -60,90 +62,63 @@ describe('sagaRouter RouteChange', () => { it('keep running a saga if its route is a fragment of the new route', () => { const mockTask = createMockTask(); - function getMockedHistory() { - let count = 0; - return { - getCurrentLocation() { - if (count === 0) { - count = 1; - return { - pathname: '/matchingroute', - }; - } - return { - pathname: '/matchingroute/test', - }; - }, - }; - } + const mockedHistory = { + location: { pathname: '/matchingroute' }, + }; const routes = { '/matchingroute': function* matchingSaga() { yield take('SOMETHING'); }, }; - const gen = sagaRouter(getMockedHistory(), routes); + const gen = sagaRouter(mockedHistory, routes); expect(gen.next().value).toEqual(take('@@router/LOCATION_CHANGE')); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( spawn(routes['/matchingroute'], {}, true), ); - expect(gen.next(mockTask).value).toEqual(take('@@router/LOCATION_CHANGE')); + + // when + mockedHistory.location = { pathname: '/matchingroute/test' }; + + // then: // since the saga should be kept running the router to be able to handle a new route change + expect(gen.next(mockTask).value).toEqual(take('@@router/LOCATION_CHANGE')); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( take('@@router/LOCATION_CHANGE'), ); }); it("stop the saga if its route don't match the current location", () => { + // given const mockTask = createMockTask(); - function getMockedHistory() { - let count = 0; - return { - getCurrentLocation() { - if (count === 0) { - count = 1; - return { - pathname: '/matchingroute', - }; - } - return { - pathname: '/anotherroute', - }; - }, - }; - } + const mockedHistory = { + location: { pathname: '/matchingroute' }, + }; const routes = { '/matchingroute': function* matchingSaga() { yield take('SOMETHING'); }, }; - const gen = sagaRouter(getMockedHistory(), routes); + const gen = sagaRouter(mockedHistory, routes); expect(gen.next().value).toEqual(take('@@router/LOCATION_CHANGE')); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( spawn(routes['/matchingroute'], {}, true), ); + + // when + mockedHistory.location = { pathname: '/anotherroute' }; + + // then expect(gen.next(mockTask).value).toEqual(take('@@router/LOCATION_CHANGE')); const expectedCancelYield = cancel(mockTask); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual(expectedCancelYield); }); it('stop unmatched saga before spawning new ones, no matter the declaration order', () => { + // given const mockTask = createMockTask(); - function getMockedHistory() { - let count = 0; - return { - getCurrentLocation() { - if (count === 0 || count === 2) { - count = 1; - return { - pathname: '/toCancelFirst', - }; - } - return { - pathname: '/toStartAfter', - }; - }, - }; - } + const mockedHistory = { + location: { pathname: '/toCancelFirst' }, + }; const routes = { '/toStartAfter': function* matchingSaga() { yield take('SOMETHING'); @@ -152,11 +127,13 @@ describe('sagaRouter RouteChange', () => { yield take('SOMETHING'); }, }; - const gen = sagaRouter(getMockedHistory(), routes); + const gen = sagaRouter(mockedHistory, routes); expect(gen.next().value).toEqual(take('@@router/LOCATION_CHANGE')); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( spawn(routes['/toCancelFirst'], {}, true), ); + + mockedHistory.location = { pathname: '/toStartAfter' }; expect(gen.next(mockTask).value).toEqual(take('@@router/LOCATION_CHANGE')); const expectedCancelYield = cancel(mockTask); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual(expectedCancelYield); @@ -170,11 +147,20 @@ describe('sagaRouter RouteChange', () => { }, }; - const anotherGen = sagaRouter(getMockedHistory(), alternateRoutes); + // when + mockedHistory.location = { pathname: '/toCancelFirst' }; + const anotherGen = sagaRouter(mockedHistory, alternateRoutes); + + // then expect(anotherGen.next().value).toEqual(take('@@router/LOCATION_CHANGE')); expect(anotherGen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( spawn(alternateRoutes['/toCancelFirst'], {}, true), ); + + // when + mockedHistory.location = { pathname: '/toStartAfter' }; + + // then expect(anotherGen.next(mockTask).value).toEqual(take('@@router/LOCATION_CHANGE')); const anotherExpectedCancelYield = cancel(mockTask); expect(anotherGen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( @@ -182,8 +168,8 @@ describe('sagaRouter RouteChange', () => { ); }); - it(`stop a saga with 'runOnExactMatch' parameter if its route is a fragment of the new route`, () => { - // GIVEN + it('stop a saga with \'runOnExactMatch\' parameter if its route is a fragment of the new route', () => { + // given const mockTask = createMockTask(); const routes = { '/resources': { @@ -198,29 +184,19 @@ describe('sagaRouter RouteChange', () => { yield take('SOMETHING'); }, }; - function getMockedHistory() { - let count = 0; - return { - getCurrentLocation() { - if (count === 0) { - count = 1; - return { - pathname: '/resources', - }; - } - return { - pathname: '/resources/action', - }; - }, - }; - } - // WHEN - const gen = sagaRouter(getMockedHistory(), routes); - // EXPECT + const mockedHistory = { + location: { pathname: '/resources' }, + }; + const gen = sagaRouter(mockedHistory, routes); expect(gen.next().value).toEqual(take('@@router/LOCATION_CHANGE')); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( spawn(routes['/resources'].saga, {}, true), ); + + // when + mockedHistory.location = { pathname: '/resources/action' }; + + // then expect(gen.next(mockTask).value).toEqual(take('@@router/LOCATION_CHANGE')); const expectedCancelYield = cancel(mockTask); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual(expectedCancelYield); @@ -228,12 +204,9 @@ describe('sagaRouter RouteChange', () => { it(`does not start the configured saga with 'runOnExactMatch' parameter, if route is a fragment of current location`, () => { - const mockHistory = { - getCurrentLocation() { - return { - pathname: '/matchingroute/childroute', - }; - }, + // given + const mockedHistory = { + location: { pathname: '/matchingroute/childroute' }, }; const routes = { '/matchingroute': { @@ -246,7 +219,11 @@ describe('sagaRouter RouteChange', () => { }, }, }; - const gen = sagaRouter(mockHistory, routes); + + // when + const gen = sagaRouter(mockedHistory, routes); + + // then expect(gen.next().value).toEqual(take('@@router/LOCATION_CHANGE')); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( take('@@router/LOCATION_CHANGE') @@ -254,7 +231,7 @@ describe('sagaRouter RouteChange', () => { }); it('restart a saga with `restartOnRouteChange` parameter if the route it was matched on is now a subset of another location', () => { - // GIVEN + // given const mockTask = createMockTask(); const routes = { '/resources': { @@ -269,31 +246,20 @@ describe('sagaRouter RouteChange', () => { yield take('SOMETHING'); }, }; - function getMockedHistory() { - let count = 0; - return { - getCurrentLocation() { - if (count === 0) { - count = 1; - return { - pathname: '/resources', - }; - } - return { - pathname: '/resources/action', - }; - }, - }; - } - // WHEN - const gen = sagaRouter(getMockedHistory(), routes); - // EXPECT + const mockedHistory = { + location: { pathname: '/resources' }, + }; + const gen = sagaRouter(mockedHistory, routes); expect(gen.next().value).toEqual(take('@@router/LOCATION_CHANGE')); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( spawn(routes['/resources'].saga, {}, true), ); + + // when + mockedHistory.location = { pathname: '/resources/action' }; + + // then: if saga restarted, it will cancel it first and then start it. expect(gen.next(mockTask).value).toEqual(take('@@router/LOCATION_CHANGE')); - // if saga restarted, it will cancel it first and then start it. const expectedCancelYield = cancel(mockTask); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual(expectedCancelYield); expect(gen.next().value).toEqual( @@ -304,28 +270,20 @@ describe('sagaRouter RouteChange', () => { describe('sagaRouter route and route params', () => { it('route params should be given to target saga as object', () => { - function getMockedHistory() { - let count = 0; - return { - getCurrentLocation() { - if (count === 0) { - count = 1; - return { - pathname: '/matchingroute/anId', - }; - } - return { - pathname: '/anotherroute', - }; - }, - }; - } + // given + const mockedHistory = { + location: { pathname: '/matchingroute/anId' }, + }; const routes = { '/matchingroute/:id': function* matchingSaga() { yield take('SOMETHING'); }, }; - const gen = sagaRouter(getMockedHistory(), routes); + + // when + const gen = sagaRouter(mockedHistory, routes); + + // then expect(gen.next().value).toEqual(take('@@router/LOCATION_CHANGE')); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( spawn(routes['/matchingroute/:id'], { id: 'anId' }, true), @@ -333,33 +291,27 @@ describe('sagaRouter route and route params', () => { }); it('if route params change, then matchings sagas should be cancel and restarted', () => { + // given const mockTask = createMockTask(); - function getMockedHistory() { - let count = 0; - return { - getCurrentLocation() { - if (count === 0) { - count = 1; - return { - pathname: '/matchingroute/anId', - }; - } - return { - pathname: '/matchingroute/anotherId', - }; - }, - }; - } + const mockedHistory = { + location: { pathname: '/matchingroute/anId' }, + }; const routes = { '/matchingroute/:id': function* matchingSaga() { yield take('SOMETHING'); }, }; - const gen = sagaRouter(getMockedHistory(), routes); + + const gen = sagaRouter(mockedHistory, routes); expect(gen.next().value).toEqual(take('@@router/LOCATION_CHANGE')); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual( spawn(routes['/matchingroute/:id'], { id: 'anId' }, true), ); + + // when + mockedHistory.location = { pathname: '/matchingroute/anotherId' }; + + // then expect(gen.next(mockTask).value).toEqual(take('@@router/LOCATION_CHANGE')); const expectedCancelYield = cancel(mockTask); expect(gen.next({ type: '@@router/LOCATION_CHANGE' }).value).toEqual(expectedCancelYield); diff --git a/packages/cmf/__tests__/store.test.js b/packages/cmf/__tests__/store.test.js index 407f4d43e1f..b08b19e7db5 100644 --- a/packages/cmf/__tests__/store.test.js +++ b/packages/cmf/__tests__/store.test.js @@ -15,14 +15,14 @@ describe('CMF store', () => { expect(typeof s.getState).toBe('function'); expect(typeof s.replaceReducer).toBe('function'); const state = s.getState(); - expect(typeof state.routing).toBe('object'); + expect(typeof state.router).toBe('object'); expect(typeof state.cmf.settings).toBe('object'); expect(typeof state.app).toBe('object'); }); it('should initialize the store without args', () => { const s = store.initialize(); const state = s.getState(); - expect(typeof state.routing).toBe('object'); + expect(typeof state.router).toBe('object'); expect(typeof state.cmf.settings).toBe('object'); expect(typeof state.app).toBe('undefined'); }); @@ -37,7 +37,7 @@ describe('CMF store', () => { }; const s = store.initialize(reducer); const state = s.getState(); - expect(typeof state.routing).toBe('object'); + expect(typeof state.router).toBe('object'); expect(typeof state.cmf.settings).toBe('object'); expect(typeof state.app).toBe('object'); expect(typeof state.heyImRoot).toBe('object'); diff --git a/packages/cmf/package.json b/packages/cmf/package.json index 9176fe4ddb6..45807b26eae 100644 --- a/packages/cmf/package.json +++ b/packages/cmf/package.json @@ -72,8 +72,8 @@ "react": "^15.6.2", "react-dom": "^15.6.2", "react-redux": "5.0.5", - "react-router": "3.2.0", - "react-router-redux": "4.0.8", + "react-router": "4.2.0", + "react-router-redux": "5.0.0-alpha.9", "react-test-renderer": "^15.6.2", "redux": "3.6.0", "redux-batched-actions": "0.2.0", @@ -98,8 +98,8 @@ "react": "^15.6.2", "react-dom": "^15.6.2", "react-redux": "5.0.5", - "react-router": "3.2.0", - "react-router-redux": "4.0.8", + "react-router": "4.2.0", + "react-router-redux": "5.0.0-alpha.9", "redux": "3.6.0", "redux-batched-actions": "0.2.0", "redux-saga": "0.15.4", diff --git a/packages/cmf/src/App.js b/packages/cmf/src/App.js index d164abf3992..dc1854dc764 100644 --- a/packages/cmf/src/App.js +++ b/packages/cmf/src/App.js @@ -17,10 +17,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Provider } from 'react-redux'; +import { createHashHistory } from 'history'; -import history from './history'; import RegistryProvider from './RegistryProvider'; -import UIRouter from './UIRouter'; +import CMFRouter from './route/CMFRouter'; /** * The React component that render your app and provide CMF environment. @@ -29,12 +29,10 @@ import UIRouter from './UIRouter'; * @return {object} ReactElement */ export default function App(props) { - const hist = props.history || history.get(props.store); + const history = props.history || createHashHistory(); return ( - - {props.children || } - + {props.children || } ); } diff --git a/packages/cmf/src/UIRouter.js b/packages/cmf/src/UIRouter.js deleted file mode 100644 index 2ebd274c261..00000000000 --- a/packages/cmf/src/UIRouter.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Internal. It contains the wrapper to make react-router run with the CMF - * settings - * @module react-cmf/lib/UIRouter - * @see react-cmf/lib/route - * @see react-cmf/lib/settings - */ -import PropTypes from 'prop-types'; -import React from 'react'; -import { Router as BaseRouter } from 'react-router'; -import { connect } from 'react-redux'; - -import api from './api'; - -/** - * @typedef {Object} Router - */ - -/** - * pure arrow function that render the router component. - * You should never need to use this, it's an internal component - * @param {object} props The waited props (history and routes) - * @param {object} context The react context with the registry - * @return {object} ReactElement - */ -const UIRouter = (props, context) => { - const routes = api.route.getRoutesFromSettings(context, props.routes, props.dispatch); - if (routes.path === '/' && routes.component) { - return ; - } - return
loading
; -}; - -UIRouter.propTypes = { - dispatch: PropTypes.func, - history: PropTypes.object, - routes: PropTypes.object, -}; -UIRouter.contextTypes = { - registry: PropTypes.object, -}; -const mapStateToProps = state => ({ routes: state.cmf.settings.routes }); -export default connect(mapStateToProps)(UIRouter); diff --git a/packages/cmf/src/history.js b/packages/cmf/src/history.js deleted file mode 100644 index cd143126887..00000000000 --- a/packages/cmf/src/history.js +++ /dev/null @@ -1,14 +0,0 @@ -import { hashHistory } from 'react-router'; -import { syncHistoryWithStore } from 'react-router-redux'; - -/** - * @param {object} store redux - * @return {object} history for the router - */ -function get(store) { - return syncHistoryWithStore(hashHistory, store); -} - -export default { - get, -}; diff --git a/packages/cmf/src/index.js b/packages/cmf/src/index.js index 6a295e98364..dc435b996c4 100644 --- a/packages/cmf/src/index.js +++ b/packages/cmf/src/index.js @@ -8,8 +8,7 @@ import cmfConnect from './cmfConnect'; import ConnectedDispatcher from './Dispatcher'; import Inject from './Inject.component.js'; import RegistryProvider from './RegistryProvider'; -import UIRouter from './UIRouter'; -import history from './history'; +import CMFRouter from './route/CMFRouter'; import store from './store'; import actions from './actions/'; import reducers from './reducers/'; @@ -46,7 +45,7 @@ export { reducers, componentState, RegistryProvider, - UIRouter, + CMFRouter, getErrorMiddleware, httpMiddleware, sagaRouter, diff --git a/packages/cmf/src/route.js b/packages/cmf/src/route.js deleted file mode 100644 index 6678e11c787..00000000000 --- a/packages/cmf/src/route.js +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Internal. Provide low level function to configure CMF to drive react-router. - * @module react-cmf/lib/route - */ - -/* eslint no-underscore-dangle: ["error", {"allow": ["_ref"] }]*/ - -import PropTypes from 'prop-types'; - -import React from 'react'; -import { connect } from 'react-redux'; -import registry from './registry'; -import { mapStateToViewProps } from './settings'; -import deprecated from './deprecated'; -import CONST from './constant'; -import component from './component'; - -const getComponentFromRegistry = deprecated( - (context, id) => component.get(id, context), - 'stop use api.route.getComponentFromRegistry. Please use api.component.get', -); - -const registerComponent = deprecated( - component.register, - 'stop use api.route.registerComponent. please use api.component.register', -); - -/** - * register a function for the router configuration - * @param {string} id - * @param {function} func - */ -function registerFunction(id, func) { - if (typeof func !== 'function') { - throw new Error('registerFunction wait for a function'); - } - registry.addToRegistry(`${CONST.REGISTRY_HOOK_PREFIX}:${id}`, func); -} - -/** - * return a function from the router configuration - * @param {string} id - * @param {object} contextcmf context - */ -function getFunction(id, context) { - return registry.getFromRegistry(`${CONST.REGISTRY_HOOK_PREFIX}:${id}`, context); -} - -/** - * DEPRECATED connection to support old component which are registred but - * not CMF connected. - * @param {object} context React context with at least the stostore - * @param {any} component React component to connect - * @param {string} view the viewId to search for in settings - * @return {any} the connected component with it's view props injected - */ -function oldConnectView(context, Component, view) { - return connect(state => mapStateToViewProps(state, { view }))(Component); -} - -export const connectView = deprecated(oldConnectView, args => { - const cName = args[1].displayName || args[1].name || 'Unknown'; - return `The component ${cName} must be connected using cmfConnect`; -}); - -/** - * Internal. Is here to replace all 'component' from an object by their - * value in the registry. It configures react-router - * @param {object} context The react context - * @param {object} item The route to adapt - * @param {object} dispatch The redux dispatcher - */ -function loadComponents(context, item, dispatch) { - /* eslint no-param-reassign: ["error", { "props": false }] */ - if (item.component) { - item.component = component.get(item.component, context); - if (item.view && !item.component.CMFContainer) { - item.component = connectView(context, item.component, item.view); - } else if (item.view && item.component.CMFContainer) { - const WithView = item.component; - item.component = props => ; - item.component.displayName = 'WithView'; - item.component.propTypes = { - view: PropTypes.string, - }; - } - } - if (item.components) { - // TODO: iterate over all keys to call loadComponents - } - if (item.getComponent) { - item.getComponent = getFunction(item.getComponent, context); - } - if (item.getComponents) { - item.getComponents = getFunction(item.getComponents, context); - } - if (item.onEnter) { - const onEnterFn = getFunction(item.onEnter, context); - item.onEnter = function onEnter(nextState, replace) { - return onEnterFn({ - router: { - nextState, - replace, - }, - dispatch, - }); - }; - } - if (item.onLeave) { - const onLeaveFn = getFunction(item.onLeave, context); - item.onLeave = function onLeave(nextState, replace) { - return onLeaveFn({ - router: { - nextState, - replace, - }, - dispatch, - }); - }; - } - if (item.childRoutes) { - item.childRoutes.forEach(route => loadComponents(context, route, dispatch)); - } - if (item.indexRoute) { - loadComponents(context, item.indexRoute, dispatch); - } -} - -/** - * get the react router configuration 'routes' from our settings - * @param {object} context The react context - * @param {object} settings The route settings - * @param {object} dispatch The redux dispatcher - * @return {object} react router config - */ -function getRoutesFromSettings(context, settings, dispatch) { - const copy = Object.assign({}, settings); - loadComponents(context, copy, dispatch); - return copy; -} - -export default { - loadComponents, - getRoutesFromSettings, - getComponentFromRegistry, - registerComponent, - registerFunction, - getFunction, -}; diff --git a/packages/cmf/src/route/CMFRoute.js b/packages/cmf/src/route/CMFRoute.js new file mode 100644 index 00000000000..d701521dbbe --- /dev/null +++ b/packages/cmf/src/route/CMFRoute.js @@ -0,0 +1,184 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Route } from 'react-router'; + +import CMFRouteHook from './CMFRouteHook'; +import api from '../api'; + +/** + * Route components memoization. + * The routes are configured once on settings loading. + * Without memoization, a new component is created at each render, + * which implies a removal of current DOM nodes and mount of new once, + * even if there is no differences. + * @type {object} Key: unique route id - value : route component + */ +const routeComponents = {}; + +/** + * Get component from CMF registry + * Connect it to CMF if not already connected + * Pass the view setting to cmfConnect + * @param view The view setting + * @param componentId The component id in registry + * @param context The cmf context + * @returns {*} The connected component + */ +export function getConnectedComponent(view, componentId, context) { + const Component = api.component.get(componentId, context); + if (view && !Component.CMFContainer) { + return api.route.connectView(context, Component, view); + } else if (view) { + return props => ; + } + return Component; +} + +/** + * Get child routes + * @param childRoutes The child routes settings + * @param context The cmf context + * @param indexRoute The parent index settings + * @param parentPath The parent path + * @param parentRouteId The parent unique identifier + * @returns {any[]} The list of Route/CMFRoute child components + */ +function getChildRoutes({ childRoutes = [], context, indexRoute, parentPath, parentRouteId }) { + const children = childRoutes.map((route, index) => ( + + )); + + if (indexRoute) { + const IndexComponent = getConnectedComponent(indexRoute.view, indexRoute.component, context); + children.push(); + } + + return children; +} + +/** + * Component wrapper that the Route will mount. + * @param Component The real component to wrap + * @param onEnter The enter hook function id in CMF registry + * @param onLeave The leave hook function id in CMF registry + * @param props + * @returns {*} The CMF route wrapper + */ +function CMFRouteComponent({ Component, onEnter, onLeave, ...props }) { + // Backward compat: add props.params + const routeComponent = ; + + if (onEnter || onLeave) { + return ( + + {routeComponent} + + ); + } + + return routeComponent; +} +CMFRouteComponent.propTypes = { + Component: PropTypes.element, + onEnter: PropTypes.func, + onLeave: PropTypes.func, + match: PropTypes.shape({ params: PropTypes.object }), +}; + +/** + * Get route absolute path + * @param parentPath The route parent path + * @param path The route path. It is considered as absolute if it starts with / + * @returns {string} The absolute path + */ +function getSafePath(parentPath = '', path) { + if (!path.startsWith('/')) { + if (parentPath.endsWith('/')) { + return `${parentPath}${path}`; + } + return `${parentPath}/${path}`; + } + return path; +} + +/** + * @param childRoutes The child routes settings + * @param cmfParentPath The parent path + * @param component The component id in CMF registry + * @param exact Indicates if the route should match the exact path + * @param indexRoute The route index settings + * @param onEnter The enter hook function id in CMF registry + * @param onLeave The leave hook function id in CMF registry + * @param path The route path + * @param routeId A unique id of this route config for component memoization + * @param view The view settings id + * @param context The CMF context + * @returns {*} The CMF route + */ +export default function CMFRoute( + { + childRoutes, + cmfParentPath, + component, + exact, + indexRoute, + onEnter, + onLeave, + path, + routeId = 'root', + view, + }, + context, +) { + const safePath = getSafePath(cmfParentPath, path); + + let memoizedComponent = routeComponents[routeId]; + if (!memoizedComponent) { + const connectedComponent = getConnectedComponent(view, component, context); + const children = getChildRoutes({ + childRoutes, + context, + indexRoute, + parentRouteId: routeId, + parentPath: safePath, + }); + memoizedComponent = function RouteComponent(props) { + return ( + + ); + }; + routeComponents[routeId] = memoizedComponent; + } + + return ; +} + +CMFRoute.propTypes = { + childRoutes: PropTypes.arrayOf(PropTypes.object), + cmfParentPath: PropTypes.string, + component: PropTypes.string, + exact: PropTypes.bool, + indexRoute: PropTypes.object, + onEnter: PropTypes.string, + onLeave: PropTypes.string, + path: PropTypes.string, + routeId: PropTypes.string, + view: PropTypes.string, +}; +CMFRoute.contextTypes = { + registry: PropTypes.object, + router: PropTypes.object, +}; +CMFRoute.displayName = 'CMFRoute'; diff --git a/packages/cmf/src/route/CMFRouteHook.js b/packages/cmf/src/route/CMFRouteHook.js new file mode 100644 index 00000000000..73a0a363aba --- /dev/null +++ b/packages/cmf/src/route/CMFRouteHook.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import omit from 'lodash/omit'; +import api from '../api'; + +/** + * CMF Route component that implements onEnter/onLeave hooks + */ +export class CMFRouteHookComponent extends React.Component { + static displayName = 'CMFRouteHooks'; + static propTypes = { + onEnter: PropTypes.string, + onLeave: PropTypes.string, + children: PropTypes.element, + dispatch: PropTypes.func, + }; + static contextTypes = { + registry: PropTypes.object, + router: PropTypes.object, + }; + static ownProps = Object.keys(CMFRouteHookComponent.propTypes); + + constructor(props, context) { + super(props, context); + this.onEnter = props.onEnter && api.route.getFunction(props.onEnter, context); + this.onLeave = props.onLeave && api.route.getFunction(props.onLeave, context); + } + + componentDidMount() { + if (!this.onEnter) { + return; + } + this.onEnter({ + router: omit(this.props, CMFRouteHookComponent.ownProps), + dispatch: this.props.dispatch, + }); + } + + componentWillUnmount() { + if (!this.onLeave) { + return; + } + this.onLeave({ + router: omit(this.props, CMFRouteHookComponent.ownProps), + dispatch: this.props.dispatch, + }); + } + + render() { + return React.Children.only(this.props.children); + } +} +export default connect()(CMFRouteHookComponent); diff --git a/packages/cmf/src/route/CMFRouter.js b/packages/cmf/src/route/CMFRouter.js new file mode 100644 index 00000000000..b4c2133bf86 --- /dev/null +++ b/packages/cmf/src/route/CMFRouter.js @@ -0,0 +1,71 @@ +/** + * Internal. It contains the wrapper to make react-router run with the CMF + * settings + * @module react-cmf/lib/UIRouter + * @see react-cmf/lib/route + * @see react-cmf/lib/settings + */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ConnectedRouter } from 'react-router-redux'; +import { connect } from 'react-redux'; + +import CMFRoute from './CMFRoute'; + +/** + * @typedef {Object} Router + */ + +/** + * Pure arrow function that render the router component. + * You should never need to use this, it's an internal component + * @example + "routes": { + "path": "/", + "component": "App", + "indexRoute": { + "component": "Redirect", + "view": "redirectToStream" + }, + "childRoutes": [ + { + "path": "home", + "component": "HomeListView", + "view": "homepage" + "childRoutes": [ + { + "path": "sub", + "component": "SubHome" + "view": "subhome" + } + ] + } + ] + } + * @param {object} props The props (history and routes) + * @return {object} ReactElement + */ +export function CMFRouterComponent(props) { + const routes = props.routes; + if (routes.path === '/' && routes.component) { + return ( + + + + ); + } + return
loading
; +} +CMFRouterComponent.displayName = 'CMFRouter'; +CMFRouterComponent.propTypes = { + history: PropTypes.object, + routes: PropTypes.object, +}; + +function mapStateToProps(state) { + return { + routes: state.cmf.settings.routes, + }; +} + +export default connect(mapStateToProps)(CMFRouterComponent); diff --git a/packages/cmf/src/route/index.js b/packages/cmf/src/route/index.js new file mode 100644 index 00000000000..6f24e4d4088 --- /dev/null +++ b/packages/cmf/src/route/index.js @@ -0,0 +1,5 @@ +import route from './route'; +import CMFRouter from './CMFRouter'; + +export { CMFRouter }; +export default route; diff --git a/packages/cmf/src/route/route.js b/packages/cmf/src/route/route.js new file mode 100644 index 00000000000..3c6d24dd5cd --- /dev/null +++ b/packages/cmf/src/route/route.js @@ -0,0 +1,69 @@ +/** + * Internal. Provide low level function to configure CMF to drive react-router. + * @module react-cmf/lib/route + */ + +/* eslint no-underscore-dangle: ["error", {"allow": ["_ref"] }]*/ + +import { connect } from 'react-redux'; +import registry from '../registry'; +import { mapStateToViewProps } from '../settings'; +import deprecated from '../deprecated'; +import CONST from '../constant'; +import component from '../component'; + +const getComponentFromRegistry = deprecated( + (context, id) => component.get(id, context), + 'stop use api.route.getComponentFromRegistry. Please use api.component.get', +); + +const registerComponent = deprecated( + component.register, + 'stop use api.route.registerComponent. please use api.component.register', +); + +/** + * register a function for the router configuration + * @param {string} id + * @param {function} func + */ +function registerFunction(id, func) { + if (typeof func !== 'function') { + throw new Error('registerFunction wait for a function'); + } + registry.addToRegistry(`${CONST.REGISTRY_HOOK_PREFIX}:${id}`, func); +} + +/** + * return a function from the router configuration + * @param {string} id + * @param {object} context cmf context + */ +function getFunction(id, context) { + return registry.getFromRegistry(`${CONST.REGISTRY_HOOK_PREFIX}:${id}`, context); +} + +/** + * DEPRECATED connection to support old component which are registred but + * not CMF connected. + * @param {object} context React context with at least the stostore + * @param {any} Component React component to connect + * @param {string} view the viewId to search for in settings + * @return {any} the connected component with it's view props injected + */ +function oldConnectView(context, Component, view) { + return connect(state => mapStateToViewProps(state, { view }))(Component); +} + +export const connectView = deprecated(oldConnectView, args => { + const cName = args[1].displayName || args[1].name || 'Unknown'; + return `The component ${cName} must be connected using cmfConnect`; +}); + +export default { + getComponentFromRegistry, + registerComponent, + registerFunction, + getFunction, + connectView, +}; diff --git a/packages/cmf/src/sagaRouter/router.js b/packages/cmf/src/sagaRouter/router.js index e6b2ebd61e9..4159ba33d84 100644 --- a/packages/cmf/src/sagaRouter/router.js +++ b/packages/cmf/src/sagaRouter/router.js @@ -159,7 +159,7 @@ export default function* sagaRouter(history, routes) { // eslint-disable-line no-constant-condition yield take('@@router/LOCATION_CHANGE'); const shouldStart = []; - const currentLocation = history.getCurrentLocation(); + const currentLocation = history.location; for (let index = 0; index < routeFragments.length; ) { const routeFragment = routeFragments[index]; const routeSaga = routes[routeFragment]; diff --git a/packages/cmf/src/store.js b/packages/cmf/src/store.js index b43e5232a52..efefa67d27e 100644 --- a/packages/cmf/src/store.js +++ b/packages/cmf/src/store.js @@ -2,12 +2,12 @@ * This module is here to help app to create the redux store * @module react-cmf/lib/store */ -import { hashHistory } from 'react-router'; import { routerReducer, routerMiddleware } from 'react-router-redux'; import { combineReducers, createStore, applyMiddleware, compose } from 'redux'; import { enableBatching } from 'redux-batched-actions'; import thunk from 'redux-thunk'; import invariant from 'invariant'; +import { createHashHistory } from 'history'; import cmfReducers from './reducers'; import httpMiddleware from './middlewares/http'; @@ -20,6 +20,7 @@ import cmfMiddleware from './middlewares/cmf'; const preReducers = []; const enhancers = []; const middlewares = [thunk, cmfMiddleware]; +const hashHistory = createHashHistory(); if (window) { if (window.devToolsExtension) { @@ -97,6 +98,9 @@ function getReducer(appReducer) { if (!reducerObject.routing) { reducerObject.routing = routerReducer; } + if (!reducerObject.router) { + reducerObject.router = routerReducer; + } return enableBatching(preApplyReducer(combineReducers(reducerObject))); } diff --git a/packages/components/src/Drawer/Drawer.component.js b/packages/components/src/Drawer/Drawer.component.js index 0cd0a687359..9b93ea57cfe 100644 --- a/packages/components/src/Drawer/Drawer.component.js +++ b/packages/components/src/Drawer/Drawer.component.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { CSSTransition, transit } from 'react-css-transition'; import classnames from 'classnames'; +import omit from 'lodash/omit'; import ActionBar from '../ActionBar'; import Action from '../Actions/Action'; import TabBar from '../TabBar'; @@ -14,55 +15,60 @@ class DrawerAnimation extends React.Component { constructor(props) { super(props); this.handleTransitionComplete = this.handleTransitionComplete.bind(this); - this.state = { transitioned: false }; + this.close = this.close.bind(this); + this.state = { transitioned: false, active: true }; } handleTransitionComplete() { - this.props.onTransitionComplete(); - this.setState({ transitioned: true }); + if (this.state.active) { + this.props.onTransitionComplete(); + this.setState({ transitioned: true }); + } else { + this.props.onClose(); + } + } + + close() { + this.setState({ active: false }); } render() { - const { children, withTransition, ...rest } = this.props; - const transitionDuration = withTransition ? DEFAULT_TRANSITION_DURATION : 0; + const restProps = omit(this.props, Object.keys(DrawerAnimation.propTypes)); return ( - {React.cloneElement(children, this.state)} + {this.props.children({ close: this.close, ...this.state })} ); } } DrawerAnimation.propTypes = { - children: PropTypes.node, - withTransition: PropTypes.bool, + children: PropTypes.func, onTransitionComplete: PropTypes.func, + onClose: PropTypes.func, }; DrawerAnimation.defaultProps = { - withTransition: true, + onClose: () => {}, onTransitionComplete: () => {}, }; -function DrawerContainer({ stacked, className, children, withTransition = true, ...rest }) { - const drawerContainerClasses = classnames( - theme['tc-drawer'], - className, - 'tc-drawer', - { - [theme['tc-drawer-transition']]: withTransition, - 'tc-drawer-transition': withTransition, - }, - { - [theme['drawer-stacked']]: stacked, - stacked, - }, - ); +function DrawerContainer({ stacked, className, children, ...rest }) { + const drawerContainerClasses = classnames(theme['tc-drawer'], className, 'tc-drawer', { + [theme['drawer-stacked']]: stacked, + stacked, + }); return (
@@ -74,7 +80,6 @@ function DrawerContainer({ stacked, className, children, withTransition = true, DrawerContainer.propTypes = { stacked: PropTypes.bool, - withTransition: PropTypes.bool, className: PropTypes.string, children: PropTypes.node.isRequired, }; @@ -176,25 +181,19 @@ function Drawer({ footerActions, onCancelAction, tabs, - withTransition, }) { if (!children) { return null; } return ( - + {tabs && (
)} -
+
{children}
60) { - @return #000; // Lighter backgorund, return dark color + @return #000; // Lighter background, return dark color } @else { @return #fff; // Darker background, return light color } @@ -25,8 +23,6 @@ $tc-drawer-tabs-background: tint($brand-primary, 90) !default; right: 0; bottom: 0; width: 50vw; - // should always stay lower than dialog z-index which is set to 1040 - z-index: 100; } @media (min-width: $screen-md-min) and (max-width: $screen-md-max) { @@ -55,6 +51,13 @@ $tc-drawer-tabs-background: tint($brand-primary, 90) !default; width: 100%; } +.tc-drawer-main { + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: hidden; +} + @media screen and (min-width: $screen-xs-max) { .drawer-stacked { .tc-drawer-header-title, @@ -130,15 +133,17 @@ $tc-drawer-tabs-background: tint($brand-primary, 90) !default; } } -:global(.tc-with-drawer-wrapper) :global(.tc-drawer.stacked)::after { - background: rgba(0, 0, 0, 0.4); - content: ' '; - height: 100%; - width: 100%; - position: absolute; - top: 0; -} +:global(.tc-with-drawer-wrapper) { + :global(.tc-drawer.stacked)::after { + background: rgba(0, 0, 0, 0.4); + content: ' '; + height: 100%; + width: 100%; + position: absolute; + top: 0; + } -:global(.tc-with-drawer-wrapper:last-child) :global(.tc-drawer.stacked)::after { - content: none; + &:last-child :global(.tc-drawer.stacked)::after { + content: none; + } } diff --git a/packages/components/src/Drawer/Drawer.test.js b/packages/components/src/Drawer/Drawer.test.js index e2b2c956c24..6d4af3877a2 100644 --- a/packages/components/src/Drawer/Drawer.test.js +++ b/packages/components/src/Drawer/Drawer.test.js @@ -1,72 +1,56 @@ import React from 'react'; -import renderer from 'react-test-renderer'; -import { mount } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import Drawer, { cancelActionComponent } from './Drawer.component'; +const drawerProps = { + title: 'My drawer', +}; + describe('Drawer', () => { it('should render', () => { - const wrapper = renderer - .create( - -

Hello world

-
, - ) - .toJSON(); - expect(wrapper).toMatchSnapshot(); - }); - it('should render without tc-drawer-transition class', () => { - const wrapper = renderer - .create( - -

Hello world

-
, - ) - .toJSON(); - expect(wrapper).toMatchSnapshot(); + const wrapper = shallow( + +

Hello world

+
, + ); + expect(wrapper.getElement()).toMatchSnapshot(); }); + it('should render using custom styles', () => { - const wrapper = renderer - .create( - -

Hello world

-
, - ) - .toJSON(); - expect(wrapper).toMatchSnapshot(); + const wrapper = shallow( + +

Hello world

+
, + ); + expect(wrapper.getElement()).toMatchSnapshot(); }); + it('should render using custom className', () => { - const wrapper = renderer - .create( - -

Hello world

-
, - ) - .toJSON(); - expect(wrapper).toMatchSnapshot(); + const wrapper = shallow( + +

Hello world

+
, + ); + expect(wrapper.getElement()).toMatchSnapshot(); }); + it('should render stacked', () => { - const wrapper = renderer - .create( - -

Hello world

-
, - ) - .toJSON(); - expect(wrapper).toMatchSnapshot(); + const wrapper = shallow( + +

Hello world

+
, + ); + expect(wrapper.getElement()).toMatchSnapshot(); }); + it('should not render if no children', () => { - const wrapper = renderer.create().toJSON(); - expect(wrapper).toMatchSnapshot(); - }); - it('should render cancelActionComponent', () => { - const wrapper = mount(cancelActionComponent({})); - expect(wrapper.find('button')).toBeTruthy(); - }); - it('should not render cancelActionComponent', () => { - expect(cancelActionComponent()).toBe(null); + const wrapper = shallow(); + expect(wrapper.getElement()).toMatchSnapshot(); }); + it('should render with tabs', () => { + // given const tabs = { items: [ { @@ -81,35 +65,97 @@ describe('Drawer', () => { onSelect: jest.fn(), selectedKey: '2', }; - const wrapper = renderer - .create( - -

Hello world

-
, - ) - .toJSON(); - expect(wrapper).toMatchSnapshot(); + + // when + const wrapper = shallow( + +

Hello world

+
, + ); + + // then + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + describe('#cancelActionComponent', () => { + it('should render cancelAction', () => { + const wrapper = mount(cancelActionComponent({})); + expect(wrapper.find('button')).toBeTruthy(); + }); + + it('should not render anything', () => { + expect(cancelActionComponent()).toBe(null); + }); + }); +}); + +describe('Drawer.Content', () => { + it('should render', () => { + const wrapper = shallow( + +

Hello world

+
, + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + it('should render content with extra className', () => { + const wrapper = shallow( + +

Hello world

+
, + ); + expect(wrapper.getElement()).toMatchSnapshot(); }); +}); + +describe('Drawer.Animation', () => { + it('should wrap drawer in a CSSTransition', () => { + // given + const DrawerContent = () =>
My drawer content
; + + // when + const wrapper = shallow({() => }); - it('render drawer content without extra className', () => { - const wrapper = renderer - .create( - -

Hello world

-
, - ) - .toJSON(); - expect(wrapper).toMatchSnapshot(); + // then + expect(wrapper.getElement()).toMatchSnapshot(); }); - it('render drawer content with extra className', () => { - const wrapper = renderer - .create( - -

Hello world

-
, - ) - .toJSON(); - expect(wrapper).toMatchSnapshot(); + it('should pass animation props to the drawer component', () => { + // given + const DrawerContent = animationProps =>
My drawer content
; + + // when + const wrapper = shallow( + + {animationProps => } + , + ); + + // then + const animationProps = wrapper.find(DrawerContent).props(); + expect(animationProps.active).toBe(true); + expect(animationProps.transitioned).toBe(false); + expect(animationProps.close).toBeDefined(); + }); + + it('should call onClose() function after close transition', () => { + // given + const DrawerContent = animationProps =>
My drawer content
; + const onClose = jest.fn(); + + const wrapper = mount( + + {animationProps => } + , + ); + + // when + const close = wrapper.find(DrawerContent).props().close; + expect(onClose).not.toBeCalled(); + close(); + + // then + expect(onClose).toBeCalled(); }); }); diff --git a/packages/components/src/Drawer/__snapshots__/Drawer.test.js.snap b/packages/components/src/Drawer/__snapshots__/Drawer.test.js.snap index c92a234596e..31ce431f8a6 100644 --- a/packages/components/src/Drawer/__snapshots__/Drawer.test.js.snap +++ b/packages/components/src/Drawer/__snapshots__/Drawer.test.js.snap @@ -1,293 +1,222 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Drawer render drawer content with extra className 1`] = ` -
-

- Hello world -

-
-`; - -exports[`Drawer render drawer content without extra className 1`] = ` -
-

- Hello world -

-
-`; - exports[`Drawer should not render if no children 1`] = `null`; exports[`Drawer should render 1`] = ` -
+
+ +

+ Hello world +

+
-
-

- Hello world -

-
-
-
+
-
+ `; exports[`Drawer should render stacked 1`] = ` -
+
+ +

+ Hello world +

+
-
-

- Hello world -

-
-
-
+
-
+ `; exports[`Drawer should render using custom className 1`] = ` -
+
+ +

+ Hello world +

+
-
-

- Hello world -

-
-
-
+
-
+ `; exports[`Drawer should render using custom styles 1`] = ` -
+
+ +

+ Hello world +

+
-
-

- Hello world -

-
-
-
+
-
+ `; exports[`Drawer should render with tabs 1`] = ` -
+
-
- -
-
+
+
+ +

+ Hello world +

+
+
-
-

- Hello world -

-
-
-
+
+ +`; + +exports[`Drawer.Animation should wrap drawer in a CSSTransition 1`] = ` + + + +`; + +exports[`Drawer.Content should render 1`] = ` +
+

+ Hello world +

`; -exports[`Drawer should render without tc-drawer-transition class 1`] = ` +exports[`Drawer.Content should render content with extra className 1`] = `
-
-
-
-

- Hello world -

-
-
-
-
-
+

+ Hello world +

`; diff --git a/packages/components/src/Layout/__snapshots__/Layout.test.js.snap b/packages/components/src/Layout/__snapshots__/Layout.test.js.snap index 12ec696453a..8ceacf32730 100644 --- a/packages/components/src/Layout/__snapshots__/Layout.test.js.snap +++ b/packages/components/src/Layout/__snapshots__/Layout.test.js.snap @@ -140,68 +140,32 @@ exports[`Layout should render layout with Drawer component 1`] = ` className="theme-tc-with-drawer-container" >
- -
-

- Hello drawers -

-

- You should not being able to read this because I'm first -

-
+

+ Hello drawers +

+

+ You should not being able to read this because I'm first +

- -
-

- Hello drawers -

-

- The content dictate the width -

-
+

+ Hello drawers +

+

+ The content dictate the width +

diff --git a/packages/components/src/WithDrawer/WithDrawer.component.js b/packages/components/src/WithDrawer/WithDrawer.component.js index 3368ffab8ac..3204c96780c 100644 --- a/packages/components/src/WithDrawer/WithDrawer.component.js +++ b/packages/components/src/WithDrawer/WithDrawer.component.js @@ -1,9 +1,5 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { CSSTransitionGroup } from 'react-css-transition'; -import get from 'lodash/get'; - -import Drawer from '../Drawer'; import theme from './withDrawer.scss'; @@ -27,21 +23,7 @@ function WithDrawer({ drawers, children }) { return (
{children} - - {drawers && - drawers.map((drawer, key) => ( - - {drawer} - - ))} - +
{drawers}
); } diff --git a/packages/components/src/WithDrawer/__snapshots__/withDrawer.test.js.snap b/packages/components/src/WithDrawer/__snapshots__/withDrawer.test.js.snap new file mode 100644 index 00000000000..43d77fff6af --- /dev/null +++ b/packages/components/src/WithDrawer/__snapshots__/withDrawer.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WithDrawer should wrap drawers in a container 1`] = ` +
+
+ My content +
+
+
+ My first drawer +
+
+ My second drawer +
+
+
+`; diff --git a/packages/components/src/WithDrawer/withDrawer.test.js b/packages/components/src/WithDrawer/withDrawer.test.js index cc1ce4f9d8c..60a58171166 100644 --- a/packages/components/src/WithDrawer/withDrawer.test.js +++ b/packages/components/src/WithDrawer/withDrawer.test.js @@ -5,15 +5,18 @@ import WithDrawer from './WithDrawer.component'; import Drawer from './../Drawer'; describe('WithDrawer', () => { - it('should inject route as key if available', () => { - const drawer = test; - const wrapper = shallow(); - expect(wrapper.children().children().key()).toEqual('path'); - }); + it('should wrap drawers in a container', () => { + // given + const drawers = [
My first drawer
,
My second drawer
]; + + // when + const wrapper = shallow( + +
My content
+
, + ); - it('should inject generated key if route isn\'t available', () => { - const drawer = test; - const wrapper = shallow(); - expect(wrapper.children().children().key()).toEqual('0'); + // then + expect(wrapper.getElement()).toMatchSnapshot(); }); }); diff --git a/packages/containers/package.json b/packages/containers/package.json index b1c55538b1f..c74b0a66d9e 100644 --- a/packages/containers/package.json +++ b/packages/containers/package.json @@ -87,9 +87,9 @@ "react-dom": "^15.6.2", "react-i18next": "^5.2.0", "react-redux": "5.0.5", - "react-router": "3.2.0", - "react-router-redux": "4.0.8", - "react-storybook-cmf": "^0.2.0", + "react-router": "4.2.0", + "react-router-redux": "5.0.0-alpha.9", + "react-storybook-cmf": "^0.3.1", "react-stub-context": "^0.7.0", "react-test-renderer": "^15.6.2", "react-virtualized": "9.10.1", diff --git a/packages/containers/src/HomeListView/HomeListView.component.js b/packages/containers/src/HomeListView/HomeListView.component.js index 522f806590a..8e9da3d1026 100644 --- a/packages/containers/src/HomeListView/HomeListView.component.js +++ b/packages/containers/src/HomeListView/HomeListView.component.js @@ -12,24 +12,10 @@ function getContent(Component, props) { return ; } -function wrapChildren(children) { - if (children && children.props && children.props.children) { - return [children, ...wrapChildren(children.props.children)]; - } else if (children && !children.props) { - // this happens ony in tests with enzyme's mount - return []; - } - return [children]; -} - function HomeListView({ getComponent, id, hasTheme, sidepanel, list, header, children }) { if (!sidepanel || !list) { return null; } - let drawers = children || []; - if (!Array.isArray(drawers)) { - drawers = wrapChildren(drawers); - } const Renderers = Inject.getAll(getComponent, { HeaderBar, SidePanel, List }); return ( @@ -39,7 +25,7 @@ function HomeListView({ getComponent, id, hasTheme, sidepanel, list, header, chi mode="TwoColumns" header={getContent(Renderers.HeaderBar, header)} one={getContent(Renderers.SidePanel, sidepanel)} - drawers={drawers} + drawers={children} > {getContent(Renderers.List, list)} diff --git a/packages/containers/src/HomeListView/__snapshots__/HomeListView.test.js.snap b/packages/containers/src/HomeListView/__snapshots__/HomeListView.test.js.snap index 64251ec951a..b46866fa51e 100644 --- a/packages/containers/src/HomeListView/__snapshots__/HomeListView.test.js.snap +++ b/packages/containers/src/HomeListView/__snapshots__/HomeListView.test.js.snap @@ -3,11 +3,9 @@ exports[`Component HomeListView should be able to render theme 1`] = ` - Hello children - , - ] +

+ Hello children +

} hasTheme={true} header={ @@ -30,48 +28,29 @@ exports[`Component HomeListView should be able to render theme 1`] = ` `; exports[`Component HomeListView should children transformed as array in props.drawer 1`] = ` -Array [ - Object { - "props": Object { - "children": Object { - "props": Object { - "children": Object { - "props": Object { - "children": null, - }, +Object { + "props": Object { + "children": Object { + "props": Object { + "children": Object { + "props": Object { + "children": null, }, - "label": "foo", }, + "label": "foo", }, - "foo": "bar", - }, - }, - Object { - "props": Object { - "children": Object { - "props": Object { - "children": null, - }, - }, - "label": "foo", - }, - }, - Object { - "props": Object { - "children": null, }, + "foo": "bar", }, -] +} `; exports[`Component HomeListView should render with element props 1`] = ` - Hello children - , - ] +

+ Hello children +

} hasTheme={undefined} header={ @@ -96,11 +75,9 @@ exports[`Component HomeListView should render with element props 1`] = ` exports[`Component HomeListView should render with object props 1`] = ` - Hello children - , - ] +

+ Hello children +

} hasTheme={undefined} header={ diff --git a/packages/generator/generators/react-cmf/templates/package.json b/packages/generator/generators/react-cmf/templates/package.json index 3704c555de3..38635105594 100644 --- a/packages/generator/generators/react-cmf/templates/package.json +++ b/packages/generator/generators/react-cmf/templates/package.json @@ -89,8 +89,8 @@ "react-i18next": "^5.2.0", "react-immutable-proptypes": "2.1.0", "react-redux": "5.0.5", - "react-router": "3.2.0", - "react-router-redux": "4.0.8", + "react-router": "4.2.0", + "react-router-redux": "5.0.0-alpha.9", "redux": "3.6.0", "redux-batched-actions": "0.2.0", "redux-logger": "3.0.6", diff --git a/packages/sagas/package.json b/packages/sagas/package.json index 513e7cbd777..6aac815a848 100644 --- a/packages/sagas/package.json +++ b/packages/sagas/package.json @@ -42,8 +42,8 @@ "react": "^15.6.2", "react-dom": "^15.6.2", "react-redux": "5.0.5", - "react-router": "3.2.0", - "react-router-redux": "4.0.8", + "react-router": "4.2.0", + "react-router-redux": "5.0.0-alpha.9", "redux": "3.6.0", "redux-batched-actions": "0.2.0", "redux-storage": "^4.1.2", @@ -83,8 +83,8 @@ "react": "^15.6.2", "react-dom": "^15.6.2", "react-redux": "5.0.5", - "react-router": "3.2.0", - "react-router-redux": "4.0.8", + "react-router": "4.2.0", + "react-router-redux": "5.0.0-alpha.9", "redux": "3.6.0", "redux-batched-actions": "0.2.0", "redux-storage": "^4.1.2", diff --git a/version.js b/version.js index bfe95d7e287..3656f86fde3 100755 --- a/version.js +++ b/version.js @@ -97,8 +97,8 @@ const VERSIONS = Object.assign({}, ADDONS, { 'rc-tooltip': '3.7.0', 'react-i18next': '^5.2.0', 'react-redux': '5.0.5', - 'react-router': '3.2.0', - 'react-router-redux': '4.0.8', + 'react-router': '4.2.0', + 'react-router-redux': '5.0.0-alpha.9', 'react-test-renderer': REACT_VERSION, 'react-virtualized': '9.10.1', reselect: '^2.5.4', @@ -180,6 +180,7 @@ const files = [ './packages/sagas/package.json', './packages/theme/package.json', './packages/datagrid/package.json', + './examples/cmf-app/package.json', ]; const templates = ['./packages/generator/generators/react-cmf/templates/package.json']; diff --git a/yarn.lock b/yarn.lock index 222e62b5734..a2f786e06e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3070,7 +3070,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-react-class@15.x, create-react-class@^15.5.1, create-react-class@^15.5.2, create-react-class@^15.5.3, create-react-class@^15.6.0, create-react-class@^15.6.2: +create-react-class@15.x, create-react-class@^15.5.2, create-react-class@^15.5.3, create-react-class@^15.6.0, create-react-class@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a" dependencies: @@ -3078,6 +3078,14 @@ create-react-class@15.x, create-react-class@^15.5.1, create-react-class@^15.5.2, loose-envify "^1.3.1" object-assign "^4.1.1" +create-react-class@^15.5.1: + version "15.6.3" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + object-assign "^4.1.1" + cross-spawn-async@^2.2.2: version "2.2.5" resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc" @@ -5562,6 +5570,16 @@ history@^3.0.0: query-string "^4.2.2" warning "^3.0.0" +history@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" + dependencies: + invariant "^2.2.1" + loose-envify "^1.2.0" + resolve-pathname "^2.2.0" + value-equal "^0.4.0" + warning "^3.0.0" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -5586,6 +5604,10 @@ hoist-non-react-statics@1.2.0, hoist-non-react-statics@1.x.x, hoist-non-react-st version "1.2.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" +hoist-non-react-statics@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -9702,6 +9724,14 @@ react-router-redux@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.8.tgz#227403596b5151e182377dab835b5d45f0f8054e" +react-router-redux@5.0.0-alpha.9: + version "5.0.0-alpha.9" + resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-5.0.0-alpha.9.tgz#825431516e0e6f1fd93b8807f6bd595e23ec3d10" + dependencies: + history "^4.7.2" + prop-types "^15.6.0" + react-router "^4.2.0" + react-router@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-3.2.0.tgz#62b6279d589b70b34e265113e4c0a9261a02ed36" @@ -9714,6 +9744,18 @@ react-router@3.2.0: prop-types "^15.5.6" warning "^3.0.0" +react-router@4.2.0, react-router@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.2.0.tgz#61f7b3e3770daeb24062dae3eedef1b054155986" + dependencies: + history "^4.7.2" + hoist-non-react-statics "^2.3.0" + invariant "^2.2.2" + loose-envify "^1.3.1" + path-to-regexp "^1.7.0" + prop-types "^15.5.4" + warning "^3.0.0" + react-split-pane@^0.1.74: version "0.1.74" resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.74.tgz#cf79fc98b51ab0763fdc778749b810a102b036ca" @@ -9738,6 +9780,13 @@ react-storybook-cmf@^0.2.0: "@kadira/storybook-addons" "^1.5.0" prop-types "15.5.10" +react-storybook-cmf@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/react-storybook-cmf/-/react-storybook-cmf-0.3.1.tgz#ee8bc105165438f4fbdbfbcbc0b1912afcb06297" + dependencies: + "@kadira/storybook-addons" "^1.5.0" + prop-types "15.5.10" + react-stub-context@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/react-stub-context/-/react-stub-context-0.7.0.tgz#a1c7b0caacf1bff0cac4758a754d7b1bb9a11b67" @@ -10395,6 +10444,10 @@ resolve-from@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" +resolve-pathname@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -12148,6 +12201,10 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" +value-equal@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" + varstream@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/varstream/-/varstream-0.3.2.tgz#18ac6494765f3ff1a35ad9a4be053bec188a5de1"