Commit 6f448bd1 authored by Rajat Jain's avatar Rajat Jain Committed by Kushal Pandya

Bring Manual Ordering on Issue List

On all the issue lists -- Group, Project and Dashboard -- this
change adds a new option for managing the lists.

"Manual Ordering" option is added which when flipped on will allow
an user to drag and drop issues around to create a relative ordering
among them.
parent 58eae2d3
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'; ...@@ -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', () => { ...@@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
projectSelect(); projectSelect();
initManualOrdering();
}); });
...@@ -2,6 +2,7 @@ import projectSelect from '~/project_select'; ...@@ -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', () => { ...@@ -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'; ...@@ -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', () => { ...@@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); new ShortcutsNavigation();
new UsersSelect(); new UsersSelect();
initManualOrdering();
}); });
.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 ...@@ -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 ...@@ -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 ...@@ -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
......
- 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
......
- 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
......
- 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 @@ ...@@ -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)
---
title: Bring Manual Ordering on Issue List
merge_request: 29410
author:
type: added
...@@ -6011,6 +6011,9 @@ msgstr "" ...@@ -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' ...@@ -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 ...@@ -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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment