Merge branch 'ee1979-gl-modal-vuex' into 'master'

Create shared gl-modal-vuex component and module

See merge request gitlab-org/gitlab-ce!24140
This commit is contained in:
Kushal Pandya 2019-01-04 12:10:21 +00:00
commit 833276cd2a
9 changed files with 353 additions and 0 deletions

View file

@ -0,0 +1,69 @@
<script>
import { mapState, mapActions } from 'vuex';
import { GlModal } from '@gitlab/ui';
/**
* This component keeps the GlModal's visibility in sync with the given vuex module.
*/
export default {
components: {
GlModal,
},
props: {
modalId: {
type: String,
required: true,
},
modalModule: {
type: String,
required: true,
},
},
computed: {
...mapState({
isVisible(state) {
return state[this.modalModule].isVisible;
},
}),
attrs() {
const { modalId, modalModule, ...attrs } = this.$attrs;
return attrs;
},
},
watch: {
isVisible(val) {
return val ? this.bsShow() : this.bsHide();
},
},
methods: {
...mapActions({
syncShow(dispatch) {
return dispatch(`${this.modalModule}/show`);
},
syncHide(dispatch) {
return dispatch(`${this.modalModule}/hide`);
},
}),
bsShow() {
this.$root.$emit('bv::show::modal', this.modalId);
},
bsHide() {
// $root.$emit is a workaround because other b-modal approaches don't work yet with gl-modal
this.$root.$emit('bv::hide::modal', this.modalId);
},
},
};
</script>
<template>
<gl-modal
v-bind="attrs"
:modal-id="modalId"
v-on="$listeners"
@shown="syncShow"
@hidden="syncHide"
>
<slot></slot>
</gl-modal>
</template>

View file

@ -0,0 +1,17 @@
import * as types from './mutation_types';
export const open = ({ commit }, data) => {
commit(types.OPEN, data);
};
export const close = ({ commit }) => {
commit(types.CLOSE);
};
export const show = ({ commit }) => {
commit(types.SHOW);
};
export const hide = ({ commit }) => {
commit(types.HIDE);
};

View file

@ -0,0 +1,10 @@
import state from './state';
import mutations from './mutations';
import * as actions from './actions';
export default () => ({
namespaced: true,
state: state(),
mutations,
actions,
});

View file

@ -0,0 +1,4 @@
export const HIDE = 'HIDE';
export const SHOW = 'SHOW';
export const OPEN = 'OPEN';
export const CLOSE = 'CLOSE';

View file

@ -0,0 +1,18 @@
import * as types from './mutation_types';
export default {
[types.SHOW](state) {
state.isVisible = true;
},
[types.HIDE](state) {
state.isVisible = false;
},
[types.OPEN](state, data) {
state.data = data;
state.isVisible = true;
},
[types.CLOSE](state) {
state.data = null;
state.isVisible = false;
},
};

View file

@ -0,0 +1,4 @@
export default () => ({
isVisible: false,
data: null,
});

View file

@ -0,0 +1,151 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlModal } from '@gitlab/ui';
import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue';
import createState from '~/vuex_shared/modules/modal/state';
const localVue = createLocalVue();
localVue.use(Vuex);
const TEST_SLOT = 'Lorem ipsum modal dolar sit.';
const TEST_MODAL_ID = 'my-modal-id';
const TEST_MODULE = 'myModal';
describe('GlModalVuex', () => {
let wrapper;
let state;
let actions;
const factory = (options = {}) => {
const store = new Vuex.Store({
modules: {
[TEST_MODULE]: {
namespaced: true,
state,
actions,
},
},
});
const propsData = {
modalId: TEST_MODAL_ID,
modalModule: TEST_MODULE,
...options.propsData,
};
wrapper = shallowMount(localVue.extend(GlModalVuex), {
...options,
localVue,
store,
propsData,
});
};
beforeEach(() => {
state = createState();
actions = {
show: jasmine.createSpy('show'),
hide: jasmine.createSpy('hide'),
};
});
it('renders gl-modal', () => {
factory({
slots: {
default: `<div>${TEST_SLOT}</div>`,
},
});
const glModal = wrapper.find(GlModal);
expect(glModal.props('modalId')).toBe(TEST_MODAL_ID);
expect(glModal.text()).toContain(TEST_SLOT);
});
it('passes props through to gl-modal', () => {
const title = 'Test Title';
const okVariant = 'success';
factory({
propsData: {
title,
okTitle: title,
okVariant,
},
});
const glModal = wrapper.find(GlModal);
expect(glModal.attributes('title')).toEqual(title);
expect(glModal.attributes('oktitle')).toEqual(title);
expect(glModal.attributes('okvariant')).toEqual(okVariant);
});
it('passes listeners through to gl-modal', () => {
const ok = jasmine.createSpy('ok');
factory({
listeners: { ok },
});
const glModal = wrapper.find(GlModal);
glModal.vm.$emit('ok');
expect(ok).toHaveBeenCalledTimes(1);
});
it('calls vuex action on show', () => {
expect(actions.show).not.toHaveBeenCalled();
factory();
const glModal = wrapper.find(GlModal);
glModal.vm.$emit('shown');
expect(actions.show).toHaveBeenCalledTimes(1);
});
it('calls vuex action on hide', () => {
expect(actions.hide).not.toHaveBeenCalled();
factory();
const glModal = wrapper.find(GlModal);
glModal.vm.$emit('hidden');
expect(actions.hide).toHaveBeenCalledTimes(1);
});
it('calls bootstrap show when isVisible changes', done => {
state.isVisible = false;
factory();
const rootEmit = spyOn(wrapper.vm.$root, '$emit');
state.isVisible = true;
localVue
.nextTick()
.then(() => {
expect(rootEmit).toHaveBeenCalledWith('bv::show::modal', TEST_MODAL_ID);
})
.then(done)
.catch(done.fail);
});
it('calls bootstrap hide when isVisible changes', done => {
state.isVisible = true;
factory();
const rootEmit = spyOn(wrapper.vm.$root, '$emit');
state.isVisible = false;
localVue
.nextTick()
.then(() => {
expect(rootEmit).toHaveBeenCalledWith('bv::hide::modal', TEST_MODAL_ID);
})
.then(done)
.catch(done.fail);
});
});

View file

@ -0,0 +1,31 @@
import * as types from '~/vuex_shared/modules/modal/mutation_types';
import * as actions from '~/vuex_shared/modules/modal/actions';
import testAction from 'spec/helpers/vuex_action_helper';
describe('Vuex ModalModule actions', () => {
describe('open', () => {
it('works', done => {
const data = { id: 7 };
testAction(actions.open, data, {}, [{ type: types.OPEN, payload: data }], [], done);
});
});
describe('close', () => {
it('works', done => {
testAction(actions.close, null, {}, [{ type: types.CLOSE }], [], done);
});
});
describe('show', () => {
it('works', done => {
testAction(actions.show, null, {}, [{ type: types.SHOW }], [], done);
});
});
describe('hide', () => {
it('works', done => {
testAction(actions.hide, null, {}, [{ type: types.HIDE }], [], done);
});
});
});

View file

@ -0,0 +1,49 @@
import mutations from '~/vuex_shared/modules/modal/mutations';
import * as types from '~/vuex_shared/modules/modal/mutation_types';
describe('Vuex ModalModule mutations', () => {
describe(types.SHOW, () => {
it('sets isVisible to true', () => {
const state = {
isVisible: false,
};
mutations[types.SHOW](state);
expect(state).toEqual({
isVisible: true,
});
});
});
describe(types.HIDE, () => {
it('sets isVisible to false', () => {
const state = {
isVisible: true,
};
mutations[types.HIDE](state);
expect(state).toEqual({
isVisible: false,
});
});
});
describe(types.OPEN, () => {
it('sets data and sets isVisible to true', () => {
const data = { id: 7 };
const state = {
isVisible: false,
data: null,
};
mutations[types.OPEN](state, data);
expect(state).toEqual({
isVisible: true,
data,
});
});
});
});