diff --git a/app/assets/javascripts/actions/actionTypes.js b/app/assets/javascripts/actions/actionTypes.js index b90da23fa..67b258d3b 100644 --- a/app/assets/javascripts/actions/actionTypes.js +++ b/app/assets/javascripts/actions/actionTypes.js @@ -8,5 +8,6 @@ export default keyMirror({ RECEIVE_USERS: null, RECEIVE_STORIES: null, RECEIVE_PAST_ITERATIONS: null, - TOGGLE_STORY: null + TOGGLE_STORY: null, + EDIT_STORY: null }); diff --git a/app/assets/javascripts/actions/story.js b/app/assets/javascripts/actions/story.js index a651460be..b7a96340e 100644 --- a/app/assets/javascripts/actions/story.js +++ b/app/assets/javascripts/actions/story.js @@ -9,3 +9,9 @@ export const toggleStory = (id) => ({ type: actionTypes.TOGGLE_STORY, id }); + +export const editStory = (id, newAttributes) => ({ + type: actionTypes.EDIT_STORY, + id, + newAttributes +}); diff --git a/app/assets/javascripts/components/story/ExpandedStory/ExpandedStoryEstimate.js b/app/assets/javascripts/components/story/ExpandedStory/ExpandedStoryEstimate.js new file mode 100644 index 000000000..dbd30e3bc --- /dev/null +++ b/app/assets/javascripts/components/story/ExpandedStory/ExpandedStoryEstimate.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { isFeature } from '../../../models/beta/story'; + +export class ExpandedStoryEstimate extends React.Component { + editStory(event) { + const newValue = event.target.value; + + this.props.onEdit({ estimate: newValue }); + }; + + render() { + const { project, story } = this.props; + + return ( +
+
+ { I18n.translate('activerecord.attributes.story.estimate') } +
+ + +
+ ); + }; +}; + +ExpandedStoryEstimate.propTypes = { + project: PropTypes.object, + story: PropTypes.object +}; + +const mapStateToProps = ({ project }) => ({ project }); + +export default connect( + mapStateToProps, + null +)(ExpandedStoryEstimate); diff --git a/app/assets/javascripts/components/story/ExpandedStory/ExpandedStoryType.js b/app/assets/javascripts/components/story/ExpandedStory/ExpandedStoryType.js new file mode 100644 index 000000000..c3dfdea1e --- /dev/null +++ b/app/assets/javascripts/components/story/ExpandedStory/ExpandedStoryType.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { types } from '../../../models/beta/story'; + +export class ExpandedStoryType extends React.Component { + editStory(event) { + const newValue = event.target.value; + + this.props.onEdit({ storyType: newValue }); + }; + + render() { + const { story } = this.props; + + return ( +
+
+ { I18n.translate('activerecord.attributes.story.story_type') } +
+ + +
+ ); + }; +}; + +ExpandedStoryType.propTypes = { + story: PropTypes.object +}; + +export default ExpandedStoryType; diff --git a/app/assets/javascripts/components/story/ExpandedStory/index.js b/app/assets/javascripts/components/story/ExpandedStory/index.js index d0cf4bbf5..dd6afd3d5 100644 --- a/app/assets/javascripts/components/story/ExpandedStory/index.js +++ b/app/assets/javascripts/components/story/ExpandedStory/index.js @@ -2,14 +2,27 @@ import React from 'react'; import PropTypes from 'prop-types'; import ExpandedStoryHistoryLocation from './ExpandedStoryHistoryLocation'; import ExpandedStoryControls from './ExpandedStoryControls'; +import ExpandedStoryEstimate from './ExpandedStoryEstimate'; +import ExpandedStoryType from './ExpandedStoryType'; +import { editStory } from '../../../actions/story'; +import { connect } from 'react-redux'; -const ExpandedStory = (props) => { - const { story, onToggle } = props; +export const ExpandedStory = (props) => { + const { story, onToggle, editStory } = props; return (
- + +
+ editStory(story.id, newAttributes)} + /> + + editStory(story.id, newAttributes)} + /> +
); }; @@ -18,4 +31,7 @@ ExpandedStory.propTypes = { story: PropTypes.object.isRequired }; -export default ExpandedStory; +export default connect( + null, + { editStory } +)(ExpandedStory); diff --git a/app/assets/javascripts/models/beta/story.js b/app/assets/javascripts/models/beta/story.js index d5b7610d0..a78193a48 100644 --- a/app/assets/javascripts/models/beta/story.js +++ b/app/assets/javascripts/models/beta/story.js @@ -58,3 +58,5 @@ export const getCompletedPoints = story => { export const isStoryNotEstimated = (storyType, estimate) => storyType === 'feature' && !estimate; export const isRelease = (storyType) => storyType === 'release'; + +export const types = ['feature', 'bug', 'release', 'chore']; diff --git a/app/assets/javascripts/reducers/stories.js b/app/assets/javascripts/reducers/stories.js index 42185fddb..cbd9a4daf 100644 --- a/app/assets/javascripts/reducers/stories.js +++ b/app/assets/javascripts/reducers/stories.js @@ -1,5 +1,5 @@ import actionTypes from 'actions/actionTypes'; -import { toggleStories } from './story'; +import { toggleStories, editStory } from './story'; const initialState = []; @@ -9,6 +9,8 @@ const storiesReducer = (state = initialState, action) => { return action.data; case actionTypes.TOGGLE_STORY: return toggleStories(state, action.id); + case actionTypes.EDIT_STORY: + return editStory(state, action.id, action.newAttributes); default: return state; }; diff --git a/app/assets/javascripts/reducers/story.js b/app/assets/javascripts/reducers/story.js index cfc77c06f..844d23537 100644 --- a/app/assets/javascripts/reducers/story.js +++ b/app/assets/javascripts/reducers/story.js @@ -3,9 +3,26 @@ export const toggleStories = (stories, id) => { if (story.id !== id) { return story; } + + const previousState = !story.collapsed ? null : story; + return { ...story, + _previousState: previousState, collapsed: !story.collapsed }; }); }; + +export const editStory = (stories, id, newAttributes) => { + return stories.map((story) => { + if (story.id !== id) { + return story; + }; + + return { + ...story, + ...newAttributes + }; + }); +}; diff --git a/app/assets/stylesheets/new_board/_story.scss b/app/assets/stylesheets/new_board/_story.scss index e53ccedb4..83dc8c176 100644 --- a/app/assets/stylesheets/new_board/_story.scss +++ b/app/assets/stylesheets/new_board/_story.scss @@ -160,10 +160,25 @@ } &--expanded { + padding: 8px 10px; background-color: $butter-2; color: $aluminium-6; - + .Story { + &__inline-block { + display: flex; + } + + &__section-title { + font-size: 12px; + font-weight: bold; + margin-bottom: 5px; + } + + &__section { + margin: 5px 5px 5px 0; + } + &__controls { @extend .btn-group; diff --git a/spec/javascripts/components/story/expanded_story/expanded_story_estimate_spec.js b/spec/javascripts/components/story/expanded_story/expanded_story_estimate_spec.js new file mode 100644 index 000000000..b7a5bda71 --- /dev/null +++ b/spec/javascripts/components/story/expanded_story/expanded_story_estimate_spec.js @@ -0,0 +1,98 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { ExpandedStoryEstimate } from 'components/story/ExpandedStory/ExpandedStoryEstimate'; + +describe('', () => { + it("renders component with 'Fibonacci' point scale in select", () => { + const project = { pointValues: ['1','2','3','5','8'] }; + const story = { estimate: null }; + + const wrapper = shallow(); + const select = wrapper.find('select').text(); + + project.pointValues.forEach((value) => { + expect(select).toContain(value); + }); + }); + + it("renders component with 'Powers of two' point scale in select", () => { + const project = { pointValues: ['1','2','4','8'] }; + const story = { estimate: null }; + + const wrapper = shallow(); + const select = wrapper.find('select').text(); + + project.pointValues.forEach((value) => { + expect(select).toContain(value); + }); + }); + + it("renders component with 'Linear' point scale in select", () => { + const project = { pointValues: ['1','2','3','4','5'] }; + const story = { estimate: null }; + + const wrapper = shallow(); + const select = wrapper.find('select').text(); + + project.pointValues.forEach((value) => { + expect(select).toContain(value); + }); + }); + + describe("When story.estimate is not null", () => { + it("sets the select defaultValue as story.estimate", () => { + const project = { pointValues: ['1','2','3','5','8'] }; + + project.pointValues.forEach((value) => { + const story = { estimate: value }; + const wrapper = shallow(); + const select = wrapper.find('select'); + + expect(select.props().defaultValue).toBe(value); + }); + }); + }); + + describe("When story.estimate is null", () => { + it("sets the select defaultValue as null", () => { + const project = { pointValues: ['1','2','3','4','5'] }; + const story = { estimate: null }; + + const wrapper = shallow(); + const select = wrapper.find('select'); + + expect(select.props().defaultValue).toBe(null); + }); + }); + + describe("When change storyType", () => { + describe("to a type that is not a feature", () =>{ + const notFeatureTypes = ['bug', 'release', 'chore']; + + it("disables estimate select", () =>{ + const project = { pointValues: ['1','2','3','4','5'] }; + + notFeatureTypes.forEach((type) => { + const story = { storyType: type }; + const wrapper = shallow(); + + const select = wrapper.find('select'); + + expect(select.props().disabled).toBe(true); + }); + }); + }); + + describe("to a feature", () =>{ + it("not disable estimate select", () =>{ + const project = { pointValues: ['1','2','3','4','5'] }; + const story = { storyType: 'bug' }; + + const wrapper = shallow(); + const select = wrapper.find('select'); + + expect(select.props().disabled).toBe(true); + }); + }); + }); +}); diff --git a/spec/javascripts/components/story/expanded_story/expanded_story_spec.js b/spec/javascripts/components/story/expanded_story/expanded_story_spec.js index a0634e87f..014c1da75 100644 --- a/spec/javascripts/components/story/expanded_story/expanded_story_spec.js +++ b/spec/javascripts/components/story/expanded_story/expanded_story_spec.js @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import ExpandedStory from 'components/story/ExpandedStory/index'; +import { ExpandedStory } from 'components/story/ExpandedStory/index'; import storyFactory from '../../../support/factories/storyFactory'; describe('', () => { diff --git a/spec/javascripts/components/story/expanded_story/expanded_story_type_spec.js b/spec/javascripts/components/story/expanded_story/expanded_story_type_spec.js new file mode 100644 index 000000000..ba3b66d85 --- /dev/null +++ b/spec/javascripts/components/story/expanded_story/expanded_story_type_spec.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ExpandedStoryType from 'components/story/ExpandedStory/ExpandedStoryType'; + +describe('', () => { + it("sets defaultValue as story.storyType in select", () => { + const storyTypes = ['feature', 'bug', 'release', 'chore']; + + storyTypes.forEach((type) => { + const story = { storyType: type }; + const wrapper = shallow(); + const select = wrapper.find('select'); + + expect(select.props().defaultValue).toBe(type); + }); + }); +}); diff --git a/spec/javascripts/components/story/story_item_spec.js b/spec/javascripts/components/story/story_item_spec.js index e36a9bcc2..31da7b7e3 100644 --- a/spec/javascripts/components/story/story_item_spec.js +++ b/spec/javascripts/components/story/story_item_spec.js @@ -1,20 +1,23 @@ import React from 'react'; import { shallow } from 'enzyme'; import { StoryItem } from 'components/story/StoryItem'; -import storyFactory from '../../support/factories/storyFactory' +import storyFactory from '../../support/factories/storyFactory'; +import ExpandedStory from 'components/story/ExpandedStory'; +import CollapsedStory from 'components/story/CollapsedStory'; + describe('', () => { it('renders the StoryItem component within a Collapsed Story', () => { const story = storyFactory({ collapsed: true }); const wrapper = shallow(); - expect(wrapper.find('CollapsedStory')).toExist(); + expect(wrapper.find(CollapsedStory)).toExist(); }); it('renders the StoryItem component within a Expanded Story', () => { const story = storyFactory({ collapsed: false }); const wrapper = shallow(); - expect(wrapper.find('ExpandedStory')).toExist(); + expect(wrapper.find(ExpandedStory)).toExist(); }); }); diff --git a/spec/javascripts/reducers/stories_spec.js b/spec/javascripts/reducers/stories_spec.js index f48c18f02..a41d71430 100644 --- a/spec/javascripts/reducers/stories_spec.js +++ b/spec/javascripts/reducers/stories_spec.js @@ -1,22 +1,28 @@ import reducer from 'reducers/stories'; -import { toggleStory } from 'actions/story'; +import { toggleStory, editStory } from 'actions/story'; describe('Stories reducer', () => { let storiesArray; - + beforeEach(() => { storiesArray = [ { id: 1, - collapsed: true + collapsed: true, + storyType: 'feature', + estimate: 1 }, { id: 2, - collapsed: true + collapsed: true, + storyType: 'feature', + estimate: 1 }, { id: 3, - collapsed: true + collapsed: true, + storyType: 'feature', + estimate: 1 } ]; }); @@ -52,4 +58,32 @@ describe('Stories reducer', () => { }); }); }); + + describe("Edit a story", () => { + it("change story type", () => { + const initialState = storiesArray; + const story = storiesArray[0]; + story.storyType = 'feature'; + + const action = editStory(story.id, {storyType: 'bug'}); + const storiesState = reducer(initialState, action); + + const changedStory = storiesState[0]; + + expect(changedStory.storyType).toEqual('bug'); + }); + + it("change story estimate", () => { + const initialState = storiesArray; + const story = storiesArray[0]; + story.estimate = 1; + + const action = editStory(story.id, {estimate: 2}); + const storiesState = reducer(initialState, action); + + const changedStory = storiesState[0]; + + expect(changedStory.estimate).toEqual(2); + }); + }); });