summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorEric Eastwood <contact@ericeastwood.com>2017-08-14 02:26:19 -0500
committerEric Eastwood <contact@ericeastwood.com>2017-09-03 22:03:17 -0500
commit90c60138db4e1f86026aac5760febe4ba066ca30 (patch)
treed08764bc1f19556a528bd43f5cc932fa552e7198 /app
parenta3af683045e0170d975eab2562a466f88d2692b8 (diff)
downloadgitlab-ce-90c60138db4e1f86026aac5760febe4ba066ca30.tar.gz
Move "Move to different project" to sidebar34261-move-move-to-sidebar
Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/34261
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/gl_dropdown.js8
-rw-r--r--app/assets/javascripts/issuable_form.js52
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue21
-rw-r--r--app/assets/javascripts/issue_show/components/fields/project_move.vue83
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue14
-rw-r--r--app/assets/javascripts/issue_show/index.js2
-rw-r--r--app/assets/javascripts/issue_show/stores/index.js1
-rw-r--r--app/assets/javascripts/right_sidebar.js9
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.js2
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js85
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js20
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js7
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js29
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js10
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss7
-rw-r--r--app/assets/stylesheets/pages/issuable.scss20
-rw-r--r--app/controllers/autocomplete_controller.rb6
-rw-r--r--app/controllers/projects/issues_controller.rb40
-rw-r--r--app/helpers/dropdowns_helper.rb6
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/views/projects/boards/components/sidebar/_due_date.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml2
-rw-r--r--app/views/shared/icons/_icon_arrow_right.svg.erb1
-rw-r--r--app/views/shared/issuable/_form.html.haml12
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml23
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/form/_issue_assignee.html.haml2
-rw-r--r--app/views/shared/issuable/form/_merge_request_assignee.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml4
30 files changed, 248 insertions, 230 deletions
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index b62acfcd445..d65bbc0d808 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -486,7 +486,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.shouldPropagate = function(e) {
var $target;
- if (this.options.multiSelect) {
+ if (this.options.multiSelect || this.options.shouldPropagate === false) {
$target = $(e.target);
if ($target && !$target.hasClass('dropdown-menu-close') &&
!$target.hasClass('dropdown-menu-close-icon') &&
@@ -546,10 +546,10 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.positionMenuAbove = function() {
- var $button = $(this.el);
var $menu = this.dropdown.find('.dropdown-menu');
- $menu.css('top', ($button.height() + $menu.height()) * -1);
+ $menu.css('top', 'initial');
+ $menu.css('bottom', '100%');
};
GitLabDropdown.prototype.hidden = function(e) {
@@ -698,7 +698,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.noResults = function() {
var html;
- return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
+ return html = '<li class="dropdown-menu-empty-link"><a href="#" class="is-focused">No matching results</a></li>';
};
GitLabDropdown.prototype.rowClicked = function(el) {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 3f848e0859b..470c39c6f76 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -10,8 +10,6 @@ import ZenMode from './zen_mode';
(function() {
this.IssuableForm = (function() {
- IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
-
IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
function IssuableForm(form) {
@@ -26,7 +24,6 @@ import ZenMode from './zen_mode';
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
this.descriptionField = this.form.find("textarea[name*='[description]']");
- this.issueMoveField = this.form.find("#move_to_project_id");
if (!(this.titleField.length && this.descriptionField.length)) {
return;
}
@@ -34,7 +31,6 @@ import ZenMode from './zen_mode';
this.form.on("submit", this.handleSubmit);
this.form.on("click", ".btn-cancel", this.resetAutosave);
this.initWip();
- this.initMoveDropdown();
$issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) {
calendar = new Pikaday({
@@ -56,12 +52,6 @@ import ZenMode from './zen_mode';
};
IssuableForm.prototype.handleSubmit = function() {
- var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null;
- if ((parseInt(fieldId, 10) || 0) > 0) {
- if (!confirm(this.issueMoveConfirmMsg)) {
- return false;
- }
- }
return this.resetAutosave();
};
@@ -113,48 +103,6 @@ import ZenMode from './zen_mode';
return this.titleField.val("WIP: " + (this.titleField.val()));
};
- IssuableForm.prototype.initMoveDropdown = function() {
- var $moveDropdown, pageSize;
- $moveDropdown = $('.js-move-dropdown');
- if ($moveDropdown.length) {
- pageSize = $moveDropdown.data('page-size');
- return $('.js-move-dropdown').select2({
- ajax: {
- url: $moveDropdown.data('projects-url'),
- quietMillis: 125,
- data: function(term, page, context) {
- return {
- search: term,
- offset_id: context
- };
- },
- results: function(data) {
- var context,
- more;
-
- if (data.length >= pageSize)
- more = true;
-
- if (data[data.length - 1])
- context = data[data.length - 1].id;
-
- return {
- results: data,
- more: more,
- context: context
- };
- }
- },
- formatResult: function(project) {
- return project.name_with_namespace;
- },
- formatSelection: function(project) {
- return project.name_with_namespace;
- }
- });
- }
- };
-
return IssuableForm;
})();
}).call(window);
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index eaaafd4c149..e115ee40219 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -17,10 +17,6 @@ export default {
required: true,
type: String,
},
- canMove: {
- required: true,
- type: Boolean,
- },
canUpdate: {
required: true,
type: Boolean,
@@ -96,10 +92,6 @@ export default {
type: String,
required: true,
},
- projectsAutocompletePath: {
- type: String,
- required: true,
- },
},
data() {
const store = new Store({
@@ -142,7 +134,6 @@ export default {
confidential: this.isConfidential,
description: this.state.descriptionText,
lockedWarningVisible: false,
- move_to_project_id: 0,
updateLoading: false,
});
}
@@ -151,16 +142,6 @@ export default {
this.showForm = false;
},
updateIssuable() {
- const canPostUpdate = this.store.formState.move_to_project_id !== 0 ?
- confirm('Are you sure you want to move this issue to another project?') : true; // eslint-disable-line no-alert
-
- if (!canPostUpdate) {
- this.store.setFormState({
- updateLoading: false,
- });
- return;
- }
-
this.service.updateIssuable(this.store.formState)
.then(res => res.json())
.then((data) => {
@@ -239,14 +220,12 @@ export default {
<form-component
v-if="canUpdate && showForm"
:form-state="formState"
- :can-move="canMove"
:can-destroy="canDestroy"
:issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
- :projects-autocomplete-path="projectsAutocompletePath"
/>
<div v-else>
<title-component
diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue
deleted file mode 100644
index e514bebc5f6..00000000000
--- a/app/assets/javascripts/issue_show/components/fields/project_move.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<script>
- import tooltip from '../../../vue_shared/directives/tooltip';
-
- export default {
- directives: {
- tooltip,
- },
- props: {
- formState: {
- type: Object,
- required: true,
- },
- projectsAutocompletePath: {
- type: String,
- required: true,
- },
- },
- mounted() {
- const $moveDropdown = $(this.$refs['move-dropdown']);
-
- $moveDropdown.select2({
- ajax: {
- url: this.projectsAutocompletePath,
- 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
- v-tooltip
- data-placement="auto top"
- 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.">
- <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 d9b53bc55cf..6a2dd502fe2 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -4,15 +4,10 @@
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue';
- import projectMove from './fields/project_move.vue';
import confidentialCheckbox from './fields/confidential_checkbox.vue';
export default {
props: {
- canMove: {
- type: Boolean,
- required: true,
- },
canDestroy: {
type: Boolean,
required: true,
@@ -42,10 +37,6 @@
type: String,
required: true,
},
- projectsAutocompletePath: {
- type: String,
- required: true,
- },
},
components: {
lockedWarning,
@@ -53,7 +44,6 @@
descriptionField,
descriptionTemplate,
editActions,
- projectMove,
confidentialCheckbox,
},
computed: {
@@ -93,10 +83,6 @@
:markdown-docs-path="markdownDocsPath" />
<confidential-checkbox
:form-state="formState" />
- <project-move
- v-if="canMove"
- :form-state="formState"
- :projects-autocomplete-path="projectsAutocompletePath" />
<edit-actions
:form-state="formState"
:can-destroy="canDestroy" />
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index 60b69b300fd..8053ef57e6c 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -28,7 +28,6 @@ document.addEventListener('DOMContentLoaded', () => {
props: {
canUpdate: this.canUpdate,
canDestroy: this.canDestroy,
- canMove: this.canMove,
endpoint: this.endpoint,
issuableRef: this.issuableRef,
initialTitleHtml: this.initialTitleHtml,
@@ -41,7 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
markdownDocsPath: this.markdownDocsPath,
projectPath: this.projectPath,
projectNamespace: this.projectNamespace,
- projectsAutocompletePath: this.projectsAutocompletePath,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
index 0c8bd6f1cc3..f4639e9ed2a 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -6,7 +6,6 @@ export default class Store {
confidential: false,
description: '',
lockedWarningVisible: false,
- move_to_project_id: 0,
updateLoading: false,
};
}
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index fa958d75fa4..4c87d46c96e 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -157,11 +157,16 @@ import SidebarHeightManager from './sidebar_height_manager';
Sidebar.prototype.openDropdown = function(blockOrName) {
var $block;
$block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
- $block.find('.edit-link').trigger('click');
if (!this.isOpen()) {
this.setCollapseAfterUpdate($block);
- return this.toggleSidebar('open');
+ this.toggleSidebar('open');
}
+
+ // Wait for the sidebar to trigger('click') open
+ // so it doesn't cause our dropdown to close preemptively
+ setTimeout(() => {
+ $block.find('.js-sidebar-dropdown-toggle').trigger('click');
+ });
};
Sidebar.prototype.setCollapseAfterUpdate = function($block) {
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js
index 5a6e47e566e..77f070d48cc 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js
@@ -36,7 +36,7 @@ export default {
/>
<a
v-if="editable"
- class="edit-link pull-right"
+ class="js-sidebar-dropdown-toggle edit-link pull-right"
href="#"
>
Edit
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
new file mode 100644
index 00000000000..1c15a1b877a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -0,0 +1,85 @@
+/* global Flash */
+
+function isValidProjectId(id) {
+ return id > 0;
+}
+
+class SidebarMoveIssue {
+ constructor(mediator, dropdownToggle, confirmButton) {
+ this.mediator = mediator;
+
+ this.$dropdownToggle = $(dropdownToggle);
+ this.$confirmButton = $(confirmButton);
+
+ this.onConfirmClickedWrapper = this.onConfirmClicked.bind(this);
+ }
+
+ init() {
+ this.initDropdown();
+ this.addEventListeners();
+ }
+
+ destroy() {
+ this.removeEventListeners();
+ }
+
+ initDropdown() {
+ this.$dropdownToggle.glDropdown({
+ search: {
+ fields: ['name_with_namespace'],
+ },
+ showMenuAbove: true,
+ selectable: true,
+ filterable: true,
+ filterRemote: true,
+ multiSelect: false,
+ // Keep the dropdown open after selecting an option
+ shouldPropagate: false,
+ data: (searchTerm, callback) => {
+ this.mediator.fetchAutocompleteProjects(searchTerm)
+ .then(callback)
+ .catch(() => new Flash('An error occured while fetching projects autocomplete.'));
+ },
+ renderRow: project => `
+ <li>
+ <a href="#" class="js-move-issue-dropdown-item">
+ ${project.name_with_namespace}
+ </a>
+ </li>
+ `,
+ clicked: (options) => {
+ const project = options.selectedObj;
+ const selectedProjectId = options.isMarking ? project.id : 0;
+ this.mediator.setMoveToProjectId(selectedProjectId);
+
+ this.$confirmButton.attr('disabled', !isValidProjectId(selectedProjectId));
+ },
+ });
+ }
+
+ addEventListeners() {
+ this.$confirmButton.on('click', this.onConfirmClickedWrapper);
+ }
+
+ removeEventListeners() {
+ this.$confirmButton.off('click', this.onConfirmClickedWrapper);
+ }
+
+ onConfirmClicked() {
+ if (isValidProjectId(this.mediator.store.moveToProjectId)) {
+ this.$confirmButton
+ .disable()
+ .addClass('is-loading');
+
+ this.mediator.moveIssue()
+ .catch(() => {
+ Flash('An error occured while moving the issue.');
+ this.$confirmButton
+ .enable()
+ .removeClass('is-loading');
+ });
+ }
+ }
+}
+
+export default SidebarMoveIssue;
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 5a82d01dc41..604648407a4 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -4,9 +4,11 @@ import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class SidebarService {
- constructor(endpoint) {
+ constructor(endpointMap) {
if (!SidebarService.singleton) {
- this.endpoint = endpoint;
+ this.endpoint = endpointMap.endpoint;
+ this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
+ this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
SidebarService.singleton = this;
}
@@ -25,4 +27,18 @@ export default class SidebarService {
emulateJSON: true,
});
}
+
+ getProjectsAutocomplete(searchTerm) {
+ return Vue.http.get(this.projectsAutocompleteEndpoint, {
+ params: {
+ search: searchTerm,
+ },
+ });
+ }
+
+ moveIssue(moveToProjectId) {
+ return Vue.http.post(this.moveIssueEndpoint, {
+ move_to_project_id: moveToProjectId,
+ });
+ }
}
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 9edded3ead6..3d8972050a9 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
import sidebarAssignees from './components/assignees/sidebar_assignees';
import confidential from './components/confidential/confidential_issue_sidebar.vue';
+import SidebarMoveIssue from './lib/sidebar_move_issue';
import Mediator from './sidebar_mediator';
@@ -31,6 +32,12 @@ function domContentLoaded() {
service: mediator.service,
},
}).$mount(confidentialEl);
+
+ new SidebarMoveIssue(
+ mediator,
+ $('.js-move-issue'),
+ $('.js-move-issue-confirmation-button'),
+ ).init();
}
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 721e92221cf..e38a8db4cc5 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -7,7 +7,11 @@ export default class SidebarMediator {
constructor(options) {
if (!SidebarMediator.singleton) {
this.store = new Store(options);
- this.service = new Service(options.endpoint);
+ this.service = new Service({
+ endpoint: options.endpoint,
+ moveIssueEndpoint: options.moveIssueEndpoint,
+ projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
+ });
SidebarMediator.singleton = this;
}
@@ -26,6 +30,10 @@ export default class SidebarMediator {
return this.service.update(field, selected.length === 0 ? [0] : selected);
}
+ setMoveToProjectId(projectId) {
+ this.store.setMoveToProjectId(projectId);
+ }
+
fetch() {
this.service.get()
.then(response => response.json())
@@ -35,4 +43,23 @@ export default class SidebarMediator {
})
.catch(() => new Flash('Error occured when fetching sidebar data'));
}
+
+ fetchAutocompleteProjects(searchTerm) {
+ return this.service.getProjectsAutocomplete(searchTerm)
+ .then(response => response.json())
+ .then((data) => {
+ this.store.setAutocompleteProjects(data);
+ return this.store.autocompleteProjects;
+ });
+ }
+
+ moveIssue() {
+ return this.service.moveIssue(this.store.moveToProjectId)
+ .then(response => response.json())
+ .then((data) => {
+ if (location.pathname !== data.web_url) {
+ gl.utils.visitUrl(data.web_url);
+ }
+ });
+ }
}
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 3356dd0191f..cc04a2a3fcf 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -13,6 +13,8 @@ export default class SidebarStore {
this.isFetching = {
assignees: true,
};
+ this.autocompleteProjects = [];
+ this.moveToProjectId = 0;
SidebarStore.singleton = this;
}
@@ -53,4 +55,12 @@ export default class SidebarStore {
removeAllAssignees() {
this.assignees = [];
}
+
+ setAutocompleteProjects(projects) {
+ this.autocompleteProjects = projects;
+ }
+
+ setMoveToProjectId(moveToProjectId) {
+ this.moveToProjectId = moveToProjectId;
+ }
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 110b171676a..487b3148b14 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -193,7 +193,7 @@
min-width: 240px;
max-width: 500px;
margin-top: 2px;
- margin-bottom: 0;
+ margin-bottom: 2px;
font-size: 14px;
font-weight: $gl-font-weight-normal;
padding: 8px 0;
@@ -622,6 +622,11 @@
border-top: 1px solid $dropdown-divider-color;
}
+.dropdown-footer-content {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
.dropdown-due-date-footer {
padding-top: 0;
margin-left: 10px;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 4b0b238a767..6523376ccc3 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -473,7 +473,7 @@
padding-top: 6px;
}
- .open .dropdown-menu {
+ .dropdown-menu {
width: 100%;
}
}
@@ -486,6 +486,24 @@
}
}
+.sidebar-move-issue-dropdown {
+ @include new-style-dropdown;
+}
+
+.sidebar-move-issue-confirmation-button {
+ width: 100%;
+
+ &.is-loading {
+ .sidebar-move-issue-confirmation-loading-icon {
+ display: inline-block;
+ }
+ }
+}
+
+.sidebar-move-issue-confirmation-loading-icon {
+ display: none;
+}
+
.detail-page-description {
padding: 16px 0;
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 59be955599d..dfc8bd0ba81 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -41,12 +41,6 @@ class AutocompleteController < ApplicationController
project = Project.find_by_id(params[:project_id])
projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id])
- no_project = {
- id: 0,
- name_with_namespace: 'No project'
- }
- projects.unshift(no_project) unless params[:offset_id].present?
-
render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 349b19f72e2..0d4266f0899 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -15,7 +15,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue
- before_action :authorize_update_issue!, only: [:edit, :update]
+ before_action :authorize_update_issue!, only: [:edit, :update, :move]
# Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request]
@@ -142,25 +142,33 @@ class Projects::IssuesController < Projects::ApplicationController
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
+ respond_to do |format|
+ format.html do
+ recaptcha_check_with_fallback { render :edit }
+ end
+
+ format.json do
+ render_issue_json
+ end
+ end
+
+ rescue ActiveRecord::StaleObjectError
+ render_conflict_response
+ end
+
+ def move
+ params.require(:move_to_project_id)
+
if params[:move_to_project_id].to_i > 0
new_project = Project.find(params[:move_to_project_id])
return render_404 unless issue.can_move?(current_user, new_project)
- move_service = Issues::MoveService.new(project, current_user)
- @issue = move_service.execute(@issue, new_project)
+ @issue = Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue)
end
respond_to do |format|
- format.html do
- recaptcha_check_with_fallback { render :edit }
- end
-
format.json do
- if @issue.valid?
- render json: serializer.represent(@issue)
- else
- render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
- end
+ render_issue_json
end
end
@@ -271,6 +279,14 @@ class Projects::IssuesController < Projects::ApplicationController
return render_404 unless @project.feature_available?(:issues, current_user)
end
+ def render_issue_json
+ if @issue.valid?
+ render json: serializer.represent(@issue)
+ else
+ render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
+ end
+ end
+
def issue_params
params.require(:issue).permit(*issue_params_attributes)
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index ff305fa39b4..5089da519df 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -97,9 +97,11 @@ module DropdownsHelper
end
end
- def dropdown_footer(&block)
+ def dropdown_footer(add_content_class: false, &block)
content_tag(:div, class: "dropdown-footer") do
- if block
+ if add_content_class
+ content_tag(:div, capture(&block), class: "dropdown-footer-content")
+ else
capture(&block)
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 0fcd3347095..d81ba2c06eb 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -207,12 +207,10 @@ module IssuablesHelper
endpoint: project_issue_path(@project, issuable),
canUpdate: can?(current_user, :update_issue, issuable),
canDestroy: can?(current_user, :destroy_issue, issuable),
- canMove: current_user ? issuable.can_move?(current_user) : false,
issuableRef: issuable.to_reference,
isConfidential: issuable.confidential,
markdownPreviewPath: preview_markdown_path(@project),
markdownDocsPath: help_page_path('user/markdown'),
- projectsAutocompletePath: autocomplete_projects_path(project_id: @project.id),
issuableTemplates: issuable_templates(issuable),
projectPath: ref_project.path,
projectNamespace: ref_project.namespace.full_path,
@@ -354,6 +352,8 @@ module IssuablesHelper
def issuable_sidebar_options(issuable, can_edit_issuable)
{
endpoint: "#{issuable_json_path(issuable)}?basic=true",
+ moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
+ projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
editable: can_edit_issuable,
currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url),
rootPath: root_path,
diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml
index f44a9d49a54..e8394eab213 100644
--- a/app/views/projects/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml
@@ -3,7 +3,7 @@
Due date
- if can?(current_user, :admin_issue, @project)
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "edit-link pull-right"
+ = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value
.value-content
%span.no-value{ "v-if" => "!issue.dueDate" }
diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml
index 7d0c35fe183..6b389736e8b 100644
--- a/app/views/projects/boards/components/sidebar/_labels.html.haml
+++ b/app/views/projects/boards/components/sidebar/_labels.html.haml
@@ -3,7 +3,7 @@
Labels
- if can?(current_user, :admin_issue, @project)
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "edit-link pull-right"
+ = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value.issuable-show-labels
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
None
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
index 002e9994ee0..a1ddb261ea3 100644
--- a/app/views/projects/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -3,7 +3,7 @@
Milestone
- if can?(current_user, :admin_issue, @project)
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "edit-link pull-right"
+ = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value
%span.no-value{ "v-if" => "!issue.milestone" }
None
diff --git a/app/views/shared/icons/_icon_arrow_right.svg.erb b/app/views/shared/icons/_icon_arrow_right.svg.erb
new file mode 100644
index 00000000000..24d64eb73bd
--- /dev/null
+++ b/app/views/shared/icons/_icon_arrow_right.svg.erb
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></svg>
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index c016aa2abcd..bb02dfa0d3a 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -29,18 +29,6 @@
= render 'shared/issuable/form/metadata', issuable: issuable, form: form
-- if issuable.can_move?(current_user)
- %hr
- .form-group
- = label_tag :move_to_project_id, 'Move', class: 'control-label'
- .col-sm-10
- .issuable-form-select-holder
- = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE }
- &nbsp;
- %span{ data: { toggle: 'tooltip', 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.' }
- = icon('question-circle')
-
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
= render 'shared/issuable/form/merge_params', issuable: issuable
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index c3f25c9d255..b07bc45512f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -34,7 +34,7 @@
Milestone
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if issuable.milestone
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 }
@@ -60,7 +60,7 @@
Due date
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
%span.value-content
- if issuable.due_date
@@ -95,7 +95,7 @@
Labels
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label|
@@ -141,5 +141,22 @@
%cite{ title: project_ref }
= project_ref
= clipboard_button(text: project_ref, title: "Copy reference to clipboard", placement: "left")
+ - if current_user && issuable.can_move?(current_user)
+ .block.js-sidebar-move-issue-block
+ .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body' }, title: 'Move issue' }
+ = custom_icon('icon_arrow_right')
+ .dropdown.sidebar-move-issue-dropdown.hide-collapsed
+ %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
+ data: { toggle: 'dropdown' } }
+ Move issue
+ .dropdown-menu.dropdown-menu-selectable
+ = dropdown_title('Move issue')
+ = dropdown_filter('Search project', search_id: 'sidebar-move-issue-dropdown-search')
+ = dropdown_content
+ = dropdown_loading
+ = dropdown_footer add_content_class: true do
+ %button.btn.btn-new.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ disabled: true }
+ Move
+ = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
%script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 57392cd7fbb..58782fa5f58 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -13,7 +13,7 @@
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
- if !signed_in
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
= sidebar_gutter_toggle_icon
diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml
index 66091d95a91..9b2b6e572e7 100644
--- a/app/views/shared/issuable/form/_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_issue_assignee.html.haml
@@ -11,7 +11,7 @@
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if assignees.any?
- assignees.each do |assignee|
diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
index 18011d528a0..bf8613b0f0d 100644
--- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml
+++ b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
@@ -9,7 +9,7 @@
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 40379f48393..2ae0145727c 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -21,7 +21,7 @@
.title
Start date
- if @project && can?(current_user, :admin_milestone, @project)
- = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'edit-link pull-right'
+ = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value
%span.value-content
- if milestone.start_date
@@ -51,7 +51,7 @@
.title.hide-collapsed
Due date
- if @project && can?(current_user, :admin_milestone, @project)
- = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'edit-link pull-right'
+ = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
%span.value-content
- if milestone.due_date