Commit 1deeaa3b authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'donald-apollo-board' into 'master'

On boards, move weight and issue count to async call

See merge request gitlab-org/gitlab!73854
parents 858e48d3 8458810d
......@@ -8,6 +8,7 @@ import defaultSortableConfig from '~/sortable/sortable_config';
import Tracking from '~/tracking';
import { toggleFormEventPrefix, DraggableItemTypes } from '../constants';
import eventHub from '../eventhub';
import listQuery from '../graphql/board_lists_deferred.query.graphql';
import BoardCard from './board_card.vue';
import BoardNewIssue from './board_new_issue.vue';
......@@ -50,11 +51,22 @@ export default {
showEpicForm: false,
};
},
apollo: {
boardList: {
query: listQuery,
variables() {
return {
id: this.list.id,
filters: this.filterParams,
};
},
},
},
computed: {
...mapState(['pageInfoByListId', 'listsFlags']),
...mapState(['pageInfoByListId', 'listsFlags', 'filterParams']),
...mapGetters(['isEpicBoard']),
listItemsCount() {
return this.isEpicBoard ? this.list.epicsCount : this.list.issuesCount;
return this.isEpicBoard ? this.list.epicsCount : this.boardList?.issuesCount;
},
paginatedIssueText() {
return sprintf(__('Showing %{pageSize} of %{total} %{issuableType}'), {
......
......@@ -20,6 +20,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
import listQuery from '../graphql/board_lists_deferred.query.graphql';
import ItemCount from './item_count.vue';
export default {
......@@ -74,7 +75,7 @@ export default {
},
},
computed: {
...mapState(['activeId']),
...mapState(['activeId', 'filterParams']),
...mapGetters(['isEpicBoard', 'isSwimlanesOn']),
isLoggedIn() {
return Boolean(this.currentUserId);
......@@ -119,14 +120,11 @@ export default {
}
return false;
},
itemsCount() {
return this.list.issuesCount;
},
countIcon() {
return 'issues';
},
itemsTooltipLabel() {
return n__(`%d issue`, `%d issues`, this.itemsCount);
return n__(`%d issue`, `%d issues`, this.boardLists?.issuesCount);
},
chevronTooltip() {
return this.list.collapsed ? this.$options.i18n.expand : this.$options.i18n.collapse;
......@@ -158,6 +156,23 @@ export default {
userCanDrag() {
return !this.disabled && isListDraggable(this.list);
},
isLoading() {
return this.$apollo.queries.boardList.loading;
},
},
apollo: {
boardList: {
query: listQuery,
variables() {
return {
id: this.list.id,
filters: this.filterParams,
};
},
skip() {
return this.isEpicBoard;
},
},
},
created() {
const localCollapsed = parseBoolean(localStorage.getItem(`${this.uniqueKey}.collapsed`));
......@@ -375,10 +390,10 @@ export default {
</gl-sprintf>
</div>
<div v-else>• {{ itemsTooltipLabel }}</div>
<div v-if="weightFeatureAvailable">
<div v-if="weightFeatureAvailable && !isLoading">
<gl-sprintf :message="__('%{totalWeight} total weight')">
<template #totalWeight>{{ list.totalWeight }}</template>
<template #totalWeight>{{ boardList.totalWeight }}</template>
</gl-sprintf>
</div>
</gl-tooltip>
......@@ -396,14 +411,18 @@ export default {
<gl-tooltip :target="() => $refs.itemCount" :title="itemsTooltipLabel" />
<span ref="itemCount" class="gl-display-inline-flex gl-align-items-center">
<gl-icon class="gl-mr-2" :name="countIcon" />
<item-count :items-size="itemsCount" :max-issue-count="list.maxIssueCount" />
<item-count
v-if="!isLoading"
:items-size="isEpicBoard ? list.epicsCount : boardList.issuesCount"
:max-issue-count="list.maxIssueCount"
/>
</span>
<!-- EE start -->
<template v-if="weightFeatureAvailable && !isEpicBoard">
<template v-if="weightFeatureAvailable && !isEpicBoard && !isLoading">
<gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" />
<span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3">
<gl-icon class="gl-mr-2" name="weight" />
{{ list.totalWeight }}
{{ boardList.totalWeight }}
</span>
</template>
<!-- EE end -->
......
......@@ -4,7 +4,6 @@ fragment BoardListShared on BoardList {
position
listType
collapsed
issuesCount
label {
id
title
......
query BoardList($id: ID!, $filters: BoardIssueInput) {
boardList(id: $id, issueFilters: $filters) {
id
issuesCount
}
}
#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
query BoardListEE(
query BoardListsEE(
$fullPath: ID!
$boardId: ID!
$id: ID
......
......@@ -30,6 +30,7 @@ import {
} from 'ee_else_ce/boards/boards_util';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import totalCountAndWeightQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
......@@ -501,9 +502,10 @@ export default {
updateIssueOrder: async ({ commit, dispatch, state }, { moveData, mutationVariables = {} }) => {
try {
const { itemId, fromListId, toListId, moveBeforeId, moveAfterId } = moveData;
const { itemId, fromListId, toListId, moveBeforeId, moveAfterId, itemNotInToList } = moveData;
const {
fullBoardId,
filterParams,
boardItems: {
[itemId]: { iid, referencePath },
},
......@@ -522,6 +524,67 @@ export default {
// 'mutationVariables' allows EE code to pass in extra parameters.
...mutationVariables,
},
update(
cache,
{
data: {
issueMoveList: {
issue: { weight },
},
},
},
) {
if (fromListId === toListId) return;
const updateFromList = () => {
const fromList = cache.readQuery({
query: totalCountAndWeightQuery,
variables: { id: fromListId, filters: filterParams },
});
const updatedFromList = {
boardList: {
__typename: 'BoardList',
id: fromList.boardList.id,
issuesCount: fromList.boardList.issuesCount - 1,
totalWeight: fromList.boardList.totalWeight - Number(weight),
},
};
cache.writeQuery({
query: totalCountAndWeightQuery,
variables: { id: fromListId, filters: filterParams },
data: updatedFromList,
});
};
const updateToList = () => {
if (!itemNotInToList) return;
const toList = cache.readQuery({
query: totalCountAndWeightQuery,
variables: { id: toListId, filters: filterParams },
});
const updatedToList = {
boardList: {
__typename: 'BoardList',
id: toList.boardList.id,
issuesCount: toList.boardList.issuesCount + 1,
totalWeight: toList.boardList.totalWeight + Number(weight),
},
};
cache.writeQuery({
query: totalCountAndWeightQuery,
variables: { id: toListId, filters: filterParams },
data: updatedToList,
});
};
updateFromList();
updateToList();
},
});
if (data?.issueMoveList?.errors.length || !data.issueMoveList) {
......@@ -565,7 +628,7 @@ export default {
},
addListNewIssue: (
{ state: { boardConfig, boardType, fullPath }, dispatch, commit },
{ state: { boardConfig, boardType, fullPath, filterParams }, dispatch, commit },
{ issueInput, list, placeholderId = `tmp-${new Date().getTime()}` },
) => {
const input = formatIssueInput(issueInput, boardConfig);
......@@ -581,6 +644,27 @@ export default {
.mutate({
mutation: issueCreateMutation,
variables: { input },
update(cache) {
const fromList = cache.readQuery({
query: totalCountAndWeightQuery,
variables: { id: list.id, filters: filterParams },
});
const updatedList = {
boardList: {
__typename: 'BoardList',
id: fromList.boardList.id,
issuesCount: fromList.boardList.issuesCount + 1,
totalWeight: fromList.boardList.totalWeight,
},
};
cache.writeQuery({
query: totalCountAndWeightQuery,
variables: { id: list.id, filters: filterParams },
data: updatedList,
});
},
})
.then(({ data }) => {
if (data.createIssue.errors.length) {
......
......@@ -5,17 +5,32 @@ import { mapGetters } from 'vuex';
/* eslint-disable @gitlab/no-runtime-template-compiler */
import BoardListHeaderFoss from '~/boards/components/board_list_header.vue';
import { n__, __, sprintf } from '~/locale';
import listQuery from '../graphql/board_lists_deferred.query.graphql';
export default {
extends: BoardListHeaderFoss,
inject: ['weightFeatureAvailable'],
apollo: {
boardList: {
query: listQuery,
variables() {
return {
id: this.list.id,
filters: this.filterParams,
};
},
skip() {
return this.isEpicBoard;
},
},
},
computed: {
...mapGetters(['isEpicBoard']),
countIcon() {
return this.isEpicBoard ? 'epic' : 'issues';
},
itemsCount() {
return this.isEpicBoard ? this.list.epicsCount : this.list.issuesCount;
return this.isEpicBoard ? this.list.epicsCount : this.boardList?.issuesCount;
},
itemsTooltipLabel() {
const { maxIssueCount } = this.list;
......@@ -31,7 +46,7 @@ export default {
: n__(`%d issue`, `%d issues`, this.itemsCount);
},
weightCountToolTip() {
const { totalWeight } = this.list;
const { totalWeight } = this.boardList;
if (this.weightFeatureAvailable) {
return sprintf(__('%{totalWeight} total weight'), { totalWeight });
......
......@@ -3,7 +3,6 @@
fragment BoardListFragment on BoardList {
...BoardListShared
maxIssueCount
totalWeight
assignee {
id
name
......
query BoardListEE($id: ID!, $filters: BoardIssueInput) {
boardList(id: $id, issueFilters: $filters) {
id
totalWeight
issuesCount
}
}
......@@ -96,7 +96,7 @@ export default {
...actionsCE,
addListNewIssue: async (
{ state: { boardConfig, boardType, fullPath }, dispatch, commit },
{ state: { boardConfig, boardType, fullPath, filterParams }, dispatch, commit },
issueInputObj,
) => {
const { iterationId } = boardConfig;
......@@ -127,6 +127,7 @@ export default {
boardConfig: { ...boardConfig, iterationId, iterationCadenceId },
boardType,
fullPath,
filterParams,
},
dispatch,
commit,
......
import { GlButton, GlButtonGroup } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import BoardListHeader from 'ee/boards/components/board_list_header.vue';
import defaultGetters from 'ee/boards/stores/getters';
import { mockList, mockLabelList } from 'jest/boards/mock_data';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { boardListQueryResponse, mockList, mockLabelList } from 'jest/boards/mock_data';
import { ListType, inactiveId } from '~/boards/constants';
import boardsEventHub from '~/boards/eventhub';
import listQuery from 'ee/boards/graphql/board_lists_deferred.query.graphql';
import sidebarEventHub from '~/sidebar/event_hub';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(VueApollo);
Vue.use(Vuex);
const listMocks = {
[ListType.assignee]: {
......@@ -34,6 +38,18 @@ const listMocks = {
describe('Board List Header Component', () => {
let store;
let wrapper;
let fakeApollo;
beforeEach(() => {
store = new Vuex.Store({ state: { activeId: inactiveId }, defaultGetters });
});
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
localStorage.clear();
});
const createComponent = ({
listType = ListType.backlog,
......@@ -41,6 +57,7 @@ describe('Board List Header Component', () => {
withLocalStorage = true,
isSwimlanesHeader = false,
weightFeatureAvailable = false,
listQueryHandler = jest.fn().mockResolvedValue(boardListQueryResponse()),
currentUserId = 1,
state = { activeId: inactiveId },
getters = {},
......@@ -61,6 +78,7 @@ describe('Board List Header Component', () => {
);
}
fakeApollo = createMockApollo([[listQuery, listQueryHandler]]);
store = new Vuex.Store({
state,
getters: {
......@@ -72,8 +90,8 @@ describe('Board List Header Component', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(BoardListHeader, {
apolloProvider: fakeApollo,
store,
localVue,
propsData: {
disabled: false,
list: listMock,
......@@ -188,10 +206,15 @@ describe('Board List Header Component', () => {
});
describe('weightFeatureAvailable', () => {
it('weightFeatureAvailable is true', () => {
it('weightFeatureAvailable is true', async () => {
createComponent({ weightFeatureAvailable: true });
expect(wrapper.find({ ref: 'weightTooltip' }).exists()).toBe(true);
await waitForPromises();
const weightTooltip = wrapper.find({ ref: 'weightTooltip' });
expect(weightTooltip.exists()).toBe(true);
expect(weightTooltip.text()).toContain(boardListQueryResponse().data.boardList.totalWeight);
});
it('weightFeatureAvailable is false', () => {
......
......@@ -1467,6 +1467,7 @@ describe('addListNewIssue', () => {
iterationCadenceId,
}),
},
update: expect.anything(),
});
});
});
......@@ -1500,6 +1501,7 @@ describe('addListNewIssue', () => {
variables: {
input: formatIssueInput(mockIssue, state.boardConfig),
},
update: expect.anything(),
});
});
});
......@@ -1536,6 +1538,7 @@ describe('addListNewIssue', () => {
variables: {
input: formatIssueInput(mockIssue, state.boardConfig),
},
update: expect.anything(),
});
});
});
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import BoardCard from '~/boards/components/board_card.vue';
......@@ -6,7 +7,15 @@ import BoardList from '~/boards/components/board_list.vue';
import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import BoardNewItem from '~/boards/components/board_new_item.vue';
import defaultState from '~/boards/stores/state';
import { mockList, mockIssuesByListId, issues, mockGroupProjects } from './mock_data';
import createMockApollo from 'helpers/mock_apollo_helper';
import listQuery from '~/boards/graphql/board_lists_deferred.query.graphql';
import {
mockList,
mockIssuesByListId,
issues,
mockGroupProjects,
boardListQueryResponse,
} from './mock_data';
export default function createComponent({
listIssueProps = {},
......@@ -15,16 +24,23 @@ export default function createComponent({
actions = {},
getters = {},
provide = {},
data = {},
state = defaultState,
stubs = {
BoardNewIssue,
BoardNewItem,
BoardCard,
},
issuesCount,
} = {}) {
const localVue = createLocalVue();
localVue.use(VueApollo);
localVue.use(Vuex);
const fakeApollo = createMockApollo([
[listQuery, jest.fn().mockResolvedValue(boardListQueryResponse(issuesCount))],
]);
const store = new Vuex.Store({
state: {
selectedProject: mockGroupProjects[0],
......@@ -68,6 +84,7 @@ export default function createComponent({
}
const component = shallowMount(BoardList, {
apolloProvider: fakeApollo,
localVue,
store,
propsData: {
......@@ -87,6 +104,11 @@ export default function createComponent({
...provide,
},
stubs,
data() {
return {
...data,
};
},
});
return component;
......
......@@ -38,7 +38,7 @@ describe('Board list component', () => {
describe('When Expanded', () => {
beforeEach(() => {
wrapper = createComponent();
wrapper = createComponent({ issuesCount: 1 });
});
it('renders component', () => {
......@@ -97,14 +97,6 @@ describe('Board list component', () => {
await wrapper.vm.$nextTick();
expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1');
});
it('shows how many more issues to load', async () => {
wrapper.vm.showCount = true;
wrapper.setProps({ list: { issuesCount: 20 } });
await wrapper.vm.$nextTick();
expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
});
});
describe('load more issues', () => {
......@@ -113,9 +105,7 @@ describe('Board list component', () => {
};
beforeEach(() => {
wrapper = createComponent({
listProps: { issuesCount: 25 },
});
wrapper = createComponent();
});
it('does not load issues if already loading', () => {
......@@ -131,13 +121,27 @@ describe('Board list component', () => {
it('shows loading more spinner', async () => {
wrapper = createComponent({
state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
data: {
showCount: true,
},
});
wrapper.vm.showCount = true;
await wrapper.vm.$nextTick();
expect(findIssueCountLoadingIcon().exists()).toBe(true);
});
it('shows how many more issues to load', async () => {
// wrapper.vm.showCount = true;
wrapper = createComponent({
data: {
showCount: true,
},
});
await wrapper.vm.$nextTick();
expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
});
});
describe('max issue count warning', () => {
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mockLabelList } from 'jest/boards/mock_data';
import { boardListQueryResponse, mockLabelList } from 'jest/boards/mock_data';
import BoardListHeader from '~/boards/components/board_list_header.vue';
import { ListType } from '~/boards/constants';
import listQuery from '~/boards/graphql/board_lists_deferred.query.graphql';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(VueApollo);
Vue.use(Vuex);
describe('Board List Header Component', () => {
let wrapper;
let store;
let fakeApollo;
const updateListSpy = jest.fn();
const toggleListCollapsedSpy = jest.fn();
......@@ -20,6 +24,7 @@ describe('Board List Header Component', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
fakeApollo = null;
localStorage.clear();
});
......@@ -29,6 +34,7 @@ describe('Board List Header Component', () => {
collapsed = false,
withLocalStorage = true,
currentUserId = 1,
listQueryHandler = jest.fn().mockResolvedValue(boardListQueryResponse()),
} = {}) => {
const boardId = '1';
......@@ -56,10 +62,12 @@ describe('Board List Header Component', () => {
getters: { isEpicBoard: () => false },
});
fakeApollo = createMockApollo([[listQuery, listQueryHandler]]);
wrapper = extendedWrapper(
shallowMount(BoardListHeader, {
apolloProvider: fakeApollo,
store,
localVue,
propsData: {
disabled: false,
list: listMock,
......
......@@ -662,3 +662,14 @@ export const mockGroupLabelsResponse = {
},
},
};
export const boardListQueryResponse = (issuesCount = 20) => ({
data: {
boardList: {
__typename: 'BoardList',
id: 'gid://gitlab/BoardList/5',
totalWeight: 5,
issuesCount,
},
},
});
......@@ -1241,6 +1241,7 @@ describe('updateIssueOrder', () => {
moveBeforeId: undefined,
moveAfterId: undefined,
},
update: expect.anything(),
};
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
......@@ -1447,6 +1448,7 @@ describe('addListNewIssue', () => {
variables: {
input: formatIssueInput(mockIssue, stateWithBoardConfig.boardConfig),
},
update: expect.anything(),
});
});
......@@ -1478,6 +1480,7 @@ describe('addListNewIssue', () => {
variables: {
input: formatIssueInput(issue, stateWithBoardConfig.boardConfig),
},
update: expect.anything(),
});
expect(payload.labelIds).toEqual(['gid://gitlab/GroupLabel/4', 'gid://gitlab/GroupLabel/5']);
expect(payload.assigneeIds).toEqual(['gid://gitlab/User/1', 'gid://gitlab/User/2']);
......
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