Commit 907dd68e authored by Phil Hughes's avatar Phil Hughes

Added move to project in issue inline edit form

[ci skip]
parent 4fcff0bf
......@@ -15,6 +15,10 @@ export default {
required: true,
type: String,
},
canMove: {
required: true,
type: Boolean,
},
canUpdate: {
required: true,
type: Boolean,
......@@ -49,6 +53,10 @@ export default {
type: String,
required: true,
},
projectsAutocompleteUrl: {
type: String,
required: true,
},
},
data() {
const store = new Store({
......@@ -79,6 +87,7 @@ export default {
this.store.formState = {
title: this.state.titleText,
description: this.state.descriptionText,
move_to_project_id: 0,
};
},
closeForm() {
......@@ -86,7 +95,12 @@ export default {
},
updateIssuable() {
this.service.updateIssuable(this.store.formState)
.then(() => {
.then(res => res.json())
.then((data) => {
if (location.pathname !== data.path) {
gl.utils.visitUrl(data.path);
}
eventHub.$emit('close.form');
})
.catch(() => {
......@@ -153,9 +167,11 @@ export default {
<form-component
v-if="canUpdate && showForm"
:form-state="formState"
:can-move="canMove"
:can-destroy="canDestroy"
:markdown-docs="markdownDocs"
:markdown-preview-url="markdownPreviewUrl" />
:markdown-preview-url="markdownPreviewUrl"
:projects-autocomplete-url="projectsAutocompleteUrl" />
<div v-else>
<title-component
:issuable-ref="issuableRef"
......
<script>
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
export default {
mixins: [
tooltipMixin,
],
props: {
formState: {
type: Object,
required: true,
},
projectsAutocompleteUrl: {
type: String,
required: true,
},
},
mounted() {
const $moveDropdown = $(this.$refs['move-dropdown']);
$moveDropdown.select2({
ajax: {
url: this.projectsAutocompleteUrl,
quietMillis: 125,
data(term, page, context) {
return {
search: term,
offset_id: context,
};
},
results(data) {
const more = data.length >= 50;
const context = data[data.length - 1] ? data[data.length - 1].id : null;
return {
results: data,
more,
context,
};
},
},
formatResult(project) {
return project.name_with_namespace;
},
formatSelection(project) {
return project.name_with_namespace;
},
})
.on('change', (e) => {
this.formState.move_to_project_id = parseInt(e.target.value, 10);
});
},
beforeDestroy() {
$(this.$refs['move-dropdown']).select2('destroy');
},
};
</script>
<template>
<fieldset>
<label
for="issuable-move"
class="sr-only">
Move
</label>
<div class="issuable-form-select-holder append-right-5">
<input
ref="move-dropdown"
type="hidden"
id="issuable-move"
data-placeholder="Move to a different project" />
</div>
<span
data-placement="auto top"
style="cursor: default"
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
ref="tooltip">
<i
class="fa fa-question-circle"
aria-hidden="true">
</i>
</span>
</fieldset>
</template>
......@@ -2,9 +2,14 @@
import titleField from './fields/title.vue';
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
import projectMove from './fields/project_move.vue';
export default {
props: {
canMove: {
type: Boolean,
required: true,
},
canDestroy: {
type: Boolean,
required: true,
......@@ -21,11 +26,16 @@
type: String,
required: true,
},
projectsAutocompleteUrl: {
type: String,
required: true,
},
},
components: {
titleField,
descriptionField,
editActions,
projectMove,
},
};
</script>
......@@ -38,6 +48,10 @@
:form-state="formState"
:markdown-preview-url="markdownPreviewUrl"
:markdown-docs="markdownDocs" />
<project-move
v-if="canMove"
:form-state="formState"
:projects-autocomplete-url="projectsAutocompleteUrl" />
<edit-actions
:can-destroy="canDestroy" />
</form>
......
......@@ -23,15 +23,18 @@ document.addEventListener('DOMContentLoaded', () => {
const {
canUpdate,
canDestroy,
canMove,
endpoint,
issuableRef,
markdownPreviewUrl,
markdownDocs,
projectsAutocompleteUrl,
} = issuableElement.dataset;
return {
canUpdate: gl.utils.convertPermissionToBoolean(canUpdate),
canDestroy: gl.utils.convertPermissionToBoolean(canDestroy),
canMove: gl.utils.convertPermissionToBoolean(canMove),
endpoint,
issuableRef,
initialTitle: issuableTitleElement.innerHTML,
......@@ -39,6 +42,7 @@ document.addEventListener('DOMContentLoaded', () => {
initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
markdownPreviewUrl,
markdownDocs,
projectsAutocompleteUrl,
};
},
render(createElement) {
......@@ -46,6 +50,7 @@ document.addEventListener('DOMContentLoaded', () => {
props: {
canUpdate: this.canUpdate,
canDestroy: this.canDestroy,
canMove: this.canMove,
endpoint: this.endpoint,
issuableRef: this.issuableRef,
initialTitle: this.initialTitle,
......@@ -53,6 +58,7 @@ document.addEventListener('DOMContentLoaded', () => {
initialDescriptionText: this.initialDescriptionText,
markdownPreviewUrl: this.markdownPreviewUrl,
markdownDocs: this.markdownDocs,
projectsAutocompleteUrl: this.projectsAutocompleteUrl,
},
});
},
......
......@@ -7,7 +7,7 @@ export default class Service {
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(this.endpoint, {}, {
this.resource = Vue.resource(`${this.endpoint}.json`, {}, {
realtimeChanges: {
method: 'GET',
url: `${this.endpoint}/realtime_changes`,
......
......@@ -15,6 +15,7 @@ export default class Store {
this.formState = {
title: '',
description: '',
move_to_project_id: 0,
};
}
......
......@@ -148,10 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController
format.json do
if @issue.valid?
render json: @issue.to_json(methods: [:task_status, :task_status_short],
include: { milestone: {},
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { methods: :text_color } })
render json: IssueSerializer.new.represent(@issue)
else
render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
end
......
class IssueEntity < IssuableEntity
include RequestAwareEntity
expose :branch_name
expose :confidential
expose :assignees, using: API::Entities::UserBasic
......@@ -7,4 +9,8 @@ class IssueEntity < IssuableEntity
expose :project_id
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
expose :path do |issue|
namespace_project_issue_path(issue.project.namespace, issue.project, issue)
end
end
......@@ -54,9 +54,11 @@
#js-issuable-app{ "data" => { "endpoint" => namespace_project_issue_path(@project.namespace, @project, @issue),
"can-update" => can?(current_user, :update_issue, @issue).to_s,
"can-destroy" => can?(current_user, :destroy_issue, @issue).to_s,
"can-move" => @issue.can_move?(current_user).to_s,
"issuable-ref" => @issue.to_reference,
"markdown-preview-url" => preview_markdown_path(@project),
"markdown-docs" => help_page_path('user/markdown'),
"projects-autocomplete-url" => autocomplete_projects_path(project_id: @project.id),
} }
%h2.title= markdown_field(@issue, :title)
- if @issue.description.present?
......
......@@ -29,12 +29,15 @@ describe('Issuable output', () => {
propsData: {
canUpdate: true,
canDestroy: true,
canMove: true,
endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
issuableRef: '#1',
initialTitle: '',
initialDescriptionHtml: '',
initialDescriptionText: '',
showForm: false,
markdownPreviewUrl: '/',
markdownDocs: '/',
projectsAutocompleteUrl: '/',
},
}).$mount();
});
......@@ -108,6 +111,46 @@ describe('Issuable output', () => {
});
});
it('does not redirect if issue has not moved', (done) => {
spyOn(gl.utils, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve();
}));
vm.updateIssuable();
setTimeout(() => {
expect(
gl.utils.visitUrl,
).not.toHaveBeenCalled();
done();
});
});
it('redirects if issue is moved', (done) => {
spyOn(gl.utils, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
json() {
return {
path: '/testing-issue-move',
};
},
});
}));
vm.updateIssuable();
setTimeout(() => {
expect(
gl.utils.visitUrl,
).toHaveBeenCalledWith('/testing-issue-move');
done();
});
});
it('closes form on error', (done) => {
spyOn(window, 'Flash').and.callThrough();
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
......
import Vue from 'vue';
import projectMove from '~/issue_show/components/fields/project_move.vue';
describe('Project move field component', () => {
let vm;
let formState;
beforeEach((done) => {
const Component = Vue.extend(projectMove);
formState = {
move_to_project_id: 0,
};
vm = new Component({
propsData: {
formState,
projectsAutocompleteUrl: '/autocomplete',
},
}).$mount();
Vue.nextTick(done);
});
it('mounts select2 element', () => {
expect(
vm.$el.querySelector('.select2-container'),
).not.toBeNull();
});
it('updates formState on change', () => {
$(vm.$refs['move-dropdown']).val(2).trigger('change');
expect(
formState.move_to_project_id,
).toBe(2);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment