Commit 4333e7a1 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '351070-fetch-recent-boards-using-graphql-endpoint' into 'master'

Issue boards - Fetch recent boards in GraphQL

See merge request gitlab-org/gitlab!80353
parents 85259e28 4cd0c645
......@@ -14,8 +14,6 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { s__ } from '~/locale';
import eventHub from '../eventhub';
......@@ -23,6 +21,8 @@ import groupBoardsQuery from '../graphql/group_boards.query.graphql';
import projectBoardsQuery from '../graphql/project_boards.query.graphql';
import groupBoardQuery from '../graphql/group_board.query.graphql';
import projectBoardQuery from '../graphql/project_board.query.graphql';
import groupRecentBoardsQuery from '../graphql/group_recent_boards.query.graphql';
import projectRecentBoardsQuery from '../graphql/project_recent_boards.query.graphql';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
......@@ -40,7 +40,7 @@ export default {
directives: {
GlModalDirective,
},
inject: ['fullPath', 'recentBoardsEndpoint'],
inject: ['fullPath'],
props: {
throttleDuration: {
type: Number,
......@@ -158,6 +158,10 @@ export default {
this.scrollFadeInitialized = false;
this.$nextTick(this.setScrollFade);
},
recentBoards() {
this.scrollFadeInitialized = false;
this.$nextTick(this.setScrollFade);
},
},
created() {
eventHub.$on('showBoardModal', this.showPage);
......@@ -173,11 +177,11 @@ export default {
cancel() {
this.showPage('');
},
boardUpdate(data) {
boardUpdate(data, boardType) {
if (!data?.[this.parentType]) {
return [];
}
return data[this.parentType].boards.edges.map(({ node }) => ({
return data[this.parentType][boardType].edges.map(({ node }) => ({
id: getIdFromGraphQLId(node.id),
name: node.name,
}));
......@@ -185,6 +189,9 @@ export default {
boardQuery() {
return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery;
},
recentBoardsQuery() {
return this.isGroupBoard ? groupRecentBoardsQuery : projectRecentBoardsQuery;
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
return;
......@@ -196,39 +203,20 @@ export default {
},
query: this.boardQuery,
loadingKey: 'loadingBoards',
update: this.boardUpdate,
update: (data) => this.boardUpdate(data, 'boards'),
});
this.loadRecentBoards();
},
loadRecentBoards() {
this.loadingRecentBoards = true;
// Follow up to fetch recent boards using GraphQL
// https://gitlab.com/gitlab-org/gitlab/-/issues/300985
axios
.get(this.recentBoardsEndpoint)
.then((res) => {
this.recentBoards = res.data;
})
.catch((err) => {
/**
* If user is unauthorized we'd still want to resolve the
* request to display all boards.
*/
if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
this.recentBoards = []; // recent boards are empty
return;
}
throw err;
})
.then(() => this.$nextTick()) // Wait for boards list in DOM
.then(() => {
this.setScrollFade();
})
.catch(() => {})
.finally(() => {
this.loadingRecentBoards = false;
});
this.$apollo.addSmartQuery('recentBoards', {
variables() {
return { fullPath: this.fullPath };
},
query: this.recentBoardsQuery,
loadingKey: 'loadingRecentBoards',
update: (data) => this.boardUpdate(data, 'recentIssueBoards'),
});
},
isScrolledUp() {
const { content } = this.$refs;
......
#import "ee_else_ce/boards/graphql/board.fragment.graphql"
query group_recent_boards($fullPath: ID!) {
group(fullPath: $fullPath) {
id
recentIssueBoards {
edges {
node {
...BoardFragment
}
}
}
}
}
#import "ee_else_ce/boards/graphql/board.fragment.graphql"
query project_recent_boards($fullPath: ID!) {
project(fullPath: $fullPath) {
id
recentIssueBoards {
edges {
node {
...BoardFragment
}
}
}
}
}
......@@ -144,7 +144,6 @@ export default () => {
mountMultipleBoardsSwitcher({
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
allowScopedLabels: $boardApp.dataset.scopedLabels,
labelsManagePath: $boardApp.dataset.labelsManagePath,
});
......
......@@ -24,7 +24,6 @@ export default (params = {}) => {
provide: {
fullPath: params.fullPath,
rootPath: params.rootPath,
recentBoardsEndpoint: params.recentBoardsEndpoint,
allowScopedLabels: params.allowScopedLabels,
labelsManagePath: params.labelsManagePath,
allowLabelCreate: parseBoolean(dataset.canAdminBoard),
......
......@@ -17,7 +17,6 @@ module BoardsHelper
can_update: can_update?.to_s,
can_admin_list: can_admin_list?.to_s,
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s,
recent_boards_endpoint: recent_boards_path,
parent: current_board_parent.model_name.param_key,
group_id: group_id,
labels_filter_base_path: build_issue_link_base,
......@@ -128,10 +127,6 @@ module BoardsHelper
}
end
def recent_boards_path
recent_project_boards_path(@project) if current_board_parent.is_a?(Project)
end
def serializer
CurrentBoardSerializer.new
end
......
......@@ -52,7 +52,8 @@ export default {
},
query: this.isEpicBoard ? this.epicBoardQuery : this.boardQuery,
loadingKey: 'loadingBoards',
update: this.isEpicBoard ? this.epicBoardUpdate : this.boardUpdate,
update: (data) =>
this.isEpicBoard ? this.epicBoardUpdate(data) : this.boardUpdate(data, 'boards'),
});
if (!this.isEpicBoard) {
......
......@@ -84,12 +84,5 @@ module EE
super
end
override :recent_boards_path
def recent_boards_path
return recent_group_boards_path(@group) if current_board_parent.is_a?(Group)
super
end
end
end
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
import { GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import BoardsSelector from 'ee/boards/components/boards_selector.vue';
import { BoardType } from '~/boards/constants';
import epicBoardQuery from 'ee/boards/graphql/epic_board.query.graphql';
import epicBoardsQuery from 'ee/boards/graphql/epic_boards.query.graphql';
import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql';
import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql';
import defaultStore from '~/boards/stores';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockGroupBoardResponse, mockProjectBoardResponse } from 'jest/boards/mock_data';
import { mockEpicBoardResponse } from '../mock_data';
import {
mockGroupBoardResponse,
mockProjectBoardResponse,
mockGroupAllBoardsResponse,
mockProjectAllBoardsResponse,
} from 'jest/boards/mock_data';
import { mockEpicBoardResponse, mockEpicBoardsResponse } from '../mock_data';
const throttleDuration = 1;
Vue.use(VueApollo);
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;
let fakeApollo;
let store;
const boards = boardGenerator(20);
const recentBoards = boardGenerator(5);
const createStore = ({
isGroupBoard = false,
......@@ -63,20 +52,26 @@ describe('BoardsSelector', () => {
});
};
const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
const getDropdownHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
const epicBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockEpicBoardResponse);
const projectBoardsQueryHandlerSuccess = jest
.fn()
.mockResolvedValue(mockProjectAllBoardsResponse);
const groupBoardsQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupAllBoardsResponse);
const epicBoardsQueryHandlerSuccess = jest.fn().mockResolvedValue(mockEpicBoardsResponse);
const createComponent = () => {
fakeApollo = createMockApollo([
[projectBoardQuery, projectBoardQueryHandlerSuccess],
[groupBoardQuery, groupBoardQueryHandlerSuccess],
[epicBoardQuery, epicBoardQueryHandlerSuccess],
[projectBoardsQuery, projectBoardsQueryHandlerSuccess],
[groupBoardsQuery, groupBoardsQueryHandlerSuccess],
[epicBoardsQuery, epicBoardsQueryHandlerSuccess],
]);
wrapper = mount(BoardsSelector, {
......@@ -94,92 +89,44 @@ describe('BoardsSelector', () => {
attachTo: document.body,
provide: {
fullPath: '',
recentBoardsEndpoint: `${TEST_HOST}/recent`,
},
});
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
[options.loadingKey]: true,
});
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
fakeApollo = null;
store = null;
mock.restore();
});
describe('fetching all board', () => {
describe('fetching all boards', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
allBoardsResponse = Promise.resolve({
data: {
group: {
boards: {
edges: boards.map((board) => ({ node: board })),
},
},
it.each`
boardType | isEpicBoard | queryHandler | notCalledHandler
${BoardType.group} | ${false} | ${groupBoardsQueryHandlerSuccess} | ${projectBoardsQueryHandlerSuccess}
${BoardType.project} | ${false} | ${projectBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess}
${BoardType.group} | ${true} | ${epicBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess}
`(
'fetches $boardType boards when isEpicBoard is $isEpicBoard',
async ({ boardType, isEpicBoard, queryHandler, notCalledHandler }) => {
createStore({
isProjectBoard: boardType === BoardType.project,
isGroupBoard: boardType === BoardType.group,
isEpicBoard,
});
createComponent();
await nextTick();
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
await nextTick();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
},
});
recentBoardsResponse = Promise.resolve({
data: recentBoards,
});
createStore();
createComponent();
mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
});
describe('loading', () => {
beforeEach(async () => {
// Wait for current board to be loaded
await nextTick();
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
});
// we are testing loading state, so don't resolve responses until after the tests
afterEach(async () => {
await Promise.all([allBoardsResponse, recentBoardsResponse]);
await nextTick();
});
it('shows loading spinner', () => {
expect(getDropdownHeaders()).toHaveLength(0);
expect(getDropdownItems()).toHaveLength(0);
expect(getLoadingIcon().exists()).toBe(true);
});
});
describe('loaded', () => {
beforeEach(async () => {
// Wait for current board to be loaded
await nextTick();
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
await wrapper.setData({
loadingBoards: false,
loadingRecentBoards: false,
});
});
it('hides loading spinner', async () => {
await nextTick();
expect(getLoadingIcon().exists()).toBe(false);
});
);
});
});
......
......@@ -21,6 +21,27 @@ export const mockEpicBoardResponse = {
},
};
export const mockEpicBoardsResponse = {
data: {
group: {
id: 'gid://gitlab/Group/114',
epicBoards: {
nodes: [
{
id: 'gid://gitlab/Boards::EpicBoard/1',
name: 'Development',
},
{
id: 'gid://gitlab/Boards::EpicBoard/2',
name: 'Marketing',
},
],
},
__typename: 'Group',
},
},
};
export const mockLabel = {
id: 'gid://gitlab/GroupLabel/121',
title: 'To Do',
......
......@@ -29,6 +29,85 @@ export const listObj = {
},
};
function boardGenerator(n) {
return new Array(n).fill().map((board, index) => {
const id = `${index}`;
const name = `board${id}`;
return {
node: {
id,
name,
weight: 0,
__typename: 'Board',
},
};
});
}
export const boards = boardGenerator(20);
export const recentIssueBoards = boardGenerator(5);
export const mockSmallProjectAllBoardsResponse = {
data: {
project: {
id: 'gid://gitlab/Project/114',
boards: { edges: boardGenerator(3) },
__typename: 'Project',
},
},
};
export const mockEmptyProjectRecentBoardsResponse = {
data: {
project: {
id: 'gid://gitlab/Project/114',
recentIssueBoards: { edges: [] },
__typename: 'Project',
},
},
};
export const mockGroupAllBoardsResponse = {
data: {
group: {
id: 'gid://gitlab/Group/114',
boards: { edges: boards },
__typename: 'Group',
},
},
};
export const mockProjectAllBoardsResponse = {
data: {
project: {
id: 'gid://gitlab/Project/1',
boards: { edges: boards },
__typename: 'Project',
},
},
};
export const mockGroupRecentBoardsResponse = {
data: {
group: {
id: 'gid://gitlab/Group/114',
recentIssueBoards: { edges: recentIssueBoards },
__typename: 'Group',
},
},
};
export const mockProjectRecentBoardsResponse = {
data: {
project: {
id: 'gid://gitlab/Project/1',
recentIssueBoards: { edges: recentIssueBoards },
__typename: 'Project',
},
},
};
export const mockGroupBoardResponse = {
data: {
workspace: {
......
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