Commit b5c2ab62 authored by Kushal Pandya's avatar Kushal Pandya

Add support for adding Epics to Todos

Add Todo toggle button to Epic sidebar to allow user to toggle
Todo status of Epic.
parent a117cbcb
......@@ -4,13 +4,15 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import epicUtils from '../utils/epic_utils';
import SidebarHeader from './sidebar_items/sidebar_header.vue';
import SidebarTodo from './sidebar_items/sidebar_todo.vue';
export default {
components: {
SidebarHeader,
SidebarTodo,
},
computed: {
...mapState(['epicId', 'sidebarCollapsed']),
...mapState(['sidebarCollapsed']),
...mapGetters(['isUserSignedIn']),
},
mounted() {
......@@ -33,6 +35,10 @@ export default {
>
<div class="issuable-sidebar js-issuable-update">
<sidebar-header :sidebar-collapsed="sidebarCollapsed" />
<sidebar-todo
v-show="sidebarCollapsed && isUserSignedIn"
:sidebar-collapsed="sidebarCollapsed"
/>
</div>
</aside>
</template>
......@@ -3,9 +3,12 @@ import { mapActions } from 'vuex';
import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
import SidebarTodo from './sidebar_todo.vue';
export default {
components: {
ToggleSidebar,
SidebarTodo,
},
props: {
sidebarCollapsed: {
......@@ -27,5 +30,6 @@ export default {
css-classes="float-right"
@toggle="toggleSidebar({ sidebarCollapsed })"
/>
<sidebar-todo v-show="!sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed" />
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import Todo from '~/sidebar/components/todo_toggle/todo.vue';
export default {
components: {
Todo,
},
props: {
sidebarCollapsed: {
type: Boolean,
required: true,
},
},
computed: {
...mapState(['epicId', 'todoExists', 'epicTodoToggleInProgress']),
...mapGetters(['isUserSignedIn']),
},
methods: {
...mapActions(['toggleTodo']),
},
};
</script>
<template>
<div :class="{ 'block todo': isUserSignedIn && sidebarCollapsed }">
<todo
:collapsed="sidebarCollapsed"
:issuable-id="epicId"
:is-todo="todoExists"
:is-action-active="epicTodoToggleInProgress"
issuable-type="epic"
@toggleTodo="toggleTodo"
/>
</div>
</template>
......@@ -63,5 +63,47 @@ export const toggleSidebar = ({ dispatch }, { sidebarCollapsed }) => {
dispatch('toggleSidebarFlag', !sidebarCollapsed);
};
/**
* Methods to handle toggling Todo from sidebar
*/
export const requestEpicTodoToggle = ({ commit }) => commit(types.REQUEST_EPIC_TODO_TOGGLE);
export const requestEpicTodoToggleSuccess = ({ commit }, data) =>
commit(types.REQUEST_EPIC_TODO_TOGGLE_SUCCESS, data);
export const requestEpicTodoToggleFailure = ({ commit, state }, data) => {
commit(types.REQUEST_EPIC_TODO_TOGGLE_FAILURE, data);
if (state.todoExists) {
flash(__('There was an error deleting the todo.'));
} else {
flash(__('There was an error adding a todo.'));
}
};
export const triggerTodoToggleEvent = (_, { count }) => {
epicUtils.triggerDocumentEvent('todo:toggle', count);
};
export const toggleTodo = ({ state, dispatch }) => {
let reqPromise;
dispatch('requestEpicTodoToggle');
if (!state.todoExists) {
reqPromise = axios.post(state.todoPath, {
issuable_id: state.epicId,
issuable_type: 'epic',
});
} else {
reqPromise = axios.delete(state.todoDeletePath);
}
reqPromise
.then(({ data }) => {
dispatch('triggerTodoToggleEvent', { count: data.count });
dispatch('requestEpicTodoToggleSuccess', { todoDeletePath: data.delete_path });
})
.catch(() => {
dispatch('requestEpicTodoToggleFailure');
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -7,3 +7,7 @@ export const REQUEST_EPIC_STATUS_CHANGE_FAILURE = 'REQUEST_EPIC_STATUS_CHANGE_FA
export const TRIGGER_ISSUABLE_EVENTS = 'TRIGGER_ISSUABLE_EVENTS';
export const TOGGLE_SIDEBAR = 'TOGGLE_SIDEBAR';
export const REQUEST_EPIC_TODO_TOGGLE = 'REQUEST_EPIC_TODO_TOGGLE';
export const REQUEST_EPIC_TODO_TOGGLE_SUCCESS = 'REQUEST_EPIC_TODO_TOGGLE_SUCCESS';
export const REQUEST_EPIC_TODO_TOGGLE_FAILURE = 'REQUEST_EPIC_TODO_TOGGLE_FAILURE';
......@@ -23,4 +23,16 @@ export default {
[types.TOGGLE_SIDEBAR](state, isSidebarCollapsed) {
state.sidebarCollapsed = isSidebarCollapsed;
},
[types.REQUEST_EPIC_TODO_TOGGLE](state) {
state.epicTodoToggleInProgress = true;
},
[types.REQUEST_EPIC_TODO_TOGGLE_SUCCESS](state, { todoDeletePath }) {
state.todoDeletePath = todoDeletePath;
state.todoExists = !state.todoExists;
state.epicTodoToggleInProgress = false;
},
[types.REQUEST_EPIC_TODO_TOGGLE_FAILURE](state) {
state.epicTodoToggleInProgress = false;
},
};
......@@ -51,5 +51,6 @@ export default () => ({
// UI status flags
epicStatusChangeInProgress: false,
epicDeleteInProgress: false,
epicTodoToggleInProgress: false,
sidebarCollapsed: false,
});
......@@ -7,6 +7,7 @@ import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper
import { mockEpicMeta, mockEpicData } from '../mock_data';
describe('EpicSidebarComponent', () => {
const originalUserId = gon.current_user_id;
let vm;
let store;
......@@ -28,6 +29,14 @@ describe('EpicSidebarComponent', () => {
});
describe('template', () => {
beforeAll(() => {
gon.current_user_id = 1;
});
afterAll(() => {
gon.current_user_id = originalUserId;
});
it('renders component container element with classes `right-sidebar-expanded`, `right-sidebar` & `epic-sidebar`', done => {
store.dispatch('toggleSidebarFlag', false);
......@@ -44,5 +53,19 @@ describe('EpicSidebarComponent', () => {
it('renders header container element with classes `issuable-sidebar` & `js-issuable-update`', () => {
expect(vm.$el.querySelector('.issuable-sidebar.js-issuable-update')).not.toBeNull();
});
it('renders Todo toggle button element when sidebar is collapsed and user is signed in', done => {
store.dispatch('toggleSidebarFlag', true);
vm.$nextTick()
.then(() => {
const todoBlockEl = vm.$el.querySelector('.block.todo');
expect(todoBlockEl).not.toBeNull();
expect(todoBlockEl.querySelector('button.btn-todo')).not.toBeNull();
})
.then(done)
.catch(done.fail);
});
});
});
......@@ -8,10 +8,11 @@ import { mockEpicMeta, mockEpicData } from '../../mock_data';
describe('SidebarHeaderComponent', () => {
let vm;
let store;
beforeEach(done => {
const Component = Vue.extend(SidebarHeader);
const store = createStore();
store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
store.dispatch('setEpicData', mockEpicData);
......@@ -40,6 +41,17 @@ describe('SidebarHeaderComponent', () => {
expect(todoEl.innerText.trim()).toBe('Todo');
});
it('renders Todo toggle button element when sidebar is expanded', done => {
vm.sidebarCollapsed = false;
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('button.btn-todo')).not.toBeNull();
})
.then(done)
.catch(done.fail);
});
it('renders toggle sidebar button element', () => {
expect(vm.$el.querySelector('button.btn-sidebar-action')).not.toBeNull();
});
......
import Vue from 'vue';
import SidebarTodo from 'ee/epic/components/sidebar_items/sidebar_todo.vue';
import createStore from 'ee/epic/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { mockEpicMeta, mockEpicData } from '../../mock_data';
describe('SidebarTodoComponent', () => {
const originalUserId = gon.current_user_id;
let vm;
let store;
beforeEach(done => {
gon.current_user_id = 1;
const Component = Vue.extend(SidebarTodo);
store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
store.dispatch('setEpicData', mockEpicData);
vm = mountComponentWithStore(Component, {
store,
props: { sidebarCollapsed: false },
});
setTimeout(done);
});
afterEach(() => {
gon.current_user_id = originalUserId;
vm.$destroy();
});
describe('template', () => {
it('renders component container element with classes `block` & `todo` when `isUserSignedIn` & `sidebarCollapsed` is `true`', done => {
vm.sidebarCollapsed = true;
vm.$nextTick()
.then(() => {
expect(vm.$el.classList.contains('block')).toBe(true);
expect(vm.$el.classList.contains('todo')).toBe(true);
})
.then(done)
.catch(done.fail);
});
it('renders Todo toggle button element', () => {
const buttonEl = vm.$el.querySelector('button.btn-todo');
expect(buttonEl).not.toBeNull();
expect(buttonEl.dataset.issuableId).toBe('1');
expect(buttonEl.dataset.issuableType).toBe('epic');
});
});
});
......@@ -15,7 +15,7 @@ describe('Epic Store Actions', () => {
let state;
beforeEach(() => {
state = Object.assign({}, defaultState);
state = Object.assign({}, defaultState());
});
describe('setEpicMeta', () => {
......@@ -247,4 +247,207 @@ describe('Epic Store Actions', () => {
);
});
});
describe('requestEpicTodoToggle', () => {
it('should set `state.epicTodoToggleInProgress` flag to `true`', done => {
testAction(
actions.requestEpicTodoToggle,
{},
state,
[{ type: 'REQUEST_EPIC_TODO_TOGGLE' }],
[],
done,
);
});
});
describe('requestEpicTodoToggleSuccess', () => {
it('should set epic state type', done => {
testAction(
actions.requestEpicTodoToggleSuccess,
{ todoDeletePath: '/foo/bar' },
state,
[{ type: 'REQUEST_EPIC_TODO_TOGGLE_SUCCESS', payload: { todoDeletePath: '/foo/bar' } }],
[],
done,
);
});
});
describe('requestEpicTodoToggleFailure', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('Should set `state.epicTodoToggleInProgress` flag to `false`', done => {
testAction(
actions.requestEpicTodoToggleFailure,
{},
state,
[{ type: 'REQUEST_EPIC_TODO_TOGGLE_FAILURE', payload: {} }],
[],
done,
);
});
it('Should show flash error with message "There was an error deleting the todo." when `state.todoExists` is `true`', done => {
actions.requestEpicTodoToggleFailure(
{
commit: () => {},
state: { todoExists: true },
},
{},
);
Vue.nextTick()
.then(() => {
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
'There was an error deleting the todo.',
);
})
.then(done)
.catch(done.fail);
});
it('Should show flash error with message "There was an error adding a todo." when `state.todoExists` is `false`', done => {
actions.requestEpicTodoToggleFailure(
{
commit: () => {},
state: { todoExists: false },
},
{},
);
Vue.nextTick()
.then(() => {
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
'There was an error adding a todo.',
);
})
.then(done)
.catch(done.fail);
});
});
describe('triggerTodoToggleEvent', () => {
it('Calls `triggerDocumentEvent` with event `todo:toggle` and passes `count` as param', () => {
spyOn(epicUtils, 'triggerDocumentEvent').and.returnValue(false);
const data = { count: 5 };
actions.triggerTodoToggleEvent({}, data);
expect(epicUtils.triggerDocumentEvent).toHaveBeenCalledWith('todo:toggle', data.count);
});
});
describe('toggleTodo', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('when `state.togoExists` is false', () => {
it('dispatches requestEpicTodoToggle, triggerTodoToggleEvent and requestEpicTodoToggleSuccess when request is successful', done => {
mock.onPost(/(.*)/).replyOnce(200, {
count: 5,
delete_path: '/foo/bar',
});
testAction(
actions.toggleTodo,
null,
{ todoExists: false },
[],
[
{
type: 'requestEpicTodoToggle',
},
{
type: 'triggerTodoToggleEvent',
payload: { count: 5 },
},
{
type: 'requestEpicTodoToggleSuccess',
payload: { todoDeletePath: '/foo/bar' },
},
],
done,
);
});
it('dispatches requestEpicTodoToggle and requestEpicTodoToggleFailure when request fails', done => {
mock.onPost(/(.*)/).replyOnce(500, {});
testAction(
actions.toggleTodo,
null,
{ todoExists: false },
[],
[
{
type: 'requestEpicTodoToggle',
},
{
type: 'requestEpicTodoToggleFailure',
},
],
done,
);
});
});
describe('when `state.togoExists` is true', () => {
it('dispatches requestEpicTodoToggle, triggerTodoToggleEvent and requestEpicTodoToggleSuccess when request is successful', done => {
mock.onDelete(/(.*)/).replyOnce(200, {
count: 5,
});
testAction(
actions.toggleTodo,
null,
{ todoExists: true },
[],
[
{
type: 'requestEpicTodoToggle',
},
{
type: 'triggerTodoToggleEvent',
payload: { count: 5 },
},
{
type: 'requestEpicTodoToggleSuccess',
payload: { todoDeletePath: undefined },
},
],
done,
);
});
it('dispatches requestEpicTodoToggle and requestEpicTodoToggleFailure when request fails', done => {
mock.onDelete(/(.*)/).replyOnce(500, {});
testAction(
actions.toggleTodo,
null,
{ todoExists: true },
[],
[
{
type: 'requestEpicTodoToggle',
},
{
type: 'requestEpicTodoToggleFailure',
},
],
done,
);
});
});
});
});
......@@ -62,4 +62,59 @@ describe('Epic Store Mutations', () => {
expect(state.sidebarCollapsed).toBe(sidebarCollapsed);
});
});
describe('REQUEST_EPIC_TODO_TOGGLE', () => {
it('Should set `epicTodoToggleInProgress` flag on state as `true`', () => {
const state = {
epicTodoToggleInProgress: false,
};
mutations[types.REQUEST_EPIC_TODO_TOGGLE](state);
expect(state.epicTodoToggleInProgress).toBe(true);
});
});
describe('REQUEST_EPIC_TODO_TOGGLE_SUCCESS', () => {
it('Should set `todoDeletePath` value on state with provided value of `todoDeletePath` param', () => {
const todoDeletePath = '/foo/bar';
const state = {};
mutations[types.REQUEST_EPIC_TODO_TOGGLE_SUCCESS](state, { todoDeletePath });
expect(state.todoDeletePath).toBe(todoDeletePath);
});
it('Should toggle value of `todoExists` value on state', () => {
const state = {
todoExists: true,
};
mutations[types.REQUEST_EPIC_TODO_TOGGLE_SUCCESS](state, {});
expect(state.todoExists).toBe(false);
});
it('Should set `epicTodoToggleInProgress` flag on state as `false`', () => {
const state = {
epicTodoToggleInProgress: true,
};
mutations[types.REQUEST_EPIC_TODO_TOGGLE_SUCCESS](state, {});
expect(state.epicTodoToggleInProgress).toBe(false);
});
});
describe('REQUEST_EPIC_TODO_TOGGLE_FAILURE', () => {
it('Should set `epicTodoToggleInProgress` flag on state as `false`', () => {
const state = {
epicTodoToggleInProgress: true,
};
mutations[types.REQUEST_EPIC_TODO_TOGGLE_FAILURE](state);
expect(state.epicTodoToggleInProgress).toBe(false);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment