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);
+ });
+ });
});