Fix disabled state while making a request
Provide props down instead of an event
This commit is contained in:
parent
44a222adea
commit
a527c9b915
8 changed files with 159 additions and 94 deletions
|
@ -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 {
|
||||||
@click="onClickAction"
|
@click="onClickAction"
|
||||||
v-tooltip
|
v-tooltip
|
||||||
:title="tooltipText"
|
:title="tooltipText"
|
||||||
class="js-ci-action 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"
|
||||||
|
|
|
@ -1,77 +1,83 @@
|
||||||
<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: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
tooltipText() {
|
tooltipText() {
|
||||||
return `${this.job.name} - ${this.job.status.label}`;
|
return `${this.job.name} - ${this.job.status.label}`;
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.stopDropdownClickPropagation();
|
this.stopDropdownClickPropagation();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
/**
|
/**
|
||||||
* When the user right clicks or cmd/ctrl + click in the job name or the action icon
|
* When the user right clicks or cmd/ctrl + click in the job name or the action icon
|
||||||
* the dropdown should not be closed so we stop propagation of the click event inside the dropdown.
|
* the dropdown should not be closed so we stop propagation
|
||||||
*
|
* of the click event inside the dropdown.
|
||||||
* Since this component is rendered multiple times per page we need to guarantee we only
|
*
|
||||||
* target the click event of this component.
|
* Since this component is rendered multiple times per page we need to guarantee we only
|
||||||
*/
|
* target the click event of this component.
|
||||||
stopDropdownClickPropagation() {
|
*/
|
||||||
$(
|
stopDropdownClickPropagation() {
|
||||||
'.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
|
$(
|
||||||
this.$el,
|
'.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">
|
||||||
|
@ -102,6 +108,7 @@
|
||||||
<job-component
|
<job-component
|
||||||
:job="item"
|
:job="item"
|
||||||
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 {
|
||||||
StageColumnComponent,
|
StageColumnComponent,
|
||||||
LoadingIcon,
|
LoadingIcon,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
isLoading: {
|
isLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -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 {
|
||||||
: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>
|
||||||
|
|
|
@ -33,7 +33,6 @@ export default {
|
||||||
ActionComponent,
|
ActionComponent,
|
||||||
JobNameComponent,
|
JobNameComponent,
|
||||||
},
|
},
|
||||||
|
|
||||||
directives: {
|
directives: {
|
||||||
tooltip,
|
tooltip,
|
||||||
},
|
},
|
||||||
|
@ -42,20 +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: {
|
||||||
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 : {};
|
||||||
|
@ -130,7 +126,7 @@ export default {
|
||||||
: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"
|
||||||
:button-disabled="actionDisabled"
|
:request-finished-for="requestFinishedFor"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -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 {
|
||||||
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 () => {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
mediator,
|
mediator,
|
||||||
actionDisabled: null,
|
requestFinishedFor: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -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 () => {
|
||||||
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 () => {
|
||||||
},
|
},
|
||||||
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.')));
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
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';
|
||||||
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', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
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', () => {
|
||||||
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', () => {
|
||||||
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue