Skip to content

Commit

Permalink
Feature expanded story labels auto complete (#456)
Browse files Browse the repository at this point in the history
* Create project_labels attribute on ProjectBoard

* Add project_labels to labels suggestions and option to add new ones

* Create board_operations project_labels test

* Change project_labels to labels

* Move deleteLabel and addLabel logic to reducer

* Fix wrong props on ExpandedStoryDescription
  • Loading branch information
kostadriano committed Jan 30, 2019
1 parent 5f61e07 commit 0a048f7
Show file tree
Hide file tree
Showing 16 changed files with 155 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Notes to Story in board V2
- Delete option to Story in board V2
- Select to Story State in board V2
- Labels to Story in board V2

### Fix
- Refactor Redux flow to remove stories duplication in board V2
Expand Down
5 changes: 4 additions & 1 deletion app/assets/javascripts/actions/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ export default keyMirror({
TOGGLE_TASK: null,
DELETE_STORY_SUCCESS: null,
ADD_NOTE: null,
DELETE_NOTE: null
DELETE_NOTE: null,
ADD_LABEL_TO_PROJECT: null,
ADD_LABEL: null,
DELETE_LABEL: null
});
24 changes: 24 additions & 0 deletions app/assets/javascripts/actions/labels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import actionTypes from './actionTypes';

export const addLabelToProject = (label) => ({
type: actionTypes.ADD_LABEL_TO_PROJECT,
label
});

export const addLabelSuccess = (storyId, label) => ({
type: actionTypes.ADD_LABEL,
storyId,
label
});

export const removeLabel = (storyId, labelName) => ({
type: actionTypes.DELETE_LABEL,
storyId,
labelName
});

export const addLabel = (storyId, label) =>
(dispatch) => {
dispatch(addLabelSuccess(storyId, label));
dispatch(addLabelToProject(label));
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,45 @@ class ExpandedStoryLabels extends React.Component {
constructor(props) {
super(props);

this.state = {
tags: props.labels
}

this.handleAddition = this.handleAddition.bind(this);
this.handleDelete = this.handleDelete.bind(this);
}

handleDelete(index) {
const { onEdit } = this.props;
const tags = this.state.tags.filter((tag, tagIndex) => tagIndex !== index);
const { labels, onRemoveLabel } = this.props;

this.setState(
{ tags }, () => {
onEdit(this.state.tags)
}
const label = labels.find(
(label, labelIndex) => labelIndex === index
);

onRemoveLabel(label.name);
}

handleAddition(tag) {
const { onEdit } = this.props;
handleAddition(label) {
const { onAddLabel } = this.props;

this.setState(
{ tags: [...this.state.tags, tag] }, () => {
onEdit(this.state.tags)
}
);
onAddLabel(label);
}

render() {
const { projectLabels, labels } = this.props;

return (
<div className="Story__section">
<div className="Story__section-title">
{I18n.t('activerecord.attributes.story.labels')}
</div>
{
<ReactTags
tags={this.state.tags}
tags={labels}
handleDelete={this.handleDelete}
suggestions={projectLabels}
handleAddition={this.handleAddition}
allowNew={true}
placeholder={I18n.t('add new label')}
allowBackspace={false}
addOnBlur={true}
delimiterChars={[',',' ']}
delimiterChars={[',', ' ']}
autoresize={false} />
}
</div>
Expand All @@ -60,7 +54,9 @@ class ExpandedStoryLabels extends React.Component {

ExpandedStoryLabels.propTypes = {
labels: PropTypes.arrayOf(PropTypes.object).isRequired,
onEdit: PropTypes.func.isRequired
onAddLabel: PropTypes.func.isRequired,
onRemoveLabel: PropTypes.func.isRequired,
projectLabels: PropTypes.arrayOf(PropTypes.object).isRequired
};

export default ExpandedStoryLabels;
25 changes: 16 additions & 9 deletions app/assets/javascripts/components/story/ExpandedStory/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import ExpandedStoryControls from './ExpandedStoryControls';
import ExpandedStoryEstimate from './ExpandedStoryEstimate';
import ExpandedStoryType from './ExpandedStoryType';
import ExpandedStoryDescription from './ExpandedStoryDescription';
import { createTask, deleteTask, toggleTask } from '../../../actions/task';
import ExpandedStoryNotes from './ExpandedStoryNotes';
import ExpandedStoryState from './ExpandedStoryState';
import ExpandedStoryTitle from './ExpandedStoryTitle';
import ExpandedStoryLabels from './ExpandedStoryLabels';
import { deleteNote, createNote } from '../../../actions/note';
import { editStory, updateStory, deleteStory } from '../../../actions/story';
import ExpandedStoryTask from './ExpandedStoryTask';
import { editStory, updateStory, deleteStory } from '../../../actions/story';
import { createTask, deleteTask, toggleTask } from '../../../actions/task';
import { deleteNote, createNote } from '../../../actions/note';
import { addLabel, removeLabel } from '../../../actions/labels';
import { connect } from 'react-redux';
import * as Story from '../../../models/beta/story';

Expand All @@ -21,12 +22,15 @@ export const ExpandedStory = ({
onToggle,
editStory,
updateStory,
deleteStory,
project,
createTask,
deleteTask,
toggleTask,
deleteNote,
createNote
createNote,
addLabel,
removeLabel
}) => {
return (
<div className="Story Story--expanded">
Expand Down Expand Up @@ -59,15 +63,16 @@ export const ExpandedStory = ({
/>

<ExpandedStoryLabels
labels={story.labels}
onAddLabel={(label) => addLabel(story.id, label)}
labels={story._editing.labels}
projectLabels={project.labels}
onRemoveLabel={(labelName) => removeLabel(story.id, labelName)}
onEdit={(value) => editStory(story.id, { labels: value })}
/>

<ExpandedStoryDescription
story={story}
onToggle={(task, status) => toggleTask(project.id, story, task, status)}
onDelete={(taskId) => deleteTask(project.id, story.id, taskId)}
onSave={(task) => createTask(project.id, story.id, task)}
onEdit={(newAttributes) => editStory(story.id, newAttributes)}
/>

<ExpandedStoryNotes
Expand Down Expand Up @@ -103,6 +108,8 @@ export default connect(
toggleTask,
deleteStory,
deleteNote,
createNote
createNote,
addLabel,
removeLabel
}
)(ExpandedStory);
14 changes: 14 additions & 0 deletions app/assets/javascripts/models/beta/label.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import _ from 'underscore';

export const splitLabels = (labels) => {
if (labels) {
return labels.split(',')
Expand All @@ -12,6 +14,18 @@ export const splitLabels = (labels) => {
return [];
};

export const removeLabel = (labels, labelName) =>
labels.filter(label => label.name !== labelName);

export const addLabel = (labels, newLabel) =>
uniqueLabels([
...labels,
newLabel
]);

export const uniqueLabels = (labels) =>
_.uniq(labels, label => label.name);

export const joinLabels = (labels) =>
getNames(labels).join(',');

Expand Down
14 changes: 14 additions & 0 deletions app/assets/javascripts/models/beta/project.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as Label from './label';

export const deserialize = (board) => ({
...board.project,
labels: Label.splitLabels(board.labels)
});

export const addLabel = (project, label) => ({
...project,
labels: Label.uniqueLabels([
...project.labels,
label
])
});
2 changes: 2 additions & 0 deletions app/assets/javascripts/models/beta/projectBoard.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import httpService from '../../services/httpService';
import changeCase from 'change-object-case';
import * as Story from './story';
import * as Project from './project';

export function get(projectId) {
return httpService
.get(`/beta/project_boards/${projectId}`)
.then(({ data }) => changeCase.camelKeys(data, { recursive: true, arrayRecursive: true }))
.then((projectBoard) => ({
...projectBoard,
project: Project.deserialize(projectBoard),
stories: projectBoard.stories.map(Story.deserialize)
}));
};
1 change: 1 addition & 0 deletions app/assets/javascripts/models/beta/story.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export const editStory = (story, newAttributes) => {
};

newStory.estimate = isFeature(newStory) ? newStory.estimate : '';
newStory.labels = Label.uniqueLabels(newStory.labels);

return {
...story,
Expand Down
3 changes: 3 additions & 0 deletions app/assets/javascripts/reducers/project.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import actionTypes from 'actions/actionTypes';
import * as Project from '../models/beta/project';

const initialState = {};

const projectReducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.RECEIVE_PROJECT:
return action.data;
case actionTypes.ADD_LABEL_TO_PROJECT:
return Project.addLabel(state, action.label);
default:
return state;
};
Expand Down
23 changes: 23 additions & 0 deletions app/assets/javascripts/reducers/stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { toggleStory, editStory, updateStory } from 'models/beta/story'
import * as Note from 'models/beta/note';
import { updateIfSameId } from '../services/updateIfSameId';
import * as Task from 'models/beta/task';
import * as Label from 'models/beta/label';

const initialState = [];

Expand Down Expand Up @@ -67,6 +68,28 @@ const storiesReducer = (state = initialState, action) => {
updateIfSameId(action.story.id, (story) => {
return Task.updateTask(story, action.task);
}));
case actionTypes.ADD_LABEL:
return state.map(
updateIfSameId(action.storyId, (story) => ({
...story,
_editing: {
...story._editing,
_isDirty: true,
labels: Label.addLabel(story._editing.labels, action.label)
}
}))
);
case actionTypes.DELETE_LABEL:
return state.map(
updateIfSameId(action.storyId, (story) => ({
...story,
_editing: {
...story._editing,
_isDirty: true,
labels: Label.removeLabel(story._editing.labels, action.labelName)
}
}))
);
default:
return state;
};
Expand Down
1 change: 1 addition & 0 deletions app/models/project_board.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ class ProjectBoard
attr_accessor :current_user
attr_accessor :current_flow
attr_accessor :default_flow
attr_accessor :labels
end
8 changes: 7 additions & 1 deletion app/operations/beta/project_board_operations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ def run
users: users,
current_user: @current_user,
current_flow: @current_flow,
default_flow: default_flow
default_flow: default_flow,
labels: project_labels
)

project_board = ::ProjectBoard.new(project_board_params)
Expand All @@ -32,6 +33,11 @@ def run

private

def project_labels
labels = @project.stories.map(&:labels).reject { |label| label.to_s.empty? }
labels.join(',').split(',').uniq.join(',')
end

def project
@project ||= @projects_scope
.friendly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import storyFactory from '../../../support/factories/storyFactory';

describe('<ExpandedStory />', () => {
it('renders children components', () => {
const story = storyFactory();
const story = storyFactory({_editing: storyFactory()});
const project = { id: 42 };
const wrapper = shallow(<ExpandedStory story={story} project={project} />);

Expand Down
4 changes: 4 additions & 0 deletions spec/javascripts/models/beta/story_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ describe('Story model', function () {
const story = {
estimate: '',
_editing: {
labels: [],
storyType: 'bug',
estimate: ''
}
Expand All @@ -258,6 +259,7 @@ describe('Story model', function () {
expect(changedStory).toEqual({
estimate: '',
_editing: {
labels: [],
storyType: newAttributes.storyType,
estimate: '',
_isDirty: true
Expand All @@ -276,6 +278,7 @@ describe('Story model', function () {

expect(changedStory).toEqual({
_editing: {
labels: [],
storyType: type,
estimate: '',
_isDirty: true
Expand All @@ -292,6 +295,7 @@ describe('Story model', function () {

expect(changedStory).toEqual({
_editing: {
labels: [],
storyType: 'feature',
estimate: newAttributes.estimate,
_isDirty: true
Expand Down
Loading

0 comments on commit 0a048f7

Please sign in to comment.