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) { ...@@ -143,7 +143,7 @@ export default function simulateDrag(options) {
const dragInterval = setInterval(() => { const dragInterval = setInterval(() => {
const progress = (new Date().getTime() - startTime) / duration; const progress = (new Date().getTime() - startTime) / duration;
const x = fromRect.cx + (toRect.cx - fromRect.cx) * progress; 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); const overEl = fromEl.ownerDocument.elementFromPoint(x, y);
simulateEvent(overEl, 'pointermove', { simulateEvent(overEl, 'pointermove', {
......
...@@ -50,9 +50,9 @@ export default { ...@@ -50,9 +50,9 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['activeId', 'filterParams']), ...mapState(['activeId', 'filterParams', 'canAdminEpic']),
treeRootWrapper() { treeRootWrapper() {
return this.canAdminList ? Draggable : 'ul'; return this.canAdminList && this.canAdminEpic ? Draggable : 'ul';
}, },
treeRootOptions() { treeRootOptions() {
const options = { const options = {
...@@ -169,6 +169,7 @@ export default { ...@@ -169,6 +169,7 @@ export default {
:index="index" :index="index"
:list="list" :list="list"
:issue="issue" :issue="issue"
:disabled="disabled || !canAdminEpic"
:is-active="isActiveIssue(issue)" :is-active="isActiveIssue(issue)"
@show="showIssue(issue)" @show="showIssue(issue)"
/> />
......
...@@ -58,7 +58,7 @@ export default { ...@@ -58,7 +58,7 @@ export default {
const epic = await this.setActiveIssueEpic(input); const epic = await this.setActiveIssueEpic(input);
if (epic && !this.getEpicById(epic.id)) { if (epic && !this.getEpicById(epic.id)) {
this.receiveEpicsSuccess([epic, ...this.epics]); this.receiveEpicsSuccess({ epics: [epic, ...this.epics] });
} }
debounceByAnimationFrame(() => { debounceByAnimationFrame(() => {
......
...@@ -7,4 +7,7 @@ fragment BoardEpicNode on BoardEpic { ...@@ -7,4 +7,7 @@ fragment BoardEpicNode on BoardEpic {
webUrl webUrl
createdAt createdAt
closedAt closedAt
userPermissions {
adminEpic
}
} }
...@@ -104,7 +104,7 @@ export default { ...@@ -104,7 +104,7 @@ export default {
})); }));
if (!withLists) { if (!withLists) {
commit(types.RECEIVE_EPICS_SUCCESS, epicsFormatted); commit(types.RECEIVE_EPICS_SUCCESS, { epics: epicsFormatted });
} }
if (epics.pageInfo?.hasNextPage) { if (epics.pageInfo?.hasNextPage) {
...@@ -117,6 +117,7 @@ export default { ...@@ -117,6 +117,7 @@ export default {
return { return {
epics: epicsFormatted, epics: epicsFormatted,
lists: lists?.nodes, lists: lists?.nodes,
canAdminEpic: epics.edges[0]?.node?.userPermissions?.adminEpic,
}; };
}) })
.catch(() => commit(types.RECEIVE_SWIMLANES_FAILURE)); .catch(() => commit(types.RECEIVE_SWIMLANES_FAILURE));
...@@ -214,7 +215,7 @@ export default { ...@@ -214,7 +215,7 @@ export default {
if (state.isShowingEpicsSwimlanes) { if (state.isShowingEpicsSwimlanes) {
dispatch('fetchEpicsSwimlanes', {}) dispatch('fetchEpicsSwimlanes', {})
.then(({ lists, epics }) => { .then(({ lists, epics, canAdminEpic }) => {
if (lists) { if (lists) {
let boardLists = lists.map(list => let boardLists = lists.map(list =>
boardsStore.updateListPosition({ ...list, doNotFetchIssues: true }), boardsStore.updateListPosition({ ...list, doNotFetchIssues: true }),
...@@ -224,7 +225,7 @@ export default { ...@@ -224,7 +225,7 @@ export default {
} }
if (epics) { if (epics) {
commit(types.RECEIVE_EPICS_SUCCESS, epics); commit(types.RECEIVE_EPICS_SUCCESS, { epics, canAdminEpic });
} }
}) })
.catch(() => commit(types.RECEIVE_SWIMLANES_FAILURE)); .catch(() => commit(types.RECEIVE_SWIMLANES_FAILURE));
......
...@@ -103,8 +103,9 @@ export default { ...@@ -103,8 +103,9 @@ export default {
state.epicsSwimlanesFetchInProgress = false; state.epicsSwimlanesFetchInProgress = false;
}, },
[mutationTypes.RECEIVE_EPICS_SUCCESS]: (state, epics) => { [mutationTypes.RECEIVE_EPICS_SUCCESS]: (state, { epics, canAdminEpic }) => {
Vue.set(state, 'epics', union(state.epics || [], epics)); Vue.set(state, 'epics', union(state.epics || [], epics));
state.canAdminEpic = canAdminEpic;
}, },
[mutationTypes.RESET_EPICS]: state => { [mutationTypes.RESET_EPICS]: state => {
......
...@@ -3,6 +3,7 @@ import createStateCE from '~/boards/stores/state'; ...@@ -3,6 +3,7 @@ import createStateCE from '~/boards/stores/state';
export default () => ({ export default () => ({
...createStateCE(), ...createStateCE(),
canAdminEpic: false,
isShowingEpicsSwimlanes: false, isShowingEpicsSwimlanes: false,
epicsSwimlanesFetchInProgress: false, epicsSwimlanesFetchInProgress: false,
epics: [], 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', () => { ...@@ -104,7 +104,7 @@ describe('fetchEpicsSwimlanes', () => {
[ [
{ {
type: types.RECEIVE_EPICS_SUCCESS, type: types.RECEIVE_EPICS_SUCCESS,
payload: [mockEpic], payload: { epics: [mockEpic] },
}, },
], ],
[], [],
...@@ -150,7 +150,7 @@ describe('fetchEpicsSwimlanes', () => { ...@@ -150,7 +150,7 @@ describe('fetchEpicsSwimlanes', () => {
[ [
{ {
type: types.RECEIVE_EPICS_SUCCESS, type: types.RECEIVE_EPICS_SUCCESS,
payload: [mockEpic], payload: { epics: [mockEpic] },
}, },
], ],
[ [
......
...@@ -213,7 +213,7 @@ describe('RECEIVE_EPICS_SUCCESS', () => { ...@@ -213,7 +213,7 @@ describe('RECEIVE_EPICS_SUCCESS', () => {
epics: {}, epics: {},
}; };
mutations.RECEIVE_EPICS_SUCCESS(state, mockEpics); mutations.RECEIVE_EPICS_SUCCESS(state, { epics: mockEpics });
expect(state.epics).toEqual(mockEpics); expect(state.epics).toEqual(mockEpics);
}); });
......
# frozen_string_literal: true # frozen_string_literal: true
module DragTo 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 js = <<~JS
simulateDrag({ simulateDrag({
scrollable: document.querySelector('#{scrollable}'), scrollable: document.querySelector('#{scrollable}'),
...@@ -14,7 +15,8 @@ module DragTo ...@@ -14,7 +15,8 @@ module DragTo
el: document.querySelectorAll('#{selector}')[#{list_to_index}], el: document.querySelectorAll('#{selector}')[#{list_to_index}],
index: #{to_index} index: #{to_index}
}, },
performDrop: #{perform_drop} performDrop: #{perform_drop},
extraHeight: #{extra_height}
}); });
JS JS
evaluate_script(js) evaluate_script(js)
...@@ -23,6 +25,7 @@ module DragTo ...@@ -23,6 +25,7 @@ module DragTo
loop while drag_active? loop while drag_active?
end end
end end
# rubocop:enable Metrics/ParameterLists
def drag_active? def drag_active?
page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').nonzero? 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