Commit 986fc63d authored by Phil Hughes's avatar Phil Hughes

Merge branch '322755-fix-issues-reorder' into 'master'

Fix issue reordering in issues page refactor

See merge request gitlab-org/gitlab!67684
parents bc3b895e f2c36be8
......@@ -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,
};
},
......@@ -516,12 +518,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() {
......@@ -558,14 +560,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 });
......@@ -573,7 +577,7 @@ export default {
},
handleSort(sortKey) {
if (this.sortKey !== sortKey) {
this.pageParams = initialPageParams;
this.pageParams = getInitialPageParams(sortKey);
}
this.sortKey = sortKey;
},
......
......@@ -109,10 +109,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,
......@@ -36,6 +38,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