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