diff --git a/src/components/AddDependencySearchResult/AddDependencySearchResult.js b/src/components/AddDependencySearchResult/AddDependencySearchResult.js index dccd7e9d..70df5d8f 100644 --- a/src/components/AddDependencySearchResult/AddDependencySearchResult.js +++ b/src/components/AddDependencySearchResult/AddDependencySearchResult.js @@ -25,6 +25,16 @@ import CustomHighlight from '../CustomHighlight'; import type { DependencyStatus } from '../../types'; +const DEPENDENCY_ACTIONS_COPY = { + idle: 'Installed', + installing: 'Installing…', + updating: 'Updating…', + deleting: 'Deleting…', + 'queued-install': 'Queued for Install', + 'queued-update': 'Queued for Update', + 'queued-delete': 'Queued for Delete', +}; + type Props = { projectId: string, currentStatus: ?DependencyStatus, @@ -76,8 +86,9 @@ const getColorForDownloadNumber = (num: number) => { class AddDependencySearchResult extends PureComponent { renderActionArea() { const { hit, projectId, currentStatus, addDependency } = this.props; + const isAlreadyInstalled = currentStatus === 'idle'; - if (currentStatus === 'idle') { + if (isAlreadyInstalled) { return ( { - { - { - installing: 'Installing..', - updating: 'Updating..', - deleting: 'Deleting..', - 'queued-install': 'Queued for Install', - 'queued-update': 'Queued for Update', - 'queued-delete': 'Queued for Delete', - }[currentStatus] - } + {DEPENDENCY_ACTIONS_COPY[currentStatus]} ); } diff --git a/src/components/DeleteDependencyButton/DeleteDependencyButton.js b/src/components/DeleteDependencyButton/DeleteDependencyButton.js index 13de8801..fccba763 100644 --- a/src/components/DeleteDependencyButton/DeleteDependencyButton.js +++ b/src/components/DeleteDependencyButton/DeleteDependencyButton.js @@ -12,6 +12,11 @@ import PixelShifter from '../PixelShifter'; const { dialog } = remote; +const DEPENDENCY_DELETE_COPY = { + idle: 'Delete', + 'queued-delete': 'Queued for Delete…', +}; + type Props = { projectId: string, dependencyName: string, @@ -75,10 +80,7 @@ class DeleteDependencyButton extends PureComponent { ) : ( - { - idle: 'Delete', - 'queued-delete': 'Queued for Delete..', - }[dependencyStatus] + DEPENDENCY_DELETE_COPY[dependencyStatus] )} ); diff --git a/src/components/DependencyManagementPane/DependencyManagementPane.js b/src/components/DependencyManagementPane/DependencyManagementPane.js index ec861d01..654c2d72 100644 --- a/src/components/DependencyManagementPane/DependencyManagementPane.js +++ b/src/components/DependencyManagementPane/DependencyManagementPane.js @@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import styled from 'styled-components'; import IconBase from 'react-icons-kit'; -import { moreHorizontal, plus } from 'react-icons-kit/feather'; +import { plus } from 'react-icons-kit/feather/plus'; import { runTask, abortTask } from '../../actions'; import { getSelectedProject } from '../../reducers/projects.reducer'; @@ -102,6 +102,44 @@ class DependencyManagementPane extends PureComponent { this.setState({ addingNewDependency: false }); }; + renderListAddon = (dependency, isSelected) => { + if ( + dependency.status === 'installing' || + dependency.status.match(/^queued-/) + ) { + return ( + + ); + } + + return ( + + {dependency.version} + + ); + }; + + renderMainContents = (selectedDependency, projectId) => { + if ( + selectedDependency.status === 'installing' || + selectedDependency.status === 'queued-install' + ) { + return ( + + ); + } + + return ( + + ); + }; + render() { const { id, dependencies } = this.props.project; const { selectedDependencyIndex, addingNewDependency } = this.state; @@ -123,32 +161,9 @@ class DependencyManagementPane extends PureComponent { onClick={() => this.selectDependency(dependency.name)} > {dependency.name} - {dependency.status === 'installing' ? ( - - ) : dependency.status.match(/^queued-/) ? ( - - - - ) : ( - - {dependency.version} - + {this.renderListAddon( + dependency, + selectedDependencyIndex === index )} ))} @@ -182,18 +197,7 @@ class DependencyManagementPane extends PureComponent { - {selectedDependency.status === 'installing' || - selectedDependency.status === 'queued-install' ? ( - - ) : ( - - )} + {this.renderMainContents(selectedDependency, id)} diff --git a/src/reducers/__snapshots__/dependencies.reducer.test.js.snap b/src/reducers/__snapshots__/dependencies.reducer.test.js.snap deleted file mode 100644 index 6b7b8e7b..00000000 --- a/src/reducers/__snapshots__/dependencies.reducer.test.js.snap +++ /dev/null @@ -1,233 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dependencies reducer should handle ADD_DEPENDENCY 1`] = ` -Object { - "foo": Object { - "redux": Object { - "description": "", - "homepage": "", - "license": "", - "location": "dependencies", - "name": "redux", - "repository": Object { - "type": "", - "url": "", - }, - "status": "queued-install", - "version": "", - }, - }, -} -`; - -exports[`dependencies reducer should handle DELETE_DEPENDENCY 1`] = ` -Object { - "foo": Object { - "redux": Object { - "description": "dependency description", - "homepage": "https://dependency-homepage.io", - "keywords": Array [ - "key", - "words", - ], - "license": "MIT", - "location": "dependencies", - "name": "redux", - "repository": Object { - "type": "git", - "url": "https://github.com/foo/bar.git", - }, - "status": "queued-delete", - "version": "3.2", - }, - }, -} -`; - -exports[`dependencies reducer should handle INSTALL_DEPENDENCIES_ERROR 1`] = ` -Object { - "foo": Object { - "redux": Object { - "description": "dependency description", - "homepage": "https://dependency-homepage.io", - "keywords": Array [ - "key", - "words", - ], - "license": "MIT", - "location": "dependencies", - "name": "redux", - "repository": Object { - "type": "git", - "url": "https://github.com/foo/bar.git", - }, - "status": "idle", - "version": "3.2", - }, - }, -} -`; - -exports[`dependencies reducer should handle INSTALL_DEPENDENCIES_FINISH 1`] = ` -Object { - "foo": Object { - "react-redux": Object { - "description": "other description", - "homepage": "https://dependency-homepage2.io", - "keywords": Array [ - "foo", - "bar", - ], - "license": "ISC", - "location": "dependencies", - "name": "react-redux", - "repository": Object { - "type": "git", - "url": "https://github.com/bar/foo.git", - }, - "status": "idle", - "version": "3.0", - }, - "redux": Object { - "description": "dependency description", - "homepage": "https://dependency-homepage.io", - "keywords": Array [ - "key", - "words", - ], - "license": "MIT", - "location": "dependencies", - "name": "redux", - "repository": Object { - "type": "git", - "url": "https://github.com/foo/bar.git", - }, - "status": "idle", - "version": "3.3", - }, - }, -} -`; - -exports[`dependencies reducer should handle INSTALL_DEPENDENCIES_START 1`] = ` -Object { - "foo": Object { - "react-redux": Object { - "description": "", - "homepage": "", - "license": "", - "location": "dependencies", - "name": "react-redux", - "repository": Object { - "type": "", - "url": "", - }, - "status": "installing", - "version": "", - }, - "redux": Object { - "description": "dependency description", - "homepage": "https://dependency-homepage.io", - "keywords": Array [ - "key", - "words", - ], - "license": "MIT", - "location": "dependencies", - "name": "redux", - "repository": Object { - "type": "git", - "url": "https://github.com/foo/bar.git", - }, - "status": "updating", - "version": "3.2", - }, - }, -} -`; - -exports[`dependencies reducer should handle LOAD_DEPENDENCY_INFO_FROM_DISK 1`] = ` -Object { - "baz": Object {}, - "foo": Object { - "redux": Object {}, - }, -} -`; - -exports[`dependencies reducer should handle UNINSTALL_DEPENDENCIES_ERROR 1`] = ` -Object { - "foo": Object { - "redux": Object { - "description": "dependency description", - "homepage": "https://dependency-homepage.io", - "keywords": Array [ - "key", - "words", - ], - "license": "MIT", - "location": "dependencies", - "name": "redux", - "repository": Object { - "type": "git", - "url": "https://github.com/foo/bar.git", - }, - "status": "idle", - "version": "3.2", - }, - }, -} -`; - -exports[`dependencies reducer should handle UNINSTALL_DEPENDENCIES_FINISH 1`] = ` -Object { - "foo": Object {}, -} -`; - -exports[`dependencies reducer should handle UNINSTALL_DEPENDENCIES_START 1`] = ` -Object { - "foo": Object { - "redux": Object { - "description": "dependency description", - "homepage": "https://dependency-homepage.io", - "keywords": Array [ - "key", - "words", - ], - "license": "MIT", - "name": "redux", - "repository": Object { - "type": "git", - "url": "https://github.com/foo/bar.git", - }, - "status": "deleting", - "version": "3.2", - }, - }, -} -`; - -exports[`dependencies reducer should handle UPDATE_DEPENDENCY 1`] = ` -Object { - "foo": Object { - "redux": Object { - "description": "dependency description", - "homepage": "https://dependency-homepage.io", - "keywords": Array [ - "key", - "words", - ], - "license": "MIT", - "location": "dependencies", - "name": "redux", - "repository": Object { - "type": "git", - "url": "https://github.com/foo/bar.git", - }, - "status": "queued-update", - "version": "3.2", - }, - }, -} -`; diff --git a/src/reducers/__snapshots__/queue.reducer.test.js.snap b/src/reducers/__snapshots__/queue.reducer.test.js.snap deleted file mode 100644 index 7f7ca831..00000000 --- a/src/reducers/__snapshots__/queue.reducer.test.js.snap +++ /dev/null @@ -1,155 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`queue reducer getNextActionForProjectId should return next action when one is present 1`] = ` -Object { - "action": "install", - "dependencies": Array [ - Object { - "name": "redux", - }, - ], -} -`; - -exports[`queue reducer should handle QUEUE_DEPENDENCY_INSTALL for mixed existing queue 1`] = ` -Object { - "foo": Array [ - Object { - "action": "install", - "dependencies": Array [ - Object { - "name": "react-redux", - }, - Object { - "name": "lodash", - "updating": undefined, - "version": undefined, - }, - ], - }, - Object { - "action": "uninstall", - "dependencies": Array [ - Object { - "name": "redux", - }, - ], - }, - ], -} -`; - -exports[`queue reducer should handle QUEUE_DEPENDENCY_INSTALL for new dependency on empty queue 1`] = ` -Object { - "foo": Array [ - Object { - "action": "install", - "active": false, - "dependencies": Array [ - Object { - "name": "redux", - "updating": undefined, - "version": "3.2", - }, - ], - }, - ], -} -`; - -exports[`queue reducer should handle QUEUE_DEPENDENCY_INSTALL for new dependency on existing queue 1`] = ` -Object { - "foo": Array [ - Object { - "action": "install", - "dependencies": Array [ - Object { - "name": "react-redux", - }, - Object { - "name": "redux", - "updating": undefined, - "version": undefined, - }, - ], - }, - ], -} -`; - -exports[`queue reducer should handle QUEUE_DEPENDENCY_INSTALL for updating dependency 1`] = ` -Object { - "foo": Array [ - Object { - "action": "install", - "active": false, - "dependencies": Array [ - Object { - "name": "redux", - "updating": true, - "version": "3.3", - }, - ], - }, - ], -} -`; - -exports[`queue reducer should handle QUEUE_DEPENDENCY_UNINSTALL 1`] = ` -Object { - "foo": Array [ - Object { - "action": "uninstall", - "active": false, - "dependencies": Array [ - Object { - "name": "redux", - }, - ], - }, - ], -} -`; - -exports[`queue reducer should handle QUEUE_DEPENDENCY_UNINSTALL for mixed existing queue 1`] = ` -Object { - "foo": Array [ - Object { - "action": "install", - "dependencies": Array [ - Object { - "name": "react-redux", - }, - ], - }, - Object { - "action": "uninstall", - "dependencies": Array [ - Object { - "name": "redux", - }, - Object { - "name": "lodash", - }, - ], - }, - ], -} -`; - -exports[`queue reducer should handle START_NEXT_ACTION_IN_QUEUE for queue with next action 1`] = ` -Object { - "foo": Array [ - Object { - "action": "uninstall", - "dependencies": Array [ - Object { - "name": "react-redux", - }, - ], - }, - ], -} -`; - -exports[`queue reducer should handle START_NEXT_ACTION_IN_QUEUE for queue with no more actions 1`] = `Object {}`; diff --git a/src/reducers/dependencies.reducer.js b/src/reducers/dependencies.reducer.js index bd6ab848..94095b14 100644 --- a/src/reducers/dependencies.reducer.js +++ b/src/reducers/dependencies.reducer.js @@ -102,11 +102,8 @@ export default (state: State = initialState, action: Action) => { return produce(state, draftState => { dependencies.forEach(dependency => { - draftState[projectId][dependency.name] = { - ...draftState[projectId][dependency.name], - ...dependency, - status: 'idle', - }; + draftState[projectId][dependency.name] = dependency; + draftState[projectId][dependency.name].status = 'idle'; }); }); } diff --git a/src/reducers/dependencies.reducer.test.js b/src/reducers/dependencies.reducer.test.js index 33939002..3821860c 100644 --- a/src/reducers/dependencies.reducer.test.js +++ b/src/reducers/dependencies.reducer.test.js @@ -28,7 +28,14 @@ describe('dependencies reducer', () => { dependencies: { redux: {} }, }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "baz": Object {}, + "foo": Object { + "redux": Object {}, + }, +} +`); }); it(`should handle ${ADD_DEPENDENCY}`, () => { @@ -42,7 +49,25 @@ describe('dependencies reducer', () => { dependencyName: 'redux', }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Object { + "redux": Object { + "description": "", + "homepage": "", + "license": "", + "location": "dependencies", + "name": "redux", + "repository": Object { + "type": "", + "url": "", + }, + "status": "queued-install", + "version": "", + }, + }, +} +`); }); it(`should handle ${UPDATE_DEPENDENCY}`, () => { @@ -69,7 +94,29 @@ describe('dependencies reducer', () => { latestVersion: '3.3', }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Object { + "redux": Object { + "description": "dependency description", + "homepage": "https://dependency-homepage.io", + "keywords": Array [ + "key", + "words", + ], + "license": "MIT", + "location": "dependencies", + "name": "redux", + "repository": Object { + "type": "git", + "url": "https://github.com/foo/bar.git", + }, + "status": "queued-update", + "version": "3.2", + }, + }, +} +`); }); it(`should handle ${DELETE_DEPENDENCY}`, () => { @@ -96,7 +143,29 @@ describe('dependencies reducer', () => { latestVersion: '3.3', }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Object { + "redux": Object { + "description": "dependency description", + "homepage": "https://dependency-homepage.io", + "keywords": Array [ + "key", + "words", + ], + "license": "MIT", + "location": "dependencies", + "name": "redux", + "repository": Object { + "type": "git", + "url": "https://github.com/foo/bar.git", + }, + "status": "queued-delete", + "version": "3.2", + }, + }, +} +`); }); it(`should handle ${INSTALL_DEPENDENCIES_START}`, () => { @@ -141,7 +210,42 @@ describe('dependencies reducer', () => { ], }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Object { + "react-redux": Object { + "description": "", + "homepage": "", + "license": "", + "location": "dependencies", + "name": "react-redux", + "repository": Object { + "type": "", + "url": "", + }, + "status": "installing", + "version": "", + }, + "redux": Object { + "description": "dependency description", + "homepage": "https://dependency-homepage.io", + "keywords": Array [ + "key", + "words", + ], + "license": "MIT", + "location": "dependencies", + "name": "redux", + "repository": Object { + "type": "git", + "url": "https://github.com/foo/bar.git", + }, + "status": "updating", + "version": "3.2", + }, + }, +} +`); }); it(`should handle ${INSTALL_DEPENDENCIES_ERROR}`, () => { @@ -186,7 +290,29 @@ describe('dependencies reducer', () => { ], }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Object { + "redux": Object { + "description": "dependency description", + "homepage": "https://dependency-homepage.io", + "keywords": Array [ + "key", + "words", + ], + "license": "MIT", + "location": "dependencies", + "name": "redux", + "repository": Object { + "type": "git", + "url": "https://github.com/foo/bar.git", + }, + "status": "idle", + "version": "3.2", + }, + }, +} +`); }); it(`should handle ${INSTALL_DEPENDENCIES_FINISH}`, () => { @@ -222,10 +348,17 @@ describe('dependencies reducer', () => { dependencies: [ { name: 'redux', + location: 'dependencies', + description: 'dependency description', + keywords: ['key', 'words'], version: '3.3', + homepage: 'https://dependency-homepage.io', + license: 'MIT', + repository: { type: 'git', url: 'https://github.com/foo/bar.git' }, }, { name: 'react-redux', + location: 'dependencies', description: 'other description', keywords: ['foo', 'bar'], version: '3.0', @@ -236,7 +369,46 @@ describe('dependencies reducer', () => { ], }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Object { + "react-redux": Object { + "description": "other description", + "homepage": "https://dependency-homepage2.io", + "keywords": Array [ + "foo", + "bar", + ], + "license": "ISC", + "location": "dependencies", + "name": "react-redux", + "repository": Object { + "type": "git", + "url": "https://github.com/bar/foo.git", + }, + "status": "idle", + "version": "3.0", + }, + "redux": Object { + "description": "dependency description", + "homepage": "https://dependency-homepage.io", + "keywords": Array [ + "key", + "words", + ], + "license": "MIT", + "location": "dependencies", + "name": "redux", + "repository": Object { + "type": "git", + "url": "https://github.com/foo/bar.git", + }, + "status": "idle", + "version": "3.3", + }, + }, +} +`); }); it(`should handle ${UNINSTALL_DEPENDENCIES_START}`, () => { @@ -265,7 +437,28 @@ describe('dependencies reducer', () => { ], }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Object { + "redux": Object { + "description": "dependency description", + "homepage": "https://dependency-homepage.io", + "keywords": Array [ + "key", + "words", + ], + "license": "MIT", + "name": "redux", + "repository": Object { + "type": "git", + "url": "https://github.com/foo/bar.git", + }, + "status": "deleting", + "version": "3.2", + }, + }, +} +`); }); it(`should handle ${UNINSTALL_DEPENDENCIES_ERROR}`, () => { @@ -295,7 +488,29 @@ describe('dependencies reducer', () => { ], }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Object { + "redux": Object { + "description": "dependency description", + "homepage": "https://dependency-homepage.io", + "keywords": Array [ + "key", + "words", + ], + "license": "MIT", + "location": "dependencies", + "name": "redux", + "repository": Object { + "type": "git", + "url": "https://github.com/foo/bar.git", + }, + "status": "idle", + "version": "3.2", + }, + }, +} +`); }); it(`should handle ${UNINSTALL_DEPENDENCIES_FINISH}`, () => { @@ -325,7 +540,11 @@ describe('dependencies reducer', () => { ], }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Object {}, +} +`); }); }); diff --git a/src/reducers/queue.reducer.js b/src/reducers/queue.reducer.js index ec7e3ab8..f320b758 100644 --- a/src/reducers/queue.reducer.js +++ b/src/reducers/queue.reducer.js @@ -5,40 +5,29 @@ import { QUEUE_DEPENDENCY_UNINSTALL, INSTALL_DEPENDENCIES_START, UNINSTALL_DEPENDENCIES_START, - START_NEXT_ACTION_IN_QUEUE, + INSTALL_DEPENDENCIES_ERROR, + INSTALL_DEPENDENCIES_FINISH, + UNINSTALL_DEPENDENCIES_ERROR, + UNINSTALL_DEPENDENCIES_FINISH, } from '../actions'; import type { Action } from 'redux'; import type { QueuedDependency, QueueAction } from '../types'; +type QueueEntry = { + action: QueueAction, + active: boolean, + dependencies: Array, +}; + type State = { - [projectId: string]: Array<{ - action: QueueAction, - active: boolean, - dependencies: Array, - }>, + [projectId: string]: Array, }; const initialState = {}; export default (state: State = initialState, action: Action) => { switch (action.type) { - case START_NEXT_ACTION_IN_QUEUE: { - const { projectId } = action; - - return produce(state, draftState => { - // remove oldest item in queue (it's just been - // completed by the dependency saga) - draftState[projectId].shift(); - - // remove the project's queue if this was the - // last entry - if (draftState[projectId].length === 0) { - delete draftState[projectId]; - } - }); - } - case QUEUE_DEPENDENCY_INSTALL: { const { projectId, name, version, updating } = action; @@ -115,6 +104,25 @@ export default (state: State = initialState, action: Action) => { }); } + case INSTALL_DEPENDENCIES_ERROR: + case INSTALL_DEPENDENCIES_FINISH: + case UNINSTALL_DEPENDENCIES_ERROR: + case UNINSTALL_DEPENDENCIES_FINISH: { + const { projectId } = action; + + return produce(state, draftState => { + // remove oldest item in queue (it's just been + // completed by the dependency saga) + draftState[projectId].shift(); + + // remove the project's queue if this was the + // last entry + if (draftState[projectId].length === 0) { + delete draftState[projectId]; + } + }); + } + default: return state; } @@ -124,10 +132,5 @@ export default (state: State = initialState, action: Action) => { // // // Selectors -export const getPackageJsonLockedForProjectId = ( - state: any, - projectId: string -) => !!state.queue[projectId]; - export const getNextActionForProjectId = (state: any, projectId: string) => state.queue[projectId] && state.queue[projectId][0]; diff --git a/src/reducers/queue.reducer.test.js b/src/reducers/queue.reducer.test.js index 5355c016..f9437e57 100644 --- a/src/reducers/queue.reducer.test.js +++ b/src/reducers/queue.reducer.test.js @@ -1,11 +1,9 @@ -import reducer, { - getPackageJsonLockedForProjectId, - getNextActionForProjectId, -} from './queue.reducer'; +import reducer, { getNextActionForProjectId } from './queue.reducer'; import { QUEUE_DEPENDENCY_INSTALL, QUEUE_DEPENDENCY_UNINSTALL, - START_NEXT_ACTION_IN_QUEUE, + INSTALL_DEPENDENCIES_START, + INSTALL_DEPENDENCIES_FINISH, } from '../actions'; describe('queue reducer', () => { @@ -13,7 +11,36 @@ describe('queue reducer', () => { expect(reducer(undefined, {})).toEqual({}); }); - it(`should handle ${START_NEXT_ACTION_IN_QUEUE} for queue with next action`, () => { + it(`should handle queue item start`, () => { + const prevState = { + foo: [ + { action: 'install', active: false, dependencies: [{ name: 'redux' }] }, + ], + }; + + const action = { + type: INSTALL_DEPENDENCIES_START, + projectId: 'foo', + }; + + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Array [ + Object { + "action": "install", + "active": true, + "dependencies": Array [ + Object { + "name": "redux", + }, + ], + }, + ], +} +`); + }); + + it(`should handle queue item completion for queue with next action`, () => { const prevState = { foo: [ { action: 'install', dependencies: [{ name: 'redux' }] }, @@ -22,24 +49,37 @@ describe('queue reducer', () => { }; const action = { - type: START_NEXT_ACTION_IN_QUEUE, + type: INSTALL_DEPENDENCIES_FINISH, projectId: 'foo', }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Array [ + Object { + "action": "uninstall", + "dependencies": Array [ + Object { + "name": "react-redux", + }, + ], + }, + ], +} +`); }); - it(`should handle ${START_NEXT_ACTION_IN_QUEUE} for queue with no more actions`, () => { + it(`should handle queue item completion for queue with no more actions`, () => { const prevState = { foo: [{ action: 'install', dependencies: [{ name: 'redux' }] }], }; const action = { - type: START_NEXT_ACTION_IN_QUEUE, + type: INSTALL_DEPENDENCIES_FINISH, projectId: 'foo', }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(`Object {}`); }); it(`should handle ${QUEUE_DEPENDENCY_INSTALL} for new dependency on empty queue`, () => { @@ -52,7 +92,23 @@ describe('queue reducer', () => { version: '3.2', }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Array [ + Object { + "action": "install", + "active": false, + "dependencies": Array [ + Object { + "name": "redux", + "updating": undefined, + "version": "3.2", + }, + ], + }, + ], +} +`); }); it(`should handle ${QUEUE_DEPENDENCY_INSTALL} for new dependency on existing queue`, () => { @@ -71,7 +127,25 @@ describe('queue reducer', () => { name: 'redux', }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Array [ + Object { + "action": "install", + "dependencies": Array [ + Object { + "name": "react-redux", + }, + Object { + "name": "redux", + "updating": undefined, + "version": undefined, + }, + ], + }, + ], +} +`); }); it(`should handle ${QUEUE_DEPENDENCY_INSTALL} for updating dependency`, () => { @@ -85,7 +159,23 @@ describe('queue reducer', () => { updating: true, }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Array [ + Object { + "action": "install", + "active": false, + "dependencies": Array [ + Object { + "name": "redux", + "updating": true, + "version": "3.3", + }, + ], + }, + ], +} +`); }); it(`should handle ${QUEUE_DEPENDENCY_UNINSTALL}`, () => { @@ -97,7 +187,21 @@ describe('queue reducer', () => { name: 'redux', }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Array [ + Object { + "action": "uninstall", + "active": false, + "dependencies": Array [ + Object { + "name": "redux", + }, + ], + }, + ], +} +`); }); it(`should handle ${QUEUE_DEPENDENCY_INSTALL} for mixed existing queue`, () => { @@ -114,7 +218,33 @@ describe('queue reducer', () => { name: 'lodash', }; - expect(reducer(prevState, action)).toMatchSnapshot(); + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Array [ + Object { + "action": "install", + "dependencies": Array [ + Object { + "name": "react-redux", + }, + Object { + "name": "lodash", + "updating": undefined, + "version": undefined, + }, + ], + }, + Object { + "action": "uninstall", + "dependencies": Array [ + Object { + "name": "redux", + }, + ], + }, + ], +} +`); }); it(`should handle ${QUEUE_DEPENDENCY_UNINSTALL} for mixed existing queue`, () => { @@ -131,36 +261,31 @@ describe('queue reducer', () => { name: 'lodash', }; - expect(reducer(prevState, action)).toMatchSnapshot(); - }); - - describe('getPackageJsonLockedForProjectId', () => { - it('should return false when no dependencies are queued', () => { - const state = { - queue: {}, - }; - - const projectId = 'foo'; - - expect(getPackageJsonLockedForProjectId(state, projectId)).toBe(false); - }); - - it('should return true when dependencies are queued', () => { - const state = { - queue: { - foo: [ - { - action: 'install', - dependencies: [{ name: 'redux' }], - }, - ], + expect(reducer(prevState, action)).toMatchInlineSnapshot(` +Object { + "foo": Array [ + Object { + "action": "install", + "dependencies": Array [ + Object { + "name": "react-redux", }, - }; - - const projectId = 'foo'; - - expect(getPackageJsonLockedForProjectId(state, projectId)).toBe(true); - }); + ], + }, + Object { + "action": "uninstall", + "dependencies": Array [ + Object { + "name": "redux", + }, + Object { + "name": "lodash", + }, + ], + }, + ], +} +`); }); describe('getNextActionForProjectId', () => { @@ -173,7 +298,17 @@ describe('queue reducer', () => { const projectId = 'foo'; - expect(getNextActionForProjectId(state, projectId)).toMatchSnapshot(); + expect(getNextActionForProjectId(state, projectId)) + .toMatchInlineSnapshot(` +Object { + "action": "install", + "dependencies": Array [ + Object { + "name": "redux", + }, + ], +} +`); }); it('should return undefined when no actions are present', () => { diff --git a/src/sagas/dependency.saga.js b/src/sagas/dependency.saga.js index a773e033..4b32acbc 100644 --- a/src/sagas/dependency.saga.js +++ b/src/sagas/dependency.saga.js @@ -1,10 +1,7 @@ // @flow import { select, call, put, takeEvery } from 'redux-saga/effects'; import { getPathForProjectId } from '../reducers/paths.reducer'; -import { - getPackageJsonLockedForProjectId, - getNextActionForProjectId, -} from '../reducers/queue.reducer'; +import { getNextActionForProjectId } from '../reducers/queue.reducer'; import { installDependencies, uninstallDependencies, @@ -37,62 +34,53 @@ import { import type { Action } from 'redux'; import type { Saga } from 'redux-saga'; -export function* addDependency({ +export function* handleAddDependency({ projectId, dependencyName, version, }: Action): Saga { - const packageJsonLocked = yield select( - getPackageJsonLockedForProjectId, - projectId - ); + const queuedAction = yield select(getNextActionForProjectId, projectId); yield put(queueDependencyInstall(projectId, dependencyName, version)); // if there are no other ongoing operations, begin install - if (!packageJsonLocked) { + if (!queuedAction) { yield put(installDependencyStart(projectId, dependencyName, version)); } } -export function* updateDependency({ +export function* handleUpdateDependency({ projectId, dependencyName, latestVersion, }: Action): Saga { - const packageJsonLocked = yield select( - getPackageJsonLockedForProjectId, - projectId - ); + const queuedAction = yield select(getNextActionForProjectId, projectId); yield put( queueDependencyInstall(projectId, dependencyName, latestVersion, true) ); - if (!packageJsonLocked) { + if (!queuedAction) { yield put( installDependencyStart(projectId, dependencyName, latestVersion, true) ); } } -export function* deleteDependency({ +export function* handleDeleteDependency({ projectId, dependencyName, }: Action): Saga { - const packageJsonLocked = yield select( - getPackageJsonLockedForProjectId, - projectId - ); + const queuedAction = yield select(getNextActionForProjectId, projectId); yield put(queueDependencyUninstall(projectId, dependencyName)); - if (!packageJsonLocked) { + if (!queuedAction) { yield put(uninstallDependencyStart(projectId, dependencyName)); } } -export function* startInstallingDependencies({ +export function* handleInstallDependenciesStart({ projectId, dependencies, }: Action): Saga { @@ -112,7 +100,7 @@ export function* startInstallingDependencies({ } } -export function* startUninstallingDependencies({ +export function* handleUninstallDependenciesStart({ projectId, dependencies, }: Action): Saga { @@ -132,14 +120,25 @@ export function* startUninstallingDependencies({ } export function* handleQueueActionCompleted({ projectId }: Action): Saga { - yield put(startNextActionInQueue(projectId)); + const nextAction = yield select(getNextActionForProjectId, projectId); + + // if there is another item in the queue, start it + if (nextAction) { + yield put(startNextActionInQueue(projectId)); + } } -export function* handleNextActionInQueue({ projectId }: Action): Saga { +export function* handleStartNextActionInQueue({ + projectId, +}: Action): Saga { const nextAction = yield select(getNextActionForProjectId, projectId); - // if the queue is empty, take no further action - if (!nextAction) return; + // if the queue is empty, log an error + if (!nextAction) { + return console.error( + `attempted to start next action in empty queue for project ${projectId}` + ); + } const actionCreator = nextAction.action === 'install' @@ -154,11 +153,14 @@ export function* handleNextActionInQueue({ projectId }: Action): Saga { // TODO: display an error message outside of the console when a dependency // action fails export default function* rootSaga(): Saga { - yield takeEvery(ADD_DEPENDENCY, addDependency); - yield takeEvery(UPDATE_DEPENDENCY, updateDependency); - yield takeEvery(DELETE_DEPENDENCY, deleteDependency); - yield takeEvery(INSTALL_DEPENDENCIES_START, startInstallingDependencies); - yield takeEvery(UNINSTALL_DEPENDENCIES_START, startUninstallingDependencies); + yield takeEvery(ADD_DEPENDENCY, handleAddDependency); + yield takeEvery(UPDATE_DEPENDENCY, handleUpdateDependency); + yield takeEvery(DELETE_DEPENDENCY, handleDeleteDependency); + yield takeEvery(INSTALL_DEPENDENCIES_START, handleInstallDependenciesStart); + yield takeEvery( + UNINSTALL_DEPENDENCIES_START, + handleUninstallDependenciesStart + ); yield takeEvery( [ INSTALL_DEPENDENCIES_ERROR, @@ -168,5 +170,5 @@ export default function* rootSaga(): Saga { ], handleQueueActionCompleted ); - yield takeEvery(START_NEXT_ACTION_IN_QUEUE, handleNextActionInQueue); + yield takeEvery(START_NEXT_ACTION_IN_QUEUE, handleStartNextActionInQueue); } diff --git a/src/sagas/dependency.saga.test.js b/src/sagas/dependency.saga.test.js index 87d1490a..e0297704 100644 --- a/src/sagas/dependency.saga.test.js +++ b/src/sagas/dependency.saga.test.js @@ -1,18 +1,15 @@ import { select, call, put, takeEvery } from 'redux-saga/effects'; import rootSaga, { - addDependency, - updateDependency, - deleteDependency, - startInstallingDependencies, - startUninstallingDependencies, + handleAddDependency, + handleUpdateDependency, + handleDeleteDependency, + handleInstallDependenciesStart, + handleUninstallDependenciesStart, handleQueueActionCompleted, - handleNextActionInQueue, + handleStartNextActionInQueue, } from './dependency.saga'; import { getPathForProjectId } from '../reducers/paths.reducer'; -import { - getPackageJsonLockedForProjectId, - getNextActionForProjectId, -} from '../reducers/queue.reducer'; +import { getNextActionForProjectId } from '../reducers/queue.reducer'; import { installDependencies, uninstallDependencies, @@ -59,21 +56,21 @@ describe('Dependency sagas', () => { let saga; beforeEach(() => { - saga = addDependency(action); + saga = handleAddDependency(action); }); it('should immediately install on empty queue', () => { - const packageJsonLocked = false; + const queuedAction = null; expect(saga.next().value).toEqual( - select(getPackageJsonLockedForProjectId, projectId) + select(getNextActionForProjectId, projectId) ); - expect(saga.next(packageJsonLocked).value).toEqual( + expect(saga.next(queuedAction).value).toEqual( put( queueDependencyInstall(projectId, dependency.name, dependency.version) ) ); - expect(saga.next(packageJsonLocked).value).toEqual( + expect(saga.next().value).toEqual( put( installDependencyStart(projectId, dependency.name, dependency.version) ) @@ -82,12 +79,16 @@ describe('Dependency sagas', () => { }); it('should queue install on non-empty queue', () => { - const packageJsonLocked = true; + const queuedAction = { + action: 'install', + active: true, + dependencies: [{ name: 'redux' }], + }; expect(saga.next().value).toEqual( - select(getPackageJsonLockedForProjectId, projectId) + select(getNextActionForProjectId, projectId) ); - expect(saga.next(packageJsonLocked).value).toEqual( + expect(saga.next(queuedAction).value).toEqual( put( queueDependencyInstall(projectId, dependency.name, dependency.version) ) @@ -110,16 +111,16 @@ describe('Dependency sagas', () => { let saga; beforeEach(() => { - saga = updateDependency(action); + saga = handleUpdateDependency(action); }); it('should immediately install on empty queue', () => { - const packageJsonLocked = false; + const queuedAction = false; expect(saga.next().value).toEqual( - select(getPackageJsonLockedForProjectId, projectId) + select(getNextActionForProjectId, projectId) ); - expect(saga.next(packageJsonLocked).value).toEqual( + expect(saga.next(queuedAction).value).toEqual( put( queueDependencyInstall( projectId, @@ -129,7 +130,7 @@ describe('Dependency sagas', () => { ) ) ); - expect(saga.next(packageJsonLocked).value).toEqual( + expect(saga.next().value).toEqual( put( installDependencyStart( projectId, @@ -143,12 +144,16 @@ describe('Dependency sagas', () => { }); it('should queue install on non-empty queue', () => { - const packageJsonLocked = true; + const queuedAction = { + action: 'install', + active: true, + dependencies: [{ name: 'redux' }], + }; expect(saga.next().value).toEqual( - select(getPackageJsonLockedForProjectId, projectId) + select(getNextActionForProjectId, projectId) ); - expect(saga.next(packageJsonLocked).value).toEqual( + expect(saga.next(queuedAction).value).toEqual( put( queueDependencyInstall( projectId, @@ -174,31 +179,35 @@ describe('Dependency sagas', () => { let saga; beforeEach(() => { - saga = deleteDependency(action); + saga = handleDeleteDependency(action); }); it('should immediately uninstall on empty queue', () => { - const packageJsonLocked = false; + const queuedAction = null; expect(saga.next().value).toEqual( - select(getPackageJsonLockedForProjectId, projectId) + select(getNextActionForProjectId, projectId) ); - expect(saga.next(packageJsonLocked).value).toEqual( + expect(saga.next(queuedAction).value).toEqual( put(queueDependencyUninstall(projectId, dependency.name)) ); - expect(saga.next(packageJsonLocked).value).toEqual( + expect(saga.next().value).toEqual( put(uninstallDependencyStart(projectId, dependency.name)) ); expect(saga.next().done).toBe(true); }); it('should queue uninstall on non-empty queue', () => { - const packageJsonLocked = true; + const queuedAction = { + action: 'install', + active: true, + dependencies: [{ name: 'redux' }], + }; expect(saga.next().value).toEqual( - select(getPackageJsonLockedForProjectId, projectId) + select(getNextActionForProjectId, projectId) ); - expect(saga.next(packageJsonLocked).value).toEqual( + expect(saga.next(queuedAction).value).toEqual( put(queueDependencyUninstall(projectId, dependency.name)) ); expect(saga.next().done).toBe(true); @@ -216,7 +225,7 @@ describe('Dependency sagas', () => { let saga; beforeEach(() => { - saga = startInstallingDependencies(action); + saga = handleInstallDependenciesStart(action); }); it('should install dependencies', () => { @@ -271,7 +280,7 @@ describe('Dependency sagas', () => { let saga; beforeEach(() => { - saga = startUninstallingDependencies(action); + saga = handleUninstallDependenciesStart(action); }); it('should uninstall dependencies', () => { @@ -305,10 +314,29 @@ describe('Dependency sagas', () => { }); describe('handleQueueActionCompleted saga', () => { - it(`should dispatch ${START_NEXT_ACTION_IN_QUEUE}`, () => { + it(`should dispatch ${START_NEXT_ACTION_IN_QUEUE} when next queue action exists`, () => { const saga = handleQueueActionCompleted({ projectId }); + const nextAction = { + action: 'install', + active: false, + dependencies: [{ name: 'redux' }], + }; - expect(saga.next().value).toEqual(put(startNextActionInQueue(projectId))); + expect(saga.next().value).toEqual( + select(getNextActionForProjectId, projectId) + ); + expect(saga.next(nextAction).value).toEqual( + put(startNextActionInQueue(projectId)) + ); + expect(saga.next().done).toBe(true); + }); + + it(`should dispatch ${START_NEXT_ACTION_IN_QUEUE} when queue is empty`, () => { + const saga = handleQueueActionCompleted({ projectId }); + + expect(saga.next().value).toEqual( + select(getNextActionForProjectId, projectId) + ); expect(saga.next().done).toBe(true); }); }); @@ -316,17 +344,21 @@ describe('Dependency sagas', () => { describe('handleNextActionInQueue saga', () => { let saga; beforeEach(() => { - saga = handleNextActionInQueue({ projectId }); + saga = handleStartNextActionInQueue({ projectId }); }); it('should do nothing if the queue is empty', () => { - const nextAction = null; + const consoleErrorOriginal = global.console.error; + global.console.error = jest.fn(); expect(saga.next().value).toEqual( select(getNextActionForProjectId, projectId) ); - expect(saga.next(nextAction).value).toEqual(undefined); + saga.next(); + expect(console.error).toBeCalled(); expect(saga.next().done).toBe(true); + + global.console.error = consoleErrorOriginal; }); it(`should dispatch ${INSTALL_DEPENDENCIES_START} if an install action is queued`, () => { @@ -363,19 +395,22 @@ describe('Dependency sagas', () => { const saga = rootSaga(); expect(saga.next().value).toEqual( - takeEvery(ADD_DEPENDENCY, addDependency) + takeEvery(ADD_DEPENDENCY, handleAddDependency) ); expect(saga.next().value).toEqual( - takeEvery(UPDATE_DEPENDENCY, updateDependency) + takeEvery(UPDATE_DEPENDENCY, handleUpdateDependency) ); expect(saga.next().value).toEqual( - takeEvery(DELETE_DEPENDENCY, deleteDependency) + takeEvery(DELETE_DEPENDENCY, handleDeleteDependency) ); expect(saga.next().value).toEqual( - takeEvery(INSTALL_DEPENDENCIES_START, startInstallingDependencies) + takeEvery(INSTALL_DEPENDENCIES_START, handleInstallDependenciesStart) ); expect(saga.next().value).toEqual( - takeEvery(UNINSTALL_DEPENDENCIES_START, startUninstallingDependencies) + takeEvery( + UNINSTALL_DEPENDENCIES_START, + handleUninstallDependenciesStart + ) ); expect(saga.next().value).toEqual( takeEvery( @@ -389,7 +424,7 @@ describe('Dependency sagas', () => { ) ); expect(saga.next().value).toEqual( - takeEvery(START_NEXT_ACTION_IN_QUEUE, handleNextActionInQueue) + takeEvery(START_NEXT_ACTION_IN_QUEUE, handleStartNextActionInQueue) ); expect(saga.next().done).toBe(true); }); diff --git a/src/services/dependencies.service.js b/src/services/dependencies.service.js index b071ba63..383b2083 100644 --- a/src/services/dependencies.service.js +++ b/src/services/dependencies.service.js @@ -4,25 +4,37 @@ import * as childProcess from 'child_process'; import type { QueuedDependency } from '../types'; -const spawnProcess = (cmd: string, cmdArgs: string[], projectPath: string) => +const spawnProcess = ( + cmd: string, + cmdArgs: string[], + projectPath: string +): Promise => new Promise((resolve, reject) => { + const output = { + stdout: '', + stderr: '', + }; const child = childProcess.spawn(cmd, cmdArgs, { cwd: projectPath, }); + + child.stdout.on('data', data => (output.stdout += data.toString())); + child.stderr.on('data', data => (output.stderr += data.toString())); child.on( 'exit', - code => (code ? reject(child.stderr) : resolve(child.stdout)) + code => (code ? reject(output.stderr) : resolve(output.stdout)) ); // logger(child) // service will be used here later }); -export const toPackageManagerArgs = ( +export const getDependencyInstallationCommand = ( dependencies: Array ): Array => { - return dependencies.map( - ({ name, version }: QueuedDependency) => - name + (version ? `@${version}` : '') + const versionedDependencies = dependencies.map( + ({ name, version }) => name + (version ? `@${version}` : '') ); + + return ['add', ...versionedDependencies, '-SE']; }; export const installDependencies = ( @@ -31,7 +43,7 @@ export const installDependencies = ( ) => spawnProcess( PACKAGE_MANAGER_CMD, - ['add', ...toPackageManagerArgs(dependencies), '-SE'], + getDependencyInstallationCommand(dependencies), projectPath ); @@ -41,7 +53,7 @@ export const uninstallDependencies = ( ) => spawnProcess( PACKAGE_MANAGER_CMD, - ['remove', ...toPackageManagerArgs(dependencies)], + ['remove', ...dependencies.map(({ name }) => name)], projectPath );