diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue index 6059beee07896e52225ad93ea8f2b53069ab1d0d..93ba338a6b3486a4483b73b13778c4575c4378fa 100644 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issues_list_app.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 { IssuableListTabs, IssuableStates } from '~/issuable_list/constants'; import { + API_PARAM, apiSortParams, CREATED_DESC, i18n, MAX_LIST_SIZE, PAGE_SIZE, + PARAM_DUE_DATE, PARAM_PAGE, PARAM_SORT, PARAM_STATE, RELATIVE_POSITION_DESC, UPDATED_DESC, + URL_PARAM, urlSortParams, } from '~/issues_list/constants'; import { - convertToApiParams, + convertToParams, convertToSearchQuery, - convertToUrlParams, + getDueDateValue, getFilterTokens, getSortKey, getSortOptions, @@ -113,6 +116,9 @@ export default { hasIssueWeightsFeature: { default: false, }, + hasMultipleIssueAssigneesFeature: { + default: false, + }, initialEmail: { default: '', }, @@ -155,6 +161,7 @@ export default { const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC; return { + dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)), exportCsvPathWithQuery: this.getExportCsvPathWithQuery(), filterTokens: getFilterTokens(window.location.search), isLoading: false, @@ -177,10 +184,10 @@ export default { return this.state === IssuableStates.Opened; }, apiFilterParams() { - return convertToApiParams(this.filterTokens); + return convertToParams(this.filterTokens, API_PARAM); }, urlFilterParams() { - return convertToUrlParams(this.filterTokens); + return convertToParams(this.filterTokens, URL_PARAM); }, searchQuery() { return convertToSearchQuery(this.filterTokens) || undefined; @@ -203,7 +210,7 @@ export default { icon: 'user', token: AuthorToken, dataType: 'user', - unique: true, + unique: !this.hasMultipleIssueAssigneesFeature, defaultAuthors: DEFAULT_NONE_ANY, fetchAuthors: this.fetchUsers, }, @@ -298,6 +305,7 @@ export default { }, urlParams() { return { + due_date: this.dueDateFilter, page: this.page, search: this.searchQuery, state: this.state, @@ -366,6 +374,7 @@ export default { return axios .get(this.endpoint, { params: { + due_date: this.dueDateFilter, page: this.page, per_page: PAGE_SIZE, search: this.searchQuery, diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js index 5f0bbd2145dd3734476ded56c14ff5b65b7bc082..54e9668d3007ecebbd3f2d3d752223b2cdbff973 100644 --- a/app/assets/javascripts/issues_list/constants.js +++ b/app/assets/javascripts/issues_list/constants.js @@ -100,10 +100,26 @@ export const i18n = { 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_SORT = 'sort'; 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 CREATED_ASC = 'CREATED_ASC'; export const CREATED_DESC = 'CREATED_DESC'; @@ -258,13 +274,16 @@ export const urlSortParams = { export const MAX_LIST_SIZE = 10; +export const API_PARAM = 'apiParam'; +export const URL_PARAM = 'urlParam'; export const NORMAL_FILTER = 'normalFilter'; export const SPECIAL_FILTER = 'specialFilter'; +export const ALTERNATIVE_FILTER = 'alternativeFilter'; export const SPECIAL_FILTER_VALUES = [FILTER_NONE, FILTER_ANY, FILTER_CURRENT]; export const filters = { author_username: { - apiParam: { + [API_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'author_username', }, @@ -272,7 +291,7 @@ export const filters = { [NORMAL_FILTER]: 'not[author_username]', }, }, - urlParam: { + [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'author_username', }, @@ -282,7 +301,7 @@ export const filters = { }, }, assignee_username: { - apiParam: { + [API_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'assignee_username', [SPECIAL_FILTER]: 'assignee_id', @@ -291,10 +310,11 @@ export const filters = { [NORMAL_FILTER]: 'not[assignee_username]', }, }, - urlParam: { + [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'assignee_username[]', [SPECIAL_FILTER]: 'assignee_id', + [ALTERNATIVE_FILTER]: 'assignee_username', }, [OPERATOR_IS_NOT]: { [NORMAL_FILTER]: 'not[assignee_username][]', @@ -302,7 +322,7 @@ export const filters = { }, }, milestone: { - apiParam: { + [API_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'milestone', }, @@ -310,7 +330,7 @@ export const filters = { [NORMAL_FILTER]: 'not[milestone]', }, }, - urlParam: { + [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'milestone_title', }, @@ -320,7 +340,7 @@ export const filters = { }, }, labels: { - apiParam: { + [API_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'labels', }, @@ -328,7 +348,7 @@ export const filters = { [NORMAL_FILTER]: 'not[labels]', }, }, - urlParam: { + [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'label_name[]', }, @@ -338,13 +358,13 @@ export const filters = { }, }, my_reaction_emoji: { - apiParam: { + [API_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'my_reaction_emoji', [SPECIAL_FILTER]: 'my_reaction_emoji', }, }, - urlParam: { + [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'my_reaction_emoji', [SPECIAL_FILTER]: 'my_reaction_emoji', @@ -352,19 +372,19 @@ export const filters = { }, }, confidential: { - apiParam: { + [API_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'confidential', }, }, - urlParam: { + [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'confidential', }, }, }, iteration: { - apiParam: { + [API_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'iteration_title', [SPECIAL_FILTER]: 'iteration_id', @@ -373,7 +393,7 @@ export const filters = { [NORMAL_FILTER]: 'not[iteration_title]', }, }, - urlParam: { + [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'iteration_title', [SPECIAL_FILTER]: 'iteration_id', @@ -384,7 +404,7 @@ export const filters = { }, }, epic_id: { - apiParam: { + [API_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'epic_id', [SPECIAL_FILTER]: 'epic_id', @@ -393,7 +413,7 @@ export const filters = { [NORMAL_FILTER]: 'not[epic_id]', }, }, - urlParam: { + [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'epic_id', [SPECIAL_FILTER]: 'epic_id', @@ -404,7 +424,7 @@ export const filters = { }, }, weight: { - apiParam: { + [API_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'weight', [SPECIAL_FILTER]: 'weight', @@ -413,7 +433,7 @@ export const filters = { [NORMAL_FILTER]: 'not[weight]', }, }, - urlParam: { + [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'weight', [SPECIAL_FILTER]: 'weight', diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js index c4bd62bce5956d406ac31c7fbcc8191805dff56d..55719f6449bb07d857645f7e341ade5f57768c16 100644 --- a/app/assets/javascripts/issues_list/index.js +++ b/app/assets/javascripts/issues_list/index.js @@ -90,6 +90,7 @@ export function mountIssuesListApp() { hasIssuableHealthStatusFeature, hasIssues, hasIssueWeightsFeature, + hasMultipleIssueAssigneesFeature, importCsvIssuesPath, initialEmail, isSignedIn, @@ -127,6 +128,7 @@ export function mountIssuesListApp() { hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature), hasIssues: parseBoolean(hasIssues), hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature), + hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature), isSignedIn: parseBoolean(isSignedIn), issuesPath, jiraIntegrationPath, diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js index b6551eb4ac85f2fb9dc0eb5a7c633d12dd2ac3ef..234fd59ca8d1a7f0e82edbdcab7cd2f7c7c9e7a4 100644 --- a/app/assets/javascripts/issues_list/utils.js +++ b/app/assets/javascripts/issues_list/utils.js @@ -4,6 +4,7 @@ import { CREATED_DESC, DUE_DATE_ASC, DUE_DATE_DESC, + DUE_DATE_VALUES, filters, LABEL_PRIORITY_DESC, MILESTONE_DUE_ASC, @@ -21,12 +22,15 @@ import { WEIGHT_ASC, WEIGHT_DESC, } from '~/issues_list/constants'; +import { isPositiveInteger } from '~/lib/utils/number_utils'; import { __ } from '~/locale'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; export const getSortKey = (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) => { const sortOptions = [ { @@ -167,28 +171,20 @@ export const getFilterTokens = (locationSearch) => { return filterTokens.concat(searchTokens); }; -const getFilterType = (data) => - SPECIAL_FILTER_VALUES.includes(data) ? SPECIAL_FILTER : NORMAL_FILTER; - -export const convertToApiParams = (filterTokens) => - filterTokens - .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, - }); - }, {}); +const getFilterType = (data, tokenType = '') => + SPECIAL_FILTER_VALUES.includes(data) || + (tokenType === 'assignee_username' && isPositiveInteger(data)) + ? SPECIAL_FILTER + : NORMAL_FILTER; -export const convertToUrlParams = (filterTokens) => +export const convertToParams = (filterTokens, paramType) => filterTokens .filter((token) => token.type !== FILTERED_SEARCH_TERM) .reduce((acc, token) => { - const filterType = getFilterType(token.value.data); - const urlParam = filters[token.type].urlParam[token.value.operator]?.[filterType]; + const filterType = getFilterType(token.value.data, token.type); + const param = filters[token.type][paramType][token.value.operator]?.[filterType]; 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, }); }, {}); diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index 63feb6f9b1d1114ce539ae28f212288839b48c80..e3500d02a79d33beffe5cece67142b3a280c614c 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -171,3 +171,13 @@ export const formattedChangeInPercent = (firstY, lastY, { nonFiniteResult = '-' export const isNumeric = (value) => { 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); diff --git a/ee/app/helpers/ee/issues_helper.rb b/ee/app/helpers/ee/issues_helper.rb index cf5aa8c7adf8b3ea8cab837f0c5abf964230d4bc..7837ec5311a5b66d1d20ce3d3d98444796c0fcf7 100644 --- a/ee/app/helpers/ee/issues_helper.rb +++ b/ee/app/helpers/ee/issues_helper.rb @@ -47,7 +47,8 @@ def issues_list_data(project, current_user, finder) data = super.merge!( 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_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 diff --git a/ee/spec/helpers/ee/issues_helper_spec.rb b/ee/spec/helpers/ee/issues_helper_spec.rb index 34e540d9f4efce76f07fd20b0848daf39c2a6cf4..2c6f22388fc2854166baefc9d03388c6b24282af 100644 --- a/ee/spec/helpers/ee/issues_helper_spec.rb +++ b/ee/spec/helpers/ee/issues_helper_spec.rb @@ -137,7 +137,7 @@ context 'when features are enabled' 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 it 'returns data with licensed features enabled' do @@ -145,6 +145,7 @@ has_blocked_issues_feature: 'true', has_issuable_health_status_feature: 'true', has_issue_weights_feature: 'true', + has_multiple_issue_assignees_feature: 'true', group_epics_path: group_epics_path(project.group, format: :json), project_iterations_path: api_v4_projects_iterations_path(id: project.id) } @@ -163,17 +164,19 @@ context 'when features are disabled' 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 it 'returns data with licensed features disabled' do expected = { has_blocked_issues_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) + expect(result).to include(expected) expect(result).not_to include(:group_epics_path) expect(result).not_to include(:project_iterations_path) diff --git a/spec/frontend/issues_list/components/issues_list_app_spec.js b/spec/frontend/issues_list/components/issues_list_app_spec.js index 9fbbeadb1d004a9f2d7a08c91f9189c79096be82..5d83bf0142f397b5f4e56e00be3dfb40a814d230 100644 --- a/spec/frontend/issues_list/components/issues_list_app_spec.js +++ b/spec/frontend/issues_list/components/issues_list_app_spec.js @@ -13,8 +13,10 @@ import IssuesListApp from '~/issues_list/components/issues_list_app.vue'; import { apiSortParams, CREATED_DESC, + DUE_DATE_OVERDUE, PAGE_SIZE, PAGE_SIZE_MANUAL, + PARAM_DUE_DATE, RELATIVE_POSITION_DESC, urlSortParams, } from '~/issues_list/constants'; @@ -217,6 +219,16 @@ describe('IssuesListApp component', () => { }); 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', () => { it('is set from the url params', () => { const page = 5; diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js index 5892a65e4342fac29d6393d1493d99ee3b65a6f3..ce2880d177a46466314a144cc731c456679982f1 100644 --- a/spec/frontend/issues_list/mock_data.js +++ b/spec/frontend/issues_list/mock_data.js @@ -8,7 +8,9 @@ export const locationSearch = [ 'author_username=homer', 'not[author_username]=marge', 'assignee_username[]=bart', - 'not[assignee_username][]=lisa', + 'assignee_username[]=lisa', + 'not[assignee_username][]=patty', + 'not[assignee_username][]=selma', 'milestone_title=season+4', 'not[milestone_title]=season+20', 'label_name[]=cartoon', @@ -26,7 +28,8 @@ export const locationSearch = [ ].join('&'); export const locationSearchWithSpecialValues = [ - 'assignee_id=None', + 'assignee_id=123', + 'assignee_username=bart', 'my_reaction_emoji=None', 'iteration_id=Current', 'epic_id=None', @@ -37,7 +40,9 @@ export const filteredTokens = [ { type: 'author_username', value: { data: 'homer', operator: OPERATOR_IS } }, { type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } }, { 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 20', operator: OPERATOR_IS_NOT } }, { type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } }, @@ -57,7 +62,8 @@ export const filteredTokens = [ ]; 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: 'iteration', value: { data: 'Current', operator: OPERATOR_IS } }, { type: 'epic_id', value: { data: 'None', operator: OPERATOR_IS } }, @@ -67,12 +73,12 @@ export const filteredTokensWithSpecialValues = [ export const apiParams = { author_username: 'homer', 'not[author_username]': 'marge', - assignee_username: 'bart', - 'not[assignee_username]': 'lisa', + assignee_username: ['bart', 'lisa'], + 'not[assignee_username]': ['patty', 'selma'], milestone: 'season 4', 'not[milestone]': 'season 20', - labels: 'cartoon,tv', - 'not[labels]': 'live action,drama', + labels: ['cartoon', 'tv'], + 'not[labels]': ['live action', 'drama'], my_reaction_emoji: 'thumbsup', confidential: 'no', iteration_title: 'season: #4', @@ -84,7 +90,8 @@ export const apiParams = { }; export const apiParamsWithSpecialValues = { - assignee_id: 'None', + assignee_id: '123', + assignee_username: 'bart', my_reaction_emoji: 'None', iteration_id: 'Current', epic_id: 'None', @@ -92,28 +99,29 @@ export const apiParamsWithSpecialValues = { }; export const urlParams = { - author_username: ['homer'], - 'not[author_username]': ['marge'], - 'assignee_username[]': ['bart'], - 'not[assignee_username][]': ['lisa'], - milestone_title: ['season 4'], - 'not[milestone_title]': ['season 20'], + author_username: 'homer', + 'not[author_username]': 'marge', + 'assignee_username[]': ['bart', 'lisa'], + 'not[assignee_username][]': ['patty', 'selma'], + milestone_title: 'season 4', + 'not[milestone_title]': 'season 20', 'label_name[]': ['cartoon', 'tv'], 'not[label_name][]': ['live action', 'drama'], - my_reaction_emoji: ['thumbsup'], - confidential: ['no'], - iteration_title: ['season: #4'], - 'not[iteration_title]': ['season: #20'], - epic_id: ['12'], - 'not[epic_id]': ['34'], - weight: ['1'], - 'not[weight]': ['3'], + my_reaction_emoji: 'thumbsup', + confidential: 'no', + iteration_title: 'season: #4', + 'not[iteration_title]': 'season: #20', + epic_id: '12', + 'not[epic_id]': '34', + weight: '1', + 'not[weight]': '3', }; export const urlParamsWithSpecialValues = { - assignee_id: ['None'], - my_reaction_emoji: ['None'], - iteration_id: ['Current'], - epic_id: ['None'], - weight: ['None'], + assignee_id: '123', + 'assignee_username[]': 'bart', + my_reaction_emoji: 'None', + iteration_id: 'Current', + epic_id: 'None', + weight: 'None', }; diff --git a/spec/frontend/issues_list/utils_spec.js b/spec/frontend/issues_list/utils_spec.js index 76a2383268748afe6a9629cd103a796c38c06541..17127753972c4f62e0570445b318402b726dba90 100644 --- a/spec/frontend/issues_list/utils_spec.js +++ b/spec/frontend/issues_list/utils_spec.js @@ -8,11 +8,11 @@ import { urlParams, urlParamsWithSpecialValues, } 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 { - convertToApiParams, + convertToParams, convertToSearchQuery, - convertToUrlParams, + getDueDateValue, getFilterTokens, getSortKey, getSortOptions, @@ -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.each` hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking @@ -70,23 +80,25 @@ describe('getFilterTokens', () => { }); }); -describe('convertToApiParams', () => { +describe('convertToParams', () => { 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', () => { - expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues); + expect(convertToParams(filteredTokensWithSpecialValues, API_PARAM)).toEqual( + apiParamsWithSpecialValues, + ); }); -}); -describe('convertToUrlParams', () => { 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', () => { - expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues); + expect(convertToParams(filteredTokensWithSpecialValues, URL_PARAM)).toEqual( + urlParamsWithSpecialValues, + ); }); }); diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js index 4dcd92116970d4dee7f80e4bd8b2e33c57b83d0f..f4483f5098b5e87e9f3a9ea226a1aedefe9fcd1e 100644 --- a/spec/frontend/lib/utils/number_utility_spec.js +++ b/spec/frontend/lib/utils/number_utility_spec.js @@ -10,6 +10,7 @@ import { changeInPercent, formattedChangeInPercent, isNumeric, + isPositiveInteger, } from '~/lib/utils/number_utils'; describe('Number Utils', () => { @@ -184,4 +185,29 @@ describe('Number Utils', () => { 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); + }); + }); });