diff options
| author | Douwe Maan <douwe@gitlab.com> | 2017-06-07 15:05:37 +0000 | 
|---|---|---|
| committer | Douwe Maan <douwe@gitlab.com> | 2017-06-07 15:05:37 +0000 | 
| commit | 0bcb1d35ecb7db43a075c0a4ae9782991f724f1a (patch) | |
| tree | 5ef82ca038eb3cb97e0eb351f33eb58261c3c536 | |
| parent | a5757c72d967acd82865f9f7cb288d5b61b0b35c (diff) | |
| parent | 1633d3d7d8b8589a3d04358d6473cfd168633a10 (diff) | |
| download | gitlab-ce-0bcb1d35ecb7db43a075c0a4ae9782991f724f1a.tar.gz | |
Merge branch 'expand-backlog-closed-lists-issue-boards' into 'master'
Expand/collapse close & backlog lists in issue boards
Closes #23917
See merge request !11820
31 files changed, 402 insertions, 143 deletions
| diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 0e4aa39226b..b94009ee76b 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -88,6 +88,8 @@ $(() => {              if (list.type === 'closed') {                list.position = Infinity;                list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' }; +            } else if (list.type === 'backlog') { +              list.position = -1;              }            }); @@ -128,7 +130,7 @@ $(() => {      },      computed: {        disabled() { -        return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length; +        return !this.store.lists.filter(list => !list.preset).length;        },        tooltipTitle() {          if (this.disabled) { diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 9ba84489910..adb7360327c 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,6 +1,7 @@  /* eslint-disable comma-dangle, space-before-function-paren, one-var */  /* global Sortable */  import Vue from 'vue'; +import AccessorUtilities from '../../lib/utils/accessor';  import boardList from './board_list';  import boardBlankState from './board_blank_state';  import './board_delete'; @@ -22,6 +23,10 @@ gl.issueBoards.Board = Vue.extend({      disabled: Boolean,      issueLinkBase: String,      rootPath: String, +    boardId: { +      type: String, +      required: true, +    },    },    data () {      return { @@ -78,7 +83,16 @@ gl.issueBoards.Board = Vue.extend({    methods: {      showNewIssueForm() {        this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; -    } +    }, +    toggleExpanded(e) { +      if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) { +        this.list.isExpanded = !this.list.isExpanded; + +        if (AccessorUtilities.isLocalStorageAccessSafe()) { +          localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); +        } +      } +    },    },    mounted () {      this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ @@ -102,4 +116,11 @@ gl.issueBoards.Board = Vue.extend({      this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);    }, +  created() { +    if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) { +      const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false'; + +      this.list.isExpanded = !isCollapsed; +    } +  },  }); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 4699ef5a51c..daef01bc93d 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -152,6 +152,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({          <div class="card-assignee">            <user-avatar-link              v-for="(assignee, index) in issue.assignees" +            :key="assignee.id"              v-if="shouldRenderAssignee(index)"              class="js-no-trigger"              :link-href="assigneeUrl(assignee)" diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index fe7ab2db85d..478a1335b2b 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -26,7 +26,8 @@ gl.issueBoards.ModalFooter = Vue.extend({    },    methods: {      addIssues() { -      const list = this.modal.selectedList || this.state.lists[0]; +      const firstListIndex = 1; +      const list = this.modal.selectedList || this.state.lists[firstListIndex];        const selectedIssues = ModalStore.getSelectedIssues();        const issueIds = selectedIssues.map(issue => issue.globalId); diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js index 8cd15df90fa..4684ea76647 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js @@ -11,7 +11,7 @@ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({    },    computed: {      selected() { -      return this.modal.selectedList || this.state.lists[0]; +      return this.modal.selectedList || this.state.lists[1];      },    },    destroyed() { diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 8514e7df1d3..548de1a4c52 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -12,7 +12,9 @@ class List {      this.position = obj.position;      this.title = obj.title;      this.type = obj.list_type; -    this.preset = ['closed', 'blank'].indexOf(this.type) > -1; +    this.preset = ['backlog', 'closed', 'blank'].indexOf(this.type) > -1; +    this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1; +    this.isExpanded = true;      this.page = 1;      this.loading = true;      this.loadingMore = false; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 7bddcdc3c1d..1e12d4ca415 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -32,10 +32,14 @@ gl.issueBoards.BoardsStore = {    },    new (listObj) {      const list = this.addList(listObj); +    const backlogList = this.findList('type', 'backlog', 'backlog');      list        .save()        .then(() => { +        // Remove any new issues from the backlog +        // as they will be visible in the new list +        list.issues.forEach(backlogList.removeIssue.bind(backlogList));          this.state.lists = _.sortBy(this.state.lists, 'position');        })        .catch(() => { @@ -48,7 +52,7 @@ gl.issueBoards.BoardsStore = {    },    shouldAddBlankState () {      // Decide whether to add the blank state -    return !(this.state.lists.filter(list => list.type !== 'closed')[0]); +    return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]);    },    addBlankState () {      if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; @@ -101,7 +105,7 @@ gl.issueBoards.BoardsStore = {        issueTo.removeLabel(listFrom.label);      } -    if (listTo.type === 'closed') { +    if (listTo.type === 'closed' && listFrom.type !== 'backlog') {        issueLists.forEach((list) => {          list.removeIssue(issue);        }); diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index e3e94c8ca50..740e383dbb5 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -96,9 +96,51 @@    @media (min-width: $screen-sm-min) {      width: 400px;    } + +  &.is-expandable { +    .board-header { +      cursor: pointer; +    } +  } + +  &.is-collapsed { +    width: 50px; + +    .board-header { +      position: absolute; +      top: 0; +      right: 0; +      bottom: 0; +      left: 0; +    } + +    .board-title { +      position: initial; +      padding: 0; +      border-bottom: 0; + +      > span { +        display: block; +        transform: rotate(90deg) translate(25px, 0); +      } +    } + +    .board-title-expandable-toggle { +      position: absolute; +      top: 50%; +      left: 50%; +      margin-left: -10px; +    } + +    .board-list-component, +    .board-issue-count-holder { +      display: none; +    } +  }  }  .board-inner { +  position: relative;    height: 100%;    font-size: $issue-boards-font-size;    background: $gray-light; diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb index 67e3c9add81..ad53bb749a0 100644 --- a/app/controllers/projects/boards/lists_controller.rb +++ b/app/controllers/projects/boards/lists_controller.rb @@ -5,7 +5,9 @@ module Projects        before_action :authorize_read_list!, only: [:index]        def index -        render json: serialize_as_json(board.lists) +        lists = ::Boards::Lists::ListService.new(project, current_user).execute(board) + +        render json: serialize_as_json(lists)        end        def create diff --git a/app/models/board.rb b/app/models/board.rb index cf8317891b5..18081a32157 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -5,6 +5,10 @@ class Board < ActiveRecord::Base    validates :project, presence: true +  def backlog_list +    lists.merge(List.backlog).take +  end +    def closed_list      lists.merge(List.closed).take    end diff --git a/app/models/list.rb b/app/models/list.rb index ba7353a1325..918275be142 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -2,7 +2,7 @@ class List < ActiveRecord::Base    belongs_to :board    belongs_to :label -  enum list_type: { label: 1, closed: 2 } +  enum list_type: { backlog: 0, label: 1, closed: 2 }    validates :board, :list_type, presence: true    validates :label, :position, presence: true, if: :label? diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index fd9ff115eab..68f6a8619e5 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -12,6 +12,7 @@ module Boards      def create_board!        board = project.boards.create +      board.lists.create(list_type: :backlog)        board.lists.create(list_type: :closed)        board diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 533e6787855..418fa9afd6e 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -3,7 +3,7 @@ module Boards      class ListService < BaseService        def execute          issues = IssuesFinder.new(current_user, filter_params).execute -        issues = without_board_labels(issues) unless list +        issues = without_board_labels(issues) unless movable_list?          issues = with_list_label(issues) if movable_list?          issues.order_by_position_and_priority        end diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb index c579ed4c869..df2a01a69e5 100644 --- a/app/services/boards/lists/list_service.rb +++ b/app/services/boards/lists/list_service.rb @@ -2,6 +2,8 @@ module Boards    module Lists      class ListService < BaseService        def execute(board) +        board.lists.create(list_type: :backlog) unless board.lists.backlog.exists? +          board.lists        end      end diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index efec69662f3..6684ecfce81 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -26,6 +26,7 @@        ":disabled" => "disabled",        ":issue-link-base" => "issueLinkBase",        ":root-path" => "rootPath", +      ":board-id" => "boardId",        ":key" => "_uid" }    = render "projects/boards/components/sidebar"    %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index bc5c727bf0d..55c4d51be14 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -1,8 +1,11 @@ -.board{ ":class" => '{ "is-draggable": !list.preset }', +.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded }',    ":data-id" => "list.id" }    .board-inner -    %header.board-header{ ":class" => '{ "has-border": list.label }', ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } +    %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" }        %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' } +        %i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable", +          ":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{ ":title" => '(list.label ? list.label.description : "")',            data: { container: "body", placement: "bottom" } }            {{ list.title }} @@ -10,13 +13,13 @@            %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }              {{ list.issuesSize }}            - if can?(current_user, :admin_issue, @project) -            %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button", +            %button.btn.btn-small.btn-default.pull-right.has-tooltip.js-no-trigger-collapse{ type: "button",                "@click" => "showNewIssueForm",                "v-if" => 'list.type !== "closed"',                "aria-label" => "New issue",                "title" => "New issue",                data: { placement: "top", container: "body" } } -              = icon("plus") +              = icon("plus", class: "js-no-trigger-collapse")          - if can?(current_user, :admin_list, @project)            %board-delete{ "inline-template" => true,              ":list" => "list", diff --git a/changelogs/unreleased/expand-backlog-closed-lists-issue-boards.yml b/changelogs/unreleased/expand-backlog-closed-lists-issue-boards.yml new file mode 100644 index 00000000000..4796f8e918b --- /dev/null +++ b/changelogs/unreleased/expand-backlog-closed-lists-issue-boards.yml @@ -0,0 +1,4 @@ +--- +title: Expand/collapse backlog & closed lists in issue boards +merge_request: +author: diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index 432f3c53c90..0f2664262e8 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do        parsed_response = JSON.parse(response.body)        expect(response).to match_response_schema('lists') -      expect(parsed_response.length).to eq 2 +      expect(parsed_response.length).to eq 3      end      context 'with unauthorized user' do diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb index f6a78811cbe..48142d3c49b 100644 --- a/spec/factories/lists.rb +++ b/spec/factories/lists.rb @@ -6,6 +6,12 @@ FactoryGirl.define do      sequence(:position)    end +  factory :backlog_list, parent: :list do +    list_type :backlog +    label nil +    position nil +  end +    factory :closed_list, parent: :list do      list_type :closed      label nil diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index 32ac265814f..2b8edac4f10 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -231,7 +231,7 @@ describe 'Issue Boards add issue modal', :feature, :js do            click_button 'Add 1 issue'          end -        page.within(first('.board')) do +        page.within(find('.board:nth-child(2)')) do            expect(page).to have_selector('.card')          end        end @@ -247,7 +247,7 @@ describe 'Issue Boards add issue modal', :feature, :js do            click_button 'Add 1 issue'          end -        page.within(find('.board:nth-child(2)')) do +        page.within(find('.board:nth-child(3)')) do            expect(page).to have_selector('.card')          end        end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index ba27db23ced..c80453b8227 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -19,7 +19,7 @@ describe 'Issue Boards', feature: true, js: true do      before do        visit namespace_project_board_path(project.namespace, project, board)        wait_for_requests -      expect(page).to have_selector('.board', count: 2) +      expect(page).to have_selector('.board', count: 3)      end      it 'shows blank state' do @@ -36,18 +36,18 @@ describe 'Issue Boards', feature: true, js: true do        page.within(find('.board-blank-state')) do          click_button("Nevermind, I'll use my own")        end -      expect(page).to have_selector('.board', count: 1) +      expect(page).to have_selector('.board', count: 2)      end      it 'creates default lists' do -      lists = ['To Do', 'Doing', 'Closed'] +      lists = ['Backlog', 'To Do', 'Doing', 'Closed']        page.within(find('.board-blank-state')) do          click_button('Add default lists')        end        wait_for_requests -      expect(page).to have_selector('.board', count: 3) +      expect(page).to have_selector('.board', count: 4)        page.all('.board').each_with_index do |list, i|          expect(list.find('.board-title')).to have_content(lists[i]) @@ -85,29 +85,25 @@ describe 'Issue Boards', feature: true, js: true do        wait_for_requests -      expect(page).to have_selector('.board', count: 3) -      expect(find('.board:nth-child(1)')).to have_selector('.card') +      expect(page).to have_selector('.board', count: 4)        expect(find('.board:nth-child(2)')).to have_selector('.card')        expect(find('.board:nth-child(3)')).to have_selector('.card') -    end - -    it 'shows lists' do -      expect(page).to have_selector('.board', count: 3) +      expect(find('.board:nth-child(4)')).to have_selector('.card')      end      it 'shows description tooltip on list title' do -      page.within('.board:nth-child(1)') do +      page.within('.board:nth-child(2)') do          expect(find('.board-title span.has-tooltip')[:title]).to eq('Test')        end      end      it 'shows issues in lists' do -      wait_for_board_cards(1, 8) -      wait_for_board_cards(2, 2) +      wait_for_board_cards(2, 8) +      wait_for_board_cards(3, 2)      end      it 'shows confidential issues with icon' do -      page.within(find('.board', match: :first)) do +      page.within(find('.board:nth-child(2)')) do          expect(page).to have_selector('.confidential-icon', count: 1)        end      end @@ -118,9 +114,9 @@ describe 'Issue Boards', feature: true, js: true do        wait_for_requests -      expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)        expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) -      expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1) +      expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) +      expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)      end      it 'search list' do @@ -129,32 +125,32 @@ describe 'Issue Boards', feature: true, js: true do        wait_for_requests -      expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) -      expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) +      expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)        expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) +      expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)      end      it 'allows user to delete board' do -      page.within(find('.board:nth-child(1)')) do +      page.within(find('.board:nth-child(2)')) do          find('.board-delete').click        end        wait_for_requests -      expect(page).to have_selector('.board', count: 2) +      expect(page).to have_selector('.board', count: 3)      end      it 'removes checkmark in new list dropdown after deleting' do        click_button 'Add list'        wait_for_requests -      page.within(find('.board:nth-child(1)')) do +      page.within(find('.board:nth-child(2)')) do          find('.board-delete').click        end        wait_for_requests -      expect(page).to have_selector('.board', count: 2) +      expect(page).to have_selector('.board', count: 3)      end      it 'infinite scrolls list' do @@ -165,18 +161,18 @@ describe 'Issue Boards', feature: true, js: true do        visit namespace_project_board_path(project.namespace, project, board)        wait_for_requests -      page.within(find('.board', match: :first)) do +      page.within(find('.board:nth-child(2)')) do          expect(page.find('.board-header')).to have_content('58')          expect(page).to have_selector('.card', count: 20)          expect(page).to have_content('Showing 20 of 58 issues') -        evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") +        evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")          wait_for_requests          expect(page).to have_selector('.card', count: 40)          expect(page).to have_content('Showing 40 of 58 issues') -        evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") +        evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")          wait_for_requests          expect(page).to have_selector('.card', count: 58) @@ -186,83 +182,83 @@ describe 'Issue Boards', feature: true, js: true do      context 'closed' do        it 'shows list of closed issues' do -        wait_for_board_cards(3, 1) +        wait_for_board_cards(4, 1)          wait_for_requests        end        it 'moves issue to closed' do -        drag(list_from_index: 0, list_to_index: 2) +        drag(list_from_index: 1, list_to_index: 3) -        wait_for_board_cards(1, 7) -        wait_for_board_cards(2, 2) +        wait_for_board_cards(2, 7)          wait_for_board_cards(3, 2) +        wait_for_board_cards(4, 2) -        expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) -        expect(find('.board:nth-child(3)')).to have_selector('.card', count: 2) -        expect(find('.board:nth-child(3)')).to have_content(issue9.title) -        expect(find('.board:nth-child(3)')).not_to have_content(planning.title) +        expect(find('.board:nth-child(2)')).not_to have_content(issue9.title) +        expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2) +        expect(find('.board:nth-child(4)')).to have_content(issue9.title) +        expect(find('.board:nth-child(4)')).not_to have_content(planning.title)        end        it 'removes all of the same issue to closed' do -        drag(list_from_index: 0, list_to_index: 2) +        drag(list_from_index: 1, list_to_index: 3) -        wait_for_board_cards(1, 7) -        wait_for_board_cards(2, 2) +        wait_for_board_cards(2, 7)          wait_for_board_cards(3, 2) +        wait_for_board_cards(4, 2) -        expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) -        expect(find('.board:nth-child(3)')).to have_content(issue9.title) -        expect(find('.board:nth-child(3)')).not_to have_content(planning.title) +        expect(find('.board:nth-child(2)')).not_to have_content(issue9.title) +        expect(find('.board:nth-child(4)')).to have_content(issue9.title) +        expect(find('.board:nth-child(4)')).not_to have_content(planning.title)        end      end      context 'lists' do        it 'changes position of list' do -        drag(list_from_index: 1, list_to_index: 0, selector: '.board-header') +        drag(list_from_index: 2, list_to_index: 1, selector: '.board-header') -        wait_for_board_cards(1, 2) -        wait_for_board_cards(2, 8) -        wait_for_board_cards(3, 1) +        wait_for_board_cards(2, 2) +        wait_for_board_cards(3, 8) +        wait_for_board_cards(4, 1) -        expect(find('.board:nth-child(1)')).to have_content(development.title) -        expect(find('.board:nth-child(1)')).to have_content(planning.title) +        expect(find('.board:nth-child(2)')).to have_content(development.title) +        expect(find('.board:nth-child(2)')).to have_content(planning.title)        end        it 'issue moves between lists' do -        drag(list_from_index: 0, from_index: 1, list_to_index: 1) +        drag(list_from_index: 1, from_index: 1, list_to_index: 2) -        wait_for_board_cards(1, 7) -        wait_for_board_cards(2, 2) -        wait_for_board_cards(3, 1) +        wait_for_board_cards(2, 7) +        wait_for_board_cards(3, 2) +        wait_for_board_cards(4, 1) -        expect(find('.board:nth-child(2)')).to have_content(issue6.title) -        expect(find('.board:nth-child(2)').all('.card').last).not_to have_content(development.title) +        expect(find('.board:nth-child(3)')).to have_content(issue6.title) +        expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)        end        it 'issue moves between lists' do -        drag(list_from_index: 1, list_to_index: 0) +        drag(list_from_index: 2, list_to_index: 1) -        wait_for_board_cards(1, 9) -        wait_for_board_cards(2, 1) +        wait_for_board_cards(2, 9)          wait_for_board_cards(3, 1) +        wait_for_board_cards(4, 1) -        expect(find('.board:nth-child(1)')).to have_content(issue7.title) -        expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title) +        expect(find('.board:nth-child(2)')).to have_content(issue7.title) +        expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)        end        it 'issue moves from closed' do -        drag(list_from_index: 2, list_to_index: 1) +        drag(list_from_index: 3, list_to_index: 2) -        expect(find('.board:nth-child(2)')).to have_content(issue8.title) +        expect(find('.board:nth-child(3)')).to have_content(issue8.title) -        wait_for_board_cards(1, 8) -        wait_for_board_cards(2, 3) -        wait_for_board_cards(3, 0) +        wait_for_board_cards(2, 8) +        wait_for_board_cards(3, 3) +        wait_for_board_cards(4, 0)        end        context 'issue card' do          it 'shows assignee' do -          page.within(find('.board', match: :first)) do +          page.within(find('.board:nth-child(2)')) do              expect(page).to have_selector('.avatar', count: 1)            end          end @@ -290,7 +286,7 @@ describe 'Issue Boards', feature: true, js: true do            wait_for_requests -          expect(page).to have_selector('.board', count: 4) +          expect(page).to have_selector('.board', count: 5)          end          it 'creates new list for Backlog label' do @@ -303,7 +299,7 @@ describe 'Issue Boards', feature: true, js: true do            wait_for_requests -          expect(page).to have_selector('.board', count: 4) +          expect(page).to have_selector('.board', count: 5)          end          it 'creates new list for Closed label' do @@ -316,7 +312,7 @@ describe 'Issue Boards', feature: true, js: true do            wait_for_requests -          expect(page).to have_selector('.board', count: 4) +          expect(page).to have_selector('.board', count: 5)          end          it 'keeps dropdown open after adding new list' do @@ -348,7 +344,7 @@ describe 'Issue Boards', feature: true, js: true do            wait_for_requests            wait_for_requests -          expect(page).to have_selector('.board', count: 4) +          expect(page).to have_selector('.board', count: 5)          end        end      end @@ -360,8 +356,8 @@ describe 'Issue Boards', feature: true, js: true do          submit_filter          wait_for_requests -        wait_for_board_cards(1, 1) -        wait_for_empty_boards((2..3)) +        wait_for_board_cards(2, 1) +        wait_for_empty_boards((3..4))        end        it 'filters by assignee' do @@ -371,8 +367,8 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_requests -        wait_for_board_cards(1, 1) -        wait_for_empty_boards((2..3)) +        wait_for_board_cards(2, 1) +        wait_for_empty_boards((3..4))        end        it 'filters by milestone' do @@ -381,9 +377,9 @@ describe 'Issue Boards', feature: true, js: true do          submit_filter          wait_for_requests -        wait_for_board_cards(1, 1) -        wait_for_board_cards(2, 0) +        wait_for_board_cards(2, 1)          wait_for_board_cards(3, 0) +        wait_for_board_cards(4, 0)        end        it 'filters by label' do @@ -392,8 +388,8 @@ describe 'Issue Boards', feature: true, js: true do          submit_filter          wait_for_requests -        wait_for_board_cards(1, 1) -        wait_for_empty_boards((2..3)) +        wait_for_board_cards(2, 1) +        wait_for_empty_boards((3..4))        end        it 'filters by label with space after reload' do @@ -403,17 +399,17 @@ describe 'Issue Boards', feature: true, js: true do          # Test after reload          page.evaluate_script 'window.location.reload()' -        wait_for_board_cards(1, 1) -        wait_for_empty_boards((2..3)) +        wait_for_board_cards(2, 1) +        wait_for_empty_boards((3..4))          wait_for_requests -        page.within(find('.board', match: :first)) do +        page.within(find('.board:nth-child(2)')) do            expect(page.find('.board-header')).to have_content('1')            expect(page).to have_selector('.card', count: 1)          end -        page.within(find('.board:nth-child(2)')) do +        page.within(find('.board:nth-child(3)')) do            expect(page.find('.board-header')).to have_content('0')            expect(page).to have_selector('.card', count: 0)          end @@ -424,12 +420,12 @@ describe 'Issue Boards', feature: true, js: true do          click_filter_link(testing.title)          submit_filter -        wait_for_board_cards(1, 1) +        wait_for_board_cards(2, 1)          find('.clear-search').click          submit_filter -        wait_for_board_cards(1, 8) +        wait_for_board_cards(2, 8)        end        it 'infinite scrolls list with label filter' do @@ -443,17 +439,17 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_requests -        page.within(find('.board', match: :first)) do +        page.within(find('.board:nth-child(2)')) do            expect(page.find('.board-header')).to have_content('51')            expect(page).to have_selector('.card', count: 20)            expect(page).to have_content('Showing 20 of 51 issues') -          evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") +          evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")            expect(page).to have_selector('.card', count: 40)            expect(page).to have_content('Showing 40 of 51 issues') -          evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") +          evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")            expect(page).to have_selector('.card', count: 51)            expect(page).to have_content('Showing all issues') @@ -471,12 +467,12 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_requests -        wait_for_board_cards(1, 1) -        wait_for_empty_boards((2..3)) +        wait_for_board_cards(2, 1) +        wait_for_empty_boards((3..4))        end        it 'filters by clicking label button on issue' do -        page.within(find('.board', match: :first)) do +        page.within(find('.board:nth-child(2)')) do            expect(page).to have_selector('.card', count: 8)            expect(find('.card', match: :first)).to have_content(bug.title)            click_button(bug.title) @@ -489,12 +485,12 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_requests -        wait_for_board_cards(1, 1) -        wait_for_empty_boards((2..3)) +        wait_for_board_cards(2, 1) +        wait_for_empty_boards((3..4))        end        it 'removes label filter by clicking label button on issue' do -        page.within(find('.board', match: :first)) do +        page.within(find('.board:nth-child(2)')) do            page.within(find('.card', match: :first)) do              click_button(bug.title)            end diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb index 6c40cb2c9eb..1c289993e28 100644 --- a/spec/features/boards/issue_ordering_spec.rb +++ b/spec/features/boards/issue_ordering_spec.rb @@ -25,11 +25,11 @@ describe 'Issue Boards', :feature, :js do        visit namespace_project_board_path(project.namespace, project, board)        wait_for_requests -      expect(page).to have_selector('.board', count: 2) +      expect(page).to have_selector('.board', count: 3)      end      it 'has un-ordered issue as last issue' do -      page.within(first('.board')) do +      page.within(find('.board:nth-child(2)')) do          expect(all('.card').last).to have_content(issue4.title)        end      end @@ -39,7 +39,7 @@ describe 'Issue Boards', :feature, :js do        wait_for_requests -      page.within(first('.board')) do +      page.within(find('.board:nth-child(2)')) do          expect(first('.card')).to have_content(issue4.title)        end      end @@ -50,7 +50,7 @@ describe 'Issue Boards', :feature, :js do        visit namespace_project_board_path(project.namespace, project, board)        wait_for_requests -      expect(page).to have_selector('.board', count: 2) +      expect(page).to have_selector('.board', count: 3)      end      it 'moves from middle to top' do @@ -113,50 +113,50 @@ describe 'Issue Boards', :feature, :js do        visit namespace_project_board_path(project.namespace, project, board)        wait_for_requests -      expect(page).to have_selector('.board', count: 3) +      expect(page).to have_selector('.board', count: 4)      end      it 'moves to top of another list' do -      drag(list_from_index: 0, list_to_index: 1) +      drag(list_from_index: 1, list_to_index: 2)        wait_for_requests -      expect(first('.board')).to have_selector('.card', count: 2) -      expect(all('.board')[1]).to have_selector('.card', count: 4) +      expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) +      expect(all('.board')[2]).to have_selector('.card', count: 4) -      page.within(all('.board')[1]) do +      page.within(all('.board')[2]) do          expect(first('.card')).to have_content(issue3.title)        end      end      it 'moves to bottom of another list' do -      drag(list_from_index: 0, list_to_index: 1, to_index: 2) +      drag(list_from_index: 1, list_to_index: 2, to_index: 2)        wait_for_requests -      expect(first('.board')).to have_selector('.card', count: 2) -      expect(all('.board')[1]).to have_selector('.card', count: 4) +      expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) +      expect(all('.board')[2]).to have_selector('.card', count: 4) -      page.within(all('.board')[1]) do +      page.within(all('.board')[2]) do          expect(all('.card').last).to have_content(issue3.title)        end      end      it 'moves to index of another list' do -      drag(list_from_index: 0, list_to_index: 1, to_index: 1) +      drag(list_from_index: 1, list_to_index: 2, to_index: 1)        wait_for_requests -      expect(first('.board')).to have_selector('.card', count: 2) -      expect(all('.board')[1]).to have_selector('.card', count: 4) +      expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) +      expect(all('.board')[2]).to have_selector('.card', count: 4) -      page.within(all('.board')[1]) do +      page.within(all('.board')[2]) do          expect(all('.card')[1]).to have_content(issue3.title)        end      end    end -  def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0) +  def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1)      drag_to(selector: selector,              scrollable: '#board-app',              list_from_index: list_from_index, diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 0e98f994018..056224dc436 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -15,15 +15,15 @@ describe 'Issue Boards new issue', feature: true, js: true do        visit namespace_project_board_path(project.namespace, project, board)        wait_for_requests -      expect(page).to have_selector('.board', count: 2) +      expect(page).to have_selector('.board', count: 3)      end      it 'displays new issue button' do -      expect(page).to have_selector('.board-issue-count-holder .btn', count: 1) +      expect(first('.board')).to have_selector('.board-issue-count-holder .btn', count: 1)      end      it 'does not display new issue button in closed list' do -      page.within('.board:nth-child(2)') do +      page.within('.board:nth-child(3)') do          expect(page).not_to have_selector('.board-issue-count-holder .btn')        end      end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 34f4d765117..235e4899707 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -13,7 +13,7 @@ describe 'Issue Boards', feature: true, js: true do    let!(:issue2)      { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }    let(:board)        { create(:board, project: project) }    let!(:list)        { create(:list, board: board, label: development, position: 0) } -  let(:card) { first('.board').first('.card') } +  let(:card) { find('.board:nth-child(2)').first('.card') }    before do      Timecop.freeze @@ -74,7 +74,7 @@ describe 'Issue Boards', feature: true, js: true do      wait_for_requests -    page.within(first('.board')) do +    page.within(find('.board:nth-child(2)')) do        expect(page).to have_selector('.card', count: 1)      end    end @@ -101,7 +101,7 @@ describe 'Issue Boards', feature: true, js: true do      end      it 'removes the assignee' do -      card_two = first('.board').find('.card:nth-child(2)') +      card_two = find('.board:nth-child(2)').find('.card:nth-child(2)')        click_card(card_two)        page.within('.assignee') do @@ -154,7 +154,7 @@ describe 'Issue Boards', feature: true, js: true do          expect(page).to have_content(user.name)        end -      page.within(first('.board')) do +      page.within(find('.board:nth-child(2)')) do          find('.card:nth-child(2)').trigger('click')        end diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index 11a4caf6628..622a1e40d07 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -10,7 +10,7 @@      "id": { "type": "integer" },      "list_type": {        "type": "string", -      "enum": ["label", "closed"] +      "enum": ["backlog", "label", "closed"]      },      "label": {        "type": ["object", "null"], diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js new file mode 100644 index 00000000000..c4e8966ad6c --- /dev/null +++ b/spec/javascripts/boards/components/board_spec.js @@ -0,0 +1,112 @@ +import Vue from 'vue'; +import '~/boards/services/board_service'; +import '~/boards/components/board'; +import '~/boards/models/list'; + +describe('Board component', () => { +  let vm; +  let el; + +  beforeEach((done) => { +    loadFixtures('boards/show.html.raw'); + +    el = document.createElement('div'); +    document.body.appendChild(el); + +    // eslint-disable-next-line no-undef +    gl.boardService = new BoardService('/', '/', 1); + +    vm = new gl.issueBoards.Board({ +      propsData: { +        boardId: '1', +        disabled: false, +        issueLinkBase: '/', +        rootPath: '/', +        // eslint-disable-next-line no-undef +        list: new List({ +          id: 1, +          position: 0, +          title: 'test', +          list_type: 'backlog', +        }), +      }, +    }).$mount(el); + +    Vue.nextTick(done); +  }); + +  afterEach(() => { +    vm.$destroy(); + +    // remove the component from the DOM +    document.querySelector('.board').remove(); + +    localStorage.removeItem(`boards.${vm.boardId}.${vm.list.type}.expanded`); +  }); + +  it('board is expandable when list type is backlog', () => { +    expect( +      vm.$el.classList.contains('is-expandable'), +    ).toBe(true); +  }); + +  it('board is expandable when list type is closed', (done) => { +    vm.list.type = 'closed'; + +    Vue.nextTick(() => { +      expect( +        vm.$el.classList.contains('is-expandable'), +      ).toBe(true); + +      done(); +    }); +  }); + +  it('board is not expandable when list type is label', (done) => { +    vm.list.type = 'label'; +    vm.list.isExpandable = false; + +    Vue.nextTick(() => { +      expect( +        vm.$el.classList.contains('is-expandable'), +      ).toBe(false); + +      done(); +    }); +  }); + +  it('collapses when clicking header', (done) => { +    vm.$el.querySelector('.board-header').click(); + +    Vue.nextTick(() => { +      expect( +        vm.$el.classList.contains('is-collapsed'), +      ).toBe(true); + +      done(); +    }); +  }); + +  it('created sets isExpanded to true from localStorage', (done) => { +    vm.$el.querySelector('.board-header').click(); + +    return Vue.nextTick() +      .then(() => { +        expect( +          vm.$el.classList.contains('is-collapsed'), +        ).toBe(true); + +        // call created manually +        vm.$options.created[0].call(vm); + +        return Vue.nextTick(); +      }) +      .then(() => { +        expect( +          vm.$el.classList.contains('is-collapsed'), +        ).toBe(true); + +        done(); +      }); +  }); +}); diff --git a/spec/javascripts/fixtures/boards.rb b/spec/javascripts/fixtures/boards.rb new file mode 100644 index 00000000000..d7c3dc0a235 --- /dev/null +++ b/spec/javascripts/fixtures/boards.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller do +  include JavaScriptFixturesHelpers + +  let(:admin) { create(:admin) } +  let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} +  let(:project) { create(:project, :repository, namespace: namespace, path: 'boards-project') } + +  render_views + +  before(:all) do +    clean_frontend_fixtures('boards/') +  end + +  before(:each) do +    sign_in(admin) +  end + +  it 'boards/show.html.raw' do |example| +    get(:index, +        namespace_id: project.namespace, +        project_id: project) + +    expect(response).to be_success +    store_frontend_fixture(response, example.description) +  end +end diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb index a8555f5b4a0..effa4633d13 100644 --- a/spec/services/boards/create_service_spec.rb +++ b/spec/services/boards/create_service_spec.rb @@ -14,8 +14,9 @@ describe Boards::CreateService, services: true do        it 'creates the default lists' do          board = service.execute -        expect(board.lists.size).to eq 1 -        expect(board.lists.first).to be_closed +        expect(board.lists.size).to eq 2 +        expect(board.lists.first).to be_backlog +        expect(board.lists.last).to be_closed        end      end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index c982031c791..a1e220c2322 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -13,6 +13,7 @@ describe Boards::Issues::ListService, services: true do      let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }      let(:p3) { create(:label, title: 'P3', project: project, priority: 3) } +    let!(:backlog) { create(:backlog_list, board: board) }      let!(:list1)   { create(:list, board: board, label: development, position: 0) }      let!(:list2)   { create(:list, board: board, label: testing, position: 1) }      let!(:closed)  { create(:closed_list, board: board) } @@ -53,12 +54,20 @@ describe Boards::Issues::ListService, services: true do          expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]        end +      it 'returns opened issues when listing issues from Backlog' do +        params = { board_id: board.id, id: backlog.id } + +        issues = described_class.new(project, user, params).execute + +        expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] +      end +        it 'returns closed issues when listing issues from Closed' do          params = { board_id: board.id, id: closed.id }          issues = described_class.new(project, user, params).execute -        expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1] +        expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]        end        it 'returns opened issues that have label list applied when listing issues from a label list' do diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb index ab9fb1bc914..68140759600 100644 --- a/spec/services/boards/lists/list_service_spec.rb +++ b/spec/services/boards/lists/list_service_spec.rb @@ -1,16 +1,33 @@  require 'spec_helper'  describe Boards::Lists::ListService, services: true do +  let(:project) { create(:empty_project) } +  let(:board) { create(:board, project: project) } +  let(:label) { create(:label, project: project) } +  let!(:list) { create(:list, board: board, label: label) } +  let(:service) { described_class.new(project, double) } +    describe '#execute' do -    it "returns board's lists" do -      project = create(:empty_project) -      board = create(:board, project: project) -      label = create(:label, project: project) -      list = create(:list, board: board, label: label) +    context 'when the board has a backlog list' do +      let!(:backlog_list) { create(:backlog_list, board: board) } + +      it 'does not create a backlog list' do +        expect { service.execute(board) }.not_to change(board.lists, :count) +      end + +      it "returns board's lists" do +        expect(service.execute(board)).to eq [backlog_list, list, board.closed_list] +      end +    end -      service = described_class.new(project, double) +    context 'when the board does not have a backlog list' do +      it 'creates a backlog list' do +        expect { service.execute(board) }.to change(board.lists, :count).by(1) +      end -      expect(service.execute(board)).to eq [list, board.closed_list] +      it "returns board's lists" do +        expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list] +      end      end    end  end diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb index a982b159b48..aace4b3adee 100644 --- a/spec/support/javascript_fixtures_helpers.rb +++ b/spec/support/javascript_fixtures_helpers.rb @@ -48,7 +48,7 @@ module JavaScriptFixturesHelpers        link_tags = doc.css('link')        link_tags.remove -      scripts = doc.css("script:not([type='text/template'])") +      scripts = doc.css("script:not([type='text/template']):not([type='text/x-template'])")        scripts.remove        fixture = doc.to_html | 
