Commit a117cbcb authored by Kushal Pandya's avatar Kushal Pandya

Add epics sidebar shell

Add shell for Epic sidebar with
Todo text and sidebar toggle button.
parent 7d9f342e
......@@ -4,11 +4,14 @@ import { mapState } from 'vuex';
import IssuableBody from '~/issue_show/components/app.vue';
import RelatedItems from 'ee/related_issues/components/related_issues_root.vue';
import EpicSidebar from './epic_sidebar.vue';
export default {
epicsPathIdSeparator: '&',
components: {
IssuableBody,
RelatedItems,
EpicSidebar,
},
computed: {
...mapState([
......@@ -76,5 +79,6 @@ export default {
css-class="js-related-issues-block"
path-id-separator="#"
/>
<epic-sidebar />
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import epicUtils from '../utils/epic_utils';
import SidebarHeader from './sidebar_items/sidebar_header.vue';
export default {
components: {
SidebarHeader,
},
computed: {
...mapState(['epicId', 'sidebarCollapsed']),
...mapGetters(['isUserSignedIn']),
},
mounted() {
this.toggleSidebarFlag(epicUtils.getCollapsedGutter());
},
methods: {
...mapActions(['toggleSidebarFlag']),
},
};
</script>
<template>
<aside
:class="{
'right-sidebar-expanded': !sidebarCollapsed,
'right-sidebar-collapsed': sidebarCollapsed,
}"
:data-signed-in="isUserSignedIn"
class="right-sidebar epic-sidebar"
>
<div class="issuable-sidebar js-issuable-update">
<sidebar-header :sidebar-collapsed="sidebarCollapsed" />
</div>
</aside>
</template>
<script>
import { mapActions } from 'vuex';
import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
export default {
components: {
ToggleSidebar,
},
props: {
sidebarCollapsed: {
type: Boolean,
required: true,
},
},
methods: {
...mapActions(['toggleSidebar']),
},
};
</script>
<template>
<div class="block issuable-sidebar-header">
<span class="issuable-header-text hide-collapsed float-left">{{ __('Todo') }}</span>
<toggle-sidebar
:collapsed="sidebarCollapsed"
css-classes="float-right"
@toggle="toggleSidebar({ sidebarCollapsed })"
/>
</div>
</template>
......@@ -50,5 +50,18 @@ export const toggleEpicStatus = ({ state, dispatch }, isEpicOpen) => {
});
};
export const toggleSidebarFlag = ({ commit }, sidebarCollapsed) =>
commit(types.TOGGLE_SIDEBAR, sidebarCollapsed);
export const toggleContainerClassAndCookie = (_, sidebarCollapsed) => {
epicUtils.toggleContainerClass('right-sidebar-expanded');
epicUtils.toggleContainerClass('right-sidebar-collapsed');
epicUtils.setCollapsedGutter(sidebarCollapsed);
};
export const toggleSidebar = ({ dispatch }, { sidebarCollapsed }) => {
dispatch('toggleContainerClassAndCookie', !sidebarCollapsed);
dispatch('toggleSidebarFlag', !sidebarCollapsed);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -2,5 +2,7 @@ import { statusType } from '../constants';
export const isEpicOpen = state => state.state === statusType.open;
export const isUserSignedIn = () => !!gon.current_user_id;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -10,10 +10,10 @@ Vue.use(Vuex);
const createStore = () =>
new Vuex.Store({
state: state(),
actions,
getters,
mutations,
state,
});
export default createStore;
......@@ -5,3 +5,5 @@ export const REQUEST_EPIC_STATUS_CHANGE = 'REQUEST_EPIC_STATUS_CHANGE';
export const REQUEST_EPIC_STATUS_CHANGE_SUCCESS = 'REQUEST_EPIC_STATUS_CHANGE_SUCCESS';
export const REQUEST_EPIC_STATUS_CHANGE_FAILURE = 'REQUEST_EPIC_STATUS_CHANGE_FAILURE';
export const TRIGGER_ISSUABLE_EVENTS = 'TRIGGER_ISSUABLE_EVENTS';
export const TOGGLE_SIDEBAR = 'TOGGLE_SIDEBAR';
......@@ -19,4 +19,8 @@ export default {
[types.REQUEST_EPIC_STATUS_CHANGE_FAILURE](state) {
state.epicStatusChangeInProgress = false;
},
[types.TOGGLE_SIDEBAR](state, isSidebarCollapsed) {
state.sidebarCollapsed = isSidebarCollapsed;
},
};
export default {
export default () => ({
// API Paths to Send/Receive Data
endpoint: '',
updateEndpoint: '',
......@@ -51,4 +51,5 @@ export default {
// UI status flags
epicStatusChangeInProgress: false,
epicDeleteInProgress: false,
};
sidebarCollapsed: false,
});
import $ from 'jquery';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
const triggerDocumentEvent = (eventName, eventParam) => {
$(document).trigger(eventName, eventParam);
......@@ -8,6 +11,18 @@ const bindDocumentEvent = (eventName, callback) => {
$(document).on(eventName, callback);
};
const toggleContainerClass = className => {
const containerEl = document.querySelector('.page-with-contextual-sidebar');
if (containerEl) {
containerEl.classList.toggle(className);
}
};
const getCollapsedGutter = () => parseBoolean(Cookies.get('collapsed_gutter'));
const setCollapsedGutter = value => Cookies.set('collapsed_gutter', value);
// This is for mocking methods from this
// file within tests using `spyOnDependency`
// which requires first param to always
......@@ -16,6 +31,9 @@ const bindDocumentEvent = (eventName, callback) => {
const epicUtils = {
triggerDocumentEvent,
bindDocumentEvent,
toggleContainerClass,
getCollapsedGutter,
setCollapsedGutter,
};
export default epicUtils;
......@@ -7,7 +7,7 @@ import { statusType } from 'ee/epic/constants';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { mockEpicMeta, mockEpicData } from '../mock_data';
describe('EpicBodyComponent', () => {
describe('EpicHeaderComponent', () => {
let vm;
let store;
......
import Vue from 'vue';
import EpicSidebar from 'ee/epic/components/epic_sidebar.vue';
import createStore from 'ee/epic/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { mockEpicMeta, mockEpicData } from '../mock_data';
describe('EpicSidebarComponent', () => {
let vm;
let store;
beforeEach(done => {
const Component = Vue.extend(EpicSidebar);
store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
store.dispatch('setEpicData', mockEpicData);
vm = mountComponentWithStore(Component, {
store,
});
setTimeout(done);
});
afterEach(() => {
vm.$destroy();
});
describe('template', () => {
it('renders component container element with classes `right-sidebar-expanded`, `right-sidebar` & `epic-sidebar`', done => {
store.dispatch('toggleSidebarFlag', false);
vm.$nextTick()
.then(() => {
expect(vm.$el.classList.contains('right-sidebar-expanded')).toBe(true);
expect(vm.$el.classList.contains('right-sidebar')).toBe(true);
expect(vm.$el.classList.contains('epic-sidebar')).toBe(true);
})
.then(done)
.catch(done.fail);
});
it('renders header container element with classes `issuable-sidebar` & `js-issuable-update`', () => {
expect(vm.$el.querySelector('.issuable-sidebar.js-issuable-update')).not.toBeNull();
});
});
});
import Vue from 'vue';
import SidebarHeader from 'ee/epic/components/sidebar_items/sidebar_header.vue';
import createStore from 'ee/epic/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { mockEpicMeta, mockEpicData } from '../../mock_data';
describe('SidebarHeaderComponent', () => {
let vm;
beforeEach(done => {
const Component = Vue.extend(SidebarHeader);
const store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
store.dispatch('setEpicData', mockEpicData);
vm = mountComponentWithStore(Component, {
store,
props: { sidebarCollapsed: false },
});
setTimeout(done);
});
afterEach(() => {
vm.$destroy();
});
describe('template', () => {
it('renders component container element with classes `block` & `issuable-sidebar-header`', () => {
expect(vm.$el.classList.contains('block')).toBe(true);
expect(vm.$el.classList.contains('issuable-sidebar-header')).toBe(true);
});
it('renders Todo text element', () => {
const todoEl = vm.$el.querySelector('.issuable-header-text.hide-collapsed.float-left');
expect(todoEl).not.toBeNull();
expect(todoEl.innerText.trim()).toBe('Todo');
});
it('renders toggle sidebar button element', () => {
expect(vm.$el.querySelector('button.btn-sidebar-action')).not.toBeNull();
});
});
});
......@@ -11,6 +11,9 @@ export const mockEpicMeta = convertObjectPropsToCamelCase(meta, {
});
export const mockEpicData = convertObjectPropsToCamelCase(
Object.assign({}, getJSONFixture('epic/mock_data.json'), initial, { endpoint: TEST_HOST }),
Object.assign({}, getJSONFixture('epic/mock_data.json'), initial, {
endpoint: TEST_HOST,
sidebarCollapsed: false,
}),
{ deep: true },
);
......@@ -185,4 +185,66 @@ describe('Epic Store Actions', () => {
});
});
});
describe('toggleSidebarFlag', () => {
it('should call `TOGGLE_SIDEBAR` mutation with param `sidebarCollapsed`', done => {
const sidebarCollapsed = true;
testAction(
actions.toggleSidebarFlag,
sidebarCollapsed,
state,
[{ type: 'TOGGLE_SIDEBAR', payload: sidebarCollapsed }],
[],
done,
);
});
});
describe('toggleContainerClassAndCookie', () => {
const sidebarCollapsed = true;
beforeEach(() => {
spyOn(epicUtils, 'toggleContainerClass').and.stub();
spyOn(epicUtils, 'setCollapsedGutter').and.stub();
});
it('should call `epicUtils.toggleContainerClass` with classes `right-sidebar-expanded` & `right-sidebar-collapsed`', () => {
actions.toggleContainerClassAndCookie({}, sidebarCollapsed);
expect(epicUtils.toggleContainerClass).toHaveBeenCalledTimes(2);
expect(epicUtils.toggleContainerClass).toHaveBeenCalledWith('right-sidebar-expanded');
expect(epicUtils.toggleContainerClass).toHaveBeenCalledWith('right-sidebar-collapsed');
});
it('should call `epicUtils.setCollapsedGutter` with param `isSidebarCollapsed`', () => {
actions.toggleContainerClassAndCookie({}, sidebarCollapsed);
expect(epicUtils.setCollapsedGutter).toHaveBeenCalledWith(sidebarCollapsed);
});
});
describe('toggleSidebar', () => {
it('dispatches toggleContainerClassAndCookie and toggleSidebarFlag actions with opposite value of `isSidebarCollapsed` param', done => {
const sidebarCollapsed = true;
testAction(
actions.toggleSidebar,
{ sidebarCollapsed },
state,
[],
[
{
type: 'toggleContainerClassAndCookie',
payload: !sidebarCollapsed,
},
{
type: 'toggleSidebarFlag',
payload: !sidebarCollapsed,
},
],
done,
);
});
});
});
......@@ -51,4 +51,15 @@ describe('Epic Store Mutations', () => {
expect(state.epicStatusChangeInProgress).toBe(false);
});
});
describe('TOGGLE_SIDEBAR', () => {
it('Should set `sidebarCollapsed` flag on state with value of provided `sidebarCollapsed` param', () => {
const state = {};
const sidebarCollapsed = true;
mutations[types.TOGGLE_SIDEBAR](state, sidebarCollapsed);
expect(state.sidebarCollapsed).toBe(sidebarCollapsed);
});
});
});
import Cookies from 'js-cookie';
import epicUtils from 'ee/epic/utils/epic_utils';
describe('epicUtils', () => {
describe('toggleContainerClass', () => {
beforeEach(() => {
setFixtures('<div class="page-with-contextual-sidebar"></div>');
});
it('toggles provided class on containerEl', () => {
const className = 'my-class';
const containerEl = document.querySelector('.page-with-contextual-sidebar');
containerEl.classList.add(className);
epicUtils.toggleContainerClass(className);
expect(containerEl.classList.contains(className)).toBe(false);
});
});
describe('getCollapsedGutter', () => {
let originalCollapsedGutter;
beforeAll(() => {
originalCollapsedGutter = Cookies.get('collapsed_gutter');
});
afterAll(() => {
Cookies.set('collapsed_gutter', originalCollapsedGutter);
});
it('gets value of Cookie flag `collapsed_gutter` as boolean', () => {
const collapsedGutterVal = true;
Cookies.set('collapsed_gutter', collapsedGutterVal);
expect(epicUtils.getCollapsedGutter()).toBe(collapsedGutterVal);
});
});
describe('setCollapsedGutter', () => {
let originalCollapsedGutter;
beforeAll(() => {
originalCollapsedGutter = Cookies.get('collapsed_gutter');
});
afterAll(() => {
Cookies.set('collapsed_gutter', originalCollapsedGutter);
});
it('sets value of Cookie flag `collapsed_gutter` with provided `value` param', () => {
const collapsedGutterVal = true;
epicUtils.setCollapsedGutter(collapsedGutterVal);
expect(Cookies.get('collapsed_gutter')).toBe(`${collapsedGutterVal}`); // Cookie value will always be string
});
});
});
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