summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2017-05-17 13:01:24 +0100
committerPhil Hughes <me@iamphill.com>2017-05-17 13:01:24 +0100
commit907dd68e0a18e995dc5772c9bc8117aa150edc25 (patch)
treef44f01a72ac5b305f7a2fc0dd3c90b992c2db165
parent4fcff0bfa2f0d8b0a9f60e93bee807334557918f (diff)
downloadgitlab-ce-907dd68e0a18e995dc5772c9bc8117aa150edc25.tar.gz
Added move to project in issue inline edit form
[ci skip]
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue20
-rw-r--r--app/assets/javascripts/issue_show/components/fields/project_move.vue84
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue14
-rw-r--r--app/assets/javascripts/issue_show/index.js6
-rw-r--r--app/assets/javascripts/issue_show/services/index.js2
-rw-r--r--app/assets/javascripts/issue_show/stores/index.js1
-rw-r--r--app/controllers/projects/issues_controller.rb5
-rw-r--r--app/serializers/issue_entity.rb6
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js45
-rw-r--r--spec/javascripts/issue_show/components/fields/project_move_spec.js38
11 files changed, 215 insertions, 8 deletions
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index eb594cfb60b..9d5e69585f1 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -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"
diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue
new file mode 100644
index 00000000000..701c7f0ea9b
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/fields/project_move.vue
@@ -0,0 +1,84 @@
+<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>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 7732a1c194a..4cfc95e9888 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -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>
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index d13e24a468b..666d144f958 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -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,
},
});
},
diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js
index 0ceff34cf8b..6f0fd0b1768 100644
--- a/app/assets/javascripts/issue_show/services/index.js
+++ b/app/assets/javascripts/issue_show/services/index.js
@@ -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`,
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
index 3232875000d..5717f0a8e74 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -15,6 +15,7 @@ export default class Store {
this.formState = {
title: '',
description: '',
+ move_to_project_id: 0,
};
}
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 46438e68d54..9d28a7ed85a 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -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
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index bc4f68710b2..6bc8d0f70c3 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -1,4 +1,6 @@
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
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 2b095648dcf..a35de038f2f 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -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?
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 22b0a0f7046..700910c241a 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -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) => {
diff --git a/spec/javascripts/issue_show/components/fields/project_move_spec.js b/spec/javascripts/issue_show/components/fields/project_move_spec.js
new file mode 100644
index 00000000000..86d35c33ff4
--- /dev/null
+++ b/spec/javascripts/issue_show/components/fields/project_move_spec.js
@@ -0,0 +1,38 @@
+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);
+ });
+});