Commit 12652090 authored by charlie ablett's avatar charlie ablett

Merge branch '233434-multiple-epic-boards-switcher' into 'master'

Epic multiple boards switcher [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!54399
parents 90bf1ae9 06de789b
...@@ -158,6 +158,18 @@ export default { ...@@ -158,6 +158,18 @@ export default {
cancel() { cancel() {
this.showPage(''); this.showPage('');
}, },
boardUpdate(data) {
if (!data?.[this.parentType]) {
return [];
}
return data[this.parentType].boards.edges.map(({ node }) => ({
id: getIdFromGraphQLId(node.id),
name: node.name,
}));
},
boardQuery() {
return this.groupId ? groupQuery : projectQuery;
},
loadBoards(toggleDropdown = true) { loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) { if (toggleDropdown && this.boards.length > 0) {
return; return;
...@@ -167,21 +179,14 @@ export default { ...@@ -167,21 +179,14 @@ export default {
variables() { variables() {
return { fullPath: this.fullPath }; return { fullPath: this.fullPath };
}, },
query() { query: this.boardQuery,
return this.groupId ? groupQuery : projectQuery;
},
loadingKey: 'loadingBoards', loadingKey: 'loadingBoards',
update(data) { update: this.boardUpdate,
if (!data?.[this.parentType]) {
return [];
}
return data[this.parentType].boards.edges.map(({ node }) => ({
id: getIdFromGraphQLId(node.id),
name: node.name,
}));
},
}); });
this.loadRecentBoards();
},
loadRecentBoards() {
this.loadingRecentBoards = true; this.loadingRecentBoards = true;
// Follow up to fetch recent boards using GraphQL // Follow up to fetch recent boards using GraphQL
// https://gitlab.com/gitlab-org/gitlab/-/issues/300985 // https://gitlab.com/gitlab-org/gitlab/-/issues/300985
......
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import BoardsSelector from '~/boards/components/boards_selector.vue'; import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue'; import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue';
import store from '~/boards/stores'; import store from '~/boards/stores';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
...@@ -51,7 +51,7 @@ export default (params = {}) => { ...@@ -51,7 +51,7 @@ export default (params = {}) => {
...mapGetters(['shouldUseGraphQL']), ...mapGetters(['shouldUseGraphQL']),
}, },
render(createElement) { render(createElement) {
if (this.shouldUseGraphQL) { if (this.shouldUseGraphQL || params.isEpicBoard) {
return createElement(BoardsSelector, { return createElement(BoardsSelector, {
props: this.boardsSelectorProps, props: this.boardsSelectorProps,
}); });
......
...@@ -2,6 +2,7 @@ import { inactiveId } from '~/boards/constants'; ...@@ -2,6 +2,7 @@ import { inactiveId } from '~/boards/constants';
export default () => ({ export default () => ({
boardType: null, boardType: null,
fullPath: null,
disabled: false, disabled: false,
isShowingLabels: true, isShowingLabels: true,
activeId: inactiveId, activeId: inactiveId,
......
<script>
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
// extends a valid Vue single file component.
/* eslint-disable @gitlab/no-runtime-template-compiler */
import { mapState } from 'vuex';
import BoardsSelectorFoss from '~/boards/components/boards_selector.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import epicBoardsQuery from '../graphql/epic_boards.query.graphql';
export default {
extends: BoardsSelectorFoss,
computed: {
...mapState(['isEpicBoard']),
},
methods: {
epicBoardUpdate(data) {
if (!data?.group) {
return [];
}
return data.group.epicBoards.nodes.map((node) => ({
id: getIdFromGraphQLId(node.id),
name: node.name,
}));
},
epicBoardQuery() {
return epicBoardsQuery;
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
return;
}
this.$apollo.addSmartQuery('boards', {
variables() {
return { fullPath: this.fullPath };
},
query: this.isEpicBoard ? this.epicBoardQuery : this.boardQuery,
loadingKey: 'loadingBoards',
update: this.isEpicBoard ? this.epicBoardUpdate : this.boardUpdate,
});
if (!this.isEpicBoard) {
this.loadRecentBoards();
}
},
},
};
</script>
query EpicBoards($fullPath: ID!) {
group(fullPath: $fullPath) {
epicBoards {
nodes {
id
name
}
}
}
}
...@@ -83,6 +83,7 @@ export default () => { ...@@ -83,6 +83,7 @@ export default () => {
fullPath: $boardApp.dataset.fullPath, fullPath: $boardApp.dataset.fullPath,
boardType: this.parent, boardType: this.parent,
disabled: this.disabled, disabled: this.disabled,
isEpicBoard: true,
boardConfig: { boardConfig: {
milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10), milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10),
milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '', milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '',
...@@ -96,7 +97,6 @@ export default () => { ...@@ -96,7 +97,6 @@ export default () => {
? parseInt($boardApp.dataset.boardWeight, 10) ? parseInt($boardApp.dataset.boardWeight, 10)
: null, : null,
}, },
isEpicBoard: true,
}); });
}, },
mounted() { mounted() {
...@@ -115,5 +115,6 @@ export default () => { ...@@ -115,5 +115,6 @@ export default () => {
mountMultipleBoardsSwitcher({ mountMultipleBoardsSwitcher({
fullPath: $boardApp.dataset.fullPath, fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint, rootPath: $boardApp.dataset.boardsEndpoint,
isEpicBoard: true,
}); });
}; };
...@@ -33,6 +33,13 @@ module EE ...@@ -33,6 +33,13 @@ module EE
super.merge(data) super.merge(data)
end end
override :board_base_url
def board_base_url
return group_epic_boards_url(@group) if board.is_a?(::Boards::EpicBoard)
super
end
override :recent_boards_path override :recent_boards_path
def recent_boards_path def recent_boards_path
return recent_group_boards_path(@group) if current_board_parent.is_a?(Group) return recent_group_boards_path(@group) if current_board_parent.is_a?(Group)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'epic boards', :js do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:epic_board) { create(:epic_board, group: group) }
let_it_be(:epic_board2) { create(:epic_board, group: group) }
context 'multiple epic boards' do
before do
stub_licensed_features(epics: true)
sign_in(user)
visit_epic_boards_page
end
it 'shows current epic board name' do
page.within('.boards-switcher') do
expect(page).to have_content(epic_board.name)
end
end
it 'shows a list of epic boards' do
in_boards_switcher_dropdown do
expect(page).to have_content(epic_board.name)
expect(page).to have_content(epic_board2.name)
end
end
it 'switches current epic board' do
in_boards_switcher_dropdown do
click_link epic_board2.name
end
wait_for_requests
page.within('.boards-switcher') do
expect(page).to have_content(epic_board2.name)
end
end
end
def visit_epic_boards_page
visit group_epic_boards_path(group)
wait_for_requests
end
def in_boards_switcher_dropdown
find('.boards-switcher').click
wait_for_requests
dropdown_selector = '.js-boards-selector .dropdown-menu'
page.within(dropdown_selector) do
yield
end
end
end
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import BoardsSelector from 'ee/boards/components/boards_selector.vue';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
const throttleDuration = 1;
const localVue = createLocalVue();
localVue.use(Vuex);
function boardGenerator(n) {
return new Array(n).fill().map((board, index) => {
const id = `${index}`;
const name = `board${id}`;
return {
id,
name,
};
});
}
describe('BoardsSelector', () => {
let wrapper;
let allBoardsResponse;
let recentBoardsResponse;
let mock;
const boards = boardGenerator(20);
const recentBoards = boardGenerator(5);
const createStore = () => {
return new Vuex.Store({
state: {
isEpicBoard: false,
},
});
};
const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
const getDropdownHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDropdown = () => wrapper.findComponent(GlDropdown);
beforeEach(() => {
mock = new MockAdapter(axios);
const $apollo = {
queries: {
boards: {
loading: false,
},
},
};
allBoardsResponse = Promise.resolve({
data: {
group: {
boards: {
edges: boards.map((board) => ({ node: board })),
},
},
},
});
recentBoardsResponse = Promise.resolve({
data: recentBoards,
});
const store = createStore();
wrapper = mount(BoardsSelector, {
localVue,
propsData: {
throttleDuration,
currentBoard: {
id: 1,
name: 'Development',
milestone_id: null,
weight: null,
assignee_id: null,
labels: [],
},
boardBaseUrl: `${TEST_HOST}/board/base/url`,
hasMissingBoards: false,
canAdminBoard: true,
multipleIssueBoardsAvailable: true,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/labels`,
projectId: 42,
groupId: 19,
scopedIssueBoardFeatureEnabled: true,
weights: [],
},
mocks: { $apollo },
attachTo: document.body,
provide: {
fullPath: '',
recentBoardsEndpoint: `${TEST_HOST}/recent`,
},
store,
});
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
wrapper.setData({
[options.loadingKey]: true,
});
});
mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
mock.restore();
});
describe('loading', () => {
// we are testing loading state, so don't resolve responses until after the tests
afterEach(async () => {
await Promise.all([allBoardsResponse, recentBoardsResponse]);
return nextTick();
});
it('shows loading spinner', () => {
expect(getDropdownHeaders()).toHaveLength(0);
expect(getDropdownItems()).toHaveLength(0);
expect(getLoadingIcon().exists()).toBe(true);
});
});
describe('loaded', () => {
beforeEach(async () => {
await wrapper.setData({
loadingBoards: false,
});
await Promise.all([allBoardsResponse, recentBoardsResponse]);
return nextTick();
});
it('hides loading spinner', async () => {
await wrapper.vm.$nextTick();
expect(getLoadingIcon().exists()).toBe(false);
});
});
});
...@@ -33,6 +33,19 @@ RSpec.describe BoardsHelper do ...@@ -33,6 +33,19 @@ RSpec.describe BoardsHelper do
end end
end end
describe '#board_base_url' do
context 'when epic board' do
let_it_be(:epic_board) { create(:epic_board, group: group) }
it 'generates the correct url' do
@board = epic_board
@group = group
expect(board_base_url).to eq "http://test.host/groups/#{group.full_path}/-/epic_boards"
end
end
end
describe '#board_data' do describe '#board_data' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:board) { create(:board, project: project) } let_it_be(:board) { create(:board, project: project) }
......
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