Commit b93b446b authored by Kyle Wiebers's avatar Kyle Wiebers

Revert "Merge branch '322755-use-graphql-in-issues-refactor' into 'master'"

This reverts merge request !62312
parent 2ee7ccf6
...@@ -50,9 +50,6 @@ export default { ...@@ -50,9 +50,6 @@ export default {
}, },
}, },
computed: { computed: {
issuableId() {
return getIdFromGraphQLId(this.issuable.id);
},
createdInPastDay() { createdInPastDay() {
const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date()); const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
return createdSecondsAgo < SECONDS_IN_DAY; return createdSecondsAgo < SECONDS_IN_DAY;
...@@ -64,7 +61,7 @@ export default { ...@@ -64,7 +61,7 @@ export default {
return this.issuable.gitlabWebUrl || this.issuable.webUrl; return this.issuable.gitlabWebUrl || this.issuable.webUrl;
}, },
authorId() { authorId() {
return getIdFromGraphQLId(this.author.id); return getIdFromGraphQLId(`${this.author.id}`);
}, },
isIssuableUrlExternal() { isIssuableUrlExternal() {
return isExternal(this.webUrl); return isExternal(this.webUrl);
...@@ -73,10 +70,10 @@ export default { ...@@ -73,10 +70,10 @@ export default {
return this.issuable.labels?.nodes || this.issuable.labels || []; return this.issuable.labels?.nodes || this.issuable.labels || [];
}, },
labelIdsString() { labelIdsString() {
return JSON.stringify(this.labels.map((label) => getIdFromGraphQLId(label.id))); return JSON.stringify(this.labels.map((label) => label.id));
}, },
assignees() { assignees() {
return this.issuable.assignees?.nodes || this.issuable.assignees || []; return this.issuable.assignees || [];
}, },
createdAt() { createdAt() {
return sprintf(__('created %{timeAgo}'), { return sprintf(__('created %{timeAgo}'), {
...@@ -84,9 +81,6 @@ export default { ...@@ -84,9 +81,6 @@ export default {
}); });
}, },
updatedAt() { updatedAt() {
if (!this.issuable.updatedAt) {
return '';
}
return sprintf(__('updated %{timeAgo}'), { return sprintf(__('updated %{timeAgo}'), {
timeAgo: getTimeago().format(this.issuable.updatedAt), timeAgo: getTimeago().format(this.issuable.updatedAt),
}); });
...@@ -163,7 +157,7 @@ export default { ...@@ -163,7 +157,7 @@ export default {
<template> <template>
<li <li
:id="`issuable_${issuableId}`" :id="`issuable_${issuable.id}`"
class="issue gl-px-5!" class="issue gl-px-5!"
:class="{ closed: issuable.closedAt, today: createdInPastDay }" :class="{ closed: issuable.closedAt, today: createdInPastDay }"
:data-labels="labelIdsString" :data-labels="labelIdsString"
...@@ -173,7 +167,7 @@ export default { ...@@ -173,7 +167,7 @@ export default {
<gl-form-checkbox <gl-form-checkbox
class="gl-mr-0" class="gl-mr-0"
:checked="checked" :checked="checked"
:data-id="issuableId" :data-id="issuable.id"
@input="$emit('checked-input', $event)" @input="$emit('checked-input', $event)"
> >
<span class="gl-sr-only">{{ issuable.title }}</span> <span class="gl-sr-only">{{ issuable.title }}</span>
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import { GlSkeletonLoading, GlPagination } from '@gitlab/ui'; import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
...@@ -212,7 +211,7 @@ export default { ...@@ -212,7 +211,7 @@ export default {
}, },
methods: { methods: {
issuableId(issuable) { issuableId(issuable) {
return getIdFromGraphQLId(issuable.id) || issuable.iid || uniqueId(); return issuable.id || issuable.iid || uniqueId();
}, },
issuableChecked(issuable) { issuableChecked(issuable) {
return this.checkedIssuables[this.issuableId(issuable)]?.checked; return this.checkedIssuables[this.issuableId(issuable)]?.checked;
......
...@@ -42,9 +42,6 @@ export default { ...@@ -42,9 +42,6 @@ export default {
} }
return __('Milestone'); return __('Milestone');
}, },
milestoneLink() {
return this.issue.milestone.webPath || this.issue.milestone.webUrl;
},
dueDate() { dueDate() {
return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true); return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true);
}, },
...@@ -52,7 +49,7 @@ export default { ...@@ -52,7 +49,7 @@ export default {
return isInPast(new Date(this.issue.dueDate)); return isInPast(new Date(this.issue.dueDate));
}, },
timeEstimate() { timeEstimate() {
return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate; return this.issue.timeStats?.humanTimeEstimate;
}, },
showHealthStatus() { showHealthStatus() {
return this.hasIssuableHealthStatusFeature && this.issue.healthStatus; return this.hasIssuableHealthStatusFeature && this.issue.healthStatus;
...@@ -88,7 +85,7 @@ export default { ...@@ -88,7 +85,7 @@ export default {
class="issuable-milestone gl-display-none gl-sm-display-inline-block! gl-mr-3" class="issuable-milestone gl-display-none gl-sm-display-inline-block! gl-mr-3"
data-testid="issuable-milestone" data-testid="issuable-milestone"
> >
<gl-link v-gl-tooltip :href="milestoneLink" :title="milestoneDate"> <gl-link v-gl-tooltip :href="issue.milestone.webUrl" :title="milestoneDate">
<gl-icon name="clock" /> <gl-icon name="clock" />
{{ issue.milestone.title }} {{ issue.milestone.title }}
</gl-link> </gl-link>
......
...@@ -9,21 +9,24 @@ import { ...@@ -9,21 +9,24 @@ import {
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql'; import { toNumber } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
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';
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants'; import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import { import {
API_PARAM,
apiSortParams,
CREATED_DESC, CREATED_DESC,
i18n, i18n,
MAX_LIST_SIZE, MAX_LIST_SIZE,
PAGE_SIZE, PAGE_SIZE,
PARAM_DUE_DATE, PARAM_DUE_DATE,
PARAM_PAGE,
PARAM_SORT, PARAM_SORT,
PARAM_STATE, PARAM_STATE,
RELATIVE_POSITION_ASC, RELATIVE_POSITION_DESC,
TOKEN_TYPE_ASSIGNEE, TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR, TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL, TOKEN_TYPE_CONFIDENTIAL,
...@@ -34,19 +37,19 @@ import { ...@@ -34,19 +37,19 @@ import {
TOKEN_TYPE_MILESTONE, TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_WEIGHT, TOKEN_TYPE_WEIGHT,
UPDATED_DESC, UPDATED_DESC,
URL_PARAM,
urlSortParams, urlSortParams,
} from '~/issues_list/constants'; } from '~/issues_list/constants';
import { import {
convertToApiParams, convertToParams,
convertToSearchQuery, convertToSearchQuery,
convertToUrlParams,
getDueDateValue, getDueDateValue,
getFilterTokens, getFilterTokens,
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 { getParameterByName } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
import { import {
DEFAULT_NONE_ANY, DEFAULT_NONE_ANY,
OPERATOR_IS_ONLY, OPERATOR_IS_ONLY,
...@@ -104,6 +107,9 @@ export default { ...@@ -104,6 +107,9 @@ export default {
emptyStateSvgPath: { emptyStateSvgPath: {
default: '', default: '',
}, },
endpoint: {
default: '',
},
exportCsvPath: { exportCsvPath: {
default: '', default: '',
}, },
...@@ -167,53 +173,15 @@ export default { ...@@ -167,53 +173,15 @@ export default {
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)), dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(), exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
filterTokens: getFilterTokens(window.location.search), filterTokens: getFilterTokens(window.location.search),
isLoading: false,
issues: [], issues: [],
page: 1, page: toNumber(getParameterByName(PARAM_PAGE)) || 1,
pageInfo: {},
pageParams: {
firstPageSize: PAGE_SIZE,
},
showBulkEditSidebar: false, showBulkEditSidebar: false,
sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey, sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
state: state || IssuableStates.Opened, state: state || IssuableStates.Opened,
totalIssues: 0, totalIssues: 0,
}; };
}, },
apollo: {
issues: {
query: getIssuesQuery,
variables() {
const filterParams = {
...this.apiFilterParams,
};
if (filterParams.epicId) {
filterParams.epicId = filterParams.epicId.split('::&').pop();
} else if (filterParams.not?.epicId) {
filterParams.not.epicId = filterParams.not.epicId.split('::&').pop();
}
return {
projectPath: this.projectPath,
search: this.searchQuery,
sort: this.sortKey,
state: this.state,
...this.pageParams,
...filterParams,
};
},
update: ({ project }) => project.issues.nodes,
result({ data }) {
this.pageInfo = data.project.issues.pageInfo;
this.totalIssues = data.project.issues.count;
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
},
error() {
createFlash({ message: this.$options.i18n.errorFetchingIssues });
},
debounce: 200,
},
},
computed: { computed: {
hasSearch() { hasSearch() {
return this.searchQuery || Object.keys(this.urlFilterParams).length; return this.searchQuery || Object.keys(this.urlFilterParams).length;
...@@ -222,22 +190,16 @@ export default { ...@@ -222,22 +190,16 @@ export default {
return this.showBulkEditSidebar || !this.issues.length; return this.showBulkEditSidebar || !this.issues.length;
}, },
isManualOrdering() { isManualOrdering() {
return this.sortKey === RELATIVE_POSITION_ASC; return this.sortKey === RELATIVE_POSITION_DESC;
}, },
isOpenTab() { isOpenTab() {
return this.state === IssuableStates.Opened; return this.state === IssuableStates.Opened;
}, },
nextPage() {
return Number(this.pageInfo.hasNextPage);
},
previousPage() {
return Number(this.pageInfo.hasPreviousPage);
},
apiFilterParams() { apiFilterParams() {
return convertToApiParams(this.filterTokens); return convertToParams(this.filterTokens, API_PARAM);
}, },
urlFilterParams() { urlFilterParams() {
return convertToUrlParams(this.filterTokens); return convertToParams(this.filterTokens, URL_PARAM);
}, },
searchQuery() { searchQuery() {
return convertToSearchQuery(this.filterTokens) || undefined; return convertToSearchQuery(this.filterTokens) || undefined;
...@@ -252,7 +214,6 @@ export default { ...@@ -252,7 +214,6 @@ export default {
dataType: 'user', dataType: 'user',
unique: true, unique: true,
defaultAuthors: [], defaultAuthors: [],
operators: OPERATOR_IS_ONLY,
fetchAuthors: this.fetchUsers, fetchAuthors: this.fetchUsers,
}, },
{ {
...@@ -279,7 +240,7 @@ export default { ...@@ -279,7 +240,7 @@ export default {
title: TOKEN_TITLE_LABEL, title: TOKEN_TITLE_LABEL,
icon: 'labels', icon: 'labels',
token: LabelToken, token: LabelToken,
defaultLabels: DEFAULT_NONE_ANY, defaultLabels: [],
fetchLabels: this.fetchLabels, fetchLabels: this.fetchLabels,
}, },
]; ];
...@@ -372,9 +333,10 @@ export default { ...@@ -372,9 +333,10 @@ export default {
return { return {
due_date: this.dueDateFilter, due_date: this.dueDateFilter,
page: this.page,
search: this.searchQuery, search: this.searchQuery,
sort: urlSortParams[this.sortKey],
state: this.state, state: this.state,
...urlSortParams[this.sortKey],
...filterParams, ...filterParams,
}; };
}, },
...@@ -384,6 +346,7 @@ export default { ...@@ -384,6 +346,7 @@ export default {
}, },
mounted() { mounted() {
eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar); eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
this.fetchIssues();
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar); eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
...@@ -423,19 +386,59 @@ export default { ...@@ -423,19 +386,59 @@ export default {
return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true); return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
}, },
fetchIterations(search) { fetchIterations(search) {
const number = Number(search); return axios.get(this.projectIterationsPath, { params: { search } });
return !search || Number.isNaN(number)
? axios.get(this.projectIterationsPath, { params: { search } })
: axios.get(this.projectIterationsPath, { params: { id: number } });
}, },
fetchUsers(search) { fetchUsers(search) {
return axios.get(this.autocompleteUsersPath, { params: { search } }); return axios.get(this.autocompleteUsersPath, { params: { search } });
}, },
fetchIssues() {
if (!this.hasProjectIssues) {
return undefined;
}
this.isLoading = true;
const filterParams = {
...this.apiFilterParams,
};
if (filterParams.epic_id) {
filterParams.epic_id = filterParams.epic_id.split('::&').pop();
} else if (filterParams['not[epic_id]']) {
filterParams['not[epic_id]'] = filterParams['not[epic_id]'].split('::&').pop();
}
return axios
.get(this.endpoint, {
params: {
due_date: this.dueDateFilter,
page: this.page,
per_page: PAGE_SIZE,
search: this.searchQuery,
state: this.state,
with_labels_details: true,
...apiSortParams[this.sortKey],
...filterParams,
},
})
.then(({ data, headers }) => {
this.page = Number(headers['x-page']);
this.totalIssues = Number(headers['x-total']);
this.issues = data.map((issue) => convertObjectPropsToCamelCase(issue, { deep: true }));
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
})
.catch(() => {
createFlash({ message: this.$options.i18n.errorFetchingIssues });
})
.finally(() => {
this.isLoading = false;
});
},
getExportCsvPathWithQuery() { getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`; return `${this.exportCsvPath}${window.location.search}`;
}, },
getStatus(issue) { getStatus(issue) {
if (issue.closedAt && issue.moved) { if (issue.closedAt && issue.movedToId) {
return this.$options.i18n.closedMoved; return this.$options.i18n.closedMoved;
} }
if (issue.closedAt) { if (issue.closedAt) {
...@@ -466,30 +469,18 @@ export default { ...@@ -466,30 +469,18 @@ export default {
}, },
handleClickTab(state) { handleClickTab(state) {
if (this.state !== state) { if (this.state !== state) {
this.pageParams = {
firstPageSize: PAGE_SIZE,
};
this.page = 1; this.page = 1;
} }
this.state = state; this.state = state;
this.fetchIssues();
}, },
handleFilter(filter) { handleFilter(filter) {
this.filterTokens = filter; this.filterTokens = filter;
this.fetchIssues();
}, },
handlePageChange(page) { handlePageChange(page) {
if (page > this.page) {
this.pageParams = {
afterCursor: this.pageInfo.endCursor,
firstPageSize: PAGE_SIZE,
};
} else {
this.pageParams = {
beforeCursor: this.pageInfo.startCursor,
lastPageSize: PAGE_SIZE,
};
}
this.page = page; this.page = page;
this.fetchIssues();
}, },
handleReorder({ newIndex, oldIndex }) { handleReorder({ newIndex, oldIndex }) {
const issueToMove = this.issues[oldIndex]; const issueToMove = this.issues[oldIndex];
...@@ -526,6 +517,7 @@ export default { ...@@ -526,6 +517,7 @@ export default {
}, },
handleSort(value) { handleSort(value) {
this.sortKey = value; this.sortKey = value;
this.fetchIssues();
}, },
toggleBulkEditSidebar(showBulkEditSidebar) { toggleBulkEditSidebar(showBulkEditSidebar) {
this.showBulkEditSidebar = showBulkEditSidebar; this.showBulkEditSidebar = showBulkEditSidebar;
...@@ -549,13 +541,14 @@ export default { ...@@ -549,13 +541,14 @@ export default {
:tabs="$options.IssuableListTabs" :tabs="$options.IssuableListTabs"
:current-tab="state" :current-tab="state"
:tab-counts="tabCounts" :tab-counts="tabCounts"
:issuables-loading="$apollo.loading" :issuables-loading="isLoading"
:is-manual-ordering="isManualOrdering" :is-manual-ordering="isManualOrdering"
:show-bulk-edit-sidebar="showBulkEditSidebar" :show-bulk-edit-sidebar="showBulkEditSidebar"
:show-pagination-controls="showPaginationControls" :show-pagination-controls="showPaginationControls"
:total-items="totalIssues"
:current-page="page" :current-page="page"
:previous-page="previousPage" :previous-page="page - 1"
:next-page="nextPage" :next-page="page + 1"
:url-params="urlParams" :url-params="urlParams"
@click-tab="handleClickTab" @click-tab="handleClickTab"
@filter="handleFilter" @filter="handleFilter"
...@@ -638,7 +631,7 @@ export default { ...@@ -638,7 +631,7 @@ export default {
</li> </li>
<blocking-issues-count <blocking-issues-count
class="gl-display-none gl-sm-display-block" class="gl-display-none gl-sm-display-block"
:blocking-issues-count="issuable.blockedByCount" :blocking-issues-count="issuable.blockingIssuesCount"
:is-list-item="true" :is-list-item="true"
/> />
</template> </template>
......
...@@ -101,6 +101,7 @@ export const i18n = { ...@@ -101,6 +101,7 @@ export const i18n = {
export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map'; export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map';
export const PARAM_DUE_DATE = 'due_date'; export const PARAM_DUE_DATE = 'due_date';
export const PARAM_PAGE = 'page';
export const PARAM_SORT = 'sort'; export const PARAM_SORT = 'sort';
export const PARAM_STATE = 'state'; export const PARAM_STATE = 'state';
...@@ -124,21 +125,21 @@ export const CREATED_ASC = 'CREATED_ASC'; ...@@ -124,21 +125,21 @@ export const CREATED_ASC = 'CREATED_ASC';
export const CREATED_DESC = 'CREATED_DESC'; export const CREATED_DESC = 'CREATED_DESC';
export const DUE_DATE_ASC = 'DUE_DATE_ASC'; export const DUE_DATE_ASC = 'DUE_DATE_ASC';
export const DUE_DATE_DESC = 'DUE_DATE_DESC'; export const DUE_DATE_DESC = 'DUE_DATE_DESC';
export const LABEL_PRIORITY_ASC = 'LABEL_PRIORITY_ASC';
export const LABEL_PRIORITY_DESC = 'LABEL_PRIORITY_DESC'; export const LABEL_PRIORITY_DESC = 'LABEL_PRIORITY_DESC';
export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC'; export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC';
export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC'; export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC';
export const POPULARITY_ASC = 'POPULARITY_ASC'; export const POPULARITY_ASC = 'POPULARITY_ASC';
export const POPULARITY_DESC = 'POPULARITY_DESC'; export const POPULARITY_DESC = 'POPULARITY_DESC';
export const PRIORITY_ASC = 'PRIORITY_ASC';
export const PRIORITY_DESC = 'PRIORITY_DESC'; export const PRIORITY_DESC = 'PRIORITY_DESC';
export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC'; export const RELATIVE_POSITION_DESC = 'RELATIVE_POSITION_DESC';
export const UPDATED_ASC = 'UPDATED_ASC'; export const UPDATED_ASC = 'UPDATED_ASC';
export const UPDATED_DESC = 'UPDATED_DESC'; export const UPDATED_DESC = 'UPDATED_DESC';
export const WEIGHT_ASC = 'WEIGHT_ASC'; export const WEIGHT_ASC = 'WEIGHT_ASC';
export const WEIGHT_DESC = 'WEIGHT_DESC'; export const WEIGHT_DESC = 'WEIGHT_DESC';
const PRIORITY_ASC_SORT = 'priority_asc'; const SORT_ASC = 'asc';
const SORT_DESC = 'desc';
const CREATED_DATE_SORT = 'created_date'; const CREATED_DATE_SORT = 'created_date';
const CREATED_ASC_SORT = 'created_asc'; const CREATED_ASC_SORT = 'created_asc';
const UPDATED_DESC_SORT = 'updated_desc'; const UPDATED_DESC_SORT = 'updated_desc';
...@@ -146,30 +147,129 @@ const UPDATED_ASC_SORT = 'updated_asc'; ...@@ -146,30 +147,129 @@ const UPDATED_ASC_SORT = 'updated_asc';
const MILESTONE_SORT = 'milestone'; const MILESTONE_SORT = 'milestone';
const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc'; const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc';
const DUE_DATE_DESC_SORT = 'due_date_desc'; const DUE_DATE_DESC_SORT = 'due_date_desc';
const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc';
const POPULARITY_ASC_SORT = 'popularity_asc'; const POPULARITY_ASC_SORT = 'popularity_asc';
const WEIGHT_DESC_SORT = 'weight_desc'; const WEIGHT_DESC_SORT = 'weight_desc';
const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc'; const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc';
const BLOCKING_ISSUES = 'blocking_issues';
export const apiSortParams = {
[PRIORITY_DESC]: {
order_by: PRIORITY,
sort: SORT_DESC,
},
[CREATED_ASC]: {
order_by: CREATED_AT,
sort: SORT_ASC,
},
[CREATED_DESC]: {
order_by: CREATED_AT,
sort: SORT_DESC,
},
[UPDATED_ASC]: {
order_by: UPDATED_AT,
sort: SORT_ASC,
},
[UPDATED_DESC]: {
order_by: UPDATED_AT,
sort: SORT_DESC,
},
[MILESTONE_DUE_ASC]: {
order_by: MILESTONE_DUE,
sort: SORT_ASC,
},
[MILESTONE_DUE_DESC]: {
order_by: MILESTONE_DUE,
sort: SORT_DESC,
},
[DUE_DATE_ASC]: {
order_by: DUE_DATE,
sort: SORT_ASC,
},
[DUE_DATE_DESC]: {
order_by: DUE_DATE,
sort: SORT_DESC,
},
[POPULARITY_ASC]: {
order_by: POPULARITY,
sort: SORT_ASC,
},
[POPULARITY_DESC]: {
order_by: POPULARITY,
sort: SORT_DESC,
},
[LABEL_PRIORITY_DESC]: {
order_by: LABEL_PRIORITY,
sort: SORT_DESC,
},
[RELATIVE_POSITION_DESC]: {
order_by: RELATIVE_POSITION,
per_page: 100,
sort: SORT_ASC,
},
[WEIGHT_ASC]: {
order_by: WEIGHT,
sort: SORT_ASC,
},
[WEIGHT_DESC]: {
order_by: WEIGHT,
sort: SORT_DESC,
},
[BLOCKING_ISSUES_DESC]: {
order_by: BLOCKING_ISSUES,
sort: SORT_DESC,
},
};
export const urlSortParams = { export const urlSortParams = {
[PRIORITY_ASC]: PRIORITY_ASC_SORT, [PRIORITY_DESC]: {
[PRIORITY_DESC]: PRIORITY, sort: PRIORITY,
[CREATED_ASC]: CREATED_ASC_SORT, },
[CREATED_DESC]: CREATED_DATE_SORT, [CREATED_ASC]: {
[UPDATED_ASC]: UPDATED_ASC_SORT, sort: CREATED_ASC_SORT,
[UPDATED_DESC]: UPDATED_DESC_SORT, },
[MILESTONE_DUE_ASC]: MILESTONE_SORT, [CREATED_DESC]: {
[MILESTONE_DUE_DESC]: MILESTONE_DUE_DESC_SORT, sort: CREATED_DATE_SORT,
[DUE_DATE_ASC]: DUE_DATE, },
[DUE_DATE_DESC]: DUE_DATE_DESC_SORT, [UPDATED_ASC]: {
[POPULARITY_ASC]: POPULARITY_ASC_SORT, sort: UPDATED_ASC_SORT,
[POPULARITY_DESC]: POPULARITY, },
[LABEL_PRIORITY_ASC]: LABEL_PRIORITY_ASC_SORT, [UPDATED_DESC]: {
[LABEL_PRIORITY_DESC]: LABEL_PRIORITY, sort: UPDATED_DESC_SORT,
[RELATIVE_POSITION_ASC]: RELATIVE_POSITION, },
[WEIGHT_ASC]: WEIGHT, [MILESTONE_DUE_ASC]: {
[WEIGHT_DESC]: WEIGHT_DESC_SORT, sort: MILESTONE_SORT,
[BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT, },
[MILESTONE_DUE_DESC]: {
sort: MILESTONE_DUE_DESC_SORT,
},
[DUE_DATE_ASC]: {
sort: DUE_DATE,
},
[DUE_DATE_DESC]: {
sort: DUE_DATE_DESC_SORT,
},
[POPULARITY_ASC]: {
sort: POPULARITY_ASC_SORT,
},
[POPULARITY_DESC]: {
sort: POPULARITY,
},
[LABEL_PRIORITY_DESC]: {
sort: LABEL_PRIORITY,
},
[RELATIVE_POSITION_DESC]: {
sort: RELATIVE_POSITION,
per_page: 100,
},
[WEIGHT_ASC]: {
sort: WEIGHT,
},
[WEIGHT_DESC]: {
sort: WEIGHT_DESC_SORT,
},
[BLOCKING_ISSUES_DESC]: {
sort: BLOCKING_ISSUES_DESC_SORT,
},
}; };
export const MAX_LIST_SIZE = 10; export const MAX_LIST_SIZE = 10;
...@@ -194,7 +294,12 @@ export const TOKEN_TYPE_WEIGHT = 'weight'; ...@@ -194,7 +294,12 @@ export const TOKEN_TYPE_WEIGHT = 'weight';
export const filters = { export const filters = {
[TOKEN_TYPE_AUTHOR]: { [TOKEN_TYPE_AUTHOR]: {
[API_PARAM]: { [API_PARAM]: {
[NORMAL_FILTER]: 'authorUsername', [OPERATOR_IS]: {
[NORMAL_FILTER]: 'author_username',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[author_username]',
},
}, },
[URL_PARAM]: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
...@@ -207,8 +312,13 @@ export const filters = { ...@@ -207,8 +312,13 @@ export const filters = {
}, },
[TOKEN_TYPE_ASSIGNEE]: { [TOKEN_TYPE_ASSIGNEE]: {
[API_PARAM]: { [API_PARAM]: {
[NORMAL_FILTER]: 'assigneeUsernames', [OPERATOR_IS]: {
[SPECIAL_FILTER]: 'assigneeId', [NORMAL_FILTER]: 'assignee_username',
[SPECIAL_FILTER]: 'assignee_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username]',
},
}, },
[URL_PARAM]: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
...@@ -223,7 +333,12 @@ export const filters = { ...@@ -223,7 +333,12 @@ export const filters = {
}, },
[TOKEN_TYPE_MILESTONE]: { [TOKEN_TYPE_MILESTONE]: {
[API_PARAM]: { [API_PARAM]: {
[NORMAL_FILTER]: 'milestoneTitle', [OPERATOR_IS]: {
[NORMAL_FILTER]: 'milestone',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[milestone]',
},
}, },
[URL_PARAM]: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
...@@ -236,13 +351,16 @@ export const filters = { ...@@ -236,13 +351,16 @@ export const filters = {
}, },
[TOKEN_TYPE_LABEL]: { [TOKEN_TYPE_LABEL]: {
[API_PARAM]: { [API_PARAM]: {
[NORMAL_FILTER]: 'labelName', [OPERATOR_IS]: {
[SPECIAL_FILTER]: 'labelName', [NORMAL_FILTER]: 'labels',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[labels]',
},
}, },
[URL_PARAM]: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'label_name[]', [NORMAL_FILTER]: 'label_name[]',
[SPECIAL_FILTER]: 'label_name[]',
}, },
[OPERATOR_IS_NOT]: { [OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]', [NORMAL_FILTER]: 'not[label_name][]',
...@@ -251,8 +369,10 @@ export const filters = { ...@@ -251,8 +369,10 @@ export const filters = {
}, },
[TOKEN_TYPE_MY_REACTION]: { [TOKEN_TYPE_MY_REACTION]: {
[API_PARAM]: { [API_PARAM]: {
[NORMAL_FILTER]: 'myReactionEmoji', [OPERATOR_IS]: {
[SPECIAL_FILTER]: 'myReactionEmoji', [NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji',
},
}, },
[URL_PARAM]: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
...@@ -263,7 +383,9 @@ export const filters = { ...@@ -263,7 +383,9 @@ export const filters = {
}, },
[TOKEN_TYPE_CONFIDENTIAL]: { [TOKEN_TYPE_CONFIDENTIAL]: {
[API_PARAM]: { [API_PARAM]: {
[NORMAL_FILTER]: 'confidential', [OPERATOR_IS]: {
[NORMAL_FILTER]: 'confidential',
},
}, },
[URL_PARAM]: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
...@@ -273,23 +395,33 @@ export const filters = { ...@@ -273,23 +395,33 @@ export const filters = {
}, },
[TOKEN_TYPE_ITERATION]: { [TOKEN_TYPE_ITERATION]: {
[API_PARAM]: { [API_PARAM]: {
[NORMAL_FILTER]: 'iterationId', [OPERATOR_IS]: {
[SPECIAL_FILTER]: 'iterationWildcardId', [NORMAL_FILTER]: 'iteration_title',
[SPECIAL_FILTER]: 'iteration_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[iteration_title]',
},
}, },
[URL_PARAM]: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'iteration_id', [NORMAL_FILTER]: 'iteration_title',
[SPECIAL_FILTER]: 'iteration_id', [SPECIAL_FILTER]: 'iteration_id',
}, },
[OPERATOR_IS_NOT]: { [OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[iteration_id]', [NORMAL_FILTER]: 'not[iteration_title]',
}, },
}, },
}, },
[TOKEN_TYPE_EPIC]: { [TOKEN_TYPE_EPIC]: {
[API_PARAM]: { [API_PARAM]: {
[NORMAL_FILTER]: 'epicId', [OPERATOR_IS]: {
[SPECIAL_FILTER]: 'epicId', [NORMAL_FILTER]: 'epic_id',
[SPECIAL_FILTER]: 'epic_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[epic_id]',
},
}, },
[URL_PARAM]: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
...@@ -303,8 +435,13 @@ export const filters = { ...@@ -303,8 +435,13 @@ export const filters = {
}, },
[TOKEN_TYPE_WEIGHT]: { [TOKEN_TYPE_WEIGHT]: {
[API_PARAM]: { [API_PARAM]: {
[NORMAL_FILTER]: 'weight', [OPERATOR_IS]: {
[SPECIAL_FILTER]: 'weight', [NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[weight]',
},
}, },
[URL_PARAM]: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
......
...@@ -73,13 +73,6 @@ export function mountIssuesListApp() { ...@@ -73,13 +73,6 @@ export function mountIssuesListApp() {
return false; return false;
} }
Vue.use(VueApollo);
const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
const apolloProvider = new VueApollo({
defaultClient,
});
const { const {
autocompleteAwardEmojisPath, autocompleteAwardEmojisPath,
autocompleteUsersPath, autocompleteUsersPath,
...@@ -90,6 +83,7 @@ export function mountIssuesListApp() { ...@@ -90,6 +83,7 @@ export function mountIssuesListApp() {
email, email,
emailsHelpPagePath, emailsHelpPagePath,
emptyStateSvgPath, emptyStateSvgPath,
endpoint,
exportCsvPath, exportCsvPath,
groupEpicsPath, groupEpicsPath,
hasBlockedIssuesFeature, hasBlockedIssuesFeature,
...@@ -121,13 +115,14 @@ export function mountIssuesListApp() { ...@@ -121,13 +115,14 @@ export function mountIssuesListApp() {
el, el,
// Currently does not use Vue Apollo, but need to provide {} for now until the // Currently does not use Vue Apollo, but need to provide {} for now until the
// issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153 // issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153
apolloProvider, apolloProvider: {},
provide: { provide: {
autocompleteAwardEmojisPath, autocompleteAwardEmojisPath,
autocompleteUsersPath, autocompleteUsersPath,
calendarPath, calendarPath,
canBulkUpdate: parseBoolean(canBulkUpdate), canBulkUpdate: parseBoolean(canBulkUpdate),
emptyStateSvgPath, emptyStateSvgPath,
endpoint,
groupEpicsPath, groupEpicsPath,
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature), hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature), hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "./issue_info.fragment.graphql"
query getProjectIssues(
$projectPath: ID!
$search: String
$sort: IssueSort
$state: IssuableState
$assigneeId: String
$authorUsername: String
$assigneeUsernames: [String!]
$milestoneTitle: [String]
$labelName: [String]
$not: NegatedIssueFilterInput
$beforeCursor: String
$afterCursor: String
$firstPageSize: Int
$lastPageSize: Int
) {
project(fullPath: $projectPath) {
issues(
search: $search
sort: $sort
state: $state
assigneeId: $assigneeId
authorUsername: $authorUsername
assigneeUsernames: $assigneeUsernames
milestoneTitle: $milestoneTitle
labelName: $labelName
not: $not
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
count
pageInfo {
...PageInfo
}
nodes {
...IssueInfo
}
}
}
}
fragment IssueInfo on Issue {
id
iid
closedAt
confidential
createdAt
downvotes
dueDate
humanTimeEstimate
moved
title
updatedAt
upvotes
userDiscussionsCount
webUrl
assignees {
nodes {
id
avatarUrl
name
username
webUrl
}
}
author {
id
avatarUrl
name
username
webUrl
}
labels {
nodes {
id
color
title
description
}
}
milestone {
id
dueDate
startDate
webPath
title
}
taskCompletionStatus {
completedCount
count
}
}
import { import {
API_PARAM,
BLOCKING_ISSUES_DESC, BLOCKING_ISSUES_DESC,
CREATED_ASC, CREATED_ASC,
CREATED_DESC, CREATED_DESC,
...@@ -7,36 +6,29 @@ import { ...@@ -7,36 +6,29 @@ import {
DUE_DATE_DESC, DUE_DATE_DESC,
DUE_DATE_VALUES, DUE_DATE_VALUES,
filters, filters,
LABEL_PRIORITY_ASC,
LABEL_PRIORITY_DESC, LABEL_PRIORITY_DESC,
MILESTONE_DUE_ASC, MILESTONE_DUE_ASC,
MILESTONE_DUE_DESC, MILESTONE_DUE_DESC,
NORMAL_FILTER, NORMAL_FILTER,
POPULARITY_ASC, POPULARITY_ASC,
POPULARITY_DESC, POPULARITY_DESC,
PRIORITY_ASC,
PRIORITY_DESC, PRIORITY_DESC,
RELATIVE_POSITION_ASC, RELATIVE_POSITION_DESC,
SPECIAL_FILTER, SPECIAL_FILTER,
SPECIAL_FILTER_VALUES, SPECIAL_FILTER_VALUES,
TOKEN_TYPE_ASSIGNEE, TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_ITERATION,
UPDATED_ASC, UPDATED_ASC,
UPDATED_DESC, UPDATED_DESC,
URL_PARAM,
urlSortParams, urlSortParams,
WEIGHT_ASC, WEIGHT_ASC,
WEIGHT_DESC, WEIGHT_DESC,
} from '~/issues_list/constants'; } from '~/issues_list/constants';
import { isPositiveInteger } from '~/lib/utils/number_utils'; import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
FILTERED_SEARCH_TERM,
OPERATOR_IS_NOT,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getSortKey = (sort) => export const getSortKey = (sort) =>
Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort); Object.keys(urlSortParams).find((key) => urlSortParams[key].sort === sort);
export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined); export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined);
...@@ -46,7 +38,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) ...@@ -46,7 +38,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 1, id: 1,
title: __('Priority'), title: __('Priority'),
sortDirection: { sortDirection: {
ascending: PRIORITY_ASC, ascending: PRIORITY_DESC,
descending: PRIORITY_DESC, descending: PRIORITY_DESC,
}, },
}, },
...@@ -94,7 +86,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) ...@@ -94,7 +86,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 7, id: 7,
title: __('Label priority'), title: __('Label priority'),
sortDirection: { sortDirection: {
ascending: LABEL_PRIORITY_ASC, ascending: LABEL_PRIORITY_DESC,
descending: LABEL_PRIORITY_DESC, descending: LABEL_PRIORITY_DESC,
}, },
}, },
...@@ -102,8 +94,8 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) ...@@ -102,8 +94,8 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 8, id: 8,
title: __('Manual'), title: __('Manual'),
sortDirection: { sortDirection: {
ascending: RELATIVE_POSITION_ASC, ascending: RELATIVE_POSITION_DESC,
descending: RELATIVE_POSITION_ASC, descending: RELATIVE_POSITION_DESC,
}, },
}, },
]; ];
...@@ -186,36 +178,12 @@ const getFilterType = (data, tokenType = '') => ...@@ -186,36 +178,12 @@ const getFilterType = (data, tokenType = '') =>
? SPECIAL_FILTER ? SPECIAL_FILTER
: NORMAL_FILTER; : NORMAL_FILTER;
const isIterationSpecialValue = (tokenType, value) => export const convertToParams = (filterTokens, paramType) =>
tokenType === TOKEN_TYPE_ITERATION && SPECIAL_FILTER_VALUES.includes(value);
export const convertToApiParams = (filterTokens) => {
const params = {};
const not = {};
filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.forEach((token) => {
const filterType = getFilterType(token.value.data, token.type);
const field = filters[token.type][API_PARAM][filterType];
const obj = token.value.operator === OPERATOR_IS_NOT ? not : params;
const data = isIterationSpecialValue(token.type, token.value.data)
? token.value.data.toUpperCase()
: token.value.data;
Object.assign(obj, {
[field]: obj[field] ? [obj[field], data].flat() : data,
});
});
return Object.keys(not).length ? Object.assign(params, { not }) : params;
};
export const convertToUrlParams = (filterTokens) =>
filterTokens filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM) .filter((token) => token.type !== FILTERED_SEARCH_TERM)
.reduce((acc, token) => { .reduce((acc, token) => {
const filterType = getFilterType(token.value.data, token.type); const filterType = getFilterType(token.value.data, token.type);
const param = filters[token.type][URL_PARAM][token.value.operator]?.[filterType]; const param = filters[token.type][paramType][token.value.operator]?.[filterType];
return Object.assign(acc, { return Object.assign(acc, {
[param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data, [param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data,
}); });
......
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
return this.value.data; return this.value.data;
}, },
activeIteration() { activeIteration() {
return this.iterations.find((iteration) => iteration.id === Number(this.currentValue)); return this.iterations.find((iteration) => iteration.title === this.currentValue);
}, },
}, },
watch: { watch: {
...@@ -99,8 +99,8 @@ export default { ...@@ -99,8 +99,8 @@ export default {
<template v-else> <template v-else>
<gl-filtered-search-suggestion <gl-filtered-search-suggestion
v-for="iteration in iterations" v-for="iteration in iterations"
:key="iteration.id" :key="iteration.title"
:value="String(iteration.id)" :value="iteration.title"
> >
{{ iteration.title }} {{ iteration.title }}
</gl-filtered-search-suggestion> </gl-filtered-search-suggestion>
......
...@@ -190,6 +190,7 @@ module IssuesHelper ...@@ -190,6 +190,7 @@ module IssuesHelper
email: current_user&.notification_email, email: current_user&.notification_email,
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'), emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
empty_state_svg_path: image_path('illustrations/issues.svg'), empty_state_svg_path: image_path('illustrations/issues.svg'),
endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
export_csv_path: export_csv_project_issues_path(project), export_csv_path: export_csv_project_issues_path(project),
has_project_issues: project_issues(project).exists?.to_s, has_project_issues: project_issues(project).exists?.to_s,
import_csv_issues_path: import_csv_namespace_project_issues_path, import_csv_issues_path: import_csv_namespace_project_issues_path,
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "~/issues_list/queries/issue_info.fragment.graphql"
query getProjectIssues(
$projectPath: ID!
$search: String
$sort: IssueSort
$state: IssuableState
$assigneeId: String
$authorUsername: String
$assigneeUsernames: [String!]
$milestoneTitle: [String]
$labelName: [String]
$iterationId: [ID]
$iterationWildcardId: IterationWildcardId
$epicId: String
$weight: String
$not: NegatedIssueFilterInput
$beforeCursor: String
$afterCursor: String
$firstPageSize: Int
$lastPageSize: Int
) {
project(fullPath: $projectPath) {
issues(
search: $search
sort: $sort
state: $state
assigneeId: $assigneeId
authorUsername: $authorUsername
assigneeUsernames: $assigneeUsernames
milestoneTitle: $milestoneTitle
labelName: $labelName
iterationId: $iterationId
iterationWildcardId: $iterationWildcardId
epicId: $epicId
weight: $weight
not: $not
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
count
pageInfo {
...PageInfo
}
nodes {
...IssueInfo
blockedByCount
healthStatus
weight
}
}
}
}
...@@ -13,10 +13,12 @@ describe('IssuesListApp component', () => { ...@@ -13,10 +13,12 @@ describe('IssuesListApp component', () => {
dueDate: '2020-12-17', dueDate: '2020-12-17',
startDate: '2020-12-10', startDate: '2020-12-10',
title: 'My milestone', title: 'My milestone',
webPath: '/milestone/webPath', webUrl: '/milestone/webUrl',
}, },
dueDate: '2020-12-12', dueDate: '2020-12-12',
humanTimeEstimate: '1w', timeStats: {
humanTimeEstimate: '1w',
},
}; };
const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]'); const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
...@@ -54,7 +56,7 @@ describe('IssuesListApp component', () => { ...@@ -54,7 +56,7 @@ describe('IssuesListApp component', () => {
expect(milestone.text()).toBe(issue.milestone.title); expect(milestone.text()).toBe(issue.milestone.title);
expect(milestone.find(GlIcon).props('name')).toBe('clock'); expect(milestone.find(GlIcon).props('name')).toBe('clock');
expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath); expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webUrl);
}); });
describe.each` describe.each`
...@@ -100,7 +102,7 @@ describe('IssuesListApp component', () => { ...@@ -100,7 +102,7 @@ describe('IssuesListApp component', () => {
const timeEstimate = wrapper.find('[data-testid="time-estimate"]'); const timeEstimate = wrapper.find('[data-testid="time-estimate"]');
expect(timeEstimate.text()).toBe(issue.humanTimeEstimate); expect(timeEstimate.text()).toBe(issue.timeStats.humanTimeEstimate);
expect(timeEstimate.attributes('title')).toBe('Estimate'); expect(timeEstimate.attributes('title')).toBe('Estimate');
expect(timeEstimate.find(GlIcon).props('name')).toBe('timer'); expect(timeEstimate.find(GlIcon).props('name')).toBe('timer');
}); });
......
import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui'; import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { import { apiParams, filteredTokens, locationSearch, urlParams } from 'jest/issues_list/mock_data';
filteredTokens,
getIssuesQueryResponse,
locationSearch,
urlParams,
} from 'jest/issues_list/mock_data';
import createFlash from '~/flash'; import createFlash from '~/flash';
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';
...@@ -20,9 +11,13 @@ import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; ...@@ -20,9 +11,13 @@ import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants'; import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import IssuesListApp from '~/issues_list/components/issues_list_app.vue'; import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
import { import {
apiSortParams,
CREATED_DESC, CREATED_DESC,
DUE_DATE_OVERDUE, DUE_DATE_OVERDUE,
PAGE_SIZE,
PAGE_SIZE_MANUAL,
PARAM_DUE_DATE, PARAM_DUE_DATE,
RELATIVE_POSITION_DESC,
TOKEN_TYPE_ASSIGNEE, TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR, TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL, TOKEN_TYPE_CONFIDENTIAL,
...@@ -45,14 +40,12 @@ describe('IssuesListApp component', () => { ...@@ -45,14 +40,12 @@ describe('IssuesListApp component', () => {
let axiosMock; let axiosMock;
let wrapper; let wrapper;
const localVue = createLocalVue();
localVue.use(VueApollo);
const defaultProvide = { const defaultProvide = {
autocompleteUsersPath: 'autocomplete/users/path', autocompleteUsersPath: 'autocomplete/users/path',
calendarPath: 'calendar/path', calendarPath: 'calendar/path',
canBulkUpdate: false, canBulkUpdate: false,
emptyStateSvgPath: 'empty-state.svg', emptyStateSvgPath: 'empty-state.svg',
endpoint: 'api/endpoint',
exportCsvPath: 'export/csv/path', exportCsvPath: 'export/csv/path',
hasBlockedIssuesFeature: true, hasBlockedIssuesFeature: true,
hasIssueWeightsFeature: true, hasIssueWeightsFeature: true,
...@@ -68,6 +61,22 @@ describe('IssuesListApp component', () => { ...@@ -68,6 +61,22 @@ describe('IssuesListApp component', () => {
signInPath: 'sign/in/path', signInPath: 'sign/in/path',
}; };
const state = 'opened';
const xPage = 1;
const xTotal = 25;
const tabCounts = {
opened: xTotal,
closed: undefined,
all: undefined,
};
const fetchIssuesResponse = {
data: [],
headers: {
'x-page': xPage,
'x-total': xTotal,
},
};
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons); const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail); const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
const findGlButton = () => wrapper.findComponent(GlButton); const findGlButton = () => wrapper.findComponent(GlButton);
...@@ -77,26 +86,19 @@ describe('IssuesListApp component', () => { ...@@ -77,26 +86,19 @@ describe('IssuesListApp component', () => {
const findGlLink = () => wrapper.findComponent(GlLink); const findGlLink = () => wrapper.findComponent(GlLink);
const findIssuableList = () => wrapper.findComponent(IssuableList); const findIssuableList = () => wrapper.findComponent(IssuableList);
const mountComponent = ({ const mountComponent = ({ provide = {}, mountFn = shallowMount } = {}) =>
provide = {}, mountFn(IssuesListApp, {
response = getIssuesQueryResponse,
mountFn = shallowMount,
} = {}) => {
const requestHandlers = [[getIssuesQuery, jest.fn().mockResolvedValue(response)]];
const apolloProvider = createMockApollo(requestHandlers);
return mountFn(IssuesListApp, {
localVue,
apolloProvider,
provide: { provide: {
...defaultProvide, ...defaultProvide,
...provide, ...provide,
}, },
}); });
};
beforeEach(() => { beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios); axiosMock = new AxiosMockAdapter(axios);
axiosMock
.onGet(defaultProvide.endpoint)
.reply(200, fetchIssuesResponse.data, fetchIssuesResponse.headers);
}); });
afterEach(() => { afterEach(() => {
...@@ -106,37 +108,28 @@ describe('IssuesListApp component', () => { ...@@ -106,37 +108,28 @@ describe('IssuesListApp component', () => {
}); });
describe('IssuableList', () => { describe('IssuableList', () => {
beforeEach(() => { beforeEach(async () => {
wrapper = mountComponent(); wrapper = mountComponent();
jest.runOnlyPendingTimers(); await waitForPromises();
}); });
it('renders', () => { it('renders', () => {
expect(findIssuableList().props()).toMatchObject({ expect(findIssuableList().props()).toMatchObject({
namespace: defaultProvide.projectPath, namespace: defaultProvide.projectPath,
recentSearchesStorageKey: 'issues', recentSearchesStorageKey: 'issues',
searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder, searchInputPlaceholder: 'Search or filter results…',
sortOptions: getSortOptions(true, true), sortOptions: getSortOptions(true, true),
initialSortBy: CREATED_DESC, initialSortBy: CREATED_DESC,
issuables: getIssuesQueryResponse.data.project.issues.nodes,
tabs: IssuableListTabs, tabs: IssuableListTabs,
currentTab: IssuableStates.Opened, currentTab: IssuableStates.Opened,
tabCounts: { tabCounts,
opened: 1, showPaginationControls: false,
closed: undefined, issuables: [],
all: undefined, totalItems: xTotal,
}, currentPage: xPage,
issuablesLoading: false, previousPage: xPage - 1,
isManualOrdering: false, nextPage: xPage + 1,
showBulkEditSidebar: false, urlParams: { page: xPage, state },
showPaginationControls: true,
currentPage: 1,
previousPage: Number(getIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage),
nextPage: Number(getIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage),
urlParams: {
sort: urlSortParams[CREATED_DESC],
state: IssuableStates.Opened,
},
}); });
}); });
}); });
...@@ -164,9 +157,9 @@ describe('IssuesListApp component', () => { ...@@ -164,9 +157,9 @@ describe('IssuesListApp component', () => {
describe('csv import/export component', () => { describe('csv import/export component', () => {
describe('when user is signed in', () => { describe('when user is signed in', () => {
const search = '?search=refactor&sort=created_date&state=opened'; it('renders', async () => {
const search = '?page=1&search=refactor&state=opened&sort=created_date';
beforeEach(() => {
global.jsdom.reconfigure({ url: `${TEST_HOST}${search}` }); global.jsdom.reconfigure({ url: `${TEST_HOST}${search}` });
wrapper = mountComponent({ wrapper = mountComponent({
...@@ -174,13 +167,11 @@ describe('IssuesListApp component', () => { ...@@ -174,13 +167,11 @@ describe('IssuesListApp component', () => {
mountFn: mount, mountFn: mount,
}); });
jest.runOnlyPendingTimers(); await waitForPromises();
});
it('renders', () => {
expect(findCsvImportExportButtons().props()).toMatchObject({ expect(findCsvImportExportButtons().props()).toMatchObject({
exportCsvPath: `${defaultProvide.exportCsvPath}${search}`, exportCsvPath: `${defaultProvide.exportCsvPath}${search}`,
issuableCount: 1, issuableCount: xTotal,
}); });
}); });
}); });
...@@ -198,7 +189,7 @@ describe('IssuesListApp component', () => { ...@@ -198,7 +189,7 @@ describe('IssuesListApp component', () => {
it('renders when user has permissions', () => { it('renders when user has permissions', () => {
wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount }); wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount });
expect(findGlButtonAt(2).text()).toBe(IssuesListApp.i18n.editIssues); expect(findGlButtonAt(2).text()).toBe('Edit issues');
}); });
it('does not render when user does not have permissions', () => { it('does not render when user does not have permissions', () => {
...@@ -224,7 +215,7 @@ describe('IssuesListApp component', () => { ...@@ -224,7 +215,7 @@ describe('IssuesListApp component', () => {
it('renders when user has permissions', () => { it('renders when user has permissions', () => {
wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount }); wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount });
expect(findGlButtonAt(2).text()).toBe(IssuesListApp.i18n.newIssueLabel); expect(findGlButtonAt(2).text()).toBe('New issue');
expect(findGlButtonAt(2).attributes('href')).toBe(defaultProvide.newIssuePath); expect(findGlButtonAt(2).attributes('href')).toBe(defaultProvide.newIssuePath);
}); });
...@@ -247,6 +238,18 @@ describe('IssuesListApp component', () => { ...@@ -247,6 +238,18 @@ describe('IssuesListApp component', () => {
}); });
}); });
describe('page', () => {
it('is set from the url params', () => {
const page = 5;
global.jsdom.reconfigure({ url: setUrlParams({ page }, TEST_HOST) });
wrapper = mountComponent();
expect(findIssuableList().props('currentPage')).toBe(page);
});
});
describe('search', () => { describe('search', () => {
it('is set from the url params', () => { it('is set from the url params', () => {
global.jsdom.reconfigure({ url: `${TEST_HOST}${locationSearch}` }); global.jsdom.reconfigure({ url: `${TEST_HOST}${locationSearch}` });
...@@ -259,15 +262,13 @@ describe('IssuesListApp component', () => { ...@@ -259,15 +262,13 @@ describe('IssuesListApp component', () => {
describe('sort', () => { describe('sort', () => {
it.each(Object.keys(urlSortParams))('is set as %s from the url params', (sortKey) => { it.each(Object.keys(urlSortParams))('is set as %s from the url params', (sortKey) => {
global.jsdom.reconfigure({ global.jsdom.reconfigure({ url: setUrlParams(urlSortParams[sortKey], TEST_HOST) });
url: setUrlParams({ sort: urlSortParams[sortKey] }, TEST_HOST),
});
wrapper = mountComponent(); wrapper = mountComponent();
expect(findIssuableList().props()).toMatchObject({ expect(findIssuableList().props()).toMatchObject({
initialSortBy: sortKey, initialSortBy: sortKey,
urlParams: { sort: urlSortParams[sortKey] }, urlParams: urlSortParams[sortKey],
}); });
}); });
}); });
...@@ -325,10 +326,12 @@ describe('IssuesListApp component', () => { ...@@ -325,10 +326,12 @@ describe('IssuesListApp component', () => {
describe('empty states', () => { describe('empty states', () => {
describe('when there are issues', () => { describe('when there are issues', () => {
describe('when search returns no results', () => { describe('when search returns no results', () => {
beforeEach(() => { beforeEach(async () => {
global.jsdom.reconfigure({ url: `${TEST_HOST}?search=no+results` }); global.jsdom.reconfigure({ url: `${TEST_HOST}?search=no+results` });
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount }); wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
await waitForPromises();
}); });
it('shows empty state', () => { it('shows empty state', () => {
...@@ -341,8 +344,10 @@ describe('IssuesListApp component', () => { ...@@ -341,8 +344,10 @@ describe('IssuesListApp component', () => {
}); });
describe('when "Open" tab has no issues', () => { describe('when "Open" tab has no issues', () => {
beforeEach(() => { beforeEach(async () => {
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount }); wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
await waitForPromises();
}); });
it('shows empty state', () => { it('shows empty state', () => {
...@@ -355,12 +360,14 @@ describe('IssuesListApp component', () => { ...@@ -355,12 +360,14 @@ describe('IssuesListApp component', () => {
}); });
describe('when "Closed" tab has no issues', () => { describe('when "Closed" tab has no issues', () => {
beforeEach(() => { beforeEach(async () => {
global.jsdom.reconfigure({ global.jsdom.reconfigure({
url: setUrlParams({ state: IssuableStates.Closed }, TEST_HOST), url: setUrlParams({ state: IssuableStates.Closed }, TEST_HOST),
}); });
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount }); wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
await waitForPromises();
}); });
it('shows empty state', () => { it('shows empty state', () => {
...@@ -529,67 +536,74 @@ describe('IssuesListApp component', () => { ...@@ -529,67 +536,74 @@ describe('IssuesListApp component', () => {
describe('events', () => { describe('events', () => {
describe('when "click-tab" event is emitted by IssuableList', () => { describe('when "click-tab" event is emitted by IssuableList', () => {
beforeEach(() => { beforeEach(() => {
axiosMock.onGet(defaultProvide.endpoint).reply(200, fetchIssuesResponse.data, {
'x-page': 2,
'x-total': xTotal,
});
wrapper = mountComponent(); wrapper = mountComponent();
findIssuableList().vm.$emit('click-tab', IssuableStates.Closed); findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
}); });
it('updates to the new tab', () => { it('makes API call to filter the list by the new state and resets the page to 1', () => {
expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed); expect(axiosMock.history.get[1].params).toMatchObject({
page: 1,
state: IssuableStates.Closed,
});
}); });
}); });
describe('when "page-change" event is emitted by IssuableList', () => { describe('when "page-change" event is emitted by IssuableList', () => {
beforeEach(() => { const data = [{ id: 10, title: 'title', state }];
const page = 2;
const totalItems = 21;
beforeEach(async () => {
axiosMock.onGet(defaultProvide.endpoint).reply(200, data, {
'x-page': page,
'x-total': totalItems,
});
wrapper = mountComponent(); wrapper = mountComponent();
findIssuableList().vm.$emit('page-change', 2); findIssuableList().vm.$emit('page-change', page);
await waitForPromises();
}); });
it('updates to the new page', () => { it('fetches issues with expected params', () => {
expect(findIssuableList().props('currentPage')).toBe(2); expect(axiosMock.history.get[1].params).toMatchObject({
page,
per_page: PAGE_SIZE,
state,
with_labels_details: true,
});
});
it('updates IssuableList with response data', () => {
expect(findIssuableList().props()).toMatchObject({
issuables: data,
totalItems,
currentPage: page,
previousPage: page - 1,
nextPage: page + 1,
urlParams: { page, state },
});
}); });
}); });
describe('when "reorder" event is emitted by IssuableList', () => { describe('when "reorder" event is emitted by IssuableList', () => {
const issueOne = { const issueOne = { id: 1, iid: 101, title: 'Issue one' };
...getIssuesQueryResponse.data.project.issues.nodes[0], const issueTwo = { id: 2, iid: 102, title: 'Issue two' };
id: 1, const issueThree = { id: 3, iid: 103, title: 'Issue three' };
iid: 101, const issueFour = { id: 4, iid: 104, title: 'Issue four' };
title: 'Issue one', const issues = [issueOne, issueTwo, issueThree, issueFour];
};
const issueTwo = { beforeEach(async () => {
...getIssuesQueryResponse.data.project.issues.nodes[0], axiosMock.onGet(defaultProvide.endpoint).reply(200, issues, fetchIssuesResponse.headers);
id: 2, wrapper = mountComponent();
iid: 102, await waitForPromises();
title: 'Issue two',
};
const issueThree = {
...getIssuesQueryResponse.data.project.issues.nodes[0],
id: 3,
iid: 103,
title: 'Issue three',
};
const issueFour = {
...getIssuesQueryResponse.data.project.issues.nodes[0],
id: 4,
iid: 104,
title: 'Issue four',
};
const response = {
data: {
project: {
issues: {
...getIssuesQueryResponse.data.project.issues,
nodes: [issueOne, issueTwo, issueThree, issueFour],
},
},
},
};
beforeEach(() => {
wrapper = mountComponent({ response });
jest.runOnlyPendingTimers();
}); });
describe('when successful', () => { describe('when successful', () => {
...@@ -630,18 +644,21 @@ describe('IssuesListApp component', () => { ...@@ -630,18 +644,21 @@ describe('IssuesListApp component', () => {
}); });
describe('when "sort" event is emitted by IssuableList', () => { describe('when "sort" event is emitted by IssuableList', () => {
it.each(Object.keys(urlSortParams))( it.each(Object.keys(apiSortParams))(
'updates to the new sort when payload is `%s`', 'fetches issues with correct params with payload `%s`',
async (sortKey) => { async (sortKey) => {
wrapper = mountComponent(); wrapper = mountComponent();
findIssuableList().vm.$emit('sort', sortKey); findIssuableList().vm.$emit('sort', sortKey);
jest.runOnlyPendingTimers(); await waitForPromises();
await nextTick();
expect(findIssuableList().props('urlParams')).toMatchObject({ expect(axiosMock.history.get[1].params).toEqual({
sort: urlSortParams[sortKey], page: xPage,
per_page: sortKey === RELATIVE_POSITION_DESC ? PAGE_SIZE_MANUAL : PAGE_SIZE,
state,
with_labels_details: true,
...apiSortParams[sortKey],
}); });
}, },
); );
...@@ -651,11 +668,13 @@ describe('IssuesListApp component', () => { ...@@ -651,11 +668,13 @@ describe('IssuesListApp component', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mountComponent(); wrapper = mountComponent();
jest.spyOn(eventHub, '$emit'); jest.spyOn(eventHub, '$emit');
});
it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', async () => {
findIssuableList().vm.$emit('update-legacy-bulk-edit'); findIssuableList().vm.$emit('update-legacy-bulk-edit');
});
it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', () => { await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit'); expect(eventHub.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit');
}); });
}); });
...@@ -667,6 +686,10 @@ describe('IssuesListApp component', () => { ...@@ -667,6 +686,10 @@ describe('IssuesListApp component', () => {
findIssuableList().vm.$emit('filter', filteredTokens); findIssuableList().vm.$emit('filter', filteredTokens);
}); });
it('makes an API call to search for issues with the search term', () => {
expect(axiosMock.history.get[1].params).toMatchObject(apiParams);
});
it('updates IssuableList with url params', () => { it('updates IssuableList with url params', () => {
expect(findIssuableList().props('urlParams')).toMatchObject(urlParams); expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
}); });
......
...@@ -3,76 +3,6 @@ import { ...@@ -3,76 +3,6 @@ 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 getIssuesQueryResponse = {
data: {
project: {
issues: {
count: 1,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'startcursor',
endCursor: 'endcursor',
},
nodes: [
{
id: 'gid://gitlab/Issue/123456',
iid: '789',
blockedByCount: 1,
closedAt: null,
confidential: false,
createdAt: '2021-05-22T04:08:01Z',
downvotes: 2,
dueDate: '2021-05-29',
healthStatus: null,
humanTimeEstimate: null,
moved: false,
title: 'Issue title',
updatedAt: '2021-05-22T04:08:01Z',
upvotes: 3,
userDiscussionsCount: 4,
webUrl: 'project/-/issues/789',
weight: 5,
assignees: {
nodes: [
{
id: 'gid://gitlab/User/234',
avatarUrl: 'avatar/url',
name: 'Marge Simpson',
username: 'msimpson',
webUrl: 'url/msimpson',
},
],
},
author: {
id: 'gid://gitlab/User/456',
avatarUrl: 'avatar/url',
name: 'Homer Simpson',
username: 'hsimpson',
webUrl: 'url/hsimpson',
},
labels: {
nodes: [
{
id: 'gid://gitlab/ProjectLabel/456',
color: '#333',
title: 'Label title',
description: 'Label description',
},
],
},
milestone: null,
taskCompletionStatus: {
completedCount: 1,
count: 2,
},
},
],
},
},
},
};
export const locationSearch = [ export const locationSearch = [
'?search=find+issues', '?search=find+issues',
'author_username=homer', 'author_username=homer',
...@@ -89,8 +19,8 @@ export const locationSearch = [ ...@@ -89,8 +19,8 @@ export const locationSearch = [
'not[label_name][]=drama', 'not[label_name][]=drama',
'my_reaction_emoji=thumbsup', 'my_reaction_emoji=thumbsup',
'confidential=no', 'confidential=no',
'iteration_id=4', 'iteration_title=season:+%234',
'not[iteration_id]=20', 'not[iteration_title]=season:+%2320',
'epic_id=gitlab-org%3A%3A%2612', 'epic_id=gitlab-org%3A%3A%2612',
'not[epic_id]=gitlab-org%3A%3A%2634', 'not[epic_id]=gitlab-org%3A%3A%2634',
'weight=1', 'weight=1',
...@@ -121,8 +51,8 @@ export const filteredTokens = [ ...@@ -121,8 +51,8 @@ export const filteredTokens = [
{ type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } }, { type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
{ type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } }, { type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
{ type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } }, { type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: '4', operator: OPERATOR_IS } }, { type: 'iteration', value: { data: 'season: #4', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } }, { type: 'iteration', value: { data: 'season: #20', operator: OPERATOR_IS_NOT } },
{ type: 'epic_id', value: { data: 'gitlab-org::&12', operator: OPERATOR_IS } }, { type: 'epic_id', value: { data: 'gitlab-org::&12', operator: OPERATOR_IS } },
{ type: 'epic_id', value: { data: 'gitlab-org::&34', operator: OPERATOR_IS_NOT } }, { type: 'epic_id', value: { data: 'gitlab-org::&34', operator: OPERATOR_IS_NOT } },
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } }, { type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
...@@ -141,32 +71,30 @@ export const filteredTokensWithSpecialValues = [ ...@@ -141,32 +71,30 @@ export const filteredTokensWithSpecialValues = [
]; ];
export const apiParams = { export const apiParams = {
authorUsername: 'homer', author_username: 'homer',
assigneeUsernames: ['bart', 'lisa'], 'not[author_username]': 'marge',
milestoneTitle: 'season 4', assignee_username: ['bart', 'lisa'],
labelName: ['cartoon', 'tv'], 'not[assignee_username]': ['patty', 'selma'],
myReactionEmoji: 'thumbsup', milestone: 'season 4',
'not[milestone]': 'season 20',
labels: ['cartoon', 'tv'],
'not[labels]': ['live action', 'drama'],
my_reaction_emoji: 'thumbsup',
confidential: 'no', confidential: 'no',
iterationId: '4', iteration_title: 'season: #4',
epicId: 'gitlab-org::&12', 'not[iteration_title]': 'season: #20',
epic_id: '12',
'not[epic_id]': 'gitlab-org::&34',
weight: '1', weight: '1',
not: { 'not[weight]': '3',
authorUsername: 'marge',
assigneeUsernames: ['patty', 'selma'],
milestoneTitle: 'season 20',
labelName: ['live action', 'drama'],
iterationId: '20',
epicId: 'gitlab-org::&34',
weight: '3',
},
}; };
export const apiParamsWithSpecialValues = { export const apiParamsWithSpecialValues = {
assigneeId: '123', assignee_id: '123',
assigneeUsernames: 'bart', assignee_username: 'bart',
myReactionEmoji: 'None', my_reaction_emoji: 'None',
iterationWildcardId: 'CURRENT', iteration_id: 'Current',
epicId: 'None', epic_id: 'None',
weight: 'None', weight: 'None',
}; };
...@@ -181,8 +109,8 @@ export const urlParams = { ...@@ -181,8 +109,8 @@ export const urlParams = {
'not[label_name][]': ['live action', 'drama'], 'not[label_name][]': ['live action', 'drama'],
my_reaction_emoji: 'thumbsup', my_reaction_emoji: 'thumbsup',
confidential: 'no', confidential: 'no',
iteration_id: '4', iteration_title: 'season: #4',
'not[iteration_id]': '20', 'not[iteration_title]': 'season: #20',
epic_id: 'gitlab-org%3A%3A%2612', epic_id: 'gitlab-org%3A%3A%2612',
'not[epic_id]': 'gitlab-org::&34', 'not[epic_id]': 'gitlab-org::&34',
weight: '1', weight: '1',
......
...@@ -8,20 +8,19 @@ import { ...@@ -8,20 +8,19 @@ 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 { API_PARAM, DUE_DATE_VALUES, URL_PARAM, urlSortParams } from '~/issues_list/constants';
import { import {
convertToUrlParams, convertToParams,
convertToSearchQuery, convertToSearchQuery,
getDueDateValue, getDueDateValue,
getFilterTokens, getFilterTokens,
getSortKey, getSortKey,
getSortOptions, getSortOptions,
convertToApiParams,
} from '~/issues_list/utils'; } from '~/issues_list/utils';
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];
expect(getSortKey(sort)).toBe(sortKey); expect(getSortKey(sort)).toBe(sortKey);
}); });
}); });
...@@ -81,26 +80,31 @@ describe('getFilterTokens', () => { ...@@ -81,26 +80,31 @@ describe('getFilterTokens', () => {
}); });
}); });
describe('convertToApiParams', () => { describe('convertToParams', () => {
it('returns api params given filtered tokens', () => { it('returns api params given filtered tokens', () => {
expect(convertToApiParams(filteredTokens)).toEqual(apiParams); expect(convertToParams(filteredTokens, API_PARAM)).toEqual({
...apiParams,
epic_id: 'gitlab-org::&12',
});
}); });
it('returns api params given filtered tokens with special values', () => { it('returns api params given filtered tokens with special values', () => {
expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues); expect(convertToParams(filteredTokensWithSpecialValues, API_PARAM)).toEqual(
apiParamsWithSpecialValues,
);
}); });
});
describe('convertToUrlParams', () => {
it('returns url params given filtered tokens', () => { it('returns url params given filtered tokens', () => {
expect(convertToUrlParams(filteredTokens)).toEqual({ expect(convertToParams(filteredTokens, URL_PARAM)).toEqual({
...urlParams, ...urlParams,
epic_id: 'gitlab-org::&12', epic_id: 'gitlab-org::&12',
}); });
}); });
it('returns url params given filtered tokens with special values', () => { it('returns url params given filtered tokens with special values', () => {
expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues); expect(convertToParams(filteredTokensWithSpecialValues, URL_PARAM)).toEqual(
urlParamsWithSpecialValues,
);
}); });
}); });
......
...@@ -302,6 +302,7 @@ RSpec.describe IssuesHelper do ...@@ -302,6 +302,7 @@ RSpec.describe IssuesHelper do
email: current_user&.notification_email, email: current_user&.notification_email,
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'), emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
empty_state_svg_path: '#', empty_state_svg_path: '#',
endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
export_csv_path: export_csv_project_issues_path(project), export_csv_path: export_csv_project_issues_path(project),
has_project_issues: project_issues(project).exists?.to_s, has_project_issues: project_issues(project).exists?.to_s,
import_csv_issues_path: '#', import_csv_issues_path: '#',
......
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