Merge branch 'ee_issue_928_backport' into 'master'
Group boards CE backport See merge request !13883
This commit is contained in:
commit
b4778a5866
78 changed files with 776 additions and 529 deletions
|
@ -6,7 +6,8 @@ const Api = {
|
|||
namespacesPath: '/api/:version/namespaces.json',
|
||||
groupProjectsPath: '/api/:version/groups/:id/projects.json',
|
||||
projectsPath: '/api/:version/projects.json',
|
||||
labelsPath: '/:namespace_path/:project_path/labels',
|
||||
projectLabelsPath: '/:namespace_path/:project_path/labels',
|
||||
groupLabelsPath: '/groups/:namespace_path/labels',
|
||||
licensePath: '/api/:version/templates/licenses/:key',
|
||||
gitignorePath: '/api/:version/templates/gitignores/:key',
|
||||
gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
|
||||
|
@ -74,9 +75,16 @@ const Api = {
|
|||
},
|
||||
|
||||
newLabel(namespacePath, projectPath, data, callback) {
|
||||
const url = Api.buildUrl(Api.labelsPath)
|
||||
.replace(':namespace_path', namespacePath)
|
||||
.replace(':project_path', projectPath);
|
||||
let url;
|
||||
|
||||
if (projectPath) {
|
||||
url = Api.buildUrl(Api.projectLabelsPath)
|
||||
.replace(':namespace_path', namespacePath)
|
||||
.replace(':project_path', projectPath);
|
||||
} else {
|
||||
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
|
||||
}
|
||||
|
||||
return $.ajax({
|
||||
url,
|
||||
type: 'POST',
|
||||
|
|
|
@ -53,7 +53,8 @@ $(() => {
|
|||
data: {
|
||||
state: Store.state,
|
||||
loading: true,
|
||||
endpoint: $boardApp.dataset.endpoint,
|
||||
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
|
||||
listsEndpoint: $boardApp.dataset.listsEndpoint,
|
||||
boardId: $boardApp.dataset.boardId,
|
||||
disabled: $boardApp.dataset.disabled === 'true',
|
||||
issueLinkBase: $boardApp.dataset.issueLinkBase,
|
||||
|
@ -68,7 +69,13 @@ $(() => {
|
|||
},
|
||||
},
|
||||
created () {
|
||||
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
|
||||
gl.boardService = new BoardService({
|
||||
boardsEndpoint: this.boardsEndpoint,
|
||||
listsEndpoint: this.listsEndpoint,
|
||||
bulkUpdatePath: this.bulkUpdatePath,
|
||||
boardId: this.boardId,
|
||||
});
|
||||
Store.rootPath = this.boardsEndpoint;
|
||||
|
||||
this.filterManager = new FilteredSearchBoards(Store.filter, true);
|
||||
this.filterManager.setup();
|
||||
|
@ -112,19 +119,21 @@ $(() => {
|
|||
gl.IssueBoardsSearch = new Vue({
|
||||
el: document.getElementById('js-add-list'),
|
||||
data: {
|
||||
filters: Store.state.filters
|
||||
filters: Store.state.filters,
|
||||
},
|
||||
mounted () {
|
||||
gl.issueBoards.newListDropdownInit();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
gl.IssueBoardsModalAddBtn = new Vue({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
el: document.getElementById('js-add-issues-btn'),
|
||||
data: {
|
||||
modal: ModalStore.store,
|
||||
store: Store.state,
|
||||
data() {
|
||||
return {
|
||||
modal: ModalStore.store,
|
||||
store: Store.state,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
disabled() {
|
||||
|
@ -133,6 +142,9 @@ $(() => {
|
|||
},
|
||||
computed: {
|
||||
disabled() {
|
||||
if (!this.store) {
|
||||
return true;
|
||||
}
|
||||
return !this.store.lists.filter(list => !list.preset).length;
|
||||
},
|
||||
tooltipTitle() {
|
||||
|
@ -145,7 +157,7 @@ $(() => {
|
|||
},
|
||||
methods: {
|
||||
updateTooltip() {
|
||||
const $tooltip = $(this.$el);
|
||||
const $tooltip = $(this.$refs.addIssuesButton);
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.disabled) {
|
||||
|
@ -165,16 +177,19 @@ $(() => {
|
|||
this.updateTooltip();
|
||||
},
|
||||
template: `
|
||||
<button
|
||||
class="btn btn-create pull-right prepend-left-10"
|
||||
type="button"
|
||||
data-placement="bottom"
|
||||
:class="{ 'disabled': disabled }"
|
||||
:title="tooltipTitle"
|
||||
:aria-disabled="disabled"
|
||||
@click="openModal">
|
||||
Add issues
|
||||
</button>
|
||||
<div class="board-extra-actions">
|
||||
<button
|
||||
class="btn btn-create prepend-left-10"
|
||||
type="button"
|
||||
data-placement="bottom"
|
||||
ref="addIssuesButton"
|
||||
:class="{ 'disabled': disabled }"
|
||||
:title="tooltipTitle"
|
||||
:aria-disabled="disabled"
|
||||
@click="openModal">
|
||||
Add issues
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -77,7 +77,7 @@ export default {
|
|||
this.showIssueForm = !this.showIssueForm;
|
||||
},
|
||||
onScroll() {
|
||||
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
|
||||
if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
|
||||
this.loadNextPage();
|
||||
}
|
||||
},
|
||||
|
@ -165,11 +165,9 @@ export default {
|
|||
v-if="loading">
|
||||
<loading-icon />
|
||||
</div>
|
||||
<transition name="slide-down">
|
||||
<board-new-issue
|
||||
:list="list"
|
||||
v-if="list.type !== 'closed' && showIssueForm"/>
|
||||
</transition>
|
||||
<board-new-issue
|
||||
:list="list"
|
||||
v-if="list.type !== 'closed' && showIssueForm"/>
|
||||
<ul
|
||||
class="board-list"
|
||||
v-show="!loading"
|
||||
|
|
|
@ -6,7 +6,10 @@ const Store = gl.issueBoards.BoardsStore;
|
|||
export default {
|
||||
name: 'BoardNewIssue',
|
||||
props: {
|
||||
list: Object,
|
||||
list: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -64,10 +64,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
|
|||
return this.issue.assignees.length > this.numberOverLimit;
|
||||
},
|
||||
cardUrl() {
|
||||
return `${this.issueLinkBase}/${this.issue.id}`;
|
||||
return `${this.issueLinkBase}/${this.issue.iid}`;
|
||||
},
|
||||
issueId() {
|
||||
return `#${this.issue.id}`;
|
||||
if (this.issue.iid) {
|
||||
return `#${this.issue.iid}`;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
showLabelFooter() {
|
||||
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
|
||||
|
@ -143,7 +146,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
|
|||
:title="issue.title">{{ issue.title }}</a>
|
||||
<span
|
||||
class="card-number"
|
||||
v-if="issue.id"
|
||||
v-if="issueId"
|
||||
>
|
||||
{{ issueId }}
|
||||
</span>
|
||||
|
|
|
@ -29,7 +29,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
|
|||
const firstListIndex = 1;
|
||||
const list = this.modal.selectedList || this.state.lists[firstListIndex];
|
||||
const selectedIssues = ModalStore.getSelectedIssues();
|
||||
const issueIds = selectedIssues.map(issue => issue.globalId);
|
||||
const issueIds = selectedIssues.map(issue => issue.id);
|
||||
|
||||
// Post the data to the backend
|
||||
gl.boardService.bulkUpdate(issueIds, {
|
||||
|
|
|
@ -27,7 +27,7 @@ gl.issueBoards.newListDropdownInit = () => {
|
|||
|
||||
$this.glDropdown({
|
||||
data(term, callback) {
|
||||
$.get($this.attr('data-labels'))
|
||||
$.get($this.attr('data-list-labels-path'))
|
||||
.then((resp) => {
|
||||
callback(resp);
|
||||
});
|
||||
|
|
|
@ -18,17 +18,33 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
issueUpdate: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
updateUrl() {
|
||||
return this.issueUpdate;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeIssue() {
|
||||
const issue = this.issue;
|
||||
const lists = issue.getLists();
|
||||
const labelIds = lists.map(list => list.label.id);
|
||||
|
||||
// Post the remove data
|
||||
gl.boardService.bulkUpdate([issue.globalId], {
|
||||
remove_label_ids: labelIds,
|
||||
}).catch(() => {
|
||||
const listLabelIds = lists.map(list => list.label.id);
|
||||
let labelIds = this.issue.labels
|
||||
.map(label => label.id)
|
||||
.filter(id => !listLabelIds.includes(id));
|
||||
if (labelIds.length === 0) {
|
||||
labelIds = [''];
|
||||
}
|
||||
const data = {
|
||||
issue: {
|
||||
label_ids: labelIds,
|
||||
},
|
||||
};
|
||||
Vue.http.patch(this.updateUrl, data).catch(() => {
|
||||
new Flash('Failed to remove issue from board, please try again.', 'alert');
|
||||
|
||||
lists.forEach((list) => {
|
||||
|
|
|
@ -7,8 +7,8 @@ import Vue from 'vue';
|
|||
|
||||
class ListIssue {
|
||||
constructor (obj, defaultAvatar) {
|
||||
this.globalId = obj.id;
|
||||
this.id = obj.iid;
|
||||
this.id = obj.id;
|
||||
this.iid = obj.iid;
|
||||
this.title = obj.title;
|
||||
this.confidential = obj.confidential;
|
||||
this.dueDate = obj.due_date;
|
||||
|
|
|
@ -4,6 +4,7 @@ class ListLabel {
|
|||
constructor (obj) {
|
||||
this.id = obj.id;
|
||||
this.title = obj.title;
|
||||
this.type = obj.type;
|
||||
this.color = obj.color;
|
||||
this.textColor = obj.text_color;
|
||||
this.description = obj.description;
|
||||
|
|
|
@ -110,11 +110,13 @@ class List {
|
|||
return gl.boardService.newIssue(this.id, issue)
|
||||
.then(resp => resp.json())
|
||||
.then((data) => {
|
||||
issue.id = data.iid;
|
||||
issue.id = data.id;
|
||||
issue.iid = data.iid;
|
||||
issue.project = data.project;
|
||||
|
||||
if (this.issuesSize > 1) {
|
||||
const moveBeforeIid = this.issues[1].id;
|
||||
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
|
||||
const moveBeforeId = this.issues[1].id;
|
||||
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -126,19 +128,19 @@ class List {
|
|||
}
|
||||
|
||||
addIssue (issue, listFrom, newIndex) {
|
||||
let moveBeforeIid = null;
|
||||
let moveAfterIid = null;
|
||||
let moveBeforeId = null;
|
||||
let moveAfterId = null;
|
||||
|
||||
if (!this.findIssue(issue.id)) {
|
||||
if (newIndex !== undefined) {
|
||||
this.issues.splice(newIndex, 0, issue);
|
||||
|
||||
if (this.issues[newIndex - 1]) {
|
||||
moveBeforeIid = this.issues[newIndex - 1].id;
|
||||
moveBeforeId = this.issues[newIndex - 1].id;
|
||||
}
|
||||
|
||||
if (this.issues[newIndex + 1]) {
|
||||
moveAfterIid = this.issues[newIndex + 1].id;
|
||||
moveAfterId = this.issues[newIndex + 1].id;
|
||||
}
|
||||
} else {
|
||||
this.issues.push(issue);
|
||||
|
@ -151,30 +153,30 @@ class List {
|
|||
if (listFrom) {
|
||||
this.issuesSize += 1;
|
||||
|
||||
this.updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid);
|
||||
this.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) {
|
||||
moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
|
||||
this.issues.splice(oldIndex, 1);
|
||||
this.issues.splice(newIndex, 0, issue);
|
||||
|
||||
gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid)
|
||||
gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId)
|
||||
.catch(() => {
|
||||
// TODO: handle request error
|
||||
});
|
||||
}
|
||||
|
||||
updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
|
||||
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid)
|
||||
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
|
||||
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
|
||||
.catch(() => {
|
||||
// TODO: handle request error
|
||||
});
|
||||
}
|
||||
|
||||
findIssue (id) {
|
||||
return this.issues.filter(issue => issue.id === id)[0];
|
||||
return this.issues.find(issue => issue.id === id);
|
||||
}
|
||||
|
||||
removeIssue (removeIssue) {
|
||||
|
|
|
@ -3,21 +3,21 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
class BoardService {
|
||||
constructor (root, bulkUpdatePath, boardId) {
|
||||
this.boards = Vue.resource(`${root}{/id}.json`, {}, {
|
||||
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
|
||||
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
|
||||
issues: {
|
||||
method: 'GET',
|
||||
url: `${root}/${boardId}/issues.json`
|
||||
url: `${gon.relative_url_root}/boards/${boardId}/issues.json`,
|
||||
}
|
||||
});
|
||||
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
|
||||
this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
|
||||
generate: {
|
||||
method: 'POST',
|
||||
url: `${root}/${boardId}/lists/generate.json`
|
||||
url: `${listsEndpoint}/generate.json`
|
||||
}
|
||||
});
|
||||
this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
|
||||
this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, {
|
||||
this.issue = Vue.resource(`${gon.relative_url_root}/boards/${boardId}/issues{/id}`, {});
|
||||
this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
|
||||
bulkUpdate: {
|
||||
method: 'POST',
|
||||
url: bulkUpdatePath,
|
||||
|
@ -60,12 +60,12 @@ class BoardService {
|
|||
return this.issues.get(data);
|
||||
}
|
||||
|
||||
moveIssue (id, from_list_id = null, to_list_id = null, move_before_iid = null, move_after_iid = null) {
|
||||
moveIssue (id, from_list_id = null, to_list_id = null, move_before_id = null, move_after_id = null) {
|
||||
return this.issue.update({ id }, {
|
||||
from_list_id,
|
||||
to_list_id,
|
||||
move_before_iid,
|
||||
move_after_iid,
|
||||
move_before_id,
|
||||
move_after_id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -183,7 +183,7 @@
|
|||
width: auto;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 200;
|
||||
z-index: 300;
|
||||
min-width: 240px;
|
||||
max-width: 500px;
|
||||
margin-top: 2px;
|
||||
|
|
|
@ -117,13 +117,12 @@
|
|||
}
|
||||
|
||||
.board-title {
|
||||
position: initial;
|
||||
padding: 0;
|
||||
border-bottom: 0;
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
transform: rotate(90deg) translate(25px, 0);
|
||||
transform: rotate(90deg) translate(35px, 10px);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,11 +150,18 @@
|
|||
}
|
||||
|
||||
.board-header {
|
||||
border-top-left-radius: $border-radius-default;
|
||||
border-top-right-radius: $border-radius-default;
|
||||
position: relative;
|
||||
|
||||
&.has-border {
|
||||
&.has-border::before {
|
||||
border-top: 3px solid;
|
||||
border-color: inherit;
|
||||
border-top-left-radius: $border-radius-default;
|
||||
border-top-right-radius: $border-radius-default;
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: calc(100% + 2px);
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin-top: -1px;
|
||||
margin-right: -1px;
|
||||
margin-left: -1px;
|
||||
|
@ -176,12 +182,16 @@
|
|||
}
|
||||
|
||||
.board-title {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: $gl-padding;
|
||||
padding-bottom: ($gl-padding + 3px);
|
||||
padding: 12px $gl-padding;
|
||||
font-size: 1em;
|
||||
border-bottom: 1px solid $border-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.board-title-text {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.board-delete {
|
||||
|
@ -221,43 +231,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.slide-down-enter {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.slide-down-enter-active {
|
||||
transition: transform $fade-in-duration;
|
||||
|
||||
+ .board-list {
|
||||
transform: translateY(-136px);
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-down-enter-to {
|
||||
+ .board-list {
|
||||
transform: translateY(0);
|
||||
transition: transform $fade-in-duration ease;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-down-leave {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.slide-down-leave-active {
|
||||
transition: all $fade-in-duration;
|
||||
transform: translateY(-136px);
|
||||
|
||||
+ .board-list {
|
||||
transition: transform $fade-in-duration ease;
|
||||
transform: translateY(-136px);
|
||||
}
|
||||
}
|
||||
|
||||
.board-list-component {
|
||||
height: calc(100% - 49px);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.board-list {
|
||||
|
@ -429,7 +406,7 @@
|
|||
}
|
||||
|
||||
.board-new-issue-form {
|
||||
z-index: 1;
|
||||
z-index: 4;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
|
|
21
app/controllers/boards/application_controller.rb
Normal file
21
app/controllers/boards/application_controller.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
module Boards
|
||||
class ApplicationController < ::ApplicationController
|
||||
respond_to :json
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
|
||||
|
||||
private
|
||||
|
||||
def board
|
||||
@board ||= Board.find(params[:board_id])
|
||||
end
|
||||
|
||||
def board_parent
|
||||
@board_parent ||= board.parent
|
||||
end
|
||||
|
||||
def record_not_found(exception)
|
||||
render json: { error: exception.message }, status: :not_found
|
||||
end
|
||||
end
|
||||
end
|
90
app/controllers/boards/issues_controller.rb
Normal file
90
app/controllers/boards/issues_controller.rb
Normal file
|
@ -0,0 +1,90 @@
|
|||
module Boards
|
||||
class IssuesController < Boards::ApplicationController
|
||||
include BoardsResponses
|
||||
|
||||
before_action :authorize_read_issue, only: [:index]
|
||||
before_action :authorize_create_issue, only: [:create]
|
||||
before_action :authorize_update_issue, only: [:update]
|
||||
skip_before_action :authenticate_user!, only: [:index]
|
||||
|
||||
def index
|
||||
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
|
||||
issues = issues.page(params[:page]).per(params[:per] || 20)
|
||||
make_sure_position_is_set(issues)
|
||||
|
||||
render json: {
|
||||
issues: serialize_as_json(issues.preload(:project)),
|
||||
size: issues.total_count
|
||||
}
|
||||
end
|
||||
|
||||
def create
|
||||
service = Boards::Issues::CreateService.new(board_parent, project, current_user, issue_params)
|
||||
issue = service.execute
|
||||
|
||||
if issue.valid?
|
||||
render json: serialize_as_json(issue)
|
||||
else
|
||||
render json: issue.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
service = Boards::Issues::MoveService.new(board_parent, current_user, move_params)
|
||||
|
||||
if service.execute(issue)
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def make_sure_position_is_set(issues)
|
||||
issues.each do |issue|
|
||||
issue.move_to_end && issue.save unless issue.relative_position
|
||||
end
|
||||
end
|
||||
|
||||
def issue
|
||||
@issue ||= issues_finder.execute.find(params[:id])
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.merge(board_id: params[:board_id], id: params[:list_id])
|
||||
.reject { |_, value| value.nil? }
|
||||
end
|
||||
|
||||
def issues_finder
|
||||
IssuesFinder.new(current_user, project_id: board_parent.id)
|
||||
end
|
||||
|
||||
def project
|
||||
board_parent
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_id, :move_after_id)
|
||||
end
|
||||
|
||||
def issue_params
|
||||
params.require(:issue)
|
||||
.permit(:title, :milestone_id, :project_id)
|
||||
.merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
|
||||
end
|
||||
|
||||
def serialize_as_json(resource)
|
||||
resource.as_json(
|
||||
labels: true,
|
||||
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
|
||||
include: {
|
||||
project: { only: [:id, :path] },
|
||||
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
|
||||
milestone: { only: [:id, :title] }
|
||||
},
|
||||
user: current_user
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
75
app/controllers/boards/lists_controller.rb
Normal file
75
app/controllers/boards/lists_controller.rb
Normal file
|
@ -0,0 +1,75 @@
|
|||
module Boards
|
||||
class ListsController < Boards::ApplicationController
|
||||
include BoardsResponses
|
||||
|
||||
before_action :authorize_admin_list, only: [:create, :update, :destroy, :generate]
|
||||
before_action :authorize_read_list, only: [:index]
|
||||
skip_before_action :authenticate_user!, only: [:index]
|
||||
|
||||
def index
|
||||
lists = Boards::Lists::ListService.new(board.parent, current_user).execute(board)
|
||||
|
||||
render json: serialize_as_json(lists)
|
||||
end
|
||||
|
||||
def create
|
||||
list = Boards::Lists::CreateService.new(board.parent, current_user, list_params).execute(board)
|
||||
|
||||
if list.valid?
|
||||
render json: serialize_as_json(list)
|
||||
else
|
||||
render json: list.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
list = board.lists.movable.find(params[:id])
|
||||
service = Boards::Lists::MoveService.new(board_parent, current_user, move_params)
|
||||
|
||||
if service.execute(list)
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
list = board.lists.destroyable.find(params[:id])
|
||||
service = Boards::Lists::DestroyService.new(board_parent, current_user)
|
||||
|
||||
if service.execute(list)
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def generate
|
||||
service = Boards::Lists::GenerateService.new(board_parent, current_user)
|
||||
|
||||
if service.execute(board)
|
||||
render json: serialize_as_json(board.lists.movable)
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def list_params
|
||||
params.require(:list).permit(:label_id)
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.require(:list).permit(:position)
|
||||
end
|
||||
|
||||
def serialize_as_json(resource)
|
||||
resource.as_json(
|
||||
only: [:id, :list_type, :position],
|
||||
methods: [:title],
|
||||
label: true
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
42
app/controllers/concerns/boards_responses.rb
Normal file
42
app/controllers/concerns/boards_responses.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
module BoardsResponses
|
||||
def authorize_read_list
|
||||
authorize_action_for!(board.parent, :read_list)
|
||||
end
|
||||
|
||||
def authorize_read_issue
|
||||
authorize_action_for!(board.parent, :read_issue)
|
||||
end
|
||||
|
||||
def authorize_update_issue
|
||||
authorize_action_for!(issue, :admin_issue)
|
||||
end
|
||||
|
||||
def authorize_create_issue
|
||||
authorize_action_for!(project, :admin_issue)
|
||||
end
|
||||
|
||||
def authorize_admin_list
|
||||
authorize_action_for!(board.parent, :admin_list)
|
||||
end
|
||||
|
||||
def authorize_action_for!(resource, ability)
|
||||
return render_403 unless can?(current_user, ability, resource)
|
||||
end
|
||||
|
||||
def respond_with_boards
|
||||
respond_with(@boards)
|
||||
end
|
||||
|
||||
def respond_with_board
|
||||
respond_with(@board)
|
||||
end
|
||||
|
||||
def respond_with(resource)
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
render json: serialize_as_json(resource)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
module Projects
|
||||
module Boards
|
||||
class ApplicationController < Projects::ApplicationController
|
||||
respond_to :json
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
|
||||
|
||||
private
|
||||
|
||||
def record_not_found(exception)
|
||||
render json: { error: exception.message }, status: :not_found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,94 +0,0 @@
|
|||
module Projects
|
||||
module Boards
|
||||
class IssuesController < Boards::ApplicationController
|
||||
before_action :authorize_read_issue!, only: [:index]
|
||||
before_action :authorize_create_issue!, only: [:create]
|
||||
before_action :authorize_update_issue!, only: [:update]
|
||||
|
||||
def index
|
||||
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
|
||||
issues = issues.page(params[:page]).per(params[:per] || 20)
|
||||
make_sure_position_is_set(issues)
|
||||
|
||||
render json: {
|
||||
issues: serialize_as_json(issues),
|
||||
size: issues.total_count
|
||||
}
|
||||
end
|
||||
|
||||
def create
|
||||
service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
|
||||
issue = service.execute
|
||||
|
||||
if issue.valid?
|
||||
render json: serialize_as_json(issue)
|
||||
else
|
||||
render json: issue.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
|
||||
|
||||
if service.execute(issue)
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def make_sure_position_is_set(issues)
|
||||
issues.each do |issue|
|
||||
issue.move_to_end && issue.save unless issue.relative_position
|
||||
end
|
||||
end
|
||||
|
||||
def issue
|
||||
@issue ||=
|
||||
IssuesFinder.new(current_user, project_id: project.id)
|
||||
.execute
|
||||
.where(iid: params[:id])
|
||||
.first!
|
||||
end
|
||||
|
||||
def authorize_read_issue!
|
||||
return render_403 unless can?(current_user, :read_issue, project)
|
||||
end
|
||||
|
||||
def authorize_create_issue!
|
||||
return render_403 unless can?(current_user, :admin_issue, project)
|
||||
end
|
||||
|
||||
def authorize_update_issue!
|
||||
return render_403 unless can?(current_user, :update_issue, issue)
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.merge(board_id: params[:board_id], id: params[:list_id])
|
||||
.reject { |_, value| value.nil? }
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_iid, :move_after_iid)
|
||||
end
|
||||
|
||||
def issue_params
|
||||
params.require(:issue).permit(:title).merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
|
||||
end
|
||||
|
||||
def serialize_as_json(resource)
|
||||
resource.as_json(
|
||||
labels: true,
|
||||
only: [:id, :iid, :title, :confidential, :due_date, :relative_position],
|
||||
include: {
|
||||
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
|
||||
milestone: { only: [:id, :title] }
|
||||
},
|
||||
user: current_user
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,86 +0,0 @@
|
|||
module Projects
|
||||
module Boards
|
||||
class ListsController < Boards::ApplicationController
|
||||
before_action :authorize_admin_list!, only: [:create, :update, :destroy, :generate]
|
||||
before_action :authorize_read_list!, only: [:index]
|
||||
|
||||
def index
|
||||
lists = ::Boards::Lists::ListService.new(project, current_user).execute(board)
|
||||
|
||||
render json: serialize_as_json(lists)
|
||||
end
|
||||
|
||||
def create
|
||||
list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute(board)
|
||||
|
||||
if list.valid?
|
||||
render json: serialize_as_json(list)
|
||||
else
|
||||
render json: list.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
list = board.lists.movable.find(params[:id])
|
||||
service = ::Boards::Lists::MoveService.new(project, current_user, move_params)
|
||||
|
||||
if service.execute(list)
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
list = board.lists.destroyable.find(params[:id])
|
||||
service = ::Boards::Lists::DestroyService.new(project, current_user)
|
||||
|
||||
if service.execute(list)
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def generate
|
||||
service = ::Boards::Lists::GenerateService.new(project, current_user)
|
||||
|
||||
if service.execute(board)
|
||||
render json: serialize_as_json(board.lists.movable)
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_admin_list!
|
||||
return render_403 unless can?(current_user, :admin_list, project)
|
||||
end
|
||||
|
||||
def authorize_read_list!
|
||||
return render_403 unless can?(current_user, :read_list, project)
|
||||
end
|
||||
|
||||
def board
|
||||
@board ||= project.boards.find(params[:board_id])
|
||||
end
|
||||
|
||||
def list_params
|
||||
params.require(:list).permit(:label_id)
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.require(:list).permit(:position)
|
||||
end
|
||||
|
||||
def serialize_as_json(resource)
|
||||
resource.as_json(
|
||||
only: [:id, :list_type, :position],
|
||||
methods: [:title],
|
||||
label: true
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,32 +1,31 @@
|
|||
class Projects::BoardsController < Projects::ApplicationController
|
||||
include BoardsResponses
|
||||
include IssuableCollections
|
||||
|
||||
before_action :authorize_read_board!, only: [:index, :show]
|
||||
before_action :assign_endpoint_vars
|
||||
|
||||
def index
|
||||
@boards = ::Boards::ListService.new(project, current_user).execute
|
||||
@boards = Boards::ListService.new(project, current_user).execute
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
render json: serialize_as_json(@boards)
|
||||
end
|
||||
end
|
||||
respond_with_boards
|
||||
end
|
||||
|
||||
def show
|
||||
@board = project.boards.find(params[:id])
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
render json: serialize_as_json(@board)
|
||||
end
|
||||
end
|
||||
respond_with_board
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_endpoint_vars
|
||||
@boards_endpoint = project_boards_url(project)
|
||||
@bulk_issues_path = bulk_update_project_issues_path(project)
|
||||
@namespace_path = project.namespace.full_path
|
||||
@labels_endpoint = project_labels_path(project)
|
||||
end
|
||||
|
||||
def authorize_read_board!
|
||||
return access_denied! unless can?(current_user, :read_board, project)
|
||||
end
|
||||
|
|
|
@ -1,15 +1,80 @@
|
|||
module BoardsHelper
|
||||
def board_data
|
||||
board = @board || @boards.first
|
||||
def board
|
||||
@board ||= @board || @boards.first
|
||||
end
|
||||
|
||||
def board_data
|
||||
{
|
||||
endpoint: project_boards_path(@project),
|
||||
boards_endpoint: @boards_endpoint,
|
||||
lists_endpoint: board_lists_url(board),
|
||||
board_id: board.id,
|
||||
disabled: "#{!can?(current_user, :admin_list, @project)}",
|
||||
issue_link_base: project_issues_path(@project),
|
||||
disabled: "#{!can?(current_user, :admin_list, current_board_parent)}",
|
||||
issue_link_base: build_issue_link_base,
|
||||
root_path: root_path,
|
||||
bulk_update_path: bulk_update_project_issues_path(@project),
|
||||
bulk_update_path: @bulk_issues_path,
|
||||
default_avatar: image_path(default_avatar)
|
||||
}
|
||||
end
|
||||
|
||||
def build_issue_link_base
|
||||
project_issues_path(@project)
|
||||
end
|
||||
|
||||
def current_board_json
|
||||
board = @board || @boards.first
|
||||
|
||||
board.to_json(
|
||||
only: [:id, :name, :milestone_id],
|
||||
include: {
|
||||
milestone: { only: [:title] }
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def board_base_url
|
||||
project_boards_path(@project)
|
||||
end
|
||||
|
||||
def multiple_boards_available?
|
||||
current_board_parent.multiple_issue_boards_available?(current_user)
|
||||
end
|
||||
|
||||
def current_board_path(board)
|
||||
@current_board_path ||= project_board_path(current_board_parent, board)
|
||||
end
|
||||
|
||||
def current_board_parent
|
||||
@current_board_parent ||= @project
|
||||
end
|
||||
|
||||
def can_admin_issue?
|
||||
can?(current_user, :admin_issue, current_board_parent)
|
||||
end
|
||||
|
||||
def board_list_data
|
||||
{
|
||||
toggle: "dropdown",
|
||||
list_labels_path: labels_filter_path(true),
|
||||
labels: labels_filter_path(true),
|
||||
labels_endpoint: @labels_endpoint,
|
||||
namespace_path: @namespace_path,
|
||||
project_path: @project&.try(:path)
|
||||
}
|
||||
end
|
||||
|
||||
def board_sidebar_user_data
|
||||
dropdown_options = issue_assignees_dropdown_options
|
||||
|
||||
{
|
||||
toggle: 'dropdown',
|
||||
field_name: 'issue[assignee_ids][]',
|
||||
first_user: current_user&.username,
|
||||
current_user: 'true',
|
||||
project_id: @project&.try(:id),
|
||||
null_user: 'true',
|
||||
multi_select: 'true',
|
||||
'dropdown-header': dropdown_options[:data][:'dropdown-header'],
|
||||
'max-select': dropdown_options[:data][:'max-select']
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -347,6 +347,14 @@ module IssuablesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def labels_path
|
||||
if @project
|
||||
project_labels_path(@project)
|
||||
elsif @group
|
||||
group_labels_path(@group)
|
||||
end
|
||||
end
|
||||
|
||||
def issuable_sidebar_options(issuable, can_edit_issuable)
|
||||
{
|
||||
endpoint: "#{issuable_json_path(issuable)}?basic=true",
|
||||
|
|
|
@ -121,13 +121,14 @@ module LabelsHelper
|
|||
end
|
||||
end
|
||||
|
||||
def labels_filter_path
|
||||
return group_labels_path(@group, :json) if @group
|
||||
|
||||
def labels_filter_path(only_group_labels = false)
|
||||
project = @target_project || @project
|
||||
|
||||
if project
|
||||
project_labels_path(project, :json)
|
||||
elsif @group
|
||||
options = { only_group_labels: only_group_labels } if only_group_labels
|
||||
group_labels_path(@group, :json, options)
|
||||
else
|
||||
dashboard_labels_path(:json)
|
||||
end
|
||||
|
|
|
@ -134,19 +134,21 @@ module SearchHelper
|
|||
end
|
||||
|
||||
def search_filter_input_options(type)
|
||||
opts = {
|
||||
id: "filtered-search-#{type}",
|
||||
placeholder: 'Search or filter results...',
|
||||
data: {
|
||||
'username-params' => @users.to_json(only: [:id, :username])
|
||||
opts =
|
||||
{
|
||||
id: "filtered-search-#{type}",
|
||||
placeholder: 'Search or filter results...',
|
||||
data: {
|
||||
'username-params' => @users.to_json(only: [:id, :username])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if @project.present?
|
||||
opts[:data]['project-id'] = @project.id
|
||||
opts[:data]['base-endpoint'] = project_path(@project)
|
||||
else
|
||||
# Group context
|
||||
opts[:data]['group-id'] = @group.id
|
||||
opts[:data]['base-endpoint'] = group_canonical_path(@group)
|
||||
end
|
||||
|
||||
|
|
|
@ -3,7 +3,19 @@ class Board < ActiveRecord::Base
|
|||
|
||||
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
||||
validates :project, presence: true
|
||||
validates :project, presence: true, if: :project_needed?
|
||||
|
||||
def project_needed?
|
||||
true
|
||||
end
|
||||
|
||||
def parent
|
||||
project
|
||||
end
|
||||
|
||||
def group_board?
|
||||
false
|
||||
end
|
||||
|
||||
def backlog_list
|
||||
lists.merge(List.backlog).take
|
||||
|
|
|
@ -10,8 +10,12 @@ module RelativePositioning
|
|||
after_save :save_positionable_neighbours
|
||||
end
|
||||
|
||||
def project_ids
|
||||
[project.id]
|
||||
end
|
||||
|
||||
def max_relative_position
|
||||
self.class.in_projects(project.id).maximum(:relative_position)
|
||||
self.class.in_projects(project_ids).maximum(:relative_position)
|
||||
end
|
||||
|
||||
def prev_relative_position
|
||||
|
@ -19,7 +23,7 @@ module RelativePositioning
|
|||
|
||||
if self.relative_position
|
||||
prev_pos = self.class
|
||||
.in_projects(project.id)
|
||||
.in_projects(project_ids)
|
||||
.where('relative_position < ?', self.relative_position)
|
||||
.maximum(:relative_position)
|
||||
end
|
||||
|
@ -32,7 +36,7 @@ module RelativePositioning
|
|||
|
||||
if self.relative_position
|
||||
next_pos = self.class
|
||||
.in_projects(project.id)
|
||||
.in_projects(project_ids)
|
||||
.where('relative_position > ?', self.relative_position)
|
||||
.minimum(:relative_position)
|
||||
end
|
||||
|
@ -59,7 +63,7 @@ module RelativePositioning
|
|||
pos_after = before.next_relative_position
|
||||
|
||||
if before.shift_after?
|
||||
issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after)
|
||||
issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_after)
|
||||
issue_to_move.move_after
|
||||
@positionable_neighbours = [issue_to_move]
|
||||
|
||||
|
@ -74,7 +78,7 @@ module RelativePositioning
|
|||
pos_before = after.prev_relative_position
|
||||
|
||||
if after.shift_before?
|
||||
issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before)
|
||||
issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_before)
|
||||
issue_to_move.move_before
|
||||
@positionable_neighbours = [issue_to_move]
|
||||
|
||||
|
|
|
@ -34,7 +34,8 @@ class Label < ActiveRecord::Base
|
|||
|
||||
scope :templates, -> { where(template: true) }
|
||||
scope :with_title, ->(title) { where(title: title) }
|
||||
scope :on_project_boards, ->(project_id) { joins(lists: :board).merge(List.movable).where(boards: { project_id: project_id }) }
|
||||
scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) }
|
||||
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
|
||||
|
||||
def self.prioritized(project)
|
||||
joins(:priorities)
|
||||
|
@ -172,6 +173,7 @@ class Label < ActiveRecord::Base
|
|||
|
||||
def as_json(options = {})
|
||||
super(options).tap do |json|
|
||||
json[:type] = self.try(:type)
|
||||
json[:priority] = priority(options[:project]) if options.key?(:project)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1486,6 +1486,14 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def multiple_issue_boards_available?(user)
|
||||
feature_available?(:multiple_issue_boards, user)
|
||||
end
|
||||
|
||||
def issue_board_milestone_available?(user = nil)
|
||||
feature_available?(:issue_board_milestone, user)
|
||||
end
|
||||
|
||||
def full_path_was
|
||||
File.join(namespace.full_path, previous_changes['path'].first)
|
||||
end
|
||||
|
|
10
app/services/boards/base_service.rb
Normal file
10
app/services/boards/base_service.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
module Boards
|
||||
class BaseService < ::BaseService
|
||||
# Parent can either a group or a project
|
||||
attr_accessor :parent, :current_user, :params
|
||||
|
||||
def initialize(parent, user, params = {})
|
||||
@parent, @current_user, @params = parent, user, params.dup
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
module Boards
|
||||
class CreateService < BaseService
|
||||
class CreateService < Boards::BaseService
|
||||
def execute
|
||||
create_board! if can_create_board?
|
||||
end
|
||||
|
@ -7,11 +7,11 @@ module Boards
|
|||
private
|
||||
|
||||
def can_create_board?
|
||||
project.boards.size == 0
|
||||
parent.boards.size == 0
|
||||
end
|
||||
|
||||
def create_board!
|
||||
board = project.boards.create(params)
|
||||
board = parent.boards.create(params)
|
||||
|
||||
if board.persisted?
|
||||
board.lists.create(list_type: :backlog)
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
module Boards
|
||||
module Issues
|
||||
class CreateService < BaseService
|
||||
class CreateService < Boards::BaseService
|
||||
attr_accessor :project
|
||||
|
||||
def initialize(parent, project, user, params = {})
|
||||
@project = project
|
||||
|
||||
super(parent, user, params)
|
||||
end
|
||||
|
||||
def execute
|
||||
create_issue(params.merge(label_ids: [list.label_id]))
|
||||
end
|
||||
|
@ -8,7 +16,7 @@ module Boards
|
|||
private
|
||||
|
||||
def board
|
||||
@board ||= project.boards.find(params.delete(:board_id))
|
||||
@board ||= parent.boards.find(params.delete(:board_id))
|
||||
end
|
||||
|
||||
def list
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module Boards
|
||||
module Issues
|
||||
class ListService < BaseService
|
||||
class ListService < Boards::BaseService
|
||||
def execute
|
||||
issues = IssuesFinder.new(current_user, filter_params).execute
|
||||
issues = without_board_labels(issues) unless movable_list? || closed_list?
|
||||
|
@ -11,7 +11,7 @@ module Boards
|
|||
private
|
||||
|
||||
def board
|
||||
@board ||= project.boards.find(params[:board_id])
|
||||
@board ||= parent.boards.find(params[:board_id])
|
||||
end
|
||||
|
||||
def list
|
||||
|
@ -33,14 +33,14 @@ module Boards
|
|||
end
|
||||
|
||||
def filter_params
|
||||
set_project
|
||||
set_parent
|
||||
set_state
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def set_project
|
||||
params[:project_id] = project.id
|
||||
def set_parent
|
||||
params[:project_id] = parent.id
|
||||
end
|
||||
|
||||
def set_state
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
module Boards
|
||||
module Issues
|
||||
class MoveService < BaseService
|
||||
class MoveService < Boards::BaseService
|
||||
def execute(issue)
|
||||
return false unless can?(current_user, :update_issue, issue)
|
||||
return false if issue_params.empty?
|
||||
|
||||
update_service.execute(issue)
|
||||
update(issue)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def board
|
||||
@board ||= project.boards.find(params[:board_id])
|
||||
@board ||= parent.boards.find(params[:board_id])
|
||||
end
|
||||
|
||||
def move_between_lists?
|
||||
|
@ -27,8 +27,8 @@ module Boards
|
|||
@moving_to_list ||= board.lists.find_by(id: params[:to_list_id])
|
||||
end
|
||||
|
||||
def update_service
|
||||
::Issues::UpdateService.new(project, current_user, issue_params)
|
||||
def update(issue)
|
||||
::Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
|
||||
end
|
||||
|
||||
def issue_params
|
||||
|
@ -42,7 +42,7 @@ module Boards
|
|||
)
|
||||
end
|
||||
|
||||
attrs[:move_between_iids] = move_between_iids if move_between_iids
|
||||
attrs[:move_between_ids] = move_between_ids if move_between_ids
|
||||
|
||||
attrs
|
||||
end
|
||||
|
@ -61,16 +61,16 @@ module Boards
|
|||
if moving_to_list.movable?
|
||||
moving_from_list.label_id
|
||||
else
|
||||
Label.on_project_boards(project.id).pluck(:label_id)
|
||||
Label.on_project_boards(parent.id).pluck(:label_id)
|
||||
end
|
||||
|
||||
Array(label_ids).compact
|
||||
end
|
||||
|
||||
def move_between_iids
|
||||
return unless params[:move_after_iid] || params[:move_before_iid]
|
||||
def move_between_ids
|
||||
return unless params[:move_after_id] || params[:move_before_id]
|
||||
|
||||
[params[:move_after_iid], params[:move_before_iid]]
|
||||
[params[:move_after_id], params[:move_before_id]]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
module Boards
|
||||
class ListService < BaseService
|
||||
class ListService < Boards::BaseService
|
||||
def execute
|
||||
create_board! if project.boards.empty?
|
||||
project.boards
|
||||
create_board! if parent.boards.empty?
|
||||
parent.boards
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_board!
|
||||
Boards::CreateService.new(project, current_user).execute
|
||||
Boards::CreateService.new(parent, current_user).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
module Boards
|
||||
module Lists
|
||||
class CreateService < BaseService
|
||||
class CreateService < Boards::BaseService
|
||||
def execute(board)
|
||||
List.transaction do
|
||||
label = available_labels.find(params[:label_id])
|
||||
label = available_labels_for(board).find(params[:label_id])
|
||||
position = next_position(board)
|
||||
|
||||
create_list(board, label, position)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def available_labels
|
||||
LabelsFinder.new(current_user, project_id: project.id).execute
|
||||
def available_labels_for(board)
|
||||
LabelsFinder.new(current_user, project_id: parent.id).execute
|
||||
end
|
||||
|
||||
def next_position(board)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module Boards
|
||||
module Lists
|
||||
class DestroyService < BaseService
|
||||
class DestroyService < Boards::BaseService
|
||||
def execute(list)
|
||||
return false unless list.destroyable?
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module Boards
|
||||
module Lists
|
||||
class GenerateService < BaseService
|
||||
class GenerateService < Boards::BaseService
|
||||
def execute(board)
|
||||
return false unless board.lists.movable.empty?
|
||||
|
||||
|
@ -15,11 +15,11 @@ module Boards
|
|||
|
||||
def create_list(board, params)
|
||||
label = find_or_create_label(params)
|
||||
Lists::CreateService.new(project, current_user, label_id: label.id).execute(board)
|
||||
Lists::CreateService.new(parent, current_user, label_id: label.id).execute(board)
|
||||
end
|
||||
|
||||
def find_or_create_label(params)
|
||||
::Labels::FindOrCreateService.new(current_user, project, params).execute
|
||||
::Labels::FindOrCreateService.new(current_user, parent, params).execute
|
||||
end
|
||||
|
||||
def label_params
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module Boards
|
||||
module Lists
|
||||
class ListService < BaseService
|
||||
class ListService < Boards::BaseService
|
||||
def execute(board)
|
||||
board.lists.create(list_type: :backlog) unless board.lists.backlog.exists?
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module Boards
|
||||
module Lists
|
||||
class MoveService < BaseService
|
||||
class MoveService < Boards::BaseService
|
||||
def execute(list)
|
||||
@board = list.board
|
||||
@old_position = list.position
|
||||
|
|
|
@ -3,7 +3,7 @@ module Issues
|
|||
include SpamCheckService
|
||||
|
||||
def execute(issue)
|
||||
handle_move_between_iids(issue)
|
||||
handle_move_between_ids(issue)
|
||||
filter_spam_check_params
|
||||
change_issue_duplicate(issue)
|
||||
move_issue_to_new_project(issue) || update(issue)
|
||||
|
@ -54,13 +54,13 @@ module Issues
|
|||
end
|
||||
end
|
||||
|
||||
def handle_move_between_iids(issue)
|
||||
return unless params[:move_between_iids]
|
||||
def handle_move_between_ids(issue)
|
||||
return unless params[:move_between_ids]
|
||||
|
||||
after_iid, before_iid = params.delete(:move_between_iids)
|
||||
after_id, before_id = params.delete(:move_between_ids)
|
||||
|
||||
issue_before = get_issue_if_allowed(issue.project, before_iid) if before_iid
|
||||
issue_after = get_issue_if_allowed(issue.project, after_iid) if after_iid
|
||||
issue_before = get_issue_if_allowed(issue.project, before_id) if before_id
|
||||
issue_after = get_issue_if_allowed(issue.project, after_id) if after_id
|
||||
|
||||
issue.move_between(issue_before, issue_after)
|
||||
end
|
||||
|
@ -87,8 +87,8 @@ module Issues
|
|||
|
||||
private
|
||||
|
||||
def get_issue_if_allowed(project, iid)
|
||||
issue = project.issues.find_by(iid: iid)
|
||||
def get_issue_if_allowed(project, id)
|
||||
issue = project.issues.find(id)
|
||||
issue if can?(current_user, :update_issue, issue)
|
||||
end
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
= render "show"
|
||||
= render "shared/boards/show", board: @boards.first
|
||||
|
|
|
@ -1 +1 @@
|
|||
= render "show"
|
||||
= render "shared/boards/show", board: @board
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
= webpack_bundle_tag 'filtered_search'
|
||||
= webpack_bundle_tag 'boards'
|
||||
|
||||
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
|
||||
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
|
||||
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
|
||||
|
||||
= render "projects/issues/head"
|
||||
|
@ -30,7 +30,7 @@
|
|||
":root-path" => "rootPath",
|
||||
":board-id" => "boardId",
|
||||
":key" => "_uid" }
|
||||
= render "projects/boards/components/sidebar"
|
||||
= render "shared/boards/components/sidebar"
|
||||
%board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'),
|
||||
"new-issue-path" => new_project_issue_path(@project),
|
||||
"milestone-path" => milestones_filter_dropdown_path,
|
|
@ -7,20 +7,26 @@
|
|||
":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded && list.position === -1, \"fa-caret-left\": !list.isExpanded && list.position !== -1 }",
|
||||
"aria-hidden": "true" }
|
||||
|
||||
%span.has-tooltip{ "v-if": "list.type !== \"label\"",
|
||||
%span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"",
|
||||
":title" => '(list.label ? list.label.description : "")' }
|
||||
{{ list.title }}
|
||||
|
||||
%span.has-tooltip{ "v-if": "list.type === \"label\"",
|
||||
":title" => '(list.label ? list.label.description : "")',
|
||||
data: { container: "body", placement: "bottom" },
|
||||
class: "label color-label title",
|
||||
class: "label color-label title board-title-text",
|
||||
":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color : \"#2e2e2e\") }" }
|
||||
{{ list.title }}
|
||||
.issue-count-badge.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
|
||||
- if can?(current_user, :admin_list, current_board_parent)
|
||||
%board-delete{ "inline-template" => true,
|
||||
":list" => "list",
|
||||
"v-if" => "!list.preset && list.id" }
|
||||
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
|
||||
= icon("trash")
|
||||
.issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' }
|
||||
%span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
|
||||
{{ list.issuesSize }}
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
- if can?(current_user, :admin_list, current_board_parent)
|
||||
%button.issue-count-badge-add-button.btn.btn-small.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
|
||||
"@click" => "showNewIssueForm",
|
||||
"v-if" => 'list.type !== "closed"',
|
||||
|
@ -28,12 +34,7 @@
|
|||
"title" => "New issue",
|
||||
data: { placement: "top", container: "body" } }
|
||||
= icon("plus", class: "js-no-trigger-collapse")
|
||||
- if can?(current_user, :admin_list, @project)
|
||||
%board-delete{ "inline-template" => true,
|
||||
":list" => "list",
|
||||
"v-if" => "!list.preset && list.id" }
|
||||
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
|
||||
= icon("trash")
|
||||
|
||||
%board-list{ "v-if" => 'list.type !== "blank"',
|
||||
":list" => "list",
|
||||
":issues" => "list.issues",
|
||||
|
@ -42,5 +43,5 @@
|
|||
":issue-link-base" => "issueLinkBase",
|
||||
":root-path" => "rootPath",
|
||||
"ref" => "board-list" }
|
||||
- if can?(current_user, :admin_list, @project)
|
||||
- if can?(current_user, :admin_list, current_board_parent)
|
||||
%board-blank-state{ "v-if" => 'list.id == "blank"' }
|
|
@ -10,18 +10,19 @@
|
|||
%br/
|
||||
%span
|
||||
= precede "#" do
|
||||
{{ issue.id }}
|
||||
{{ issue.iid }}
|
||||
%a.gutter-toggle.pull-right{ role: "button",
|
||||
href: "#",
|
||||
"@click.prevent" => "closeSidebar",
|
||||
"aria-label" => "Toggle sidebar" }
|
||||
= custom_icon("icon_close", size: 15)
|
||||
.js-issuable-update
|
||||
= render "projects/boards/components/sidebar/assignee"
|
||||
= render "projects/boards/components/sidebar/milestone"
|
||||
= render "projects/boards/components/sidebar/due_date"
|
||||
= render "projects/boards/components/sidebar/labels"
|
||||
= render "projects/boards/components/sidebar/notifications"
|
||||
= render "shared/boards/components/sidebar/assignee"
|
||||
= render "shared/boards/components/sidebar/milestone"
|
||||
= render "shared/boards/components/sidebar/due_date"
|
||||
= render "shared/boards/components/sidebar/labels"
|
||||
= render "shared/boards/components/sidebar/notifications"
|
||||
%remove-btn{ ":issue" => "issue",
|
||||
":issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'",
|
||||
":list" => "list",
|
||||
"v-if" => "canRemove" }
|
|
@ -2,13 +2,13 @@
|
|||
%template{ "v-if" => "issue.assignees" }
|
||||
%assignee-title{ ":number-of-assignees" => "issue.assignees.length",
|
||||
":loading" => "loadingAssignees",
|
||||
":editable" => can?(current_user, :admin_issue, @project) }
|
||||
":editable" => can_admin_issue? }
|
||||
%assignees.value{ "root-path" => "#{root_url}",
|
||||
":users" => "issue.assignees",
|
||||
":editable" => can?(current_user, :admin_issue, @project),
|
||||
":editable" => can_admin_issue?,
|
||||
"@assign-self" => "assignSelf" }
|
||||
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
- if can_admin_issue?
|
||||
.selectbox.hide-collapsed
|
||||
%input.js-vue{ type: "hidden",
|
||||
name: "issue[assignee_ids][]",
|
||||
|
@ -20,9 +20,9 @@
|
|||
":data-username" => "assignee.username" }
|
||||
.dropdown
|
||||
- dropdown_options = issue_assignees_dropdown_options
|
||||
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: { toggle: 'dropdown', field_name: 'issue[assignee_ids][]', first_user: current_user&.username, current_user: 'true', project_id: @project.id, null_user: 'true', multi_select: 'true', 'dropdown-header': dropdown_options[:data][:'dropdown-header'], 'max-select': dropdown_options[:data][:'max-select'] },
|
||||
":data-issuable-id" => "issue.id",
|
||||
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
|
||||
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: board_sidebar_user_data,
|
||||
":data-issuable-id" => "issue.iid",
|
||||
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
|
||||
= dropdown_options[:title]
|
||||
= icon("chevron-down")
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
|
|
@ -1,7 +1,7 @@
|
|||
.block.due_date
|
||||
.title
|
||||
Due date
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
- if can_admin_issue?
|
||||
= icon("spinner spin", class: "block-loading")
|
||||
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
|
||||
.value
|
||||
|
@ -10,12 +10,12 @@
|
|||
No due date
|
||||
%span.bold{ "v-if" => "issue.dueDate" }
|
||||
{{ issue.dueDate | due-date }}
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
- if can_admin_issue?
|
||||
%span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
|
||||
\-
|
||||
%a.js-remove-due-date{ href: "#", role: "button" }
|
||||
remove due date
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
- if can_admin_issue?
|
||||
.selectbox
|
||||
%input{ type: "hidden",
|
||||
name: "issue[due_date]",
|
||||
|
@ -23,7 +23,7 @@
|
|||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
|
||||
data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" },
|
||||
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
|
||||
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
|
||||
%span.dropdown-toggle-text Due date
|
||||
= icon('chevron-down')
|
||||
.dropdown-menu.dropdown-menu-due-date
|
|
@ -1,7 +1,7 @@
|
|||
.block.labels
|
||||
.title
|
||||
Labels
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
- if can_admin_issue?
|
||||
= icon("spinner spin", class: "block-loading")
|
||||
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
|
||||
.value.issuable-show-labels
|
||||
|
@ -11,7 +11,7 @@
|
|||
"v-for" => "label in issue.labels" }
|
||||
%span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
|
||||
{{ label.title }}
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
- if can_admin_issue?
|
||||
.selectbox
|
||||
%input{ type: "hidden",
|
||||
name: "issue[label_names][]",
|
||||
|
@ -19,12 +19,19 @@
|
|||
":value" => "label.id" }
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
|
||||
data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project.id, labels: project_labels_path(@project, :json), namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path) },
|
||||
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
|
||||
data: { toggle: "dropdown",
|
||||
field_name: "issue[label_names][]",
|
||||
show_no: "true",
|
||||
show_any: "true",
|
||||
project_id: @project&.try(:id),
|
||||
labels: labels_filter_path(false),
|
||||
namespace_path: @project.try(:namespace).try(:full_path),
|
||||
project_path: @project.try(:path) },
|
||||
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
|
||||
%span.dropdown-toggle-text
|
||||
Label
|
||||
= icon('chevron-down')
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
|
||||
= render partial: "shared/issuable/label_page_default"
|
||||
- if can? current_user, :admin_label, @project and @project
|
||||
- if can?(current_user, :admin_label, current_board_parent)
|
||||
= render partial: "shared/issuable/label_page_create"
|
|
@ -1,7 +1,7 @@
|
|||
.block.milestone
|
||||
.title
|
||||
Milestone
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
- if can_admin_issue?
|
||||
= icon("spinner spin", class: "block-loading")
|
||||
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
|
||||
.value
|
||||
|
@ -9,17 +9,17 @@
|
|||
None
|
||||
%span.bold.has-tooltip{ "v-if" => "issue.milestone" }
|
||||
{{ issue.milestone.title }}
|
||||
- if can?(current_user, :admin_issue, @project)
|
||||
- if can_admin_issue?
|
||||
.selectbox
|
||||
%input{ type: "hidden",
|
||||
":value" => "issue.milestone.id",
|
||||
name: "issue[milestone_id]",
|
||||
"v-if" => "issue.milestone" }
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: project_milestones_path(@project, :json), ability_name: "issue", use_id: "true", default_no: "true" },
|
||||
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
|
||||
":data-selected" => "milestoneTitle",
|
||||
":data-issuable-id" => "issue.id",
|
||||
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
|
||||
":data-issuable-id" => "issue.iid",
|
||||
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
|
||||
Milestone
|
||||
= icon("chevron-down")
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-selectable
|
|
@ -1,5 +1,5 @@
|
|||
- if current_user
|
||||
.block.light.subscription{ ":data-url" => "'#{project_issues_path(@project)}/' + issue.id + '/toggle_subscription'" }
|
||||
.block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" }
|
||||
%span.issuable-header-text.hide-collapsed.pull-left
|
||||
Notifications
|
||||
%button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
|
1
app/views/shared/boards/index.html.haml
Normal file
1
app/views/shared/boards/index.html.haml
Normal file
|
@ -0,0 +1 @@
|
|||
= render "show"
|
1
app/views/shared/boards/show.html.haml
Normal file
1
app/views/shared/boards/show.html.haml
Normal file
|
@ -0,0 +1 @@
|
|||
= render "show"
|
|
@ -8,20 +8,19 @@
|
|||
- if show_boards_content
|
||||
.issue-board-dropdown-content
|
||||
%p
|
||||
Create lists from the labels you use in your project. Issues with that
|
||||
label will automatically be added to the list.
|
||||
Create lists from labels. Issues with that label appear in that list.
|
||||
= dropdown_filter(filter_placeholder)
|
||||
= dropdown_content
|
||||
- if @project && show_footer
|
||||
- if current_board_parent && show_footer
|
||||
= dropdown_footer do
|
||||
%ul.dropdown-footer-list
|
||||
- if can?(current_user, :admin_label, @project)
|
||||
- if can?(current_user, :admin_label, current_board_parent)
|
||||
%li
|
||||
%a.dropdown-toggle-page{ href: "#" }
|
||||
Create new label
|
||||
%li
|
||||
= link_to project_labels_path(@project), :"data-is-link" => true do
|
||||
- if show_create && @project && can?(current_user, :admin_label, @project)
|
||||
= link_to labels_path, :"data-is-link" => true do
|
||||
- if show_create && can?(current_user, :admin_label, current_board_parent)
|
||||
Manage labels
|
||||
- else
|
||||
View labels
|
||||
|
|
|
@ -104,13 +104,13 @@
|
|||
= icon('times')
|
||||
.filter-dropdown-container
|
||||
- if type == :boards
|
||||
- if can?(current_user, :admin_list, @project)
|
||||
- if can?(current_user, :admin_list, board.parent)
|
||||
.dropdown.prepend-left-10#js-add-list
|
||||
%button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path) } }
|
||||
%button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
|
||||
Add list
|
||||
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
|
||||
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
|
||||
- if can?(current_user, :admin_label, @project)
|
||||
- if can?(current_user, :admin_label, board.parent)
|
||||
= render partial: "shared/issuable/label_page_create"
|
||||
= dropdown_loading
|
||||
#js-add-issues-btn.prepend-left-10
|
||||
|
|
|
@ -74,6 +74,19 @@ Rails.application.routes.draw do
|
|||
# Notification settings
|
||||
resources :notification_settings, only: [:create, :update]
|
||||
|
||||
# Boards resources shared between group and projects
|
||||
resources :boards do
|
||||
resources :lists, module: :boards, only: [:index, :create, :update, :destroy] do
|
||||
collection do
|
||||
post :generate
|
||||
end
|
||||
|
||||
resources :issues, only: [:index, :create, :update]
|
||||
end
|
||||
|
||||
resources :issues, module: :boards, only: [:index, :update]
|
||||
end
|
||||
|
||||
draw :import
|
||||
draw :uploads
|
||||
draw :explore
|
||||
|
|
|
@ -343,19 +343,7 @@ constraints(ProjectUrlConstrainer.new) do
|
|||
|
||||
get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
|
||||
|
||||
resources :boards, only: [:index, :show] do
|
||||
scope module: :boards do
|
||||
resources :issues, only: [:index, :update]
|
||||
|
||||
resources :lists, only: [:index, :create, :update, :destroy] do
|
||||
collection do
|
||||
post :generate
|
||||
end
|
||||
|
||||
resources :issues, only: [:index, :create]
|
||||
end
|
||||
end
|
||||
end
|
||||
resources :boards, only: [:index, :show, :create, :update, :destroy]
|
||||
|
||||
resources :todos, only: [:create]
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
|
|||
|
||||
- [Discussions](user/discussions/index.md) Threads, comments, and resolvable discussions in issues, commits, and merge requests.
|
||||
- [Issues](user/project/issues/index.md)
|
||||
- [Issue Board](user/project/issue_board.md)
|
||||
- [Project issue Board](user/project/issue_board.md)
|
||||
- [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests.
|
||||
- [Labels](user/project/labels.md): Categorize your issues or merge requests based on descriptive titles.
|
||||
- [Merge Requests](user/project/merge_requests/index.md)
|
||||
|
|
|
@ -26,6 +26,7 @@ module Gitlab
|
|||
apple-touch-icon.png
|
||||
assets
|
||||
autocomplete
|
||||
boards
|
||||
ci
|
||||
dashboard
|
||||
deploy.html
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Projects::Boards::IssuesController do
|
||||
describe Boards::IssuesController do
|
||||
let(:project) { create(:project) }
|
||||
let(:board) { create(:board, project: project) }
|
||||
let(:user) { create(:user) }
|
||||
|
@ -133,6 +133,22 @@ describe Projects::Boards::IssuesController do
|
|||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid board id' do
|
||||
it 'returns a not found 404 response' do
|
||||
create_issue user: user, board: 999, list: list1, title: 'New issue'
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid list id' do
|
||||
it 'returns a not found 404 response' do
|
||||
create_issue user: user, board: board, list: 999, title: 'New issue'
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unauthorized user' do
|
||||
|
@ -146,17 +162,15 @@ describe Projects::Boards::IssuesController do
|
|||
def create_issue(user:, board:, list:, title:)
|
||||
sign_in(user)
|
||||
|
||||
post :create, namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
board_id: board.to_param,
|
||||
post :create, board_id: board.to_param,
|
||||
list_id: list.to_param,
|
||||
issue: { title: title },
|
||||
issue: { title: title, project_id: project.id },
|
||||
format: :json
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH update' do
|
||||
let(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
|
||||
let!(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
|
||||
|
||||
context 'with valid params' do
|
||||
it 'returns a successful 200 response' do
|
||||
|
@ -186,7 +200,7 @@ describe Projects::Boards::IssuesController do
|
|||
end
|
||||
|
||||
it 'returns a not found 404 response for invalid issue id' do
|
||||
move user: user, board: board, issue: 999, from_list_id: list1.id, to_list_id: list2.id
|
||||
move user: user, board: board, issue: double(id: 999), from_list_id: list1.id, to_list_id: list2.id
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
|
@ -210,9 +224,9 @@ describe Projects::Boards::IssuesController do
|
|||
sign_in(user)
|
||||
|
||||
patch :update, namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
project_id: project.id,
|
||||
board_id: board.to_param,
|
||||
id: issue.to_param,
|
||||
id: issue.id,
|
||||
from_list_id: from_list_id,
|
||||
to_list_id: to_list_id,
|
||||
format: :json
|
|
@ -1,6 +1,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Projects::Boards::ListsController do
|
||||
describe Boards::ListsController do
|
||||
let(:project) { create(:project) }
|
||||
let(:board) { create(:board, project: project) }
|
||||
let(:user) { create(:user) }
|
|
@ -7,6 +7,7 @@ FactoryGirl.define do
|
|||
group nil
|
||||
project_id nil
|
||||
group_id nil
|
||||
parent nil
|
||||
end
|
||||
|
||||
trait :active do
|
||||
|
@ -26,6 +27,9 @@ FactoryGirl.define do
|
|||
milestone.project = evaluator.project
|
||||
elsif evaluator.project_id
|
||||
milestone.project_id = evaluator.project_id
|
||||
elsif evaluator.parent
|
||||
id = evaluator.parent.id
|
||||
evaluator.parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id
|
||||
else
|
||||
milestone.project = create(:project)
|
||||
end
|
||||
|
|
6
spec/fixtures/api/schemas/issue.json
vendored
6
spec/fixtures/api/schemas/issue.json
vendored
|
@ -8,10 +8,15 @@
|
|||
"properties" : {
|
||||
"id": { "type": "integer" },
|
||||
"iid": { "type": "integer" },
|
||||
"project_id": { "type": ["integer", "null"] },
|
||||
"title": { "type": "string" },
|
||||
"confidential": { "type": "boolean" },
|
||||
"due_date": { "type": ["date", "null"] },
|
||||
"relative_position": { "type": "integer" },
|
||||
"project": {
|
||||
"id": { "type": "integer" },
|
||||
"path": { "type": "string" }
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -34,6 +39,7 @@
|
|||
"type": "string",
|
||||
"pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
|
||||
},
|
||||
"type": { "type": "string" },
|
||||
"title": { "type": "string" },
|
||||
"priority": { "type": ["integer", "null"] }
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* global BoardService */
|
||||
/* global mockBoardService */
|
||||
import Vue from 'vue';
|
||||
import '~/boards/stores/boards_store';
|
||||
import boardBlankState from '~/boards/components/board_blank_state';
|
||||
|
@ -12,7 +13,7 @@ describe('Boards blank state', () => {
|
|||
const Comp = Vue.extend(boardBlankState);
|
||||
|
||||
gl.issueBoards.BoardsStore.create();
|
||||
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
|
||||
gl.boardService = mockBoardService();
|
||||
|
||||
spyOn(gl.boardService, 'generateDefaultLists').and.callFake(() => new Promise((resolve, reject) => {
|
||||
if (fail) {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
/* global listObj */
|
||||
/* global boardsMockInterceptor */
|
||||
/* global BoardService */
|
||||
/* global mockBoardService */
|
||||
|
||||
import Vue from 'vue';
|
||||
import '~/boards/models/assignee';
|
||||
|
@ -14,13 +15,13 @@ import '~/boards/stores/boards_store';
|
|||
import boardCard from '~/boards/components/board_card';
|
||||
import './mock_data';
|
||||
|
||||
describe('Issue card', () => {
|
||||
describe('Board card', () => {
|
||||
let vm;
|
||||
|
||||
beforeEach((done) => {
|
||||
Vue.http.interceptors.push(boardsMockInterceptor);
|
||||
|
||||
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
|
||||
gl.boardService = mockBoardService();
|
||||
gl.issueBoards.BoardsStore.create();
|
||||
gl.issueBoards.BoardsStore.detail.issue = {};
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
/* global List */
|
||||
/* global listObj */
|
||||
/* global ListIssue */
|
||||
/* global mockBoardService */
|
||||
import Vue from 'vue';
|
||||
import _ from 'underscore';
|
||||
import Sortable from 'vendor/Sortable';
|
||||
|
@ -24,7 +25,7 @@ describe('Board list component', () => {
|
|||
|
||||
document.body.appendChild(el);
|
||||
Vue.http.interceptors.push(boardsMockInterceptor);
|
||||
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
|
||||
gl.boardService = mockBoardService();
|
||||
gl.issueBoards.BoardsStore.create();
|
||||
gl.IssueBoardsApp = new Vue();
|
||||
|
||||
|
@ -32,6 +33,7 @@ describe('Board list component', () => {
|
|||
const list = new List(listObj);
|
||||
const issue = new ListIssue({
|
||||
title: 'Testing',
|
||||
id: 1,
|
||||
iid: 1,
|
||||
confidential: false,
|
||||
labels: [],
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
/* global BoardService */
|
||||
/* global List */
|
||||
/* global listObj */
|
||||
/* global mockBoardService */
|
||||
|
||||
import Vue from 'vue';
|
||||
import boardNewIssue from '~/boards/components/board_new_issue';
|
||||
|
@ -35,7 +36,7 @@ describe('Issue boards new issue form', () => {
|
|||
const BoardNewIssueComp = Vue.extend(boardNewIssue);
|
||||
|
||||
Vue.http.interceptors.push(boardsMockInterceptor);
|
||||
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
|
||||
gl.boardService = mockBoardService();
|
||||
gl.issueBoards.BoardsStore.create();
|
||||
gl.IssueBoardsApp = new Vue();
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
/* global listObj */
|
||||
/* global listObjDuplicate */
|
||||
/* global ListIssue */
|
||||
/* global mockBoardService */
|
||||
|
||||
import Vue from 'vue';
|
||||
import Cookies from 'js-cookie';
|
||||
|
@ -20,7 +21,7 @@ import './mock_data';
|
|||
describe('Store', () => {
|
||||
beforeEach(() => {
|
||||
Vue.http.interceptors.push(boardsMockInterceptor);
|
||||
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
|
||||
gl.boardService = mockBoardService();
|
||||
gl.issueBoards.BoardsStore.create();
|
||||
|
||||
spyOn(gl.boardService, 'moveIssue').and.callFake(() => new Promise((resolve) => {
|
||||
|
@ -78,7 +79,7 @@ describe('Store', () => {
|
|||
it('persists new list', (done) => {
|
||||
gl.issueBoards.BoardsStore.new({
|
||||
title: 'Test',
|
||||
type: 'label',
|
||||
list_type: 'label',
|
||||
label: {
|
||||
id: 1,
|
||||
title: 'Testing',
|
||||
|
@ -210,6 +211,7 @@ describe('Store', () => {
|
|||
it('moves issue in list', (done) => {
|
||||
const issue = new ListIssue({
|
||||
title: 'Testing',
|
||||
id: 2,
|
||||
iid: 2,
|
||||
confidential: false,
|
||||
labels: [],
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
/* global mockBoardService */
|
||||
import Vue from 'vue';
|
||||
import '~/boards/services/board_service';
|
||||
import '~/boards/components/board';
|
||||
import '~/boards/models/list';
|
||||
import '../mock_data';
|
||||
|
||||
describe('Board component', () => {
|
||||
let vm;
|
||||
|
@ -13,8 +15,12 @@ describe('Board component', () => {
|
|||
el = document.createElement('div');
|
||||
document.body.appendChild(el);
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
gl.boardService = new BoardService('/', '/', 1);
|
||||
gl.boardService = mockBoardService({
|
||||
boardsEndpoint: '/',
|
||||
listsEndpoint: '/',
|
||||
bulkUpdatePath: '/',
|
||||
boardId: 1,
|
||||
});
|
||||
|
||||
vm = new gl.issueBoards.Board({
|
||||
propsData: {
|
||||
|
|
|
@ -37,6 +37,7 @@ describe('Issue card component', () => {
|
|||
list = listObj;
|
||||
issue = new ListIssue({
|
||||
title: 'Testing',
|
||||
id: 1,
|
||||
iid: 1,
|
||||
confidential: false,
|
||||
labels: [list.label],
|
||||
|
@ -238,65 +239,63 @@ describe('Issue card component', () => {
|
|||
});
|
||||
|
||||
describe('labels', () => {
|
||||
describe('exists', () => {
|
||||
beforeEach((done) => {
|
||||
component.issue.addLabel(label1);
|
||||
beforeEach((done) => {
|
||||
component.issue.addLabel(label1);
|
||||
|
||||
Vue.nextTick(() => done());
|
||||
Vue.nextTick(() => done());
|
||||
});
|
||||
|
||||
it('renders list label', () => {
|
||||
expect(
|
||||
component.$el.querySelectorAll('.label').length,
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it('renders label', () => {
|
||||
const nodes = [];
|
||||
component.$el.querySelectorAll('.label').forEach((label) => {
|
||||
nodes.push(label.title);
|
||||
});
|
||||
|
||||
it('renders list label', () => {
|
||||
expect(
|
||||
component.$el.querySelectorAll('.label').length,
|
||||
).toBe(2);
|
||||
expect(
|
||||
nodes.includes(label1.description),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('sets label description as title', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.label').getAttribute('title'),
|
||||
).toContain(label1.description);
|
||||
});
|
||||
|
||||
it('sets background color of button', () => {
|
||||
const nodes = [];
|
||||
component.$el.querySelectorAll('.label').forEach((label) => {
|
||||
nodes.push(label.style.backgroundColor);
|
||||
});
|
||||
|
||||
it('renders label', () => {
|
||||
const nodes = [];
|
||||
component.$el.querySelectorAll('.label').forEach((label) => {
|
||||
nodes.push(label.title);
|
||||
});
|
||||
expect(
|
||||
nodes.includes(label1.color),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
expect(
|
||||
nodes.includes(label1.description),
|
||||
).toBe(true);
|
||||
});
|
||||
it('does not render label if label does not have an ID', (done) => {
|
||||
component.issue.addLabel(new ListLabel({
|
||||
title: 'closed',
|
||||
}));
|
||||
|
||||
it('sets label description as title', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.label').getAttribute('title'),
|
||||
).toContain(label1.description);
|
||||
});
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(
|
||||
component.$el.querySelectorAll('.label').length,
|
||||
).toBe(2);
|
||||
expect(
|
||||
component.$el.textContent,
|
||||
).not.toContain('closed');
|
||||
|
||||
it('sets background color of button', () => {
|
||||
const nodes = [];
|
||||
component.$el.querySelectorAll('.label').forEach((label) => {
|
||||
nodes.push(label.style.backgroundColor);
|
||||
});
|
||||
|
||||
expect(
|
||||
nodes.includes(label1.color),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render label if label does not have an ID', (done) => {
|
||||
component.issue.addLabel(new ListLabel({
|
||||
title: 'closed',
|
||||
}));
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(
|
||||
component.$el.querySelectorAll('.label').length,
|
||||
).toBe(2);
|
||||
expect(
|
||||
component.$el.textContent,
|
||||
).not.toContain('closed');
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* eslint-disable comma-dangle */
|
||||
/* global BoardService */
|
||||
/* global ListIssue */
|
||||
/* global mockBoardService */
|
||||
|
||||
import Vue from 'vue';
|
||||
import '~/lib/utils/url_utility';
|
||||
|
@ -16,11 +17,12 @@ describe('Issue model', () => {
|
|||
let issue;
|
||||
|
||||
beforeEach(() => {
|
||||
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
|
||||
gl.boardService = mockBoardService();
|
||||
gl.issueBoards.BoardsStore.create();
|
||||
|
||||
issue = new ListIssue({
|
||||
title: 'Testing',
|
||||
id: 1,
|
||||
iid: 1,
|
||||
confidential: false,
|
||||
labels: [{
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* eslint-disable comma-dangle */
|
||||
/* global boardsMockInterceptor */
|
||||
/* global BoardService */
|
||||
/* global mockBoardService */
|
||||
/* global List */
|
||||
/* global ListIssue */
|
||||
/* global listObj */
|
||||
|
@ -22,7 +23,9 @@ describe('List model', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
Vue.http.interceptors.push(boardsMockInterceptor);
|
||||
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
|
||||
gl.boardService = mockBoardService({
|
||||
bulkUpdatePath: '/test/issue-boards/board/1/lists',
|
||||
});
|
||||
gl.issueBoards.BoardsStore.create();
|
||||
|
||||
list = new List(listObj);
|
||||
|
@ -92,6 +95,7 @@ describe('List model', () => {
|
|||
const listDup = new List(listObjDuplicate);
|
||||
const issue = new ListIssue({
|
||||
title: 'Testing',
|
||||
id: _.random(10000),
|
||||
iid: _.random(10000),
|
||||
confidential: false,
|
||||
labels: [list.label, listDup.label],
|
||||
|
@ -118,6 +122,7 @@ describe('List model', () => {
|
|||
for (let i = 0; i < 30; i += 1) {
|
||||
list.issues.push(new ListIssue({
|
||||
title: 'Testing',
|
||||
id: _.random(10000) + i,
|
||||
iid: _.random(10000) + i,
|
||||
confidential: false,
|
||||
labels: [list.label],
|
||||
|
@ -137,7 +142,7 @@ describe('List model', () => {
|
|||
it('does not increase page number if issue count is less than the page size', () => {
|
||||
list.issues.push(new ListIssue({
|
||||
title: 'Testing',
|
||||
iid: _.random(10000),
|
||||
id: _.random(10000),
|
||||
confidential: false,
|
||||
labels: [list.label],
|
||||
assignees: [],
|
||||
|
@ -156,7 +161,7 @@ describe('List model', () => {
|
|||
spyOn(gl.boardService, 'newIssue').and.returnValue(Promise.resolve({
|
||||
json() {
|
||||
return {
|
||||
iid: 42,
|
||||
id: 42,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
@ -165,14 +170,14 @@ describe('List model', () => {
|
|||
it('adds new issue to top of list', (done) => {
|
||||
list.issues.push(new ListIssue({
|
||||
title: 'Testing',
|
||||
iid: _.random(10000),
|
||||
id: _.random(10000),
|
||||
confidential: false,
|
||||
labels: [list.label],
|
||||
assignees: [],
|
||||
}));
|
||||
const dummyIssue = new ListIssue({
|
||||
title: 'new issue',
|
||||
iid: _.random(10000),
|
||||
id: _.random(10000),
|
||||
confidential: false,
|
||||
labels: [list.label],
|
||||
assignees: [],
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* global BoardService */
|
||||
/* eslint-disable comma-dangle, no-unused-vars, quote-props */
|
||||
|
||||
const listObj = {
|
||||
|
@ -28,19 +29,19 @@ const listObjDuplicate = {
|
|||
|
||||
const BoardsMockData = {
|
||||
'GET': {
|
||||
'/test/issue-boards/board/1/lists{/id}/issues': {
|
||||
'/test/boards/1{/id}/issues': {
|
||||
issues: [{
|
||||
title: 'Testing',
|
||||
id: 1,
|
||||
iid: 1,
|
||||
confidential: false,
|
||||
labels: [],
|
||||
assignees: [],
|
||||
}],
|
||||
size: 1
|
||||
}
|
||||
},
|
||||
'POST': {
|
||||
'/test/issue-boards/board/1/lists{/id}': listObj
|
||||
'/test/boards/1{/id}': listObj
|
||||
},
|
||||
'PUT': {
|
||||
'/test/issue-boards/board/1/lists{/id}': {}
|
||||
|
@ -58,7 +59,22 @@ const boardsMockInterceptor = (request, next) => {
|
|||
}));
|
||||
};
|
||||
|
||||
const mockBoardService = (opts = {}) => {
|
||||
const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/board';
|
||||
const listsEndpoint = opts.listsEndpoint || '/test/boards/1';
|
||||
const bulkUpdatePath = opts.bulkUpdatePath || '';
|
||||
const boardId = opts.boardId || '1';
|
||||
|
||||
return new BoardService({
|
||||
boardsEndpoint,
|
||||
listsEndpoint,
|
||||
bulkUpdatePath,
|
||||
boardId,
|
||||
});
|
||||
};
|
||||
|
||||
window.listObj = listObj;
|
||||
window.listObjDuplicate = listObjDuplicate;
|
||||
window.BoardsMockData = BoardsMockData;
|
||||
window.boardsMockInterceptor = boardsMockInterceptor;
|
||||
window.mockBoardService = mockBoardService;
|
||||
|
|
|
@ -18,6 +18,7 @@ describe('Modal store', () => {
|
|||
|
||||
issue = new ListIssue({
|
||||
title: 'Testing',
|
||||
id: 1,
|
||||
iid: 1,
|
||||
confidential: false,
|
||||
labels: [],
|
||||
|
@ -25,6 +26,7 @@ describe('Modal store', () => {
|
|||
});
|
||||
issue2 = new ListIssue({
|
||||
title: 'Testing',
|
||||
id: 1,
|
||||
iid: 2,
|
||||
confidential: false,
|
||||
labels: [],
|
||||
|
|
|
@ -8,7 +8,7 @@ describe Boards::Issues::CreateService do
|
|||
let(:label) { create(:label, project: project, name: 'in-progress') }
|
||||
let!(:list) { create(:list, board: board, label: label, position: 0) }
|
||||
|
||||
subject(:service) { described_class.new(project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
|
||||
subject(:service) { described_class.new(board.parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
|
||||
|
||||
before do
|
||||
project.team << [user, :developer]
|
||||
|
|
|
@ -98,7 +98,7 @@ describe Boards::Issues::MoveService do
|
|||
issue.move_to_end && issue.save!
|
||||
end
|
||||
|
||||
params.merge!(move_after_iid: issue1.iid, move_before_iid: issue2.iid)
|
||||
params.merge!(move_after_id: issue1.id, move_before_id: issue2.id)
|
||||
|
||||
described_class.new(project, user, params).execute(issue)
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ describe Issues::UpdateService, :mailer do
|
|||
issue.save
|
||||
end
|
||||
|
||||
opts[:move_between_iids] = [issue1.iid, issue2.iid]
|
||||
opts[:move_between_ids] = [issue1.id, issue2.id]
|
||||
|
||||
update_issue(opts)
|
||||
|
||||
|
|
Loading…
Reference in a new issue