Skip to content
Snippets Groups Projects
Commit 90c60138 authored by Eric Eastwood's avatar Eric Eastwood
Browse files

Move "Move to different project" to sidebar

parent a3af6830
No related branches found
No related tags found
No related merge requests found
Showing
with 219 additions and 207 deletions
Loading
@@ -486,7 +486,7 @@ GitLabDropdown = (function() {
Loading
@@ -486,7 +486,7 @@ GitLabDropdown = (function() {
   
GitLabDropdown.prototype.shouldPropagate = function(e) { GitLabDropdown.prototype.shouldPropagate = function(e) {
var $target; var $target;
if (this.options.multiSelect) { if (this.options.multiSelect || this.options.shouldPropagate === false) {
$target = $(e.target); $target = $(e.target);
if ($target && !$target.hasClass('dropdown-menu-close') && if ($target && !$target.hasClass('dropdown-menu-close') &&
!$target.hasClass('dropdown-menu-close-icon') && !$target.hasClass('dropdown-menu-close-icon') &&
Loading
@@ -546,10 +546,10 @@ GitLabDropdown = (function() {
Loading
@@ -546,10 +546,10 @@ GitLabDropdown = (function() {
}; };
   
GitLabDropdown.prototype.positionMenuAbove = function() { GitLabDropdown.prototype.positionMenuAbove = function() {
var $button = $(this.el);
var $menu = this.dropdown.find('.dropdown-menu'); 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) { GitLabDropdown.prototype.hidden = function(e) {
Loading
@@ -698,7 +698,7 @@ GitLabDropdown = (function() {
Loading
@@ -698,7 +698,7 @@ GitLabDropdown = (function() {
   
GitLabDropdown.prototype.noResults = function() { GitLabDropdown.prototype.noResults = function() {
var html; 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) { GitLabDropdown.prototype.rowClicked = function(el) {
Loading
Loading
Loading
@@ -10,8 +10,6 @@ import ZenMode from './zen_mode';
Loading
@@ -10,8 +10,6 @@ import ZenMode from './zen_mode';
   
(function() { (function() {
this.IssuableForm = (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; IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
   
function IssuableForm(form) { function IssuableForm(form) {
Loading
@@ -26,7 +24,6 @@ import ZenMode from './zen_mode';
Loading
@@ -26,7 +24,6 @@ import ZenMode from './zen_mode';
new ZenMode(); new ZenMode();
this.titleField = this.form.find("input[name*='[title]']"); this.titleField = this.form.find("input[name*='[title]']");
this.descriptionField = this.form.find("textarea[name*='[description]']"); this.descriptionField = this.form.find("textarea[name*='[description]']");
this.issueMoveField = this.form.find("#move_to_project_id");
if (!(this.titleField.length && this.descriptionField.length)) { if (!(this.titleField.length && this.descriptionField.length)) {
return; return;
} }
Loading
@@ -34,7 +31,6 @@ import ZenMode from './zen_mode';
Loading
@@ -34,7 +31,6 @@ import ZenMode from './zen_mode';
this.form.on("submit", this.handleSubmit); this.form.on("submit", this.handleSubmit);
this.form.on("click", ".btn-cancel", this.resetAutosave); this.form.on("click", ".btn-cancel", this.resetAutosave);
this.initWip(); this.initWip();
this.initMoveDropdown();
$issuableDueDate = $('#issuable-due-date'); $issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) { if ($issuableDueDate.length) {
calendar = new Pikaday({ calendar = new Pikaday({
Loading
@@ -56,12 +52,6 @@ import ZenMode from './zen_mode';
Loading
@@ -56,12 +52,6 @@ import ZenMode from './zen_mode';
}; };
   
IssuableForm.prototype.handleSubmit = function() { 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(); return this.resetAutosave();
}; };
   
Loading
@@ -113,48 +103,6 @@ import ZenMode from './zen_mode';
Loading
@@ -113,48 +103,6 @@ import ZenMode from './zen_mode';
return this.titleField.val("WIP: " + (this.titleField.val())); 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; return IssuableForm;
})(); })();
}).call(window); }).call(window);
Loading
@@ -17,10 +17,6 @@ export default {
Loading
@@ -17,10 +17,6 @@ export default {
required: true, required: true,
type: String, type: String,
}, },
canMove: {
required: true,
type: Boolean,
},
canUpdate: { canUpdate: {
required: true, required: true,
type: Boolean, type: Boolean,
Loading
@@ -96,10 +92,6 @@ export default {
Loading
@@ -96,10 +92,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectsAutocompletePath: {
type: String,
required: true,
},
}, },
data() { data() {
const store = new Store({ const store = new Store({
Loading
@@ -142,7 +134,6 @@ export default {
Loading
@@ -142,7 +134,6 @@ export default {
confidential: this.isConfidential, confidential: this.isConfidential,
description: this.state.descriptionText, description: this.state.descriptionText,
lockedWarningVisible: false, lockedWarningVisible: false,
move_to_project_id: 0,
updateLoading: false, updateLoading: false,
}); });
} }
Loading
@@ -151,16 +142,6 @@ export default {
Loading
@@ -151,16 +142,6 @@ export default {
this.showForm = false; this.showForm = false;
}, },
updateIssuable() { 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) this.service.updateIssuable(this.store.formState)
.then(res => res.json()) .then(res => res.json())
.then((data) => { .then((data) => {
Loading
@@ -239,14 +220,12 @@ export default {
Loading
@@ -239,14 +220,12 @@ export default {
<form-component <form-component
v-if="canUpdate && showForm" v-if="canUpdate && showForm"
:form-state="formState" :form-state="formState"
:can-move="canMove"
:can-destroy="canDestroy" :can-destroy="canDestroy"
:issuable-templates="issuableTemplates" :issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:project-path="projectPath" :project-path="projectPath"
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:projects-autocomplete-path="projectsAutocompletePath"
/> />
<div v-else> <div v-else>
<title-component <title-component
Loading
Loading
<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>
Loading
@@ -4,15 +4,10 @@
Loading
@@ -4,15 +4,10 @@
import descriptionField from './fields/description.vue'; import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue'; import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue'; import descriptionTemplate from './fields/description_template.vue';
import projectMove from './fields/project_move.vue';
import confidentialCheckbox from './fields/confidential_checkbox.vue'; import confidentialCheckbox from './fields/confidential_checkbox.vue';
   
export default { export default {
props: { props: {
canMove: {
type: Boolean,
required: true,
},
canDestroy: { canDestroy: {
type: Boolean, type: Boolean,
required: true, required: true,
Loading
@@ -42,10 +37,6 @@
Loading
@@ -42,10 +37,6 @@
type: String, type: String,
required: true, required: true,
}, },
projectsAutocompletePath: {
type: String,
required: true,
},
}, },
components: { components: {
lockedWarning, lockedWarning,
Loading
@@ -53,7 +44,6 @@
Loading
@@ -53,7 +44,6 @@
descriptionField, descriptionField,
descriptionTemplate, descriptionTemplate,
editActions, editActions,
projectMove,
confidentialCheckbox, confidentialCheckbox,
}, },
computed: { computed: {
Loading
@@ -93,10 +83,6 @@
Loading
@@ -93,10 +83,6 @@
:markdown-docs-path="markdownDocsPath" /> :markdown-docs-path="markdownDocsPath" />
<confidential-checkbox <confidential-checkbox
:form-state="formState" /> :form-state="formState" />
<project-move
v-if="canMove"
:form-state="formState"
:projects-autocomplete-path="projectsAutocompletePath" />
<edit-actions <edit-actions
:form-state="formState" :form-state="formState"
:can-destroy="canDestroy" /> :can-destroy="canDestroy" />
Loading
Loading
Loading
@@ -28,7 +28,6 @@ document.addEventListener('DOMContentLoaded', () => {
Loading
@@ -28,7 +28,6 @@ document.addEventListener('DOMContentLoaded', () => {
props: { props: {
canUpdate: this.canUpdate, canUpdate: this.canUpdate,
canDestroy: this.canDestroy, canDestroy: this.canDestroy,
canMove: this.canMove,
endpoint: this.endpoint, endpoint: this.endpoint,
issuableRef: this.issuableRef, issuableRef: this.issuableRef,
initialTitleHtml: this.initialTitleHtml, initialTitleHtml: this.initialTitleHtml,
Loading
@@ -41,7 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
Loading
@@ -41,7 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
markdownDocsPath: this.markdownDocsPath, markdownDocsPath: this.markdownDocsPath,
projectPath: this.projectPath, projectPath: this.projectPath,
projectNamespace: this.projectNamespace, projectNamespace: this.projectNamespace,
projectsAutocompletePath: this.projectsAutocompletePath,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
updatedByName: this.updatedByName, updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath, updatedByPath: this.updatedByPath,
Loading
Loading
Loading
@@ -6,7 +6,6 @@ export default class Store {
Loading
@@ -6,7 +6,6 @@ export default class Store {
confidential: false, confidential: false,
description: '', description: '',
lockedWarningVisible: false, lockedWarningVisible: false,
move_to_project_id: 0,
updateLoading: false, updateLoading: false,
}; };
} }
Loading
Loading
Loading
@@ -157,11 +157,16 @@ import SidebarHeightManager from './sidebar_height_manager';
Loading
@@ -157,11 +157,16 @@ import SidebarHeightManager from './sidebar_height_manager';
Sidebar.prototype.openDropdown = function(blockOrName) { Sidebar.prototype.openDropdown = function(blockOrName) {
var $block; var $block;
$block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName; $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
$block.find('.edit-link').trigger('click');
if (!this.isOpen()) { if (!this.isOpen()) {
this.setCollapseAfterUpdate($block); 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) { Sidebar.prototype.setCollapseAfterUpdate = function($block) {
Loading
Loading
Loading
@@ -36,7 +36,7 @@ export default {
Loading
@@ -36,7 +36,7 @@ export default {
/> />
<a <a
v-if="editable" v-if="editable"
class="edit-link pull-right" class="js-sidebar-dropdown-toggle edit-link pull-right"
href="#" href="#"
> >
Edit Edit
Loading
Loading
/* 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;
Loading
@@ -4,9 +4,11 @@ import VueResource from 'vue-resource';
Loading
@@ -4,9 +4,11 @@ import VueResource from 'vue-resource';
Vue.use(VueResource); Vue.use(VueResource);
   
export default class SidebarService { export default class SidebarService {
constructor(endpoint) { constructor(endpointMap) {
if (!SidebarService.singleton) { if (!SidebarService.singleton) {
this.endpoint = endpoint; this.endpoint = endpointMap.endpoint;
this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
   
SidebarService.singleton = this; SidebarService.singleton = this;
} }
Loading
@@ -25,4 +27,18 @@ export default class SidebarService {
Loading
@@ -25,4 +27,18 @@ export default class SidebarService {
emulateJSON: true, 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,
});
}
} }
Loading
@@ -2,6 +2,7 @@ import Vue from 'vue';
Loading
@@ -2,6 +2,7 @@ import Vue from 'vue';
import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
import sidebarAssignees from './components/assignees/sidebar_assignees'; import sidebarAssignees from './components/assignees/sidebar_assignees';
import confidential from './components/confidential/confidential_issue_sidebar.vue'; import confidential from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
   
import Mediator from './sidebar_mediator'; import Mediator from './sidebar_mediator';
   
Loading
@@ -31,6 +32,12 @@ function domContentLoaded() {
Loading
@@ -31,6 +32,12 @@ function domContentLoaded() {
service: mediator.service, service: mediator.service,
}, },
}).$mount(confidentialEl); }).$mount(confidentialEl);
new SidebarMoveIssue(
mediator,
$('.js-move-issue'),
$('.js-move-issue-confirmation-button'),
).init();
} }
   
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker'); new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
Loading
Loading
Loading
@@ -7,7 +7,11 @@ export default class SidebarMediator {
Loading
@@ -7,7 +7,11 @@ export default class SidebarMediator {
constructor(options) { constructor(options) {
if (!SidebarMediator.singleton) { if (!SidebarMediator.singleton) {
this.store = new Store(options); 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; SidebarMediator.singleton = this;
} }
   
Loading
@@ -26,6 +30,10 @@ export default class SidebarMediator {
Loading
@@ -26,6 +30,10 @@ export default class SidebarMediator {
return this.service.update(field, selected.length === 0 ? [0] : selected); return this.service.update(field, selected.length === 0 ? [0] : selected);
} }
   
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}
fetch() { fetch() {
this.service.get() this.service.get()
.then(response => response.json()) .then(response => response.json())
Loading
@@ -35,4 +43,23 @@ export default class SidebarMediator {
Loading
@@ -35,4 +43,23 @@ export default class SidebarMediator {
}) })
.catch(() => new Flash('Error occured when fetching sidebar data')); .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);
}
});
}
} }
Loading
@@ -13,6 +13,8 @@ export default class SidebarStore {
Loading
@@ -13,6 +13,8 @@ export default class SidebarStore {
this.isFetching = { this.isFetching = {
assignees: true, assignees: true,
}; };
this.autocompleteProjects = [];
this.moveToProjectId = 0;
   
SidebarStore.singleton = this; SidebarStore.singleton = this;
} }
Loading
@@ -53,4 +55,12 @@ export default class SidebarStore {
Loading
@@ -53,4 +55,12 @@ export default class SidebarStore {
removeAllAssignees() { removeAllAssignees() {
this.assignees = []; this.assignees = [];
} }
setAutocompleteProjects(projects) {
this.autocompleteProjects = projects;
}
setMoveToProjectId(moveToProjectId) {
this.moveToProjectId = moveToProjectId;
}
} }
Loading
@@ -193,7 +193,7 @@
Loading
@@ -193,7 +193,7 @@
min-width: 240px; min-width: 240px;
max-width: 500px; max-width: 500px;
margin-top: 2px; margin-top: 2px;
margin-bottom: 0; margin-bottom: 2px;
font-size: 14px; font-size: 14px;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
padding: 8px 0; padding: 8px 0;
Loading
@@ -622,6 +622,11 @@
Loading
@@ -622,6 +622,11 @@
border-top: 1px solid $dropdown-divider-color; border-top: 1px solid $dropdown-divider-color;
} }
   
.dropdown-footer-content {
padding-left: 10px;
padding-right: 10px;
}
.dropdown-due-date-footer { .dropdown-due-date-footer {
padding-top: 0; padding-top: 0;
margin-left: 10px; margin-left: 10px;
Loading
Loading
Loading
@@ -473,7 +473,7 @@
Loading
@@ -473,7 +473,7 @@
padding-top: 6px; padding-top: 6px;
} }
   
.open .dropdown-menu { .dropdown-menu {
width: 100%; width: 100%;
} }
} }
Loading
@@ -486,6 +486,24 @@
Loading
@@ -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 { .detail-page-description {
padding: 16px 0; padding: 16px 0;
   
Loading
Loading
Loading
@@ -41,12 +41,6 @@ class AutocompleteController < ApplicationController
Loading
@@ -41,12 +41,6 @@ class AutocompleteController < ApplicationController
project = Project.find_by_id(params[:project_id]) project = Project.find_by_id(params[:project_id])
projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_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) render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
end end
   
Loading
Loading
Loading
@@ -15,7 +15,7 @@ class Projects::IssuesController < Projects::ApplicationController
Loading
@@ -15,7 +15,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
   
# Allow modify issue # 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 # Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request] before_action :authorize_create_merge_request!, only: [:create_merge_request]
Loading
@@ -142,25 +142,33 @@ class Projects::IssuesController < Projects::ApplicationController
Loading
@@ -142,25 +142,33 @@ class Projects::IssuesController < Projects::ApplicationController
   
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) @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 if params[:move_to_project_id].to_i > 0
new_project = Project.find(params[:move_to_project_id]) new_project = Project.find(params[:move_to_project_id])
return render_404 unless issue.can_move?(current_user, new_project) return render_404 unless issue.can_move?(current_user, new_project)
   
move_service = Issues::MoveService.new(project, current_user) @issue = Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue)
@issue = move_service.execute(@issue, new_project)
end end
   
respond_to do |format| respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do format.json do
if @issue.valid? render_issue_json
render json: serializer.represent(@issue)
else
render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
end
end end
end end
   
Loading
@@ -271,6 +279,14 @@ class Projects::IssuesController < Projects::ApplicationController
Loading
@@ -271,6 +279,14 @@ class Projects::IssuesController < Projects::ApplicationController
return render_404 unless @project.feature_available?(:issues, current_user) return render_404 unless @project.feature_available?(:issues, current_user)
end 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 def issue_params
params.require(:issue).permit(*issue_params_attributes) params.require(:issue).permit(*issue_params_attributes)
end end
Loading
Loading
Loading
@@ -97,9 +97,11 @@ module DropdownsHelper
Loading
@@ -97,9 +97,11 @@ module DropdownsHelper
end end
end end
   
def dropdown_footer(&block) def dropdown_footer(add_content_class: false, &block)
content_tag(:div, class: "dropdown-footer") do 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) capture(&block)
end end
end end
Loading
Loading
Loading
@@ -207,12 +207,10 @@ module IssuablesHelper
Loading
@@ -207,12 +207,10 @@ module IssuablesHelper
endpoint: project_issue_path(@project, issuable), endpoint: project_issue_path(@project, issuable),
canUpdate: can?(current_user, :update_issue, issuable), canUpdate: can?(current_user, :update_issue, issuable),
canDestroy: can?(current_user, :destroy_issue, issuable), canDestroy: can?(current_user, :destroy_issue, issuable),
canMove: current_user ? issuable.can_move?(current_user) : false,
issuableRef: issuable.to_reference, issuableRef: issuable.to_reference,
isConfidential: issuable.confidential, isConfidential: issuable.confidential,
markdownPreviewPath: preview_markdown_path(@project), markdownPreviewPath: preview_markdown_path(@project),
markdownDocsPath: help_page_path('user/markdown'), markdownDocsPath: help_page_path('user/markdown'),
projectsAutocompletePath: autocomplete_projects_path(project_id: @project.id),
issuableTemplates: issuable_templates(issuable), issuableTemplates: issuable_templates(issuable),
projectPath: ref_project.path, projectPath: ref_project.path,
projectNamespace: ref_project.namespace.full_path, projectNamespace: ref_project.namespace.full_path,
Loading
@@ -354,6 +352,8 @@ module IssuablesHelper
Loading
@@ -354,6 +352,8 @@ module IssuablesHelper
def issuable_sidebar_options(issuable, can_edit_issuable) def issuable_sidebar_options(issuable, can_edit_issuable)
{ {
endpoint: "#{issuable_json_path(issuable)}?basic=true", 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, editable: can_edit_issuable,
currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url), currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url),
rootPath: root_path, rootPath: root_path,
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment