Commit 627eba55 authored by Phil Hughes's avatar Phil Hughes

Merge branch '33697-remove-ujs-action-big-graph' into 'master'

Remove UJS actions from pipeline graph

Closes #37837

See merge request gitlab-org/gitlab-ce!18509
parents d7f68bcf 6673934d
...@@ -32,26 +32,38 @@ export default { ...@@ -32,26 +32,38 @@ export default {
required: true, required: true,
}, },
buttonDisabled: { requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
data() {
return {
isDisabled: false,
linkRequested: '',
};
},
computed: { computed: {
cssClass() { cssClass() {
const actionIconDash = dasherize(this.actionIcon); const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`; return `${actionIconDash} js-icon-${actionIconDash}`;
}, },
isDisabled() { },
return this.buttonDisabled === this.link; watch: {
requestFinishedFor() {
if (this.requestFinishedFor === this.linkRequested) {
this.isDisabled = false;
}
}, },
}, },
methods: { methods: {
onClickAction() { onClickAction() {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link); eventHub.$emit('graphAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
}, },
}, },
}; };
...@@ -62,7 +74,8 @@ export default { ...@@ -62,7 +74,8 @@ export default {
@click="onClickAction" @click="onClickAction"
v-tooltip v-tooltip
:title="tooltipText" :title="tooltipText"
class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper" class="js-ci-action btn btn-blank
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass" :class="cssClass"
data-container="body" data-container="body"
:disabled="isDisabled" :disabled="isDisabled"
......
<script>
import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
*/
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
tooltipText: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
actionMethod: {
type: String,
required: true,
},
actionIcon: {
type: String,
required: true,
},
},
};
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body"
aria-label="Job's action"
>
<icon :name="actionIcon" />
</a>
</template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import jobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue'; import JobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
/** /**
* Renders the dropdown for the pipeline graph. * Renders the dropdown for the pipeline graph.
* *
* The following object should be provided as `job`: * The following object should be provided as `job`:
* *
* { * {
* "id": 4256, * "id": 4256,
* "name": "test", * "name": "test",
* "status": { * "status": {
* "icon": "icon_status_success", * "icon": "icon_status_success",
* "text": "passed", * "text": "passed",
* "label": "passed", * "label": "passed",
* "group": "success", * "group": "success",
* "details_path": "/root/ci-mock/builds/4256", * "details_path": "/root/ci-mock/builds/4256",
* "action": { * "action": {
* "icon": "retry", * "icon": "retry",
* "title": "Retry", * "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry", * "path": "/root/ci-mock/builds/4256/retry",
* "method": "post" * "method": "post"
* } * }
* } * }
* } * }
*/ */
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
jobComponent, JobComponent,
jobNameComponent, JobNameComponent,
}, },
props: { props: {
job: { job: {
type: Object, type: Object,
required: true, required: true,
},
}, },
requestFinishedFor: {
computed: { type: String,
tooltipText() { required: false,
return `${this.job.name} - ${this.job.status.label}`; default: '',
},
}, },
},
mounted() { computed: {
this.stopDropdownClickPropagation(); tooltipText() {
return `${this.job.name} - ${this.job.status.label}`;
}, },
},
mounted() {
this.stopDropdownClickPropagation();
},
methods: { methods: {
/** /**
* When the user right clicks or cmd/ctrl + click in the job name * When the user right clicks or cmd/ctrl + click in the job name or the action icon
* the dropdown should not be closed and the link should open in another tab, * the dropdown should not be closed so we stop propagation
* so we stop propagation of the click event inside the dropdown. * of the click event inside the dropdown.
* *
* Since this component is rendered multiple times per page we need to guarantee we only * Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component. * target the click event of this component.
*/ */
stopDropdownClickPropagation() { stopDropdownClickPropagation() {
$(this.$el $(
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
.on('click', (e) => { this.$el,
e.stopPropagation(); ).on('click', e => {
}); e.stopPropagation();
}, });
}, },
}; },
};
</script> </script>
<template> <template>
<div class="ci-job-dropdown-container"> <div class="ci-job-dropdown-container">
...@@ -101,8 +107,8 @@ ...@@ -101,8 +107,8 @@
:key="i"> :key="i">
<job-component <job-component
:job="item" :job="item"
:is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
:request-finished-for="requestFinishedFor"
/> />
</li> </li>
</ul> </ul>
......
...@@ -7,7 +7,6 @@ export default { ...@@ -7,7 +7,6 @@ export default {
StageColumnComponent, StageColumnComponent,
LoadingIcon, LoadingIcon,
}, },
props: { props: {
isLoading: { isLoading: {
type: Boolean, type: Boolean,
...@@ -17,10 +16,10 @@ export default { ...@@ -17,10 +16,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
actionDisabled: { requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
...@@ -75,7 +74,7 @@ export default { ...@@ -75,7 +74,7 @@ export default {
:key="stage.name" :key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:action-disabled="actionDisabled" :request-finished-for="requestFinishedFor"
/> />
</ul> </ul>
</div> </div>
......
<script> <script>
import ActionComponent from './action_component.vue'; import ActionComponent from './action_component.vue';
import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
...@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip'; ...@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default { export default {
components: { components: {
ActionComponent, ActionComponent,
DropdownActionComponent,
JobNameComponent, JobNameComponent,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -44,26 +41,17 @@ export default { ...@@ -44,26 +41,17 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
cssClassJobName: { cssClassJobName: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
requestFinishedFor: {
isDropdown: {
type: Boolean,
required: false,
default: false,
},
actionDisabled: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
computed: { computed: {
status() { status() {
return this.job && this.job.status ? this.job.status : {}; return this.job && this.job.status ? this.job.status : {};
...@@ -134,19 +122,11 @@ export default { ...@@ -134,19 +122,11 @@ export default {
</div> </div>
<action-component <action-component
v-if="hasAction && !isDropdown" v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:button-disabled="actionDisabled"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
:tooltip-text="status.action.title" :tooltip-text="status.action.title"
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
:action-method="status.action.method" :request-finished-for="requestFinishedFor"
/> />
</div> </div>
</template> </template>
...@@ -29,10 +29,11 @@ export default { ...@@ -29,10 +29,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
actionDisabled: {
requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
...@@ -74,12 +75,12 @@ export default { ...@@ -74,12 +75,12 @@ export default {
v-if="job.size === 1" v-if="job.size === 1"
:job="job" :job="job"
css-class-job-name="build-content" css-class-job-name="build-content"
:action-disabled="actionDisabled"
/> />
<dropdown-job-component <dropdown-job-component
v-if="job.size > 1" v-if="job.size > 1"
:job="job" :job="job"
:request-finished-for="requestFinishedFor"
/> />
</li> </li>
......
...@@ -25,7 +25,7 @@ export default () => { ...@@ -25,7 +25,7 @@ export default () => {
data() { data() {
return { return {
mediator, mediator,
actionDisabled: null, requestFinishedFor: null,
}; };
}, },
created() { created() {
...@@ -36,15 +36,17 @@ export default () => { ...@@ -36,15 +36,17 @@ export default () => {
}, },
methods: { methods: {
postAction(action) { postAction(action) {
this.actionDisabled = action; // Click was made, reset this variable
this.requestFinishedFor = null;
this.mediator.service.postAction(action) this.mediator.service
.postAction(action)
.then(() => { .then(() => {
this.mediator.refreshPipeline(); this.mediator.refreshPipeline();
this.actionDisabled = null; this.requestFinishedFor = action;
}) })
.catch(() => { .catch(() => {
this.actionDisabled = null; this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.')); Flash(__('An error occurred while making the request.'));
}); });
}, },
...@@ -54,7 +56,7 @@ export default () => { ...@@ -54,7 +56,7 @@ export default () => {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline, pipeline: this.mediator.store.state.pipeline,
actionDisabled: this.actionDisabled, requestFinishedFor: this.requestFinishedFor,
}, },
}); });
}, },
...@@ -79,7 +81,8 @@ export default () => { ...@@ -79,7 +81,8 @@ export default () => {
}, },
methods: { methods: {
postAction(action) { postAction(action) {
this.mediator.service.postAction(action.path) this.mediator.service
.postAction(action.path)
.then(() => this.mediator.refreshPipeline()) .then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.'))); .catch(() => Flash(__('An error occurred while making the request.')));
}, },
......
...@@ -468,6 +468,14 @@ ...@@ -468,6 +468,14 @@
margin-bottom: 10px; margin-bottom: 10px;
white-space: normal; white-space: normal;
.ci-job-dropdown-container {
// override dropdown.scss
.dropdown-menu li button {
padding: 0;
text-align: center;
}
}
// ensure .build-content has hover style when action-icon is hovered // ensure .build-content has hover style when action-icon is hovered
.ci-job-dropdown-container:hover .build-content { .ci-job-dropdown-container:hover .build-content {
@extend .build-content:hover; @extend .build-content:hover;
......
---
title: Prevent pipeline actions in dropdown to redirct to a new page
merge_request:
author:
type: fixed
...@@ -6,7 +6,7 @@ import mountComponent from '../../helpers/vue_mount_component_helper'; ...@@ -6,7 +6,7 @@ import mountComponent from '../../helpers/vue_mount_component_helper';
describe('pipeline graph action component', () => { describe('pipeline graph action component', () => {
let component; let component;
beforeEach((done) => { beforeEach(done => {
const ActionComponent = Vue.extend(actionComponent); const ActionComponent = Vue.extend(actionComponent);
component = mountComponent(ActionComponent, { component = mountComponent(ActionComponent, {
tooltipText: 'bar', tooltipText: 'bar',
...@@ -22,7 +22,7 @@ describe('pipeline graph action component', () => { ...@@ -22,7 +22,7 @@ describe('pipeline graph action component', () => {
}); });
it('should emit an event with the provided link', () => { it('should emit an event with the provided link', () => {
eventHub.$on('graphAction', (link) => { eventHub.$on('graphAction', link => {
expect(link).toEqual('foo'); expect(link).toEqual('foo');
}); });
}); });
...@@ -31,7 +31,7 @@ describe('pipeline graph action component', () => { ...@@ -31,7 +31,7 @@ describe('pipeline graph action component', () => {
expect(component.$el.getAttribute('data-original-title')).toEqual('bar'); expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
}); });
it('should update bootstrap tooltip when title changes', (done) => { it('should update bootstrap tooltip when title changes', done => {
component.tooltipText = 'changed'; component.tooltipText = 'changed';
setTimeout(() => { setTimeout(() => {
...@@ -44,4 +44,45 @@ describe('pipeline graph action component', () => { ...@@ -44,4 +44,45 @@ describe('pipeline graph action component', () => {
expect(component.$el.querySelector('.ci-action-icon-wrapper')).toBeDefined(); expect(component.$el.querySelector('.ci-action-icon-wrapper')).toBeDefined();
expect(component.$el.querySelector('svg')).toBeDefined(); expect(component.$el.querySelector('svg')).toBeDefined();
}); });
it('disables the button when clicked', done => {
component.$el.click();
component.$nextTick(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
done();
});
});
it('re-enabled the button when `requestFinishedFor` matches `linkRequested`', done => {
component.$el.click();
component
.$nextTick()
.then(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
component.requestFinishedFor = 'foo';
})
.then(() => {
expect(component.$el.getAttribute('disabled')).toBeNull();
})
.then(done)
.catch(done.fail);
});
it('does not re-enable the button when `requestFinishedFor` does not matches `linkRequested`', done => {
component.$el.click();
component
.$nextTick()
.then(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
component.requestFinishedFor = 'bar';
})
.then(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
})
.then(done)
.catch(done.fail);
});
}); });
import Vue from 'vue';
import dropdownActionComponent from '~/pipelines/components/graph/dropdown_action_component.vue';
describe('action component', () => {
let component;
beforeEach((done) => {
const DropdownActionComponent = Vue.extend(dropdownActionComponent);
component = new DropdownActionComponent({
propsData: {
tooltipText: 'bar',
link: 'foo',
actionMethod: 'post',
actionIcon: 'cancel',
},
}).$mount();
Vue.nextTick(done);
});
it('should render a link', () => {
expect(component.$el.getAttribute('href')).toEqual('foo');
});
it('should render the provided title as a bootstrap tooltip', () => {
expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
});
it('should render an svg', () => {
expect(component.$el.querySelector('svg')).toBeDefined();
});
});
...@@ -93,17 +93,6 @@ describe('pipeline graph job component', () => { ...@@ -93,17 +93,6 @@ describe('pipeline graph job component', () => {
}); });
}); });
describe('dropdown', () => {
it('should render the dropdown action icon', () => {
component = mountComponent(JobComponent, {
job: mockJob,
isDropdown: true,
});
expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined();
});
});
it('should render provided class name', () => { it('should render provided class name', () => {
component = mountComponent(JobComponent, { component = mountComponent(JobComponent, {
job: mockJob, job: mockJob,
......
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