Commit 04cc135e authored by Fabio Huser's avatar Fabio Huser Committed by Paul Slaughter

Replace pipeline custom action array header with slot and buttons

Closes #195352

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22839
parent 36a8c8fe
<script>
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import { GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
import ciHeader from '~/vue_shared/components/header_ci_component.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub';
import { __ } from '~/locale';
......@@ -12,6 +13,10 @@ export default {
ciHeader,
GlLoadingIcon,
GlModal,
LoadingButton,
},
directives: {
GlModal: GlModalDirective,
},
props: {
pipeline: {
......@@ -25,7 +30,9 @@ export default {
},
data() {
return {
actions: this.getActions(),
isCanceling: false,
isRetrying: false,
isDeleting: false,
};
},
......@@ -43,67 +50,18 @@ export default {
},
},
watch: {
pipeline() {
this.actions = this.getActions();
},
},
methods: {
onActionClicked(action) {
if (action.modal) {
this.$root.$emit('bv::show::modal', action.modal);
} else {
this.postAction(action);
}
cancelPipeline() {
this.isCanceling = true;
eventHub.$emit('headerPostAction', this.pipeline.cancel_path);
},
postAction(action) {
const index = this.actions.indexOf(action);
this.$set(this.actions[index], 'isLoading', true);
eventHub.$emit('headerPostAction', action);
retryPipeline() {
this.isRetrying = true;
eventHub.$emit('headerPostAction', this.pipeline.retry_path);
},
deletePipeline() {
const index = this.actions.findIndex(action => action.modal === DELETE_MODAL_ID);
this.$set(this.actions[index], 'isLoading', true);
eventHub.$emit('headerDeleteAction', this.actions[index]);
},
getActions() {
const actions = [];
if (this.pipeline.retry_path) {
actions.push({
label: __('Retry'),
path: this.pipeline.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary',
isLoading: false,
});
}
if (this.pipeline.cancel_path) {
actions.push({
label: __('Cancel running'),
path: this.pipeline.cancel_path,
cssClass: 'js-btn-cancel-pipeline btn btn-danger',
isLoading: false,
});
}
if (this.pipeline.delete_path) {
actions.push({
label: __('Delete'),
path: this.pipeline.delete_path,
modal: DELETE_MODAL_ID,
cssClass: 'js-btn-delete-pipeline btn btn-danger btn-inverted',
isLoading: false,
});
}
return actions;
this.isDeleting = true;
eventHub.$emit('headerDeleteAction', this.pipeline.delete_path);
},
},
DELETE_MODAL_ID,
......@@ -117,10 +75,38 @@ export default {
:item-id="pipeline.id"
:time="pipeline.created_at"
:user="pipeline.user"
:actions="actions"
item-name="Pipeline"
@actionClicked="onActionClicked"
/>
>
<loading-button
v-if="pipeline.retry_path"
:loading="isRetrying"
:disabled="isRetrying"
class="js-retry-button btn btn-inverted-secondary"
container-class="d-inline"
:label="__('Retry')"
@click="retryPipeline()"
/>
<loading-button
v-if="pipeline.cancel_path"
:loading="isCanceling"
:disabled="isCanceling"
class="js-btn-cancel-pipeline btn btn-danger"
container-class="d-inline"
:label="__('Cancel running')"
@click="cancelPipeline()"
/>
<loading-button
v-if="pipeline.delete_path"
v-gl-modal="$options.DELETE_MODAL_ID"
:loading="isDeleting"
:disabled="isDeleting"
class="js-btn-delete-pipeline btn btn-danger btn-inverted"
container-class="d-inline"
:label="__('Delete')"
/>
</ci-header>
<gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default append-bottom-default" />
......
......@@ -70,16 +70,16 @@ export default () => {
eventHub.$off('headerDeleteAction', this.deleteAction);
},
methods: {
postAction(action) {
postAction(path) {
this.mediator.service
.postAction(action.path)
.postAction(path)
.then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.')));
},
deleteAction(action) {
deleteAction(path) {
this.mediator.stopPipelinePoll();
this.mediator.service
.deleteAction(action.path)
.deleteAction(path)
.then(({ request }) => redirectTo(setUrlFragment(request.responseURL, 'delete_success')))
.catch(() => Flash(__('An error occurred while deleting the pipeline.')));
},
......
......@@ -4,7 +4,6 @@ import { __, sprintf } from '~/locale';
import CiIconBadge from './ci_badge_link.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
......@@ -20,7 +19,6 @@ export default {
UserAvatarImage,
GlLink,
GlButton,
LoadingButton,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -47,11 +45,6 @@ export default {
required: false,
default: () => ({}),
},
actions: {
type: Array,
required: false,
default: () => [],
},
hasSidebarButton: {
type: Boolean,
required: false,
......@@ -71,9 +64,6 @@ export default {
},
methods: {
onClickAction(action) {
this.$emit('actionClicked', action);
},
onClickSidebarButton() {
this.$emit('clickedSidebarButton');
},
......@@ -115,18 +105,8 @@ export default {
</template>
</section>
<section v-if="actions.length" class="header-action-buttons">
<template v-for="(action, i) in actions">
<loading-button
:key="i"
:loading="action.isLoading"
:disabled="action.isLoading"
:class="action.cssClass"
container-class="d-inline"
:label="action.label"
@click="onClickAction(action)"
/>
</template>
<section v-if="$slots.default" class="header-action-buttons">
<slot></slot>
</section>
<gl-button
v-if="hasSidebarButton"
......
---
title: Replace custom action array in CI header bar with <slot>
merge_request: 22839
author: Fabio Huser
type: other
......@@ -8,6 +8,7 @@ describe('Pipeline details header', () => {
let props;
beforeEach(() => {
spyOn(eventHub, '$emit');
HeaderComponent = Vue.extend(headerComponent);
const threeWeeksAgo = new Date();
......@@ -33,8 +34,9 @@ describe('Pipeline details header', () => {
email: 'foo@bar.com',
avatar_url: 'link',
},
retry_path: 'path',
delete_path: 'path',
retry_path: 'retry',
cancel_path: 'cancel',
delete_path: 'delete',
},
isLoading: false,
};
......@@ -43,9 +45,14 @@ describe('Pipeline details header', () => {
});
afterEach(() => {
eventHub.$off();
vm.$destroy();
});
const findDeleteModal = () => document.getElementById(headerComponent.DELETE_MODAL_ID);
const findDeleteModalSubmit = () =>
[...findDeleteModal().querySelectorAll('.btn')].find(x => x.textContent === 'Delete pipeline');
it('should render provided pipeline info', () => {
expect(
vm.$el
......@@ -56,22 +63,46 @@ describe('Pipeline details header', () => {
});
describe('action buttons', () => {
it('should call postAction when retry button action is clicked', done => {
eventHub.$on('headerPostAction', action => {
expect(action.path).toEqual('path');
done();
});
it('should not trigger eventHub when nothing happens', () => {
expect(eventHub.$emit).not.toHaveBeenCalled();
});
it('should call postAction when retry button action is clicked', () => {
vm.$el.querySelector('.js-retry-button').click();
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry');
});
it('should call postAction when cancel button action is clicked', () => {
vm.$el.querySelector('.js-btn-cancel-pipeline').click();
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
});
it('should fire modal event when delete button action is clicked', done => {
vm.$root.$on('bv::modal::show', action => {
expect(action.componentId).toEqual('pipeline-delete-modal');
done();
it('does not show delete modal', () => {
expect(findDeleteModal()).not.toBeVisible();
});
describe('when delete button action is clicked', () => {
beforeEach(done => {
vm.$el.querySelector('.js-btn-delete-pipeline').click();
// Modal needs two ticks to show
vm.$nextTick()
.then(() => vm.$nextTick())
.then(done)
.catch(done.fail);
});
vm.$el.querySelector('.js-btn-delete-pipeline').click();
it('should show delete modal', () => {
expect(findDeleteModal()).toBeVisible();
});
it('should call delete when modal is submitted', () => {
findDeleteModalSubmit().click();
expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete');
});
});
});
});
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
import headerCi from '~/vue_shared/components/header_ci_component.vue';
describe('Header CI Component', () => {
......@@ -27,14 +27,6 @@ describe('Header CI Component', () => {
email: 'foo@bar.com',
avatar_url: 'link',
},
actions: [
{
label: 'Retry',
path: 'path',
cssClass: 'btn',
isLoading: false,
},
],
hasSidebarButton: true,
};
});
......@@ -43,6 +35,8 @@ describe('Header CI Component', () => {
vm.$destroy();
});
const findActionButtons = () => vm.$el.querySelector('.header-action-buttons');
describe('render', () => {
beforeEach(() => {
vm = mountComponent(HeaderCi, props);
......@@ -68,24 +62,23 @@ describe('Header CI Component', () => {
expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name);
});
it('should render provided actions', () => {
const btn = vm.$el.querySelector('.btn');
it('should render sidebar toggle button', () => {
expect(vm.$el.querySelector('.js-sidebar-build-toggle')).not.toBeNull();
});
expect(btn.tagName).toEqual('BUTTON');
expect(btn.textContent.trim()).toEqual(props.actions[0].label);
it('should not render header action buttons when empty', () => {
expect(findActionButtons()).toBeNull();
});
});
it('should show loading icon', done => {
vm.actions[0].isLoading = true;
describe('slot', () => {
it('should render header action buttons', () => {
vm = mountComponentWithSlots(HeaderCi, { props, slots: { default: 'Test Actions' } });
Vue.nextTick(() => {
expect(vm.$el.querySelector('.btn .gl-spinner').getAttribute('style')).toBeFalsy();
done();
});
});
const buttons = findActionButtons();
it('should render sidebar toggle button', () => {
expect(vm.$el.querySelector('.js-sidebar-build-toggle')).not.toBeNull();
expect(buttons).not.toBeNull();
expect(buttons.textContent).toEqual('Test Actions');
});
});
......
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