Added move to project in issue inline edit form
[ci skip]
This commit is contained in:
parent
4fcff0bfa2
commit
907dd68e0a
11 changed files with 215 additions and 8 deletions
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue