Skip to content
Snippets Groups Projects
Commit 05f66d13 authored by Winnie Hellmann's avatar Winnie Hellmann
Browse files

Add new modal Vue component

parent dd633bc1
No related branches found
No related tags found
No related merge requests found
<script>
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
import modal from '~/vue_shared/components/modal.vue';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
 
export default {
components: {
modal,
GlModal,
},
props: {
url: {
Loading
Loading
@@ -17,7 +17,7 @@
},
computed: {
text() {
return s__('AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.');
return s__('AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.');
},
},
methods: {
Loading
Loading
@@ -28,7 +28,7 @@
redirectTo(response.request.responseURL);
})
.catch((error) => {
Flash(s__('AdminArea|Stopping jobs failed'));
createFlash(s__('AdminArea|Stopping jobs failed'));
throw error;
});
},
Loading
Loading
@@ -37,11 +37,13 @@
</script>
 
<template>
<modal
<gl-modal
id="stop-jobs-modal"
:title="s__('AdminArea|Stop all jobs?')"
:text="text"
kind="danger"
:primary-button-label="s__('AdminArea|Stop jobs')"
@submit="onSubmit" />
:header-title-text="s__('AdminArea|Stop all jobs?')"
footer-primary-button-variant="danger"
:footer-primary-button-text="s__('AdminArea|Stop jobs')"
@submit="onSubmit"
>
{{ text }}
</gl-modal>
</template>
Loading
Loading
@@ -8,22 +8,23 @@ Vue.use(Translate);
 
export default () => {
const stopJobsButton = document.getElementById('stop-jobs-button');
// eslint-disable-next-line no-new
new Vue({
el: '#stop-jobs-modal',
components: {
stopJobsModal,
},
mounted() {
stopJobsButton.classList.remove('disabled');
},
render(createElement) {
return createElement('stop-jobs-modal', {
props: {
url: stopJobsButton.dataset.url,
},
});
},
});
if (stopJobsButton) {
// eslint-disable-next-line no-new
new Vue({
el: '#stop-jobs-modal',
components: {
stopJobsModal,
},
mounted() {
stopJobsButton.classList.remove('disabled');
},
render(createElement) {
return createElement('stop-jobs-modal', {
props: {
url: stopJobsButton.dataset.url,
},
});
},
});
}
};
<script>
const buttonVariants = [
'danger',
'primary',
'success',
'warning',
];
export default {
name: 'GlModal',
props: {
id: {
type: String,
required: false,
default: null,
},
headerTitleText: {
type: String,
required: false,
default: '',
},
footerPrimaryButtonVariant: {
type: String,
required: false,
default: 'primary',
validator: value => buttonVariants.indexOf(value) !== -1,
},
footerPrimaryButtonText: {
type: String,
required: false,
default: '',
},
},
methods: {
emitCancel(event) {
this.$emit('cancel', event);
},
emitSubmit(event) {
this.$emit('submit', event);
},
},
};
</script>
<template>
<div
:id="id"
class="modal fade"
tabindex="-1"
role="dialog"
>
<div
class="modal-dialog"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<slot name="header">
<button
type="button"
class="close"
data-dismiss="modal"
:aria-label="s__('Modal|Close')"
@click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">
<slot name="title">
{{ headerTitleText }}
</slot>
</h4>
</slot>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button
type="button"
class="btn"
data-dismiss="modal"
@click="emitCancel($event)"
>
{{ s__('Modal|Cancel') }}
</button>
<button
type="button"
class="btn"
:class="`btn-${footerPrimaryButtonVariant}`"
data-dismiss="modal"
@click="emitSubmit($event)"
>
{{ footerPrimaryButtonText }}
</button>
</slot>
</div>
</div>
</div>
</div>
</template>
Loading
Loading
@@ -7,10 +7,9 @@
- build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
 
.nav-controls
- if @all_builds.running_or_pending.any?
#stop-jobs-modal
- if @all_builds.running_or_pending.any?
#stop-jobs-modal
.nav-controls
%button#stop-jobs-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#stop-jobs-modal',
url: cancel_all_admin_jobs_path } }
Loading
Loading
---
title: Add new modal Vue component
merge_request: 17108
author:
type: changed
# Components
## Contents
* [Dropdowns](#dropdowns)
* [Modals](#modals)
## Dropdowns
See also the [corresponding UX guide](../ux_guide/components.md#dropdowns).
### How to style a bootstrap dropdown
1. Use the HTML structure provided by the [docs][bootstrap-dropdowns]
1. Add a specific class to the top level `.dropdown` element
```Haml
.dropdown.my-dropdown
%button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
%span.dropdown-toggle-text
Toggle Dropdown
= icon('chevron-down')
%ul.dropdown-menu
%li
%a
item!
```
Or use the helpers
```Haml
.dropdown.my-dropdown
= dropdown_toggle('Toogle!', { toggle: 'dropdown' })
= dropdown_content
%li
%a
item!
```
[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
## Modals
See also the [corresponding UX guide](../ux_guide/components.md#modals).
We have a reusable Vue component for modals: [vue_shared/components/gl-modal.vue](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/vue_shared/components/gl-modal.vue)
Here is an example of how to use it:
```html
<gl-modal
id="dogs-out-modal"
:header-title-text="s__('ModalExample|Let the dogs out?')"
footer-primary-button-variant="danger"
:footer-primary-button-text="s__('ModalExample|Let them out')"
@submit="letOut(theDogs)"
>
{{ s__('ModalExample|You’re about to let the dogs out.') }}
</gl-modal>
```
![example modal](img/gl-modal.png)
# Dropdowns
## How to style a bootstrap dropdown
1. Use the HTML structure provided by the [docs][bootstrap-dropdowns]
1. Add a specific class to the top level `.dropdown` element
```Haml
.dropdown.my-dropdown
%button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
%span.dropdown-toggle-text
Toggle Dropdown
= icon('chevron-down')
%ul.dropdown-menu
%li
%a
item!
```
Or use the helpers
```Haml
.dropdown.my-dropdown
= dropdown_toggle('Toogle!', { toggle: 'dropdown' })
= dropdown_content
%li
%a
item!
```
[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
This page has moved [here](components.md#dropdowns).
doc/development/fe_guide/img/gl-modal.png

25.3 KiB

Loading
Loading
@@ -77,8 +77,10 @@ Axios specific practices and gotchas.
## [Icons](icons.md)
How we use SVG for our Icons.
 
## [Dropdowns](dropdowns.md)
How we use dropdowns.
## [Components](components.md)
How we use UI components.
---
 
## Style Guides
Loading
Loading
import Vue from 'vue';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
const modalComponent = Vue.extend(GlModal);
describe('GlModal', () => {
let vm;
afterEach(() => {
vm.$destroy();
});
describe('props', () => {
describe('with id', () => {
const props = {
id: 'my-modal',
};
beforeEach(() => {
vm = mountComponent(modalComponent, props);
});
it('assigns the id to the modal', () => {
expect(vm.$el.id).toBe(props.id);
});
});
describe('without id', () => {
beforeEach(() => {
vm = mountComponent(modalComponent, { });
});
it('does not add an id attribute to the modal', () => {
expect(vm.$el.hasAttribute('id')).toBe(false);
});
});
describe('with headerTitleText', () => {
const props = {
headerTitleText: 'my title text',
};
beforeEach(() => {
vm = mountComponent(modalComponent, props);
});
it('sets the modal title', () => {
const modalTitle = vm.$el.querySelector('.modal-title');
expect(modalTitle.innerHTML.trim()).toBe(props.headerTitleText);
});
});
describe('with footerPrimaryButtonVariant', () => {
const props = {
footerPrimaryButtonVariant: 'danger',
};
beforeEach(() => {
vm = mountComponent(modalComponent, props);
});
it('sets the primary button class', () => {
const primaryButton = vm.$el.querySelector('.modal-footer button:last-of-type');
expect(primaryButton).toHaveClass(`btn-${props.footerPrimaryButtonVariant}`);
});
});
describe('with footerPrimaryButtonText', () => {
const props = {
footerPrimaryButtonText: 'my button text',
};
beforeEach(() => {
vm = mountComponent(modalComponent, props);
});
it('sets the primary button text', () => {
const primaryButton = vm.$el.querySelector('.modal-footer button:last-of-type');
expect(primaryButton.innerHTML.trim()).toBe(props.footerPrimaryButtonText);
});
});
});
it('works with data-toggle="modal"', (done) => {
setFixtures(`
<button id="modal-button" data-toggle="modal" data-target="#my-modal"></button>
<div id="modal-container"></div>
`);
const modalContainer = document.getElementById('modal-container');
const modalButton = document.getElementById('modal-button');
vm = mountComponent(modalComponent, {
id: 'my-modal',
}, modalContainer);
$(vm.$el).on('shown.bs.modal', () => done());
modalButton.click();
});
describe('methods', () => {
const dummyEvent = 'not really an event';
beforeEach(() => {
vm = mountComponent(modalComponent, { });
spyOn(vm, '$emit');
});
describe('emitCancel', () => {
it('emits a cancel event', () => {
vm.emitCancel(dummyEvent);
expect(vm.$emit).toHaveBeenCalledWith('cancel', dummyEvent);
});
});
describe('emitSubmit', () => {
it('emits a submit event', () => {
vm.emitSubmit(dummyEvent);
expect(vm.$emit).toHaveBeenCalledWith('submit', dummyEvent);
});
});
});
describe('slots', () => {
const slotContent = 'this should go into the slot';
const modalWithSlot = (slotName) => {
let template;
if (slotName) {
template = `
<gl-modal>
<template slot="${slotName}">${slotContent}</template>
</gl-modal>
`;
} else {
template = `<gl-modal>${slotContent}</gl-modal>`;
}
return Vue.extend({
components: {
GlModal,
},
template,
});
};
describe('default slot', () => {
beforeEach(() => {
vm = mountComponent(modalWithSlot());
});
it('sets the modal body', () => {
const modalBody = vm.$el.querySelector('.modal-body');
expect(modalBody.innerHTML).toBe(slotContent);
});
});
describe('header slot', () => {
beforeEach(() => {
vm = mountComponent(modalWithSlot('header'));
});
it('sets the modal header', () => {
const modalHeader = vm.$el.querySelector('.modal-header');
expect(modalHeader.innerHTML).toBe(slotContent);
});
});
describe('title slot', () => {
beforeEach(() => {
vm = mountComponent(modalWithSlot('title'));
});
it('sets the modal title', () => {
const modalTitle = vm.$el.querySelector('.modal-title');
expect(modalTitle.innerHTML).toBe(slotContent);
});
});
describe('footer slot', () => {
beforeEach(() => {
vm = mountComponent(modalWithSlot('footer'));
});
it('sets the modal footer', () => {
const modalFooter = vm.$el.querySelector('.modal-footer');
expect(modalFooter.innerHTML).toBe(slotContent);
});
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment