Commit ae385098 authored by Coung Ngo's avatar Coung Ngo Committed by Olena Horal-Koretska

Add due_date and multiple assignees to issues page refactor

parent 43a97e56
...@@ -16,22 +16,25 @@ import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; ...@@ -16,22 +16,25 @@ 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, apiSortParams,
CREATED_DESC, CREATED_DESC,
i18n, i18n,
MAX_LIST_SIZE, MAX_LIST_SIZE,
PAGE_SIZE, PAGE_SIZE,
PARAM_DUE_DATE,
PARAM_PAGE, PARAM_PAGE,
PARAM_SORT, PARAM_SORT,
PARAM_STATE, PARAM_STATE,
RELATIVE_POSITION_DESC, RELATIVE_POSITION_DESC,
UPDATED_DESC, UPDATED_DESC,
URL_PARAM,
urlSortParams, urlSortParams,
} from '~/issues_list/constants'; } from '~/issues_list/constants';
import { import {
convertToApiParams, convertToParams,
convertToSearchQuery, convertToSearchQuery,
convertToUrlParams, getDueDateValue,
getFilterTokens, getFilterTokens,
getSortKey, getSortKey,
getSortOptions, getSortOptions,
...@@ -113,6 +116,9 @@ export default { ...@@ -113,6 +116,9 @@ export default {
hasIssueWeightsFeature: { hasIssueWeightsFeature: {
default: false, default: false,
}, },
hasMultipleIssueAssigneesFeature: {
default: false,
},
initialEmail: { initialEmail: {
default: '', default: '',
}, },
...@@ -155,6 +161,7 @@ export default { ...@@ -155,6 +161,7 @@ export default {
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC; const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
return { return {
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(), exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
filterTokens: getFilterTokens(window.location.search), filterTokens: getFilterTokens(window.location.search),
isLoading: false, isLoading: false,
...@@ -177,10 +184,10 @@ export default { ...@@ -177,10 +184,10 @@ export default {
return this.state === IssuableStates.Opened; return this.state === IssuableStates.Opened;
}, },
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;
...@@ -203,7 +210,7 @@ export default { ...@@ -203,7 +210,7 @@ export default {
icon: 'user', icon: 'user',
token: AuthorToken, token: AuthorToken,
dataType: 'user', dataType: 'user',
unique: true, unique: !this.hasMultipleIssueAssigneesFeature,
defaultAuthors: DEFAULT_NONE_ANY, defaultAuthors: DEFAULT_NONE_ANY,
fetchAuthors: this.fetchUsers, fetchAuthors: this.fetchUsers,
}, },
...@@ -298,6 +305,7 @@ export default { ...@@ -298,6 +305,7 @@ export default {
}, },
urlParams() { urlParams() {
return { return {
due_date: this.dueDateFilter,
page: this.page, page: this.page,
search: this.searchQuery, search: this.searchQuery,
state: this.state, state: this.state,
...@@ -366,6 +374,7 @@ export default { ...@@ -366,6 +374,7 @@ export default {
return axios return axios
.get(this.endpoint, { .get(this.endpoint, {
params: { params: {
due_date: this.dueDateFilter,
page: this.page, page: this.page,
per_page: PAGE_SIZE, per_page: PAGE_SIZE,
search: this.searchQuery, search: this.searchQuery,
......
...@@ -100,10 +100,26 @@ export const i18n = { ...@@ -100,10 +100,26 @@ 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_PAGE = 'page'; 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';
export const DUE_DATE_NONE = '0';
export const DUE_DATE_ANY = '';
export const DUE_DATE_OVERDUE = 'overdue';
export const DUE_DATE_WEEK = 'week';
export const DUE_DATE_MONTH = 'month';
export const DUE_DATE_NEXT_MONTH_AND_PREVIOUS_TWO_WEEKS = 'next_month_and_previous_two_weeks';
export const DUE_DATE_VALUES = [
DUE_DATE_NONE,
DUE_DATE_ANY,
DUE_DATE_OVERDUE,
DUE_DATE_WEEK,
DUE_DATE_MONTH,
DUE_DATE_NEXT_MONTH_AND_PREVIOUS_TWO_WEEKS,
];
export const BLOCKING_ISSUES_DESC = 'BLOCKING_ISSUES_DESC'; export const BLOCKING_ISSUES_DESC = 'BLOCKING_ISSUES_DESC';
export const CREATED_ASC = 'CREATED_ASC'; export const CREATED_ASC = 'CREATED_ASC';
export const CREATED_DESC = 'CREATED_DESC'; export const CREATED_DESC = 'CREATED_DESC';
...@@ -258,13 +274,16 @@ export const urlSortParams = { ...@@ -258,13 +274,16 @@ export const urlSortParams = {
export const MAX_LIST_SIZE = 10; export const MAX_LIST_SIZE = 10;
export const API_PARAM = 'apiParam';
export const URL_PARAM = 'urlParam';
export const NORMAL_FILTER = 'normalFilter'; export const NORMAL_FILTER = 'normalFilter';
export const SPECIAL_FILTER = 'specialFilter'; export const SPECIAL_FILTER = 'specialFilter';
export const ALTERNATIVE_FILTER = 'alternativeFilter';
export const SPECIAL_FILTER_VALUES = [FILTER_NONE, FILTER_ANY, FILTER_CURRENT]; export const SPECIAL_FILTER_VALUES = [FILTER_NONE, FILTER_ANY, FILTER_CURRENT];
export const filters = { export const filters = {
author_username: { author_username: {
apiParam: { [API_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'author_username', [NORMAL_FILTER]: 'author_username',
}, },
...@@ -272,7 +291,7 @@ export const filters = { ...@@ -272,7 +291,7 @@ export const filters = {
[NORMAL_FILTER]: 'not[author_username]', [NORMAL_FILTER]: 'not[author_username]',
}, },
}, },
urlParam: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'author_username', [NORMAL_FILTER]: 'author_username',
}, },
...@@ -282,7 +301,7 @@ export const filters = { ...@@ -282,7 +301,7 @@ export const filters = {
}, },
}, },
assignee_username: { assignee_username: {
apiParam: { [API_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'assignee_username', [NORMAL_FILTER]: 'assignee_username',
[SPECIAL_FILTER]: 'assignee_id', [SPECIAL_FILTER]: 'assignee_id',
...@@ -291,10 +310,11 @@ export const filters = { ...@@ -291,10 +310,11 @@ export const filters = {
[NORMAL_FILTER]: 'not[assignee_username]', [NORMAL_FILTER]: 'not[assignee_username]',
}, },
}, },
urlParam: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'assignee_username[]', [NORMAL_FILTER]: 'assignee_username[]',
[SPECIAL_FILTER]: 'assignee_id', [SPECIAL_FILTER]: 'assignee_id',
[ALTERNATIVE_FILTER]: 'assignee_username',
}, },
[OPERATOR_IS_NOT]: { [OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username][]', [NORMAL_FILTER]: 'not[assignee_username][]',
...@@ -302,7 +322,7 @@ export const filters = { ...@@ -302,7 +322,7 @@ export const filters = {
}, },
}, },
milestone: { milestone: {
apiParam: { [API_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'milestone', [NORMAL_FILTER]: 'milestone',
}, },
...@@ -310,7 +330,7 @@ export const filters = { ...@@ -310,7 +330,7 @@ export const filters = {
[NORMAL_FILTER]: 'not[milestone]', [NORMAL_FILTER]: 'not[milestone]',
}, },
}, },
urlParam: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'milestone_title', [NORMAL_FILTER]: 'milestone_title',
}, },
...@@ -320,7 +340,7 @@ export const filters = { ...@@ -320,7 +340,7 @@ export const filters = {
}, },
}, },
labels: { labels: {
apiParam: { [API_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'labels', [NORMAL_FILTER]: 'labels',
}, },
...@@ -328,7 +348,7 @@ export const filters = { ...@@ -328,7 +348,7 @@ export const filters = {
[NORMAL_FILTER]: 'not[labels]', [NORMAL_FILTER]: 'not[labels]',
}, },
}, },
urlParam: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'label_name[]', [NORMAL_FILTER]: 'label_name[]',
}, },
...@@ -338,13 +358,13 @@ export const filters = { ...@@ -338,13 +358,13 @@ export const filters = {
}, },
}, },
my_reaction_emoji: { my_reaction_emoji: {
apiParam: { [API_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'my_reaction_emoji', [NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji', [SPECIAL_FILTER]: 'my_reaction_emoji',
}, },
}, },
urlParam: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'my_reaction_emoji', [NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji', [SPECIAL_FILTER]: 'my_reaction_emoji',
...@@ -352,19 +372,19 @@ export const filters = { ...@@ -352,19 +372,19 @@ export const filters = {
}, },
}, },
confidential: { confidential: {
apiParam: { [API_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'confidential', [NORMAL_FILTER]: 'confidential',
}, },
}, },
urlParam: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'confidential', [NORMAL_FILTER]: 'confidential',
}, },
}, },
}, },
iteration: { iteration: {
apiParam: { [API_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'iteration_title', [NORMAL_FILTER]: 'iteration_title',
[SPECIAL_FILTER]: 'iteration_id', [SPECIAL_FILTER]: 'iteration_id',
...@@ -373,7 +393,7 @@ export const filters = { ...@@ -373,7 +393,7 @@ export const filters = {
[NORMAL_FILTER]: 'not[iteration_title]', [NORMAL_FILTER]: 'not[iteration_title]',
}, },
}, },
urlParam: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'iteration_title', [NORMAL_FILTER]: 'iteration_title',
[SPECIAL_FILTER]: 'iteration_id', [SPECIAL_FILTER]: 'iteration_id',
...@@ -384,7 +404,7 @@ export const filters = { ...@@ -384,7 +404,7 @@ export const filters = {
}, },
}, },
epic_id: { epic_id: {
apiParam: { [API_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'epic_id', [NORMAL_FILTER]: 'epic_id',
[SPECIAL_FILTER]: 'epic_id', [SPECIAL_FILTER]: 'epic_id',
...@@ -393,7 +413,7 @@ export const filters = { ...@@ -393,7 +413,7 @@ export const filters = {
[NORMAL_FILTER]: 'not[epic_id]', [NORMAL_FILTER]: 'not[epic_id]',
}, },
}, },
urlParam: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'epic_id', [NORMAL_FILTER]: 'epic_id',
[SPECIAL_FILTER]: 'epic_id', [SPECIAL_FILTER]: 'epic_id',
...@@ -404,7 +424,7 @@ export const filters = { ...@@ -404,7 +424,7 @@ export const filters = {
}, },
}, },
weight: { weight: {
apiParam: { [API_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'weight', [NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight', [SPECIAL_FILTER]: 'weight',
...@@ -413,7 +433,7 @@ export const filters = { ...@@ -413,7 +433,7 @@ export const filters = {
[NORMAL_FILTER]: 'not[weight]', [NORMAL_FILTER]: 'not[weight]',
}, },
}, },
urlParam: { [URL_PARAM]: {
[OPERATOR_IS]: { [OPERATOR_IS]: {
[NORMAL_FILTER]: 'weight', [NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight', [SPECIAL_FILTER]: 'weight',
......
...@@ -90,6 +90,7 @@ export function mountIssuesListApp() { ...@@ -90,6 +90,7 @@ export function mountIssuesListApp() {
hasIssuableHealthStatusFeature, hasIssuableHealthStatusFeature,
hasIssues, hasIssues,
hasIssueWeightsFeature, hasIssueWeightsFeature,
hasMultipleIssueAssigneesFeature,
importCsvIssuesPath, importCsvIssuesPath,
initialEmail, initialEmail,
isSignedIn, isSignedIn,
...@@ -127,6 +128,7 @@ export function mountIssuesListApp() { ...@@ -127,6 +128,7 @@ export function mountIssuesListApp() {
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature), hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssues: parseBoolean(hasIssues), hasIssues: parseBoolean(hasIssues),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature), hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature),
isSignedIn: parseBoolean(isSignedIn), isSignedIn: parseBoolean(isSignedIn),
issuesPath, issuesPath,
jiraIntegrationPath, jiraIntegrationPath,
......
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
CREATED_DESC, CREATED_DESC,
DUE_DATE_ASC, DUE_DATE_ASC,
DUE_DATE_DESC, DUE_DATE_DESC,
DUE_DATE_VALUES,
filters, filters,
LABEL_PRIORITY_DESC, LABEL_PRIORITY_DESC,
MILESTONE_DUE_ASC, MILESTONE_DUE_ASC,
...@@ -21,12 +22,15 @@ import { ...@@ -21,12 +22,15 @@ import {
WEIGHT_ASC, WEIGHT_ASC,
WEIGHT_DESC, WEIGHT_DESC,
} from '~/issues_list/constants'; } from '~/issues_list/constants';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
export const getSortKey = (sort) => export const getSortKey = (sort) =>
Object.keys(urlSortParams).find((key) => urlSortParams[key].sort === sort); Object.keys(urlSortParams).find((key) => urlSortParams[key].sort === sort);
export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined);
export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) => { export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) => {
const sortOptions = [ const sortOptions = [
{ {
...@@ -167,28 +171,20 @@ export const getFilterTokens = (locationSearch) => { ...@@ -167,28 +171,20 @@ export const getFilterTokens = (locationSearch) => {
return filterTokens.concat(searchTokens); return filterTokens.concat(searchTokens);
}; };
const getFilterType = (data) => const getFilterType = (data, tokenType = '') =>
SPECIAL_FILTER_VALUES.includes(data) ? SPECIAL_FILTER : NORMAL_FILTER; SPECIAL_FILTER_VALUES.includes(data) ||
(tokenType === 'assignee_username' && isPositiveInteger(data))
export const convertToApiParams = (filterTokens) => ? SPECIAL_FILTER
filterTokens : NORMAL_FILTER;
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.reduce((acc, token) => {
const filterType = getFilterType(token.value.data);
const apiParam = filters[token.type].apiParam[token.value.operator][filterType];
return Object.assign(acc, {
[apiParam]: acc[apiParam] ? `${acc[apiParam]},${token.value.data}` : token.value.data,
});
}, {});
export const convertToUrlParams = (filterTokens) => export const convertToParams = (filterTokens, paramType) =>
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); const filterType = getFilterType(token.value.data, token.type);
const urlParam = filters[token.type].urlParam[token.value.operator]?.[filterType]; const param = filters[token.type][paramType][token.value.operator]?.[filterType];
return Object.assign(acc, { return Object.assign(acc, {
[urlParam]: acc[urlParam] ? acc[urlParam].concat(token.value.data) : [token.value.data], [param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data,
}); });
}, {}); }, {});
......
...@@ -171,3 +171,13 @@ export const formattedChangeInPercent = (firstY, lastY, { nonFiniteResult = '-' ...@@ -171,3 +171,13 @@ export const formattedChangeInPercent = (firstY, lastY, { nonFiniteResult = '-'
export const isNumeric = (value) => { export const isNumeric = (value) => {
return !Number.isNaN(parseInt(value, 10)); return !Number.isNaN(parseInt(value, 10));
}; };
const numberRegex = /^[0-9]+$/;
/**
* Checks whether the value is a positive number or 0, or a string with equivalent value
*
* @param value
* @return {boolean}
*/
export const isPositiveInteger = (value) => numberRegex.test(value);
...@@ -47,7 +47,8 @@ module EE ...@@ -47,7 +47,8 @@ module EE
data = super.merge!( data = super.merge!(
has_blocked_issues_feature: project.feature_available?(:blocked_issues).to_s, has_blocked_issues_feature: project.feature_available?(:blocked_issues).to_s,
has_issuable_health_status_feature: project.feature_available?(:issuable_health_status).to_s, has_issuable_health_status_feature: project.feature_available?(:issuable_health_status).to_s,
has_issue_weights_feature: project.feature_available?(:issue_weights).to_s has_issue_weights_feature: project.feature_available?(:issue_weights).to_s,
has_multiple_issue_assignees_feature: project.feature_available?(:multiple_issue_assignees).to_s
) )
if project.feature_available?(:epics) && project.group if project.feature_available?(:epics) && project.group
......
...@@ -137,7 +137,7 @@ RSpec.describe EE::IssuesHelper do ...@@ -137,7 +137,7 @@ RSpec.describe EE::IssuesHelper do
context 'when features are enabled' do context 'when features are enabled' do
before do before do
stub_licensed_features(epics: true, iterations: true, issue_weights: true, issuable_health_status: true, blocked_issues: true) stub_licensed_features(epics: true, iterations: true, issue_weights: true, issuable_health_status: true, blocked_issues: true, multiple_issue_assignees: true)
end end
it 'returns data with licensed features enabled' do it 'returns data with licensed features enabled' do
...@@ -145,6 +145,7 @@ RSpec.describe EE::IssuesHelper do ...@@ -145,6 +145,7 @@ RSpec.describe EE::IssuesHelper do
has_blocked_issues_feature: 'true', has_blocked_issues_feature: 'true',
has_issuable_health_status_feature: 'true', has_issuable_health_status_feature: 'true',
has_issue_weights_feature: 'true', has_issue_weights_feature: 'true',
has_multiple_issue_assignees_feature: 'true',
group_epics_path: group_epics_path(project.group, format: :json), group_epics_path: group_epics_path(project.group, format: :json),
project_iterations_path: api_v4_projects_iterations_path(id: project.id) project_iterations_path: api_v4_projects_iterations_path(id: project.id)
} }
...@@ -163,17 +164,19 @@ RSpec.describe EE::IssuesHelper do ...@@ -163,17 +164,19 @@ RSpec.describe EE::IssuesHelper do
context 'when features are disabled' do context 'when features are disabled' do
before do before do
stub_licensed_features(epics: false, iterations: false, issue_weights: false, issuable_health_status: false, blocked_issues: false) stub_licensed_features(epics: false, iterations: false, issue_weights: false, issuable_health_status: false, blocked_issues: false, multiple_issue_assignees: false)
end end
it 'returns data with licensed features disabled' do it 'returns data with licensed features disabled' do
expected = { expected = {
has_blocked_issues_feature: 'false', has_blocked_issues_feature: 'false',
has_issuable_health_status_feature: 'false', has_issuable_health_status_feature: 'false',
has_issue_weights_feature: 'false' has_issue_weights_feature: 'false',
has_multiple_issue_assignees_feature: 'false'
} }
result = helper.issues_list_data(project, current_user, finder) result = helper.issues_list_data(project, current_user, finder)
expect(result).to include(expected) expect(result).to include(expected)
expect(result).not_to include(:group_epics_path) expect(result).not_to include(:group_epics_path)
expect(result).not_to include(:project_iterations_path) expect(result).not_to include(:project_iterations_path)
......
...@@ -13,8 +13,10 @@ import IssuesListApp from '~/issues_list/components/issues_list_app.vue'; ...@@ -13,8 +13,10 @@ import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
import { import {
apiSortParams, apiSortParams,
CREATED_DESC, CREATED_DESC,
DUE_DATE_OVERDUE,
PAGE_SIZE, PAGE_SIZE,
PAGE_SIZE_MANUAL, PAGE_SIZE_MANUAL,
PARAM_DUE_DATE,
RELATIVE_POSITION_DESC, RELATIVE_POSITION_DESC,
urlSortParams, urlSortParams,
} from '~/issues_list/constants'; } from '~/issues_list/constants';
...@@ -217,6 +219,16 @@ describe('IssuesListApp component', () => { ...@@ -217,6 +219,16 @@ describe('IssuesListApp component', () => {
}); });
describe('initial url params', () => { describe('initial url params', () => {
describe('due_date', () => {
it('is set from the url params', () => {
global.jsdom.reconfigure({ url: `${TEST_HOST}?${PARAM_DUE_DATE}=${DUE_DATE_OVERDUE}` });
wrapper = mountComponent();
expect(findIssuableList().props('urlParams')).toMatchObject({ due_date: DUE_DATE_OVERDUE });
});
});
describe('page', () => { describe('page', () => {
it('is set from the url params', () => { it('is set from the url params', () => {
const page = 5; const page = 5;
......
...@@ -8,7 +8,9 @@ export const locationSearch = [ ...@@ -8,7 +8,9 @@ export const locationSearch = [
'author_username=homer', 'author_username=homer',
'not[author_username]=marge', 'not[author_username]=marge',
'assignee_username[]=bart', 'assignee_username[]=bart',
'not[assignee_username][]=lisa', 'assignee_username[]=lisa',
'not[assignee_username][]=patty',
'not[assignee_username][]=selma',
'milestone_title=season+4', 'milestone_title=season+4',
'not[milestone_title]=season+20', 'not[milestone_title]=season+20',
'label_name[]=cartoon', 'label_name[]=cartoon',
...@@ -26,7 +28,8 @@ export const locationSearch = [ ...@@ -26,7 +28,8 @@ export const locationSearch = [
].join('&'); ].join('&');
export const locationSearchWithSpecialValues = [ export const locationSearchWithSpecialValues = [
'assignee_id=None', 'assignee_id=123',
'assignee_username=bart',
'my_reaction_emoji=None', 'my_reaction_emoji=None',
'iteration_id=Current', 'iteration_id=Current',
'epic_id=None', 'epic_id=None',
...@@ -37,7 +40,9 @@ export const filteredTokens = [ ...@@ -37,7 +40,9 @@ export const filteredTokens = [
{ type: 'author_username', value: { data: 'homer', operator: OPERATOR_IS } }, { type: 'author_username', value: { data: 'homer', operator: OPERATOR_IS } },
{ type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } }, { type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } },
{ type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } }, { type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS_NOT } }, { type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'patty', operator: OPERATOR_IS_NOT } },
{ type: 'assignee_username', value: { data: 'selma', operator: OPERATOR_IS_NOT } },
{ type: 'milestone', value: { data: 'season 4', operator: OPERATOR_IS } }, { type: 'milestone', value: { data: 'season 4', operator: OPERATOR_IS } },
{ type: 'milestone', value: { data: 'season 20', operator: OPERATOR_IS_NOT } }, { type: 'milestone', value: { data: 'season 20', operator: OPERATOR_IS_NOT } },
{ type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } }, { type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } },
...@@ -57,7 +62,8 @@ export const filteredTokens = [ ...@@ -57,7 +62,8 @@ export const filteredTokens = [
]; ];
export const filteredTokensWithSpecialValues = [ export const filteredTokensWithSpecialValues = [
{ type: 'assignee_username', value: { data: 'None', operator: OPERATOR_IS } }, { type: 'assignee_username', value: { data: '123', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
{ type: 'my_reaction_emoji', value: { data: 'None', operator: OPERATOR_IS } }, { type: 'my_reaction_emoji', value: { data: 'None', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: 'Current', operator: OPERATOR_IS } }, { type: 'iteration', value: { data: 'Current', operator: OPERATOR_IS } },
{ type: 'epic_id', value: { data: 'None', operator: OPERATOR_IS } }, { type: 'epic_id', value: { data: 'None', operator: OPERATOR_IS } },
...@@ -67,12 +73,12 @@ export const filteredTokensWithSpecialValues = [ ...@@ -67,12 +73,12 @@ export const filteredTokensWithSpecialValues = [
export const apiParams = { export const apiParams = {
author_username: 'homer', author_username: 'homer',
'not[author_username]': 'marge', 'not[author_username]': 'marge',
assignee_username: 'bart', assignee_username: ['bart', 'lisa'],
'not[assignee_username]': 'lisa', 'not[assignee_username]': ['patty', 'selma'],
milestone: 'season 4', milestone: 'season 4',
'not[milestone]': 'season 20', 'not[milestone]': 'season 20',
labels: 'cartoon,tv', labels: ['cartoon', 'tv'],
'not[labels]': 'live action,drama', 'not[labels]': ['live action', 'drama'],
my_reaction_emoji: 'thumbsup', my_reaction_emoji: 'thumbsup',
confidential: 'no', confidential: 'no',
iteration_title: 'season: #4', iteration_title: 'season: #4',
...@@ -84,7 +90,8 @@ export const apiParams = { ...@@ -84,7 +90,8 @@ export const apiParams = {
}; };
export const apiParamsWithSpecialValues = { export const apiParamsWithSpecialValues = {
assignee_id: 'None', assignee_id: '123',
assignee_username: 'bart',
my_reaction_emoji: 'None', my_reaction_emoji: 'None',
iteration_id: 'Current', iteration_id: 'Current',
epic_id: 'None', epic_id: 'None',
...@@ -92,28 +99,29 @@ export const apiParamsWithSpecialValues = { ...@@ -92,28 +99,29 @@ export const apiParamsWithSpecialValues = {
}; };
export const urlParams = { export const urlParams = {
author_username: ['homer'], author_username: 'homer',
'not[author_username]': ['marge'], 'not[author_username]': 'marge',
'assignee_username[]': ['bart'], 'assignee_username[]': ['bart', 'lisa'],
'not[assignee_username][]': ['lisa'], 'not[assignee_username][]': ['patty', 'selma'],
milestone_title: ['season 4'], milestone_title: 'season 4',
'not[milestone_title]': ['season 20'], 'not[milestone_title]': 'season 20',
'label_name[]': ['cartoon', 'tv'], 'label_name[]': ['cartoon', 'tv'],
'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_title: ['season: #4'], iteration_title: 'season: #4',
'not[iteration_title]': ['season: #20'], 'not[iteration_title]': 'season: #20',
epic_id: ['12'], epic_id: '12',
'not[epic_id]': ['34'], 'not[epic_id]': '34',
weight: ['1'], weight: '1',
'not[weight]': ['3'], 'not[weight]': '3',
}; };
export const urlParamsWithSpecialValues = { export const urlParamsWithSpecialValues = {
assignee_id: ['None'], assignee_id: '123',
my_reaction_emoji: ['None'], 'assignee_username[]': 'bart',
iteration_id: ['Current'], my_reaction_emoji: 'None',
epic_id: ['None'], iteration_id: 'Current',
weight: ['None'], epic_id: 'None',
weight: 'None',
}; };
...@@ -8,11 +8,11 @@ import { ...@@ -8,11 +8,11 @@ import {
urlParams, urlParams,
urlParamsWithSpecialValues, urlParamsWithSpecialValues,
} from 'jest/issues_list/mock_data'; } from 'jest/issues_list/mock_data';
import { urlSortParams } from '~/issues_list/constants'; import { API_PARAM, DUE_DATE_VALUES, URL_PARAM, urlSortParams } from '~/issues_list/constants';
import { import {
convertToApiParams, convertToParams,
convertToSearchQuery, convertToSearchQuery,
convertToUrlParams, getDueDateValue,
getFilterTokens, getFilterTokens,
getSortKey, getSortKey,
getSortOptions, getSortOptions,
...@@ -25,6 +25,16 @@ describe('getSortKey', () => { ...@@ -25,6 +25,16 @@ describe('getSortKey', () => {
}); });
}); });
describe('getDueDateValue', () => {
it.each(DUE_DATE_VALUES)('returns the argument when it is `%s`', (value) => {
expect(getDueDateValue(value)).toBe(value);
});
it('returns undefined when the argument is invalid', () => {
expect(getDueDateValue('invalid value')).toBeUndefined();
});
});
describe('getSortOptions', () => { describe('getSortOptions', () => {
describe.each` describe.each`
hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking
...@@ -70,23 +80,25 @@ describe('getFilterTokens', () => { ...@@ -70,23 +80,25 @@ 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);
}); });
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(urlParams); expect(convertToParams(filteredTokens, URL_PARAM)).toEqual(urlParams);
}); });
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,
);
}); });
}); });
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
changeInPercent, changeInPercent,
formattedChangeInPercent, formattedChangeInPercent,
isNumeric, isNumeric,
isPositiveInteger,
} from '~/lib/utils/number_utils'; } from '~/lib/utils/number_utils';
describe('Number Utils', () => { describe('Number Utils', () => {
...@@ -184,4 +185,29 @@ describe('Number Utils', () => { ...@@ -184,4 +185,29 @@ describe('Number Utils', () => {
expect(isNumeric(value)).toBe(outcome); expect(isNumeric(value)).toBe(outcome);
}); });
}); });
describe.each`
value | outcome
${0} | ${true}
${'0'} | ${true}
${12345} | ${true}
${'12345'} | ${true}
${-1} | ${false}
${'-1'} | ${false}
${1.01} | ${false}
${'1.01'} | ${false}
${'abcd'} | ${false}
${'100abcd'} | ${false}
${'abcd100'} | ${false}
${''} | ${false}
${false} | ${false}
${true} | ${false}
${undefined} | ${false}
${null} | ${false}
${Infinity} | ${false}
`('isPositiveInteger', ({ value, outcome }) => {
it(`when called with ${typeof value} ${value} it returns ${outcome}`, () => {
expect(isPositiveInteger(value)).toBe(outcome);
});
});
}); });
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