diff options
author | Sean McGivern <sean@mcgivern.me.uk> | 2017-09-07 16:42:23 +0300 |
---|---|---|
committer | Sean McGivern <sean@mcgivern.me.uk> | 2017-09-07 16:42:23 +0300 |
commit | b4778a586665fe522ae3f4f7a567766b696b847d (patch) | |
tree | 133d25f06731111382632e8f42b81532b9b19b81 | |
parent | cb555da178573ceac01f0b76060572545857364a (diff) | |
parent | a1a839c99f7d9ce69ce0712d93951dba216ecb11 (diff) |
Merge branch 'ee_issue_928_backport' into 'master'
Group boards CE backport
See merge request !13883
78 files changed, 774 insertions, 527 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 8acddd6194c..38d1effc77c 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -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', diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 89c14180149..ea00efe4b46 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -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> `, }); }); diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index bebca17fb1e..6159680f1e6 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -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" diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js index 4af8b0c7713..541b8049855 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js +++ b/app/assets/javascripts/boards/components/board_new_issue.js @@ -6,7 +6,10 @@ const Store = gl.issueBoards.BoardsStore; export default { name: 'BoardNewIssue', props: { - list: Object, + list: { + type: Object, + required: true, + }, }, data() { return { diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 9a5d87ede7e..bf474879024 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -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> diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index 478a1335b2b..a656f0546c0 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -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, { diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 72bb9e10fbc..d7f203b3f96 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -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); }); diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index 6a900d4abd0..1e623cf58b7 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -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) => { diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 6c2d8a3781b..407db176446 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -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; diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/boards/models/label.js index 9af88d167d6..98c1ec014c4 100644 --- a/app/assets/javascripts/boards/models/label.js +++ b/app/assets/javascripts/boards/models/label.js @@ -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; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 08f7c5ddcd2..df2809e1805 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -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) { diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 3742507b236..38eea38f949 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -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, }); } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d4a1bb8402c..cf557d94bdd 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -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; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 314dd2d1a21..700be173039 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -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; } diff --git a/app/controllers/boards/application_controller.rb b/app/controllers/boards/application_controller.rb new file mode 100644 index 00000000000..b2675025fc0 --- /dev/null +++ b/app/controllers/boards/application_controller.rb @@ -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 diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb new file mode 100644 index 00000000000..8d4ec2d6d9d --- /dev/null +++ b/app/controllers/boards/issues_controller.rb @@ -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 diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb new file mode 100644 index 00000000000..381fd4d7508 --- /dev/null +++ b/app/controllers/boards/lists_controller.rb @@ -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 diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb new file mode 100644 index 00000000000..2c9c095a5d7 --- /dev/null +++ b/app/controllers/concerns/boards_responses.rb @@ -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 diff --git a/app/controllers/projects/boards/application_controller.rb b/app/controllers/projects/boards/application_controller.rb deleted file mode 100644 index dad38fff6b9..00000000000 --- a/app/controllers/projects/boards/application_controller.rb +++ /dev/null @@ -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 diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb deleted file mode 100644 index 653e7bc7e40..00000000000 --- a/app/controllers/projects/boards/issues_controller.rb +++ /dev/null @@ -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 diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb deleted file mode 100644 index ad53bb749a0..00000000000 --- a/app/controllers/projects/boards/lists_controller.rb +++ /dev/null @@ -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 diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 808affa4f98..d1b99ecce4a 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -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 - - respond_to do |format| - format.html - format.json do - render json: serialize_as_json(@boards) - end - end + @boards = Boards::ListService.new(project, current_user).execute + + 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 diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 8b33c362a9c..4bd61aa8f86 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -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 diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index ce2999e6696..66e1e607e01 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -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", diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index e60513b35c7..e1ba7898ee6 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -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 diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 98e824a8c65..af6683a548b 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -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 diff --git a/app/models/board.rb b/app/models/board.rb index 97d0f550925..5bb7d3d3722 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -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 diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 7cb9a28a284..e961c97e337 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -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] diff --git a/app/models/label.rb b/app/models/label.rb index 674bb3f2720..958141a7358 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index fdd516ec2ae..18800921c6c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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 diff --git a/app/services/boards/base_service.rb b/app/services/boards/base_service.rb new file mode 100644 index 00000000000..72822ffffa1 --- /dev/null +++ b/app/services/boards/base_service.rb @@ -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 diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 9eedb9e65a2..bd0bb387662 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -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) diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb index c0d7ff5b585..7c4a79f555e 100644 --- a/app/services/boards/issues/create_service.rb +++ b/app/services/boards/issues/create_service.rb @@ -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 diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index eb345fead2d..d85d93e251b 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -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 diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index ecabb2a48e4..797d6df7c1a 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -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 diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb index 84f1fc3a4e2..6d0dd0a9f99 100644 --- a/app/services/boards/list_service.rb +++ b/app/services/boards/list_service.rb @@ -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 diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index fe0d762ccd2..183556a1d6b 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -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) diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb index f986e05944c..d75c5fd3dc6 100644 --- a/app/services/boards/lists/destroy_service.rb +++ b/app/services/boards/lists/destroy_service.rb @@ -1,6 +1,6 @@ module Boards module Lists - class DestroyService < BaseService + class DestroyService < Boards::BaseService def execute(list) return false unless list.destroyable? diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb index 939f9bfd068..05d4ab5dbcc 100644 --- a/app/services/boards/lists/generate_service.rb +++ b/app/services/boards/lists/generate_service.rb @@ -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 diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb index df2a01a69e5..e57c95294af 100644 --- a/app/services/boards/lists/list_service.rb +++ b/app/services/boards/lists/list_service.rb @@ -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? diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb index f2a68865f7b..7d0730e8332 100644 --- a/app/services/boards/lists/move_service.rb +++ b/app/services/boards/lists/move_service.rb @@ -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 diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index deb4990eb4f..b4ca3966505 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -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 diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml index 2a5b8b1441e..bb56769bd3f 100644 --- a/app/views/projects/boards/index.html.haml +++ b/app/views/projects/boards/index.html.haml @@ -1 +1 @@ -= render "show" += render "shared/boards/show", board: @boards.first diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml index 2a5b8b1441e..e5b5f6404bb 100644 --- a/app/views/projects/boards/show.html.haml +++ b/app/views/projects/boards/show.html.haml @@ -1 +1 @@ -= render "show" += render "shared/boards/show", board: @board diff --git a/app/views/projects/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index 303e20e8780..1a50b7d4b69 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -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, diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index 64f5f6d7ba0..ce0aa72ab00 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -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"' } diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml index 09d70f658a3..b3f73e96b81 100644 --- a/app/views/projects/boards/components/_sidebar.html.haml +++ b/app/views/shared/boards/components/_sidebar.html.haml @@ -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" } diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/shared/boards/components/sidebar/_assignee.html.haml index 8d957613be1..3d2e8471a60 100644 --- a/app/views/projects/boards/components/sidebar/_assignee.html.haml +++ b/app/views/shared/boards/components/sidebar/_assignee.html.haml @@ -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 diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/shared/boards/components/sidebar/_due_date.html.haml index e8394eab213..db794d6f855 100644 --- a/app/views/projects/boards/components/sidebar/_due_date.html.haml +++ b/app/views/shared/boards/components/sidebar/_due_date.html.haml @@ -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 diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index 6b389736e8b..1f540bdaf93 100644 --- a/app/views/projects/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -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" diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml index a1ddb261ea3..d09c7c218e0 100644 --- a/app/views/projects/boards/components/sidebar/_milestone.html.haml +++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml @@ -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 diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/shared/boards/components/sidebar/_notifications.html.haml index aaddd7e249f..9b989c23cab 100644 --- a/app/views/projects/boards/components/sidebar/_notifications.html.haml +++ b/app/views/shared/boards/components/sidebar/_notifications.html.haml @@ -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" } diff --git a/app/views/shared/boards/index.html.haml b/app/views/shared/boards/index.html.haml new file mode 100644 index 00000000000..2a5b8b1441e --- /dev/null +++ b/app/views/shared/boards/index.html.haml @@ -0,0 +1 @@ += render "show" diff --git a/app/views/shared/boards/show.html.haml b/app/views/shared/boards/show.html.haml new file mode 100644 index 00000000000..2a5b8b1441e --- /dev/null +++ b/app/views/shared/boards/show.html.haml @@ -0,0 +1 @@ += render "show" diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index e8feff32d26..ad031e6af80 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -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 diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index e81789ea7a2..161b1c9fd72 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -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 diff --git a/config/routes.rb b/config/routes.rb index ce7ab1d20f6..5683725c8a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/config/routes/project.rb b/config/routes/project.rb index a15e7f8a344..b36d13888cd 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -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] diff --git a/doc/README.md b/doc/README.md index b250fa08382..a59f71e83a5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -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) diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index 894bd5efae5..7c02c9c5c48 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -26,6 +26,7 @@ module Gitlab apple-touch-icon.png assets autocomplete + boards ci dashboard deploy.html diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 3f6c1092163..dfa06c78d46 100644 --- a/spec/controllers/projects/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -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 diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb index 65beec16307..b11fce0fa58 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/boards/lists_controller_spec.rb @@ -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) } diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb index 2f75bf12cd7..b5298b2f969 100644 --- a/spec/factories/milestones.rb +++ b/spec/factories/milestones.rb @@ -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 diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index ff86437fdd5..e1f62508933 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -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"] } }, diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js index 47baf83512f..2ee3792dd65 100644 --- a/spec/javascripts/boards/board_blank_state_spec.js +++ b/spec/javascripts/boards/board_blank_state_spec.js @@ -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) { diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 447b244c71f..83b13b06dc1 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -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 = {}; diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index a89be911667..6bd00943a8f 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -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: [], diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index eac2eecb6bc..02e6692dda8 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -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(); diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 5ea160b7790..9e5b0bd3efe 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -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: [], diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js index c4e8966ad6c..8dacac20cad 100644 --- a/spec/javascripts/boards/components/board_spec.js +++ b/spec/javascripts/boards/components/board_spec.js @@ -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: { diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 47aaa57e6b9..7d430ec35e2 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -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 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 label', () => { - const nodes = []; - component.$el.querySelectorAll('.label').forEach((label) => { - nodes.push(label.title); - }); + expect( + nodes.includes(label1.description), + ).toBe(true); + }); - 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 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('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); + }); - 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', + })); - 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'); - 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); }); }); }); diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index cd1497bc5e6..022d286d5df 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -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: [{ diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index db50829a276..d4627223a12 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -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: [], diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index a64c3964ee3..0a93086985e 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -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; diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js index 32e6d04df9f..7eecb58a4c3 100644 --- a/spec/javascripts/boards/modal_store_spec.js +++ b/spec/javascripts/boards/modal_store_spec.js @@ -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: [], diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb index f2ddaa903da..1a56164dba4 100644 --- a/spec/services/boards/issues/create_service_spec.rb +++ b/spec/services/boards/issues/create_service_spec.rb @@ -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] diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index 63dfe80d672..464ff9f94b3 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -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) diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 85f46838351..15a50b85f19 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -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) |