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:
commit
833276cd2a
9 changed files with 353 additions and 0 deletions
|
@ -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>
|
17
app/assets/javascripts/vuex_shared/modules/modal/actions.js
Normal file
17
app/assets/javascripts/vuex_shared/modules/modal/actions.js
Normal 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);
|
||||||
|
};
|
10
app/assets/javascripts/vuex_shared/modules/modal/index.js
Normal file
10
app/assets/javascripts/vuex_shared/modules/modal/index.js
Normal 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,
|
||||||
|
});
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const HIDE = 'HIDE';
|
||||||
|
export const SHOW = 'SHOW';
|
||||||
|
export const OPEN = 'OPEN';
|
||||||
|
export const CLOSE = 'CLOSE';
|
|
@ -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;
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default () => ({
|
||||||
|
isVisible: false,
|
||||||
|
data: null,
|
||||||
|
});
|
151
spec/javascripts/vue_shared/components/gl_modal_vuex_spec.js
Normal file
151
spec/javascripts/vue_shared/components/gl_modal_vuex_spec.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
31
spec/javascripts/vuex_shared/modules/modal/actions_spec.js
Normal file
31
spec/javascripts/vuex_shared/modules/modal/actions_spec.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
49
spec/javascripts/vuex_shared/modules/modal/mutations_spec.js
Normal file
49
spec/javascripts/vuex_shared/modules/modal/mutations_spec.js
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue