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>
|
<script>
|
||||||
|
/* global Flash */
|
||||||
import Visibility from 'visibilityjs';
|
import Visibility from 'visibilityjs';
|
||||||
import Poll from '../../lib/utils/poll';
|
import Poll from '../../lib/utils/poll';
|
||||||
|
import eventHub from '../event_hub';
|
||||||
import Service from '../services/index';
|
import Service from '../services/index';
|
||||||
import Store from '../stores';
|
import Store from '../stores';
|
||||||
import titleComponent from './title.vue';
|
import titleComponent from './title.vue';
|
||||||
import descriptionComponent from './description.vue';
|
import descriptionComponent from './description.vue';
|
||||||
|
import editActions from './edit_actions.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
@ -34,6 +37,10 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
showForm: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
|
@ -45,16 +52,43 @@ export default {
|
||||||
return {
|
return {
|
||||||
store,
|
store,
|
||||||
state: store.state,
|
state: store.state,
|
||||||
|
formState: store.formState,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
descriptionComponent,
|
descriptionComponent,
|
||||||
titleComponent,
|
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() {
|
created() {
|
||||||
const resource = new Service(this.endpoint);
|
this.service = new Service(this.endpoint);
|
||||||
const poll = new Poll({
|
this.poll = new Poll({
|
||||||
resource,
|
resource: this.service,
|
||||||
method: 'getData',
|
method: 'getData',
|
||||||
successCallback: (res) => {
|
successCallback: (res) => {
|
||||||
this.store.updateState(res.json());
|
this.store.updateState(res.json());
|
||||||
|
@ -65,16 +99,23 @@ export default {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!Visibility.hidden()) {
|
if (!Visibility.hidden()) {
|
||||||
poll.makeRequest();
|
this.poll.makeRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
Visibility.change(() => {
|
Visibility.change(() => {
|
||||||
if (!Visibility.hidden()) {
|
if (!Visibility.hidden()) {
|
||||||
poll.restart();
|
this.poll.restart();
|
||||||
} else {
|
} 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>
|
</script>
|
||||||
|
@ -92,5 +133,7 @@ export default {
|
||||||
:description-text="state.descriptionText"
|
:description-text="state.descriptionText"
|
||||||
:updated-at="state.updatedAt"
|
:updated-at="state.updatedAt"
|
||||||
:task-status="state.taskStatus" />
|
:task-status="state.taskStatus" />
|
||||||
|
<edit-actions
|
||||||
|
v-if="canUpdate && showForm" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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: {
|
methods: {
|
||||||
openForm() {
|
openForm() {
|
||||||
this.showForm = true;
|
this.showForm = true;
|
||||||
console.log(this.showForm);
|
},
|
||||||
|
closeForm() {
|
||||||
|
this.showForm = false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
eventHub.$on('open.form', this.openForm);
|
eventHub.$on('open.form', this.openForm);
|
||||||
|
eventHub.$on('close.form', this.closeForm);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
eventHub.$off('open.form', this.openForm);
|
eventHub.$off('open.form', this.openForm);
|
||||||
|
eventHub.$off('close.form', this.closeForm);
|
||||||
},
|
},
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement('issuable-app', {
|
return createElement('issuable-app', {
|
||||||
|
@ -57,6 +61,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
initialTitle: this.initialTitle,
|
initialTitle: this.initialTitle,
|
||||||
initialDescriptionHtml: this.initialDescriptionHtml,
|
initialDescriptionHtml: this.initialDescriptionHtml,
|
||||||
initialDescriptionText: this.initialDescriptionText,
|
initialDescriptionText: this.initialDescriptionText,
|
||||||
|
showForm: this.showForm,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,10 +7,25 @@ export default class Service {
|
||||||
constructor(endpoint) {
|
constructor(endpoint) {
|
||||||
this.endpoint = 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() {
|
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: '',
|
taskStatus: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
};
|
};
|
||||||
|
this.formState = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState(data) {
|
updateState(data) {
|
||||||
|
|
|
@ -14,7 +14,16 @@ module IssuableActions
|
||||||
|
|
||||||
name = issuable.human_class_name
|
name = issuable.human_class_name
|
||||||
flash[:notice] = "The #{name} was successfully deleted."
|
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
|
end
|
||||||
|
|
||||||
def bulk_update
|
def bulk_update
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
|
|
||||||
.issue-details.issuable-details
|
.issue-details.issuable-details
|
||||||
.detail-page-description.content-block
|
.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,
|
"can-update" => can?(current_user, :update_issue, @issue).to_s,
|
||||||
"issuable-ref" => @issue.to_reference,
|
"issuable-ref" => @issue.to_reference,
|
||||||
} }
|
} }
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Vue from 'vue';
|
||||||
import '~/render_math';
|
import '~/render_math';
|
||||||
import '~/render_gfm';
|
import '~/render_gfm';
|
||||||
import issuableApp from '~/issue_show/components/app.vue';
|
import issuableApp from '~/issue_show/components/app.vue';
|
||||||
|
import eventHub from '~/issue_show/event_hub';
|
||||||
import issueShowData from '../mock_data';
|
import issueShowData from '../mock_data';
|
||||||
|
|
||||||
const issueShowInterceptor = data => (request, next) => {
|
const issueShowInterceptor = data => (request, next) => {
|
||||||
|
@ -22,6 +23,8 @@ describe('Issuable output', () => {
|
||||||
const IssuableDescriptionComponent = Vue.extend(issuableApp);
|
const IssuableDescriptionComponent = Vue.extend(issuableApp);
|
||||||
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
|
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
|
||||||
|
|
||||||
|
spyOn(eventHub, '$emit');
|
||||||
|
|
||||||
vm = new IssuableDescriptionComponent({
|
vm = new IssuableDescriptionComponent({
|
||||||
propsData: {
|
propsData: {
|
||||||
canUpdate: true,
|
canUpdate: true,
|
||||||
|
@ -30,6 +33,7 @@ describe('Issuable output', () => {
|
||||||
initialTitle: '',
|
initialTitle: '',
|
||||||
initialDescriptionHtml: '',
|
initialDescriptionHtml: '',
|
||||||
initialDescriptionText: '',
|
initialDescriptionText: '',
|
||||||
|
showForm: true,
|
||||||
},
|
},
|
||||||
}).$mount();
|
}).$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