Fix disabled state while making a request

Provide props down instead of an event
This commit is contained in:
Filipa Lacerda 2018-04-20 15:43:58 +01:00
parent 44a222adea
commit a527c9b915
8 changed files with 159 additions and 94 deletions

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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.')));
}, },

View file

@ -0,0 +1,5 @@
---
title: Prevent pipeline actions in dropdown to redirct to a new page
merge_request:
author:
type: fixed

View file

@ -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);
});
}); });