Commit eb88d422 authored by Phil Hughes's avatar Phil Hughes

Merge branch '7751-add-refactored-epics-sidebar-todos-support' into 'master'

[Part 2] Add support for Todos within sidebar of refactored Epics app

See merge request gitlab-org/gitlab-ee!9299
parents bc64f596 b5c2ab62
......@@ -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