Added inline issue edit form actions
[ci skip]
This commit is contained in:
parent
66539563c8
commit
86700b97d3
9 changed files with 367 additions and 11 deletions
|
@ -1,10 +1,13 @@
|
|||
<script>
|
||||
/* global Flash */
|
||||
import Visibility from 'visibilityjs';
|
||||
import Poll from '../../lib/utils/poll';
|
||||
import eventHub from '../event_hub';
|
||||
import Service from '../services/index';
|
||||
import Store from '../stores';
|
||||
import titleComponent from './title.vue';
|
||||
import descriptionComponent from './description.vue';
|
||||
import editActions from './edit_actions.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -34,6 +37,10 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
showForm: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const store = new Store({
|
||||
|
@ -45,16 +52,43 @@ export default {
|
|||
return {
|
||||
store,
|
||||
state: store.state,
|
||||
formState: store.formState,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
descriptionComponent,
|
||||
titleComponent,
|
||||
editActions,
|
||||
},
|
||||
methods: {
|
||||
updateIssuable() {
|
||||
this.service.updateIssuable(this.formState)
|
||||
.then(() => {
|
||||
eventHub.$emit('close.form');
|
||||
})
|
||||
.catch(() => {
|
||||
eventHub.$emit('close.form');
|
||||
return new Flash('Error updating issue');
|
||||
});
|
||||
},
|
||||
deleteIssuable() {
|
||||
this.service.deleteIssuable()
|
||||
.then((data) => {
|
||||
gl.utils.visitUrl(data.path);
|
||||
|
||||
// Stop the poll so we don't get 404's with the issue not existing
|
||||
this.poll.stop();
|
||||
})
|
||||
.catch(() => {
|
||||
eventHub.$emit('close.form');
|
||||
return new Flash('Error deleting issue');
|
||||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const resource = new Service(this.endpoint);
|
||||
const poll = new Poll({
|
||||
resource,
|
||||
this.service = new Service(this.endpoint);
|
||||
this.poll = new Poll({
|
||||
resource: this.service,
|
||||
method: 'getData',
|
||||
successCallback: (res) => {
|
||||
this.store.updateState(res.json());
|
||||
|
@ -65,16 +99,23 @@ export default {
|
|||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
poll.makeRequest();
|
||||
this.poll.makeRequest();
|
||||
}
|
||||
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden()) {
|
||||
poll.restart();
|
||||
this.poll.restart();
|
||||
} else {
|
||||
poll.stop();
|
||||
this.poll.stop();
|
||||
}
|
||||
});
|
||||
|
||||
eventHub.$on('delete.issuable', this.deleteIssuable);
|
||||
eventHub.$on('update.issuable', this.updateIssuable);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('delete.issuable', this.deleteIssuable);
|
||||
eventHub.$off('update.issuable', this.updateIssuable);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -92,5 +133,7 @@ export default {
|
|||
:description-text="state.descriptionText"
|
||||
:updated-at="state.updatedAt"
|
||||
:task-status="state.taskStatus" />
|
||||
<edit-actions
|
||||
v-if="canUpdate && showForm" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
deleteLoading: false,
|
||||
updateLoading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateIssuable() {
|
||||
this.updateLoading = true;
|
||||
eventHub.$emit('update.issuable');
|
||||
},
|
||||
closeForm() {
|
||||
eventHub.$emit('close.form');
|
||||
},
|
||||
deleteIssuable() {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (confirm('Issue will be removed! Are you sure?')) {
|
||||
this.deleteLoading = true;
|
||||
|
||||
eventHub.$emit('delete.issuable');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prepend-top-default append-bottom-default clearfix">
|
||||
<button
|
||||
class="btn btn-save pull-left"
|
||||
:class="{ disabled: updateLoading }"
|
||||
type="submit"
|
||||
:disabled="updateLoading"
|
||||
@click="updateIssuable">
|
||||
Save changes
|
||||
<i
|
||||
class="fa fa-spinner fa-spin"
|
||||
aria-hidden="true"
|
||||
v-if="updateLoading">
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-default pull-right"
|
||||
type="button"
|
||||
@click="closeForm">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger pull-right append-right-default"
|
||||
:class="{ disabled: deleteLoading }"
|
||||
type="button"
|
||||
:disabled="deleteLoading"
|
||||
@click="deleteIssuable">
|
||||
Delete
|
||||
<i
|
||||
class="fa fa-spinner fa-spin"
|
||||
aria-hidden="true"
|
||||
v-if="deleteLoading">
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -39,14 +39,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
methods: {
|
||||
openForm() {
|
||||
this.showForm = true;
|
||||
console.log(this.showForm);
|
||||
},
|
||||
closeForm() {
|
||||
this.showForm = false;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
eventHub.$on('open.form', this.openForm);
|
||||
eventHub.$on('close.form', this.closeForm);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('open.form', this.openForm);
|
||||
eventHub.$off('close.form', this.closeForm);
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('issuable-app', {
|
||||
|
@ -57,6 +61,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
initialTitle: this.initialTitle,
|
||||
initialDescriptionHtml: this.initialDescriptionHtml,
|
||||
initialDescriptionText: this.initialDescriptionText,
|
||||
showForm: this.showForm,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -7,10 +7,25 @@ export default class Service {
|
|||
constructor(endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
|
||||
this.resource = Vue.resource(this.endpoint);
|
||||
this.resource = Vue.resource(this.endpoint, {}, {
|
||||
rendered_title: {
|
||||
method: 'GET',
|
||||
url: `${this.endpoint}/rendered_title`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getData() {
|
||||
return this.resource.get();
|
||||
return this.resource.rendered_title();
|
||||
}
|
||||
|
||||
deleteIssuable() {
|
||||
return this.resource.delete()
|
||||
.then(res => res.json());
|
||||
}
|
||||
|
||||
updateIssuable(data) {
|
||||
return this.resource.update(data)
|
||||
.then(res => res.json());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ export default class Store {
|
|||
taskStatus: '',
|
||||
updatedAt: '',
|
||||
};
|
||||
this.formState = {};
|
||||
}
|
||||
|
||||
updateState(data) {
|
||||
|
|
|
@ -14,7 +14,16 @@ module IssuableActions
|
|||
|
||||
name = issuable.human_class_name
|
||||
flash[:notice] = "The #{name} was successfully deleted."
|
||||
redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
|
||||
index_path = polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to index_path }
|
||||
format.json do
|
||||
render json: {
|
||||
path: index_path
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def bulk_update
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
|
||||
.issue-details.issuable-details
|
||||
.detail-page-description.content-block
|
||||
#js-issuable-app{ "data" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
|
||||
#js-issuable-app{ "data" => { "endpoint" => namespace_project_issue_path(@project.namespace, @project, @issue),
|
||||
"can-update" => can?(current_user, :update_issue, @issue).to_s,
|
||||
"issuable-ref" => @issue.to_reference,
|
||||
} }
|
||||
|
|
|
@ -2,6 +2,7 @@ import Vue from 'vue';
|
|||
import '~/render_math';
|
||||
import '~/render_gfm';
|
||||
import issuableApp from '~/issue_show/components/app.vue';
|
||||
import eventHub from '~/issue_show/event_hub';
|
||||
import issueShowData from '../mock_data';
|
||||
|
||||
const issueShowInterceptor = data => (request, next) => {
|
||||
|
@ -22,6 +23,8 @@ describe('Issuable output', () => {
|
|||
const IssuableDescriptionComponent = Vue.extend(issuableApp);
|
||||
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
|
||||
|
||||
spyOn(eventHub, '$emit');
|
||||
|
||||
vm = new IssuableDescriptionComponent({
|
||||
propsData: {
|
||||
canUpdate: true,
|
||||
|
@ -30,6 +33,7 @@ describe('Issuable output', () => {
|
|||
initialTitle: '',
|
||||
initialDescriptionHtml: '',
|
||||
initialDescriptionText: '',
|
||||
showForm: true,
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
@ -57,4 +61,106 @@ describe('Issuable output', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateIssuable', () => {
|
||||
it('correctly updates issuable data', (done) => {
|
||||
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
|
||||
resolve();
|
||||
}));
|
||||
|
||||
vm.updateIssuable();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
vm.service.updateIssuable,
|
||||
).toHaveBeenCalledWith(vm.formState);
|
||||
expect(
|
||||
eventHub.$emit,
|
||||
).toHaveBeenCalledWith('close.form');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('closes form on error', (done) => {
|
||||
spyOn(window, 'Flash').and.callThrough();
|
||||
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
|
||||
reject();
|
||||
}));
|
||||
|
||||
vm.updateIssuable();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
eventHub.$emit,
|
||||
).toHaveBeenCalledWith('close.form');
|
||||
expect(
|
||||
window.Flash,
|
||||
).toHaveBeenCalledWith('Error updating issue');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteIssuable', () => {
|
||||
it('changes URL when deleted', (done) => {
|
||||
spyOn(gl.utils, 'visitUrl');
|
||||
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
|
||||
resolve({
|
||||
path: '/test',
|
||||
});
|
||||
}));
|
||||
|
||||
vm.deleteIssuable();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
gl.utils.visitUrl,
|
||||
).toHaveBeenCalledWith('/test');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('stops polling when deleteing', (done) => {
|
||||
spyOn(gl.utils, 'visitUrl');
|
||||
spyOn(vm.poll, 'stop');
|
||||
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
|
||||
resolve({
|
||||
path: '/test',
|
||||
});
|
||||
}));
|
||||
|
||||
vm.deleteIssuable();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
vm.poll.stop,
|
||||
).toHaveBeenCalledWith();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('closes form on error', (done) => {
|
||||
spyOn(window, 'Flash').and.callThrough();
|
||||
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve, reject) => {
|
||||
reject();
|
||||
}));
|
||||
|
||||
vm.deleteIssuable();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
eventHub.$emit,
|
||||
).toHaveBeenCalledWith('close.form');
|
||||
expect(
|
||||
window.Flash,
|
||||
).toHaveBeenCalledWith('Error deleting issue');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
111
spec/javascripts/issue_show/components/edit_actions_spec.js
Normal file
111
spec/javascripts/issue_show/components/edit_actions_spec.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
import Vue from 'vue';
|
||||
import editActions from '~/issue_show/components/edit_actions.vue';
|
||||
import eventHub from '~/issue_show/event_hub';
|
||||
|
||||
describe('Edit Actions components', () => {
|
||||
let vm;
|
||||
|
||||
beforeEach((done) => {
|
||||
const Component = Vue.extend(editActions);
|
||||
|
||||
spyOn(eventHub, '$emit');
|
||||
|
||||
vm = new Component().$mount();
|
||||
|
||||
Vue.nextTick(done);
|
||||
});
|
||||
|
||||
it('renders all buttons as enabled', () => {
|
||||
expect(
|
||||
vm.$el.querySelectorAll('.disabled').length,
|
||||
).toBe(0);
|
||||
|
||||
expect(
|
||||
vm.$el.querySelectorAll('[disabled]').length,
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
describe('updateIssuable', () => {
|
||||
it('sends update.issauble event when clicking save button', () => {
|
||||
vm.$el.querySelector('.btn-save').click();
|
||||
|
||||
expect(
|
||||
eventHub.$emit,
|
||||
).toHaveBeenCalledWith('update.issuable');
|
||||
});
|
||||
|
||||
it('shows loading icon after clicking save button', (done) => {
|
||||
vm.$el.querySelector('.btn-save').click();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
vm.$el.querySelector('.btn-save .fa'),
|
||||
).not.toBeNull();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('disabled button after clicking save button', (done) => {
|
||||
vm.$el.querySelector('.btn-save').click();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
vm.$el.querySelector('.btn-save').getAttribute('disabled'),
|
||||
).toBe('disabled');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeForm', () => {
|
||||
it('emits close.form when clicking cancel', () => {
|
||||
vm.$el.querySelector('.btn-default').click();
|
||||
|
||||
expect(
|
||||
eventHub.$emit,
|
||||
).toHaveBeenCalledWith('close.form');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteIssuable', () => {
|
||||
it('sends delete.issuable event when clicking save button', () => {
|
||||
spyOn(window, 'confirm').and.returnValue(true);
|
||||
vm.$el.querySelector('.btn-danger').click();
|
||||
|
||||
expect(
|
||||
eventHub.$emit,
|
||||
).toHaveBeenCalledWith('delete.issuable');
|
||||
});
|
||||
|
||||
it('shows loading icon after clicking delete button', (done) => {
|
||||
spyOn(window, 'confirm').and.returnValue(true);
|
||||
vm.$el.querySelector('.btn-danger').click();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
vm.$el.querySelector('.btn-danger .fa'),
|
||||
).not.toBeNull();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does no actions when confirm is false', (done) => {
|
||||
spyOn(window, 'confirm').and.returnValue(false);
|
||||
vm.$el.querySelector('.btn-danger').click();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
eventHub.$emit,
|
||||
).not.toHaveBeenCalledWith('delete.issuable');
|
||||
expect(
|
||||
vm.$el.querySelector('.btn-danger .fa'),
|
||||
).toBeNull();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue