Merge branch 'fe-issue-reorder' into 'master'
Bring Manual Ordering on Issue List See merge request gitlab-org/gitlab-ce!29410
This commit is contained in:
		
						commit
						2b9ddc2f99
					
				| 
						 | 
					@ -0,0 +1,58 @@
 | 
				
			||||||
 | 
					import Sortable from 'sortablejs';
 | 
				
			||||||
 | 
					import { s__ } from '~/locale';
 | 
				
			||||||
 | 
					import createFlash from '~/flash';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getBoardSortableDefaultOptions,
 | 
				
			||||||
 | 
					  sortableStart,
 | 
				
			||||||
 | 
					} from '~/boards/mixins/sortable_default_options';
 | 
				
			||||||
 | 
					import axios from '~/lib/utils/axios_utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
 | 
				
			||||||
 | 
					  axios
 | 
				
			||||||
 | 
					    .put(`${url}/reorder`, {
 | 
				
			||||||
 | 
					      move_before_id,
 | 
				
			||||||
 | 
					      move_after_id,
 | 
				
			||||||
 | 
					      group_full_path: issueList.dataset.groupFullPath,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .catch(() => {
 | 
				
			||||||
 | 
					      createFlash(s__("ManualOrdering|Couldn't save the order of the issues"));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const initManualOrdering = () => {
 | 
				
			||||||
 | 
					  const issueList = document.querySelector('.manual-ordering');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!issueList || !(gon.features && gon.features.manualSorting)) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Sortable.create(
 | 
				
			||||||
 | 
					    issueList,
 | 
				
			||||||
 | 
					    getBoardSortableDefaultOptions({
 | 
				
			||||||
 | 
					      scroll: true,
 | 
				
			||||||
 | 
					      dataIdAttr: 'data-id',
 | 
				
			||||||
 | 
					      fallbackOnBody: false,
 | 
				
			||||||
 | 
					      group: {
 | 
				
			||||||
 | 
					        name: 'issues',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      draggable: 'li.issue',
 | 
				
			||||||
 | 
					      onStart: () => {
 | 
				
			||||||
 | 
					        sortableStart();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onUpdate: event => {
 | 
				
			||||||
 | 
					        const el = event.item;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const url = el.getAttribute('url');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const prev = el.previousElementSibling;
 | 
				
			||||||
 | 
					        const next = el.nextElementSibling;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const beforeId = prev && parseInt(prev.dataset.id, 10);
 | 
				
			||||||
 | 
					        const afterId = next && parseInt(next.dataset.id, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        updateIssue(url, issueList, { move_after_id: afterId, move_before_id: beforeId });
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default initManualOrdering;
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@ import projectSelect from '~/project_select';
 | 
				
			||||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
 | 
					import initFilteredSearch from '~/pages/search/init_filtered_search';
 | 
				
			||||||
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
 | 
					import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
 | 
				
			||||||
import { FILTERED_SEARCH } from '~/pages/constants';
 | 
					import { FILTERED_SEARCH } from '~/pages/constants';
 | 
				
			||||||
 | 
					import initManualOrdering from '~/manual_ordering';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
document.addEventListener('DOMContentLoaded', () => {
 | 
					document.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
  initFilteredSearch({
 | 
					  initFilteredSearch({
 | 
				
			||||||
| 
						 | 
					@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  projectSelect();
 | 
					  projectSelect();
 | 
				
			||||||
 | 
					  initManualOrdering();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@ import projectSelect from '~/project_select';
 | 
				
			||||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
 | 
					import initFilteredSearch from '~/pages/search/init_filtered_search';
 | 
				
			||||||
import { FILTERED_SEARCH } from '~/pages/constants';
 | 
					import { FILTERED_SEARCH } from '~/pages/constants';
 | 
				
			||||||
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
 | 
					import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
 | 
				
			||||||
 | 
					import initManualOrdering from '~/manual_ordering';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
document.addEventListener('DOMContentLoaded', () => {
 | 
					document.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
  IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
 | 
					  IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
 | 
				
			||||||
| 
						 | 
					@ -12,4 +13,5 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
    filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
 | 
					    filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  projectSelect();
 | 
					  projectSelect();
 | 
				
			||||||
 | 
					  initManualOrdering();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
 | 
				
			||||||
import { FILTERED_SEARCH } from '~/pages/constants';
 | 
					import { FILTERED_SEARCH } from '~/pages/constants';
 | 
				
			||||||
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
 | 
					import { ISSUABLE_INDEX } from '~/pages/projects/constants';
 | 
				
			||||||
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
 | 
					import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
 | 
				
			||||||
 | 
					import initManualOrdering from '~/manual_ordering';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
document.addEventListener('DOMContentLoaded', () => {
 | 
					document.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
  IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
 | 
					  IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
 | 
				
			||||||
| 
						 | 
					@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  new ShortcutsNavigation();
 | 
					  new ShortcutsNavigation();
 | 
				
			||||||
  new UsersSelect();
 | 
					  new UsersSelect();
 | 
				
			||||||
 | 
					  initManualOrdering();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,18 @@
 | 
				
			||||||
.issues-list {
 | 
					.issues-list {
 | 
				
			||||||
 | 
					  &.manual-ordering {
 | 
				
			||||||
 | 
					    background-color: $gray-light;
 | 
				
			||||||
 | 
					    border-radius: $border-radius-default;
 | 
				
			||||||
 | 
					    padding: $gl-padding-8;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .issue {
 | 
				
			||||||
 | 
					      background-color: $white-light;
 | 
				
			||||||
 | 
					      margin-bottom: $gl-padding-8;
 | 
				
			||||||
 | 
					      border-radius: $border-radius-default;
 | 
				
			||||||
 | 
					      border: 1px solid $gray-100;
 | 
				
			||||||
 | 
					      box-shadow: 0 1px 2px $issue-boards-card-shadow;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .issue {
 | 
					  .issue {
 | 
				
			||||||
    padding: 10px 0 10px $gl-padding;
 | 
					    padding: 10px 0 10px $gl-padding;
 | 
				
			||||||
    position: relative;
 | 
					    position: relative;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,10 @@ class GroupsController < Groups::ApplicationController
 | 
				
			||||||
  include PreviewMarkdown
 | 
					  include PreviewMarkdown
 | 
				
			||||||
  include RecordUserLastActivity
 | 
					  include RecordUserLastActivity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_action do
 | 
				
			||||||
 | 
					    push_frontend_feature_flag(:manual_sorting)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  respond_to :html
 | 
					  respond_to :html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) }
 | 
					  prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,10 @@ class Projects::IssuesController < Projects::ApplicationController
 | 
				
			||||||
  include SpammableActions
 | 
					  include SpammableActions
 | 
				
			||||||
  include RecordUserLastActivity
 | 
					  include RecordUserLastActivity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_action do
 | 
				
			||||||
 | 
					    push_frontend_feature_flag(:manual_sorting)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def issue_except_actions
 | 
					  def issue_except_actions
 | 
				
			||||||
    %i[index calendar new create bulk_update import_csv]
 | 
					    %i[index calendar new create bulk_update import_csv]
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ module IssuesHelper
 | 
				
			||||||
    classes = ["issue"]
 | 
					    classes = ["issue"]
 | 
				
			||||||
    classes << "closed" if issue.closed?
 | 
					    classes << "closed" if issue.closed?
 | 
				
			||||||
    classes << "today" if issue.today?
 | 
					    classes << "today" if issue.today?
 | 
				
			||||||
 | 
					    classes << "user-can-drag" if @sort == 'relative_position'
 | 
				
			||||||
    classes.join(' ')
 | 
					    classes.join(' ')
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
 | 
					- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
%ul.content-list.issues-list.issuable-list
 | 
					%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position') }
 | 
				
			||||||
  = render partial: "projects/issues/issue", collection: @issues
 | 
					  = render partial: "projects/issues/issue", collection: @issues
 | 
				
			||||||
  - if @issues.blank?
 | 
					  - if @issues.blank?
 | 
				
			||||||
    = render empty_state_path
 | 
					    = render empty_state_path
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
- if @issues.to_a.any?
 | 
					- if @issues.to_a.any?
 | 
				
			||||||
  .card.card-small.card-without-border
 | 
					  .card.card-small.card-without-border
 | 
				
			||||||
    %ul.content-list.issues-list.issuable-list
 | 
					    %ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position'), data: { group_full_path: @group&.full_path } }
 | 
				
			||||||
      = render partial: 'projects/issues/issue', collection: @issues
 | 
					      = render partial: 'projects/issues/issue', collection: @issues
 | 
				
			||||||
  = paginate @issues, theme: "gitlab"
 | 
					  = paginate @issues, theme: "gitlab"
 | 
				
			||||||
- else
 | 
					- else
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
- sort_value = @sort
 | 
					- sort_value = @sort
 | 
				
			||||||
- sort_title = issuable_sort_option_title(sort_value)
 | 
					- sort_title = issuable_sort_option_title(sort_value)
 | 
				
			||||||
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
 | 
					- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
 | 
				
			||||||
 | 
					- manual_sorting = viewing_issues && controller.controller_name != 'dashboard' && Feature.enabled?(:manual_sorting)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown.inline.prepend-left-10.issue-sort-dropdown
 | 
					.dropdown.inline.prepend-left-10.issue-sort-dropdown
 | 
				
			||||||
  .btn-group{ role: 'group' }
 | 
					  .btn-group{ role: 'group' }
 | 
				
			||||||
| 
						 | 
					@ -17,6 +18,6 @@
 | 
				
			||||||
          = sortable_item(sort_title_due_date,          page_filter_path(sort: sort_value_due_date),          sort_title) if viewing_issues
 | 
					          = sortable_item(sort_title_due_date,          page_filter_path(sort: sort_value_due_date),          sort_title) if viewing_issues
 | 
				
			||||||
          = sortable_item(sort_title_popularity,        page_filter_path(sort: sort_value_popularity),        sort_title)
 | 
					          = sortable_item(sort_title_popularity,        page_filter_path(sort: sort_value_popularity),        sort_title)
 | 
				
			||||||
          = sortable_item(sort_title_label_priority,    page_filter_path(sort: sort_value_label_priority),    sort_title)
 | 
					          = sortable_item(sort_title_label_priority,    page_filter_path(sort: sort_value_label_priority),    sort_title)
 | 
				
			||||||
          = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues && Feature.enabled?(:manual_sorting)
 | 
					          = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if manual_sorting
 | 
				
			||||||
          = render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title)
 | 
					          = render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title)
 | 
				
			||||||
    = issuable_sort_direction_button(sort_value)
 | 
					    = issuable_sort_direction_button(sort_value)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					title: Bring Manual Ordering on Issue List
 | 
				
			||||||
 | 
					merge_request: 29410
 | 
				
			||||||
 | 
					author:
 | 
				
			||||||
 | 
					type: added
 | 
				
			||||||
| 
						 | 
					@ -6011,6 +6011,9 @@ msgstr ""
 | 
				
			||||||
msgid "Manual job"
 | 
					msgid "Manual job"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					msgid "ManualOrdering|Couldn't save the order of the issues"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "Map a FogBugz account ID to a GitLab user"
 | 
					msgid "Map a FogBugz account ID to a GitLab user"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@ require 'spec_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe 'Group issues page' do
 | 
					describe 'Group issues page' do
 | 
				
			||||||
  include FilteredSearchHelpers
 | 
					  include FilteredSearchHelpers
 | 
				
			||||||
 | 
					  include DragTo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let(:group) { create(:group) }
 | 
					  let(:group) { create(:group) }
 | 
				
			||||||
  let(:project) { create(:project, :public, group: group)}
 | 
					  let(:project) { create(:project, :public, group: group)}
 | 
				
			||||||
| 
						 | 
					@ -99,4 +100,49 @@ describe 'Group issues page' do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  context 'manual ordering' do
 | 
				
			||||||
 | 
					    let!(:issue1) { create(:issue, project: project, title: 'Issue #1') }
 | 
				
			||||||
 | 
					    let!(:issue2) { create(:issue, project: project, title: 'Issue #2') }
 | 
				
			||||||
 | 
					    let!(:issue3) { create(:issue, project: project, title: 'Issue #3') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'displays all issues' do
 | 
				
			||||||
 | 
					      visit issues_group_path(group, sort: 'relative_position')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      page.within('.issues-list') do
 | 
				
			||||||
 | 
					        expect(page).to have_selector('li.issue', count: 3)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'has manual-ordering css applied' do
 | 
				
			||||||
 | 
					      visit issues_group_path(group, sort: 'relative_position')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(page).to have_selector('.manual-ordering')
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'each issue item has a user-can-drag css applied' do
 | 
				
			||||||
 | 
					      visit issues_group_path(group, sort: 'relative_position')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      page.within('.manual-ordering') do
 | 
				
			||||||
 | 
					        expect(page).to have_selector('.issue.user-can-drag', count: 3)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'issues should be draggable and persist order', :js do
 | 
				
			||||||
 | 
					      visit issues_group_path(group, sort: 'relative_position')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      drag_to(selector: '.manual-ordering',
 | 
				
			||||||
 | 
					        scrollable: '#board-app',
 | 
				
			||||||
 | 
					        list_from_index: 0,
 | 
				
			||||||
 | 
					        from_index: 0,
 | 
				
			||||||
 | 
					        to_index: 2,
 | 
				
			||||||
 | 
					        list_to_index: 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      page.within('.manual-ordering') do
 | 
				
			||||||
 | 
					        expect(find('.issue:nth-child(1) .title')).to have_content('Issue #2')
 | 
				
			||||||
 | 
					        expect(find('.issue:nth-child(2) .title')).to have_content('Issue #1')
 | 
				
			||||||
 | 
					        expect(find('.issue:nth-child(3) .title')).to have_content('Issue #3')
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue