Merge branch '46381-dropdown-mr-widget' into 'master'
Resolve "Dropdown actions in mini pipeline graph in mr widget don't work" Closes #46381 See merge request gitlab-org/gitlab-ce!18976
This commit is contained in:
commit
7f7f874184
12 changed files with 141 additions and 117 deletions
|
@ -1,11 +1,21 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
import Icon from '../../../vue_shared/components/icon.vue';
|
||||
import { dasherize } from '../../../lib/utils/text_utility';
|
||||
import eventHub from '../../event_hub';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { dasherize } from '~/lib/utils/text_utility';
|
||||
import { __ } from '~/locale';
|
||||
import createFlash from '~/flash';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
/**
|
||||
* Renders either a cancel, retry or play icon pointing to the given path.
|
||||
* Renders either a cancel, retry or play icon button and handles the post request
|
||||
*
|
||||
* Used in:
|
||||
* - mr widget mini pipeline graph: `mr_widget_pipeline.vue`
|
||||
* - pipelines table
|
||||
* - pipelines table in merge request page
|
||||
* - pipelines table in commit page
|
||||
* - pipelines detail page in big graph
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
|
@ -32,16 +42,10 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDisabled: false,
|
||||
linkRequested: '',
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -51,19 +55,28 @@ export default {
|
|||
return `${actionIconDash} js-icon-${actionIconDash}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
requestFinishedFor() {
|
||||
if (this.requestFinishedFor === this.linkRequested) {
|
||||
this.isDisabled = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* The request should not be handled here.
|
||||
* However due to this component being used in several
|
||||
* different apps it avoids repetition & complexity.
|
||||
*
|
||||
*/
|
||||
onClickAction() {
|
||||
$(this.$el).tooltip('hide');
|
||||
eventHub.$emit('postAction', this.link);
|
||||
this.linkRequested = this.link;
|
||||
|
||||
this.isDisabled = true;
|
||||
|
||||
axios.post(`${this.link}.json`)
|
||||
.then(() => {
|
||||
this.isDisabled = false;
|
||||
this.$emit('pipelineActionRequestComplete');
|
||||
})
|
||||
.catch(() => {
|
||||
this.isDisabled = false;
|
||||
|
||||
createFlash(__('An error occurred while making the request.'));
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -42,11 +42,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -76,11 +71,15 @@ export default {
|
|||
e.stopPropagation();
|
||||
});
|
||||
},
|
||||
|
||||
pipelineActionRequestComplete() {
|
||||
this.$emit('pipelineActionRequestComplete');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="ci-job-dropdown-container">
|
||||
<div class="ci-job-dropdown-container dropdown">
|
||||
<button
|
||||
v-tooltip
|
||||
type="button"
|
||||
|
@ -110,7 +109,7 @@ export default {
|
|||
<job-component
|
||||
:job="item"
|
||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -16,11 +16,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -51,6 +46,10 @@ export default {
|
|||
|
||||
return className;
|
||||
},
|
||||
|
||||
refreshPipelineGraph() {
|
||||
this.$emit('refreshPipelineGraph');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -74,7 +73,7 @@ export default {
|
|||
:key="stage.name"
|
||||
:stage-connector-class="stageConnectorClass(index, stage)"
|
||||
:is-first-column="isFirstColumn(index)"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
@refreshPipelineGraph="refreshPipelineGraph"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -46,11 +46,6 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
status() {
|
||||
|
@ -84,6 +79,11 @@ export default {
|
|||
return this.job.status && this.job.status.action && this.job.status.action.path;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
pipelineActionRequestComplete() {
|
||||
this.$emit('pipelineActionRequestComplete');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
@ -126,7 +126,7 @@ export default {
|
|||
:tooltip-text="status.action.title"
|
||||
:link="status.action.path"
|
||||
:action-icon="status.action.icon"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -29,12 +29,6 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -49,6 +43,10 @@ export default {
|
|||
buildConnnectorClass(index) {
|
||||
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
|
||||
},
|
||||
|
||||
pipelineActionRequestComplete() {
|
||||
this.$emit('refreshPipelineGraph');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -75,12 +73,13 @@ export default {
|
|||
v-if="job.size === 1"
|
||||
:job="job"
|
||||
css-class-job-name="build-content"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
|
||||
<dropdown-job-component
|
||||
v-if="job.size > 1"
|
||||
:job="job"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
|
||||
</li>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import CommitComponent from '../../vue_shared/components/commit.vue';
|
||||
import LoadingButton from '../../vue_shared/components/loading_button.vue';
|
||||
import Icon from '../../vue_shared/components/icon.vue';
|
||||
import { PIPELINES_TABLE } from '../constants';
|
||||
|
||||
/**
|
||||
* Pipeline table row.
|
||||
|
@ -46,6 +47,7 @@
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
pipelinesTable: PIPELINES_TABLE,
|
||||
data() {
|
||||
return {
|
||||
isRetrying: false,
|
||||
|
@ -297,6 +299,7 @@
|
|||
v-for="(stage, index) in pipeline.details.stages"
|
||||
:key="index">
|
||||
<pipeline-stage
|
||||
:type="$options.pipelinesTable"
|
||||
:stage="stage"
|
||||
:update-dropdown="updateGraphDropdown"
|
||||
/>
|
||||
|
|
|
@ -21,6 +21,7 @@ import Icon from '../../vue_shared/components/icon.vue';
|
|||
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import JobComponent from './graph/job_component.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import { PIPELINES_TABLE } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -44,6 +45,12 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -133,6 +140,16 @@ export default {
|
|||
isDropdownOpen() {
|
||||
return this.$el.classList.contains('open');
|
||||
},
|
||||
|
||||
pipelineActionRequestComplete() {
|
||||
if (this.type === PIPELINES_TABLE) {
|
||||
// warn the table to update
|
||||
eventHub.$emit('refreshPipelinesTable');
|
||||
} else {
|
||||
// close the dropdown in mr widget
|
||||
$(this.$refs.dropdown).dropdown('toggle');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -151,6 +168,7 @@ export default {
|
|||
id="stageDropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
ref="dropdown"
|
||||
>
|
||||
|
||||
<span
|
||||
|
@ -188,6 +206,7 @@ export default {
|
|||
<job-component
|
||||
:job="job"
|
||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
|
||||
export const PIPELINES_TABLE = 'PIPELINES_TABLE';
|
||||
|
|
|
@ -55,11 +55,13 @@ export default {
|
|||
eventHub.$on('postAction', this.postAction);
|
||||
eventHub.$on('retryPipeline', this.postAction);
|
||||
eventHub.$on('clickedDropdown', this.updateTable);
|
||||
eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('postAction', this.postAction);
|
||||
eventHub.$off('retryPipeline', this.postAction);
|
||||
eventHub.$off('clickedDropdown', this.updateTable);
|
||||
eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
|
||||
},
|
||||
destroyed() {
|
||||
this.poll.stop();
|
||||
|
|
|
@ -25,30 +25,14 @@ export default () => {
|
|||
data() {
|
||||
return {
|
||||
mediator,
|
||||
requestFinishedFor: null,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
eventHub.$on('postAction', this.postAction);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('postAction', this.postAction);
|
||||
},
|
||||
methods: {
|
||||
postAction(action) {
|
||||
// Click was made, reset this variable
|
||||
this.requestFinishedFor = null;
|
||||
|
||||
this.mediator.service
|
||||
.postAction(action)
|
||||
.then(() => {
|
||||
this.mediator.refreshPipeline();
|
||||
this.requestFinishedFor = action;
|
||||
})
|
||||
.catch(() => {
|
||||
this.requestFinishedFor = action;
|
||||
Flash(__('An error occurred while making the request.'));
|
||||
});
|
||||
requestRefreshPipelineGraph() {
|
||||
// When an action is clicked
|
||||
// (wether in the dropdown or in the main nodes, we refresh the big graph)
|
||||
this.mediator.refreshPipeline()
|
||||
.catch(() => Flash(__('An error occurred while making the request.')));
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
|
@ -56,7 +40,9 @@ export default () => {
|
|||
props: {
|
||||
isLoading: this.mediator.state.isLoading,
|
||||
pipeline: this.mediator.store.state.pipeline,
|
||||
requestFinishedFor: this.requestFinishedFor,
|
||||
},
|
||||
on: {
|
||||
refreshPipelineGraph: this.requestRefreshPipelineGraph,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import Vue from 'vue';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import actionComponent from '~/pipelines/components/graph/action_component.vue';
|
||||
import eventHub from '~/pipelines/event_hub';
|
||||
import mountComponent from '../../helpers/vue_mount_component_helper';
|
||||
|
||||
describe('pipeline graph action component', () => {
|
||||
let component;
|
||||
let mock;
|
||||
|
||||
beforeEach(done => {
|
||||
const ActionComponent = Vue.extend(actionComponent);
|
||||
mock = new MockAdapter(axios);
|
||||
|
||||
mock.onPost('foo.json').reply(200);
|
||||
|
||||
component = mountComponent(ActionComponent, {
|
||||
tooltipText: 'bar',
|
||||
link: 'foo',
|
||||
|
@ -18,15 +24,10 @@ describe('pipeline graph action component', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
component.$destroy();
|
||||
});
|
||||
|
||||
it('should emit an event with the provided link', () => {
|
||||
eventHub.$on('postAction', link => {
|
||||
expect(link).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the provided title as a bootstrap tooltip', () => {
|
||||
expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
|
||||
});
|
||||
|
@ -34,10 +35,12 @@ describe('pipeline graph action component', () => {
|
|||
it('should update bootstrap tooltip when title changes', done => {
|
||||
component.tooltipText = 'changed';
|
||||
|
||||
setTimeout(() => {
|
||||
component.$nextTick()
|
||||
.then(() => {
|
||||
expect(component.$el.getAttribute('data-original-title')).toBe('changed');
|
||||
done();
|
||||
});
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should render an svg', () => {
|
||||
|
@ -45,44 +48,18 @@ describe('pipeline graph action component', () => {
|
|||
expect(component.$el.querySelector('svg')).toBeDefined();
|
||||
});
|
||||
|
||||
it('disables the button when clicked', done => {
|
||||
describe('on click', () => {
|
||||
it('emits `pipelineActionRequestComplete` after a successfull request', done => {
|
||||
spyOn(component, '$emit');
|
||||
|
||||
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()
|
||||
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');
|
||||
expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -102,4 +102,31 @@ describe('Pipelines stage component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipelineActionRequestComplete', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet('path.json').reply(200, stageReply);
|
||||
|
||||
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
|
||||
});
|
||||
|
||||
describe('within pipeline table', () => {
|
||||
it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', done => {
|
||||
spyOn(eventHub, '$emit');
|
||||
|
||||
component.type = 'PIPELINES_TABLE';
|
||||
component.$el.querySelector('button').click();
|
||||
|
||||
setTimeout(() => {
|
||||
component.$el.querySelector('.js-ci-action').click();
|
||||
component.$nextTick()
|
||||
.then(() => {
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue