Commit f2c36be8 authored by Coung Ngo's avatar Coung Ngo

Fix issue reordering in issues page refactor

The issues page refactor initially used the REST API.
Parts of it have since been refactored to use the GraphQL API.
When parts were refactored to use GraphQL, this broke the issue
reordering. This commit fixes this.

https://gitlab.com/gitlab-org/gitlab/-/issues/322755
parent 1e84bc12
......@@ -12,7 +12,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import createFlash from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
......@@ -20,7 +20,6 @@ import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import {
CREATED_DESC,
i18n,
initialPageParams,
issuesCountSmartQueryBase,
MAX_LIST_SIZE,
PAGE_SIZE,
......@@ -46,12 +45,13 @@ import {
convertToUrlParams,
getDueDateValue,
getFilterTokens,
getInitialPageParams,
getSortKey,
getSortOptions,
} from '~/issues_list/utils';
import axios from '~/lib/utils/axios_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { getParameterByName } from '~/lib/utils/url_utility';
import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
import {
DEFAULT_NONE_ANY,
OPERATOR_IS_ONLY,
......@@ -73,6 +73,7 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
import eventHub from '../eventhub';
import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql';
import searchIterationsQuery from '../queries/search_iterations.query.graphql';
import searchLabelsQuery from '../queries/search_labels.query.graphql';
import searchMilestonesQuery from '../queries/search_milestones.query.graphql';
......@@ -161,6 +162,7 @@ export default {
},
data() {
const state = getParameterByName(PARAM_STATE);
const sortKey = getSortKey(getParameterByName(PARAM_SORT));
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
return {
......@@ -169,9 +171,9 @@ export default {
filterTokens: getFilterTokens(window.location.search),
issues: [],
pageInfo: {},
pageParams: initialPageParams,
pageParams: getInitialPageParams(sortKey),
showBulkEditSidebar: false,
sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
sortKey: sortKey || defaultSortKey,
state: state || IssuableStates.Opened,
};
},
......@@ -517,12 +519,12 @@ export default {
},
handleClickTab(state) {
if (this.state !== state) {
this.pageParams = initialPageParams;
this.pageParams = getInitialPageParams(this.sortKey);
}
this.state = state;
},
handleFilter(filter) {
this.pageParams = initialPageParams;
this.pageParams = getInitialPageParams(this.sortKey);
this.filterTokens = filter;
},
handleNextPage() {
......@@ -559,14 +561,16 @@ export default {
}
return axios
.put(`${this.issuesPath}/${issueToMove.iid}/reorder`, {
move_before_id: isMovingToBeginning ? null : moveBeforeId,
move_after_id: isMovingToEnd ? null : moveAfterId,
.put(joinPaths(this.issuesPath, issueToMove.iid, 'reorder'), {
move_before_id: isMovingToBeginning ? null : getIdFromGraphQLId(moveBeforeId),
move_after_id: isMovingToEnd ? null : getIdFromGraphQLId(moveAfterId),
})
.then(() => {
// Move issue to new position in list
this.issues.splice(oldIndex, 1);
this.issues.splice(newIndex, 0, issueToMove);
const serializedVariables = JSON.stringify(this.queryVariables);
this.$apollo.mutate({
mutation: reorderIssuesMutation,
variables: { oldIndex, newIndex, serializedVariables },
});
})
.catch(() => {
createFlash({ message: this.$options.i18n.reorderError });
......@@ -574,7 +578,7 @@ export default {
},
handleSort(sortKey) {
if (this.sortKey !== sortKey) {
this.pageParams = initialPageParams;
this.pageParams = getInitialPageParams(sortKey);
}
this.sortKey = sortKey;
},
......
......@@ -107,10 +107,14 @@ export const PARAM_DUE_DATE = 'due_date';
export const PARAM_SORT = 'sort';
export const PARAM_STATE = 'state';
export const initialPageParams = {
export const defaultPageSizeParams = {
firstPageSize: PAGE_SIZE,
};
export const largePageSizeParams = {
firstPageSize: PAGE_SIZE_MANUAL,
};
export const DUE_DATE_NONE = '0';
export const DUE_DATE_ANY = '';
export const DUE_DATE_OVERDUE = 'overdue';
......
import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
......@@ -82,7 +84,27 @@ export function mountIssuesListApp() {
Vue.use(VueApollo);
const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
const resolvers = {
Mutation: {
reorderIssues: (_, { oldIndex, newIndex, serializedVariables }, { cache }) => {
const variables = JSON.parse(serializedVariables);
const sourceData = cache.readQuery({ query: getIssuesQuery, variables });
const data = produce(sourceData, (draftData) => {
const issues = draftData.project.issues.nodes.slice();
const issueToMove = issues[oldIndex];
issues.splice(oldIndex, 1);
issues.splice(newIndex, 0, issueToMove);
draftData.project.issues.nodes = issues;
});
cache.writeQuery({ query: getIssuesQuery, variables, data });
},
},
};
const defaultClient = createDefaultClient(resolvers, { assumeImmutableResults: true });
const apolloProvider = new VueApollo({
defaultClient,
});
......
mutation reorderIssues($oldIndex: Int, $newIndex: Int, $serializedVariables: String) {
reorderIssues(
oldIndex: $oldIndex
newIndex: $newIndex
serializedVariables: $serializedVariables
) @client
}
......@@ -3,12 +3,14 @@ import {
BLOCKING_ISSUES_DESC,
CREATED_ASC,
CREATED_DESC,
defaultPageSizeParams,
DUE_DATE_ASC,
DUE_DATE_DESC,
DUE_DATE_VALUES,
filters,
LABEL_PRIORITY_ASC,
LABEL_PRIORITY_DESC,
largePageSizeParams,
MILESTONE_DUE_ASC,
MILESTONE_DUE_DESC,
NORMAL_FILTER,
......@@ -35,6 +37,9 @@ import {
OPERATOR_IS_NOT,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getInitialPageParams = (sortKey) =>
sortKey === RELATIVE_POSITION_ASC ? largePageSizeParams : defaultPageSizeParams;
export const getSortKey = (sort) =>
Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort);
......
......@@ -18,7 +18,7 @@ import {
getIssuesCountQueryResponse,
} from 'jest/issues_list/mock_data';
import createFlash from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
......@@ -43,6 +43,7 @@ import eventHub from '~/issues_list/eventhub';
import { getSortOptions } from '~/issues_list/utils';
import axios from '~/lib/utils/axios_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { joinPaths } from '~/lib/utils/url_utility';
jest.mock('~/flash');
jest.mock('~/lib/utils/scroll_utils', () => ({
......@@ -621,25 +622,25 @@ describe('IssuesListApp component', () => {
const issueOne = {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/1',
iid: 101,
iid: '101',
title: 'Issue one',
};
const issueTwo = {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/2',
iid: 102,
iid: '102',
title: 'Issue two',
};
const issueThree = {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/3',
iid: 103,
iid: '103',
title: 'Issue three',
};
const issueFour = {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/4',
iid: 104,
iid: '104',
title: 'Issue four',
};
const response = {
......@@ -658,9 +659,36 @@ describe('IssuesListApp component', () => {
jest.runOnlyPendingTimers();
});
describe('when successful', () => {
describe.each`
description | issueToMove | oldIndex | newIndex | moveBeforeId | moveAfterId
${'to the beginning of the list'} | ${issueThree} | ${2} | ${0} | ${null} | ${issueOne.id}
${'down the list'} | ${issueOne} | ${0} | ${1} | ${issueTwo.id} | ${issueThree.id}
${'up the list'} | ${issueThree} | ${2} | ${1} | ${issueOne.id} | ${issueTwo.id}
${'to the end of the list'} | ${issueTwo} | ${1} | ${3} | ${issueFour.id} | ${null}
`(
'when moving issue $description',
({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => {
it('makes API call to reorder the issue', async () => {
findIssuableList().vm.$emit('reorder', { oldIndex, newIndex });
await waitForPromises();
expect(axiosMock.history.put[0]).toMatchObject({
url: joinPaths(defaultProvide.issuesPath, issueToMove.iid, 'reorder'),
data: JSON.stringify({
move_before_id: getIdFromGraphQLId(moveBeforeId),
move_after_id: getIdFromGraphQLId(moveAfterId),
}),
});
});
},
);
});
describe('when unsuccessful', () => {
it('displays an error message', async () => {
axiosMock.onPut(`${defaultProvide.issuesPath}/${issueOne.iid}/reorder`).reply(500);
axiosMock.onPut(joinPaths(defaultProvide.issuesPath, issueOne.iid, 'reorder')).reply(500);
findIssuableList().vm.$emit('reorder', { oldIndex: 0, newIndex: 1 });
......
......@@ -8,17 +8,36 @@ import {
urlParams,
urlParamsWithSpecialValues,
} from 'jest/issues_list/mock_data';
import { DUE_DATE_VALUES, urlSortParams } from '~/issues_list/constants';
import {
defaultPageSizeParams,
DUE_DATE_VALUES,
largePageSizeParams,
RELATIVE_POSITION_ASC,
urlSortParams,
} from '~/issues_list/constants';
import {
convertToApiParams,
convertToSearchQuery,
convertToUrlParams,
getDueDateValue,
getFilterTokens,
getInitialPageParams,
getSortKey,
getSortOptions,
} from '~/issues_list/utils';
describe('getInitialPageParams', () => {
it.each(Object.keys(urlSortParams))(
'returns the correct page params for sort key %s',
(sortKey) => {
const expectedPageParams =
sortKey === RELATIVE_POSITION_ASC ? largePageSizeParams : defaultPageSizeParams;
expect(getInitialPageParams(sortKey)).toBe(expectedPageParams);
},
);
});
describe('getSortKey', () => {
it.each(Object.keys(urlSortParams))('returns %s given the correct inputs', (sortKey) => {
const sort = urlSortParams[sortKey];
......
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