Commit 274083a5 authored by Florie Guibert's avatar Florie Guibert

Swimlanes - D&D issue permissions and tests

Only allow user to drag & drop when user can admin epics
Add rspec tests
parent 511ff0a8
......@@ -143,7 +143,7 @@ export default function simulateDrag(options) {
const dragInterval = setInterval(() => {
const progress = (new Date().getTime() - startTime) / duration;
const x = fromRect.cx + (toRect.cx - fromRect.cx) * progress;
const y = fromRect.cy + (toRect.cy - fromRect.cy) * progress;
const y = fromRect.cy + (toRect.cy - fromRect.cy + options.extraHeight) * progress;
const overEl = fromEl.ownerDocument.elementFromPoint(x, y);
simulateEvent(overEl, 'pointermove', {
......
......@@ -50,9 +50,9 @@ export default {
};
},
computed: {
...mapState(['activeId', 'filterParams']),
...mapState(['activeId', 'filterParams', 'canAdminEpic']),
treeRootWrapper() {
return this.canAdminList ? Draggable : 'ul';
return this.canAdminList && this.canAdminEpic ? Draggable : 'ul';
},
treeRootOptions() {
const options = {
......@@ -169,6 +169,7 @@ export default {
:index="index"
:list="list"
:issue="issue"
:disabled="disabled || !canAdminEpic"
:is-active="isActiveIssue(issue)"
@show="showIssue(issue)"
/>
......
......@@ -58,7 +58,7 @@ export default {
const epic = await this.setActiveIssueEpic(input);
if (epic && !this.getEpicById(epic.id)) {
this.receiveEpicsSuccess([epic, ...this.epics]);
this.receiveEpicsSuccess({ epics: [epic, ...this.epics] });
}
debounceByAnimationFrame(() => {
......
......@@ -7,4 +7,7 @@ fragment BoardEpicNode on BoardEpic {
webUrl
createdAt
closedAt
userPermissions {
adminEpic
}
}
......@@ -104,7 +104,7 @@ export default {
}));
if (!withLists) {
commit(types.RECEIVE_EPICS_SUCCESS, epicsFormatted);
commit(types.RECEIVE_EPICS_SUCCESS, { epics: epicsFormatted });
}
if (epics.pageInfo?.hasNextPage) {
......@@ -117,6 +117,7 @@ export default {
return {
epics: epicsFormatted,
lists: lists?.nodes,
canAdminEpic: epics.edges[0]?.node?.userPermissions?.adminEpic,
};
})
.catch(() => commit(types.RECEIVE_SWIMLANES_FAILURE));
......@@ -214,7 +215,7 @@ export default {
if (state.isShowingEpicsSwimlanes) {
dispatch('fetchEpicsSwimlanes', {})
.then(({ lists, epics }) => {
.then(({ lists, epics, canAdminEpic }) => {
if (lists) {
let boardLists = lists.map(list =>
boardsStore.updateListPosition({ ...list, doNotFetchIssues: true }),
......@@ -224,7 +225,7 @@ export default {
}
if (epics) {
commit(types.RECEIVE_EPICS_SUCCESS, epics);
commit(types.RECEIVE_EPICS_SUCCESS, { epics, canAdminEpic });
}
})
.catch(() => commit(types.RECEIVE_SWIMLANES_FAILURE));
......
......@@ -103,8 +103,9 @@ export default {
state.epicsSwimlanesFetchInProgress = false;
},
[mutationTypes.RECEIVE_EPICS_SUCCESS]: (state, epics) => {
[mutationTypes.RECEIVE_EPICS_SUCCESS]: (state, { epics, canAdminEpic }) => {
Vue.set(state, 'epics', union(state.epics || [], epics));
state.canAdminEpic = canAdminEpic;
},
[mutationTypes.RESET_EPICS]: state => {
......
......@@ -3,6 +3,7 @@ import createStateCE from '~/boards/stores/state';
export default () => ({
...createStateCE(),
canAdminEpic: false,
isShowingEpicsSwimlanes: false,
epicsSwimlanesFetchInProgress: false,
epics: [],
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'epics swimlanes', :js do
include DragTo
include MobileHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:label) { create(:label, project: project, name: 'Label 1') }
let_it_be(:list) { create(:list, board: board, label: label, position: 0) }
let_it_be(:issue1) { create(:issue, project: project, labels: [label]) }
let_it_be(:issue2) { create(:issue, project: project) }
let_it_be(:issue3) { create(:issue, project: project, state: :closed) }
let_it_be(:issue4) { create(:issue, project: project) }
let_it_be(:epic1) { create(:epic, group: group) }
let_it_be(:epic2) { create(:epic, group: group) }
let_it_be(:epic_issue1) { create(:epic_issue, epic: epic1, issue: issue1) }
let_it_be(:epic_issue2) { create(:epic_issue, epic: epic2, issue: issue2) }
let_it_be(:epic_issue3) { create(:epic_issue, epic: epic2, issue: issue3) }
before do
project.add_maintainer(user)
group.add_maintainer(user)
stub_licensed_features(epics: true)
sign_in(user)
visit_board_page
select_epics
end
context 'drag and drop issue' do
it 'between epics' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_first_epic(0, 1)
wait_for_board_cards_in_second_epic(1, 1)
drag(list_from_index: 4, list_to_index: 1)
wait_for_board_cards_in_first_epic(1, 1)
wait_for_board_cards_in_second_epic(1, 0)
end
it 'from epic to unassigned issues lane' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_second_epic(1, 1)
drag(list_from_index: 4, list_to_index: 7)
wait_for_board_cards_in_second_epic(1, 0)
wait_for_board_cards_in_unassigned_lane(1, 1)
end
it 'from unassigned issues lane to epic' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_unassigned_lane(0, 1)
drag(list_from_index: 6, list_to_index: 3)
wait_for_board_cards_in_second_epic(0, 1)
wait_for_board_cards_in_unassigned_lane(0, 0)
end
it 'between lists within epic lane' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_first_epic(0, 1)
drag(list_from_index: 0, list_to_index: 1)
wait_for_board_cards(1, 1)
wait_for_board_cards(2, 2)
wait_for_board_cards_in_first_epic(0, 0)
wait_for_board_cards_in_first_epic(1, 1)
end
it 'between lists within unassigned lane' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_unassigned_lane(0, 1)
drag(list_from_index: 6, list_to_index: 7)
wait_for_board_cards(1, 1)
wait_for_board_cards(2, 2)
wait_for_board_cards_in_unassigned_lane(0, 0)
wait_for_board_cards_in_unassigned_lane(1, 1)
end
it 'between lists and epics' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_second_epic(1, 1)
drag(list_from_index: 4, list_to_index: 2)
wait_for_board_cards(2, 0)
wait_for_board_cards(3, 2)
wait_for_board_cards_in_first_epic(2, 2)
end
end
def visit_board_page
visit project_boards_path(project)
wait_for_requests
end
def select_epics
page.within('.board-swimlanes-toggle-wrapper') do
page.find('.dropdown-toggle').click
page.find('.dropdown-item', text: 'Epic').click
end
end
def drag(selector: '.board-cell', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, perform_drop: true)
# ensure there is enough horizontal space for four boards
resize_window(2000, 1200)
drag_to(selector: selector,
scrollable: '#board-app',
list_from_index: list_from_index,
from_index: from_index,
to_index: to_index,
list_to_index: list_to_index,
perform_drop: perform_drop,
extra_height: 50)
end
def wait_for_board_cards(board_number, expected_cards)
page.within(find(".board-swimlanes-headers .board:nth-child(#{board_number})")) do
expect(page.find('.board-header')).to have_content(expected_cards.to_s)
end
end
def wait_for_board_cards_in_first_epic(board_number, expected_cards)
page.within(all("[data-testid='board-epic-lane-issues']")[0]) do
page.within(all(".board")[board_number]) do
expect(page).to have_selector('.board-card', count: expected_cards)
end
end
end
def wait_for_board_cards_in_second_epic(board_number, expected_cards)
page.within(all("[data-testid='board-epic-lane-issues']")[1]) do
page.within(all(".board")[board_number]) do
expect(page).to have_selector('.board-card', count: expected_cards)
end
end
end
def wait_for_board_cards_in_unassigned_lane(board_number, expected_cards)
page.within(find("[data-testid='board-lane-unassigned-issues']")) do
page.within(all(".board")[board_number]) do
expect(page).to have_selector('.board-card', count: expected_cards)
end
end
end
end
......@@ -104,7 +104,7 @@ describe('fetchEpicsSwimlanes', () => {
[
{
type: types.RECEIVE_EPICS_SUCCESS,
payload: [mockEpic],
payload: { epics: [mockEpic] },
},
],
[],
......@@ -150,7 +150,7 @@ describe('fetchEpicsSwimlanes', () => {
[
{
type: types.RECEIVE_EPICS_SUCCESS,
payload: [mockEpic],
payload: { epics: [mockEpic] },
},
],
[
......
......@@ -213,7 +213,7 @@ describe('RECEIVE_EPICS_SUCCESS', () => {
epics: {},
};
mutations.RECEIVE_EPICS_SUCCESS(state, mockEpics);
mutations.RECEIVE_EPICS_SUCCESS(state, { epics: mockEpics });
expect(state.epics).toEqual(mockEpics);
});
......
# frozen_string_literal: true
module DragTo
def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000, perform_drop: true)
# rubocop:disable Metrics/ParameterLists
def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000, perform_drop: true, extra_height: 0)
js = <<~JS
simulateDrag({
scrollable: document.querySelector('#{scrollable}'),
......@@ -14,7 +15,8 @@ module DragTo
el: document.querySelectorAll('#{selector}')[#{list_to_index}],
index: #{to_index}
},
performDrop: #{perform_drop}
performDrop: #{perform_drop},
extraHeight: #{extra_height}
});
JS
evaluate_script(js)
......@@ -23,6 +25,7 @@ module DragTo
loop while drag_active?
end
end
# rubocop:enable Metrics/ParameterLists
def drag_active?
page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').nonzero?
......
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