diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 09:55:51 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 09:55:51 +0000 |
commit | e8d2c2579383897a1dd7f9debd359abe8ae8373d (patch) | |
tree | c42be41678c2586d49a75cabce89322082698334 /app/assets/javascripts/issues_list | |
parent | fc845b37ec3a90aaa719975f607740c22ba6a113 (diff) | |
download | gitlab-ce-e8d2c2579383897a1dd7f9debd359abe8ae8373d.tar.gz |
Add latest changes from gitlab-org/gitlab@14-1-stable-eev14.1.0-rc42
Diffstat (limited to 'app/assets/javascripts/issues_list')
13 files changed, 318 insertions, 291 deletions
diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue index 51cad662ebf..b13a389b963 100644 --- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue @@ -6,15 +6,11 @@ import { GlSafeHtmlDirective as SafeHtml, } from '@gitlab/ui'; import { toNumber, omit } from 'lodash'; -import { deprecatedCreateFlash as flash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; -import { - scrollToElement, - urlParamsToObject, - historyPushState, - getParameterByName, -} from '~/lib/utils/common_utils'; -import { setUrlParams } from '~/lib/utils/url_utility'; +import { scrollToElement, historyPushState } from '~/lib/utils/common_utils'; +// eslint-disable-next-line import/no-deprecated +import { setUrlParams, urlParamsToObject, getParameterByName } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import initManualOrdering from '~/manual_ordering'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; @@ -82,10 +78,7 @@ export default { isBulkEditing: false, issuables: [], loading: false, - page: - getParameterByName('page', window.location.href) !== null - ? toNumber(getParameterByName('page')) - : 1, + page: getParameterByName('page') !== null ? toNumber(getParameterByName('page')) : 1, selection: {}, totalItems: 0, }; @@ -265,10 +258,13 @@ export default { }) .catch(() => { this.loading = false; - return flash(__('An error occurred while loading issues')); + return createFlash({ + message: __('An error occurred while loading issues'), + }); }); }, getQueryObject() { + // eslint-disable-next-line import/no-deprecated return urlParamsToObject(window.location.search); }, onPaginate(newPage) { diff --git a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue index 70d73aca925..07492b0046c 100644 --- a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue +++ b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue @@ -115,7 +115,7 @@ export default { {{ timeEstimate }} </span> <weight-count - class="gl-display-none gl-sm-display-inline-block gl-mr-3" + class="issuable-weight gl-display-none gl-sm-display-inline-block gl-mr-3" :weight="issue.weight" /> <issue-health-status 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 dbf7717b248..6563094ef72 100644 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue @@ -11,45 +11,47 @@ import { import fuzzaldrinPlus from 'fuzzaldrin-plus'; import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql'; import createFlash from '~/flash'; +import { TYPE_USER } from '~/graphql_shared/constants'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants'; import { - API_PARAM, CREATED_DESC, i18n, initialPageParams, + issuesCountSmartQueryBase, MAX_LIST_SIZE, PAGE_SIZE, PARAM_DUE_DATE, PARAM_SORT, PARAM_STATE, - RELATIVE_POSITION_DESC, + RELATIVE_POSITION_ASC, TOKEN_TYPE_ASSIGNEE, TOKEN_TYPE_AUTHOR, TOKEN_TYPE_CONFIDENTIAL, - TOKEN_TYPE_MY_REACTION, TOKEN_TYPE_EPIC, TOKEN_TYPE_ITERATION, TOKEN_TYPE_LABEL, TOKEN_TYPE_MILESTONE, + TOKEN_TYPE_MY_REACTION, TOKEN_TYPE_WEIGHT, UPDATED_DESC, - URL_PARAM, urlSortParams, } from '~/issues_list/constants'; import { - convertToParams, + convertToApiParams, convertToSearchQuery, + convertToUrlParams, getDueDateValue, getFilterTokens, getSortKey, getSortOptions, } from '~/issues_list/utils'; import axios from '~/lib/utils/axios_utils'; -import { getParameterByName } from '~/lib/utils/common_utils'; import { scrollUp } from '~/lib/utils/scroll_utils'; +import { getParameterByName } from '~/lib/utils/url_utility'; import { DEFAULT_NONE_ANY, OPERATOR_IS_ONLY, @@ -71,6 +73,10 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; import eventHub from '../eventhub'; +import searchIterationsQuery from '../queries/search_iterations.query.graphql'; +import searchLabelsQuery from '../queries/search_labels.query.graphql'; +import searchMilestonesQuery from '../queries/search_milestones.query.graphql'; +import searchUsersQuery from '../queries/search_users.query.graphql'; import IssueCardTimeInfo from './issue_card_time_info.vue'; export default { @@ -95,9 +101,6 @@ export default { autocompleteAwardEmojisPath: { default: '', }, - autocompleteUsersPath: { - default: '', - }, calendarPath: { default: '', }, @@ -119,6 +122,9 @@ export default { hasIssueWeightsFeature: { default: false, }, + hasIterationsFeature: { + default: false, + }, hasMultipleIssueAssigneesFeature: { default: false, }, @@ -140,15 +146,6 @@ export default { newIssuePath: { default: '', }, - projectIterationsPath: { - default: '', - }, - projectLabelsPath: { - default: '', - }, - projectMilestonesPath: { - default: '', - }, projectPath: { default: '', }, @@ -176,26 +173,17 @@ export default { showBulkEditSidebar: false, sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey, state: state || IssuableStates.Opened, - totalIssues: 0, }; }, apollo: { issues: { query: getIssuesQuery, variables() { - return { - projectPath: this.projectPath, - search: this.searchQuery, - sort: this.sortKey, - state: this.state, - ...this.pageParams, - ...this.apiFilterParams, - }; + return this.queryVariables; }, - update: ({ project }) => project.issues.nodes, + update: ({ project }) => project?.issues.nodes ?? [], result({ data }) { - this.pageInfo = data.project.issues.pageInfo; - this.totalIssues = data.project.issues.count; + this.pageInfo = data.project?.issues.pageInfo ?? {}; this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery(); }, error(error) { @@ -206,8 +194,55 @@ export default { }, debounce: 200, }, + countOpened: { + ...issuesCountSmartQueryBase, + variables() { + return { + ...this.queryVariables, + state: IssuableStates.Opened, + }; + }, + skip() { + return !this.hasProjectIssues; + }, + }, + countClosed: { + ...issuesCountSmartQueryBase, + variables() { + return { + ...this.queryVariables, + state: IssuableStates.Closed, + }; + }, + skip() { + return !this.hasProjectIssues; + }, + }, + countAll: { + ...issuesCountSmartQueryBase, + variables() { + return { + ...this.queryVariables, + state: IssuableStates.All, + }; + }, + skip() { + return !this.hasProjectIssues; + }, + }, }, computed: { + queryVariables() { + return { + isSignedIn: this.isSignedIn, + projectPath: this.projectPath, + search: this.searchQuery, + sort: this.sortKey, + state: this.state, + ...this.pageParams, + ...this.apiFilterParams, + }; + }, hasSearch() { return this.searchQuery || Object.keys(this.urlFilterParams).length; }, @@ -215,32 +250,30 @@ export default { return this.showBulkEditSidebar || !this.issues.length; }, isManualOrdering() { - return this.sortKey === RELATIVE_POSITION_DESC; + return this.sortKey === RELATIVE_POSITION_ASC; }, isOpenTab() { return this.state === IssuableStates.Opened; }, apiFilterParams() { - return convertToParams(this.filterTokens, API_PARAM); + return convertToApiParams(this.filterTokens); }, urlFilterParams() { - return convertToParams(this.filterTokens, URL_PARAM); + return convertToUrlParams(this.filterTokens); }, searchQuery() { return convertToSearchQuery(this.filterTokens) || undefined; }, searchTokens() { - let preloadedAuthors = []; + const preloadedAuthors = []; if (gon.current_user_id) { - preloadedAuthors = [ - { - id: gon.current_user_id, - name: gon.current_user_fullname, - username: gon.current_username, - avatar_url: gon.current_user_avatar_url, - }, - ]; + preloadedAuthors.push({ + id: convertToGraphQLId(TYPE_USER, gon.current_user_id), + name: gon.current_user_fullname, + username: gon.current_username, + avatar_url: gon.current_user_avatar_url, + }); } const tokens = [ @@ -252,6 +285,7 @@ export default { dataType: 'user', unique: true, defaultAuthors: [], + operators: OPERATOR_IS_ONLY, fetchAuthors: this.fetchUsers, preloadedAuthors, }, @@ -280,7 +314,7 @@ export default { title: TOKEN_TITLE_LABEL, icon: 'labels', token: LabelToken, - defaultLabels: [], + defaultLabels: DEFAULT_NONE_ANY, fetchLabels: this.fetchLabels, }, ]; @@ -310,7 +344,7 @@ export default { }); } - if (this.projectIterationsPath) { + if (this.hasIterationsFeature) { tokens.push({ type: TOKEN_TYPE_ITERATION, title: TOKEN_TITLE_ITERATION, @@ -329,6 +363,7 @@ export default { token: EpicToken, unique: true, idProperty: 'id', + useIdValue: true, fetchEpics: this.fetchEpics, }); } @@ -346,37 +381,28 @@ export default { return tokens; }, showPaginationControls() { - return this.issues.length > 0; + return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage); }, sortOptions() { return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature); }, tabCounts() { - return Object.values(IssuableStates).reduce( - (acc, state) => ({ - ...acc, - [state]: this.state === state ? this.totalIssues : undefined, - }), - {}, - ); + return { + [IssuableStates.Opened]: this.countOpened, + [IssuableStates.Closed]: this.countClosed, + [IssuableStates.All]: this.countAll, + }; + }, + currentTabCount() { + return this.tabCounts[this.state] ?? 0; }, urlParams() { - const filterParams = { - ...this.urlFilterParams, - }; - - if (filterParams.epic_id) { - filterParams.epic_id = encodeURIComponent(filterParams.epic_id); - } else if (filterParams['not[epic_id]']) { - filterParams['not[epic_id]'] = encodeURIComponent(filterParams['not[epic_id]']); - } - return { due_date: this.dueDateFilter, search: this.searchQuery, + sort: urlSortParams[this.sortKey], state: this.state, - ...urlSortParams[this.sortKey], - ...filterParams, + ...this.urlFilterParams, }; }, }, @@ -418,16 +444,42 @@ export default { : epics.filter((epic) => epic.id === number); }, fetchLabels(search) { - return this.fetchWithCache(this.projectLabelsPath, 'labels', 'title', search); + return this.$apollo + .query({ + query: searchLabelsQuery, + variables: { projectPath: this.projectPath, search }, + }) + .then(({ data }) => data.project.labels.nodes); }, fetchMilestones(search) { - return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true); + return this.$apollo + .query({ + query: searchMilestonesQuery, + variables: { projectPath: this.projectPath, search }, + }) + .then(({ data }) => data.project.milestones.nodes); }, fetchIterations(search) { - return axios.get(this.projectIterationsPath, { params: { search } }); + const id = Number(search); + const variables = + !search || Number.isNaN(id) + ? { projectPath: this.projectPath, search } + : { projectPath: this.projectPath, id }; + + return this.$apollo + .query({ + query: searchIterationsQuery, + variables, + }) + .then(({ data }) => data.project.iterations.nodes); }, fetchUsers(search) { - return axios.get(this.autocompleteUsersPath, { params: { search } }); + return this.$apollo + .query({ + query: searchUsersQuery, + variables: { projectPath: this.projectPath, search }, + }) + .then(({ data }) => data.project.projectMembers.nodes.map((member) => member.user)); }, getExportCsvPathWithQuery() { return `${this.exportCsvPath}${window.location.search}`; @@ -450,7 +502,9 @@ export default { }, async handleBulkUpdateClick() { if (!this.hasInitBulkEdit) { - const initBulkUpdateSidebar = await import('~/issuable_init_bulk_update_sidebar'); + const initBulkUpdateSidebar = await import( + '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar' + ); initBulkUpdateSidebar.default.init('issuable_'); const usersSelect = await import('~/users_select'); @@ -469,6 +523,7 @@ export default { this.state = state; }, handleFilter(filter) { + this.pageParams = initialPageParams; this.filterTokens = filter; }, handleNextPage() { @@ -581,7 +636,7 @@ export default { v-if="isSignedIn" class="gl-md-mr-3" :export-csv-path="exportCsvPathWithQuery" - :issuable-count="totalIssues" + :issuable-count="currentTabCount" /> <gl-button v-if="canBulkUpdate" @@ -609,7 +664,7 @@ export default { v-gl-tooltip class="gl-display-none gl-sm-display-block" :title="$options.i18n.relatedMergeRequests" - data-testid="issuable-mr" + data-testid="merge-requests" > <gl-icon name="merge-request" /> {{ issuable.mergeRequestsCount }} @@ -617,7 +672,7 @@ export default { <li v-if="issuable.upvotes" v-gl-tooltip - class="gl-display-none gl-sm-display-block" + class="issuable-upvotes gl-display-none gl-sm-display-block" :title="$options.i18n.upvotes" data-testid="issuable-upvotes" > @@ -627,7 +682,7 @@ export default { <li v-if="issuable.downvotes" v-gl-tooltip - class="gl-display-none gl-sm-display-block" + class="issuable-downvotes gl-display-none gl-sm-display-block" :title="$options.i18n.downvotes" data-testid="issuable-downvotes" > @@ -635,9 +690,10 @@ export default { {{ issuable.downvotes }} </li> <blocking-issues-count - class="gl-display-none gl-sm-display-block" - :blocking-issues-count="issuable.blockedByCount" + class="blocking-issues gl-display-none gl-sm-display-block" + :blocking-issues-count="issuable.blockingCount" :is-list-item="true" + data-testid="blocking-issues" /> </template> @@ -692,7 +748,7 @@ export default { <csv-import-export-buttons class="gl-mr-3" :export-csv-path="exportCsvPathWithQuery" - :issuable-count="totalIssues" + :issuable-count="currentTabCount" /> </template> </gl-empty-state> diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js index 76006f9081d..d94d4b9a19a 100644 --- a/app/assets/javascripts/issues_list/constants.js +++ b/app/assets/javascripts/issues_list/constants.js @@ -1,3 +1,5 @@ +import getIssuesCountQuery from 'ee_else_ce/issues_list/queries/get_issues_count.query.graphql'; +import createFlash from '~/flash'; import { __, s__ } from '~/locale'; import { FILTER_ANY, @@ -68,6 +70,7 @@ export const i18n = { confidentialYes: __('Yes'), downvotes: __('Downvotes'), editIssues: __('Edit issues'), + errorFetchingCounts: __('An error occurred while getting issue counts'), errorFetchingIssues: __('An error occurred while loading issues'), jiraIntegrationMessage: s__( 'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.', @@ -94,7 +97,7 @@ export const i18n = { relatedMergeRequests: __('Related merge requests'), reorderError: __('An error occurred while reordering issues.'), rssLabel: __('Subscribe to RSS feed'), - searchPlaceholder: __('Search or filter results…'), + searchPlaceholder: __('Search or filter results...'), upvotes: __('Upvotes'), }; @@ -128,21 +131,21 @@ export const CREATED_ASC = 'CREATED_ASC'; export const CREATED_DESC = 'CREATED_DESC'; export const DUE_DATE_ASC = 'DUE_DATE_ASC'; 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 MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC'; export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC'; export const POPULARITY_ASC = 'POPULARITY_ASC'; export const POPULARITY_DESC = 'POPULARITY_DESC'; +export const PRIORITY_ASC = 'PRIORITY_ASC'; export const PRIORITY_DESC = 'PRIORITY_DESC'; -export const RELATIVE_POSITION_DESC = 'RELATIVE_POSITION_DESC'; +export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC'; export const UPDATED_ASC = 'UPDATED_ASC'; export const UPDATED_DESC = 'UPDATED_DESC'; export const WEIGHT_ASC = 'WEIGHT_ASC'; export const WEIGHT_DESC = 'WEIGHT_DESC'; -const SORT_ASC = 'asc'; -const SORT_DESC = 'desc'; - +const PRIORITY_ASC_SORT = 'priority_asc'; const CREATED_DATE_SORT = 'created_date'; const CREATED_ASC_SORT = 'created_asc'; const UPDATED_DESC_SORT = 'updated_desc'; @@ -150,129 +153,30 @@ const UPDATED_ASC_SORT = 'updated_asc'; const MILESTONE_SORT = 'milestone'; const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc'; const DUE_DATE_DESC_SORT = 'due_date_desc'; +const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc'; const POPULARITY_ASC_SORT = 'popularity_asc'; const WEIGHT_DESC_SORT = 'weight_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 = { - [PRIORITY_DESC]: { - sort: PRIORITY, - }, - [CREATED_ASC]: { - sort: CREATED_ASC_SORT, - }, - [CREATED_DESC]: { - sort: CREATED_DATE_SORT, - }, - [UPDATED_ASC]: { - sort: UPDATED_ASC_SORT, - }, - [UPDATED_DESC]: { - sort: UPDATED_DESC_SORT, - }, - [MILESTONE_DUE_ASC]: { - sort: MILESTONE_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, - }, + [PRIORITY_ASC]: PRIORITY_ASC_SORT, + [PRIORITY_DESC]: PRIORITY, + [CREATED_ASC]: CREATED_ASC_SORT, + [CREATED_DESC]: CREATED_DATE_SORT, + [UPDATED_ASC]: UPDATED_ASC_SORT, + [UPDATED_DESC]: UPDATED_DESC_SORT, + [MILESTONE_DUE_ASC]: MILESTONE_SORT, + [MILESTONE_DUE_DESC]: MILESTONE_DUE_DESC_SORT, + [DUE_DATE_ASC]: DUE_DATE, + [DUE_DATE_DESC]: DUE_DATE_DESC_SORT, + [POPULARITY_ASC]: POPULARITY_ASC_SORT, + [POPULARITY_DESC]: POPULARITY, + [LABEL_PRIORITY_ASC]: LABEL_PRIORITY_ASC_SORT, + [LABEL_PRIORITY_DESC]: LABEL_PRIORITY, + [RELATIVE_POSITION_ASC]: RELATIVE_POSITION, + [WEIGHT_ASC]: WEIGHT, + [WEIGHT_DESC]: WEIGHT_DESC_SORT, + [BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT, }; export const MAX_LIST_SIZE = 10; @@ -297,12 +201,7 @@ export const TOKEN_TYPE_WEIGHT = 'weight'; export const filters = { [TOKEN_TYPE_AUTHOR]: { [API_PARAM]: { - [OPERATOR_IS]: { - [NORMAL_FILTER]: 'author_username', - }, - [OPERATOR_IS_NOT]: { - [NORMAL_FILTER]: 'not[author_username]', - }, + [NORMAL_FILTER]: 'authorUsername', }, [URL_PARAM]: { [OPERATOR_IS]: { @@ -315,13 +214,8 @@ export const filters = { }, [TOKEN_TYPE_ASSIGNEE]: { [API_PARAM]: { - [OPERATOR_IS]: { - [NORMAL_FILTER]: 'assignee_username', - [SPECIAL_FILTER]: 'assignee_id', - }, - [OPERATOR_IS_NOT]: { - [NORMAL_FILTER]: 'not[assignee_username]', - }, + [NORMAL_FILTER]: 'assigneeUsernames', + [SPECIAL_FILTER]: 'assigneeId', }, [URL_PARAM]: { [OPERATOR_IS]: { @@ -336,12 +230,7 @@ export const filters = { }, [TOKEN_TYPE_MILESTONE]: { [API_PARAM]: { - [OPERATOR_IS]: { - [NORMAL_FILTER]: 'milestone', - }, - [OPERATOR_IS_NOT]: { - [NORMAL_FILTER]: 'not[milestone]', - }, + [NORMAL_FILTER]: 'milestoneTitle', }, [URL_PARAM]: { [OPERATOR_IS]: { @@ -354,16 +243,13 @@ export const filters = { }, [TOKEN_TYPE_LABEL]: { [API_PARAM]: { - [OPERATOR_IS]: { - [NORMAL_FILTER]: 'labels', - }, - [OPERATOR_IS_NOT]: { - [NORMAL_FILTER]: 'not[labels]', - }, + [NORMAL_FILTER]: 'labelName', + [SPECIAL_FILTER]: 'labelName', }, [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'label_name[]', + [SPECIAL_FILTER]: 'label_name[]', }, [OPERATOR_IS_NOT]: { [NORMAL_FILTER]: 'not[label_name][]', @@ -372,10 +258,8 @@ export const filters = { }, [TOKEN_TYPE_MY_REACTION]: { [API_PARAM]: { - [OPERATOR_IS]: { - [NORMAL_FILTER]: 'my_reaction_emoji', - [SPECIAL_FILTER]: 'my_reaction_emoji', - }, + [NORMAL_FILTER]: 'myReactionEmoji', + [SPECIAL_FILTER]: 'myReactionEmoji', }, [URL_PARAM]: { [OPERATOR_IS]: { @@ -386,9 +270,7 @@ export const filters = { }, [TOKEN_TYPE_CONFIDENTIAL]: { [API_PARAM]: { - [OPERATOR_IS]: { - [NORMAL_FILTER]: 'confidential', - }, + [NORMAL_FILTER]: 'confidential', }, [URL_PARAM]: { [OPERATOR_IS]: { @@ -398,33 +280,23 @@ export const filters = { }, [TOKEN_TYPE_ITERATION]: { [API_PARAM]: { - [OPERATOR_IS]: { - [NORMAL_FILTER]: 'iteration_title', - [SPECIAL_FILTER]: 'iteration_id', - }, - [OPERATOR_IS_NOT]: { - [NORMAL_FILTER]: 'not[iteration_title]', - }, + [NORMAL_FILTER]: 'iterationId', + [SPECIAL_FILTER]: 'iterationWildcardId', }, [URL_PARAM]: { [OPERATOR_IS]: { - [NORMAL_FILTER]: 'iteration_title', + [NORMAL_FILTER]: 'iteration_id', [SPECIAL_FILTER]: 'iteration_id', }, [OPERATOR_IS_NOT]: { - [NORMAL_FILTER]: 'not[iteration_title]', + [NORMAL_FILTER]: 'not[iteration_id]', }, }, }, [TOKEN_TYPE_EPIC]: { [API_PARAM]: { - [OPERATOR_IS]: { - [NORMAL_FILTER]: 'epic_id', - [SPECIAL_FILTER]: 'epic_id', - }, - [OPERATOR_IS_NOT]: { - [NORMAL_FILTER]: 'not[epic_id]', - }, + [NORMAL_FILTER]: 'epicId', + [SPECIAL_FILTER]: 'epicId', }, [URL_PARAM]: { [OPERATOR_IS]: { @@ -438,13 +310,8 @@ export const filters = { }, [TOKEN_TYPE_WEIGHT]: { [API_PARAM]: { - [OPERATOR_IS]: { - [NORMAL_FILTER]: 'weight', - [SPECIAL_FILTER]: 'weight', - }, - [OPERATOR_IS_NOT]: { - [NORMAL_FILTER]: 'not[weight]', - }, + [NORMAL_FILTER]: 'weight', + [SPECIAL_FILTER]: 'weight', }, [URL_PARAM]: { [OPERATOR_IS]: { @@ -457,3 +324,15 @@ export const filters = { }, }, }; + +export const issuesCountSmartQueryBase = { + query: getIssuesCountQuery, + context: { + isSingleRequest: true, + }, + update: ({ project }) => project?.issues.count, + error(error) { + createFlash({ message: i18n.errorFetchingCounts, captureError: true, error }); + }, + debounce: 200, +}; diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js index 97b9a9a115d..71ceb9bef55 100644 --- a/app/assets/javascripts/issues_list/index.js +++ b/app/assets/javascripts/issues_list/index.js @@ -1,6 +1,5 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { IssuableType } from '~/issue_show/constants'; import IssuesListApp from '~/issues_list/components/issues_list_app.vue'; import createDefaultClient from '~/lib/graphql'; import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; @@ -82,7 +81,6 @@ export function mountIssuesListApp() { const { autocompleteAwardEmojisPath, - autocompleteUsersPath, calendarPath, canBulkUpdate, canEdit, @@ -95,6 +93,7 @@ export function mountIssuesListApp() { hasBlockedIssuesFeature, hasIssuableHealthStatusFeature, hasIssueWeightsFeature, + hasIterationsFeature, hasMultipleIssueAssigneesFeature, hasProjectIssues, importCsvIssuesPath, @@ -106,9 +105,6 @@ export function mountIssuesListApp() { maxAttachmentSize, newIssuePath, projectImportJiraPath, - projectIterationsPath, - projectLabelsPath, - projectMilestonesPath, projectPath, quickActionsHelpPath, resetPath, @@ -122,7 +118,6 @@ export function mountIssuesListApp() { apolloProvider, provide: { autocompleteAwardEmojisPath, - autocompleteUsersPath, calendarPath, canBulkUpdate: parseBoolean(canBulkUpdate), emptyStateSvgPath, @@ -130,15 +125,13 @@ export function mountIssuesListApp() { hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature), hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature), hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature), + hasIterationsFeature: parseBoolean(hasIterationsFeature), hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature), hasProjectIssues: parseBoolean(hasProjectIssues), isSignedIn: parseBoolean(isSignedIn), issuesPath, jiraIntegrationPath, newIssuePath, - projectIterationsPath, - projectLabelsPath, - projectMilestonesPath, projectPath, rssPath, showNewIssueLink: parseBoolean(showNewIssueLink), @@ -156,7 +149,6 @@ export function mountIssuesListApp() { // For IssuableByEmail component emailsHelpPagePath, initialEmail, - issuableType: IssuableType.Issue, markdownHelpPath, quickActionsHelpPath, resetPath, diff --git a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql index afd53084ca0..124190915c0 100644 --- a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql +++ b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql @@ -2,6 +2,7 @@ #import "./issue.fragment.graphql" query getProjectIssues( + $isSignedIn: Boolean = false $projectPath: ID! $search: String $sort: IssueSort @@ -33,7 +34,6 @@ query getProjectIssues( first: $firstPageSize last: $lastPageSize ) { - count pageInfo { ...PageInfo } diff --git a/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql new file mode 100644 index 00000000000..a1742859640 --- /dev/null +++ b/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql @@ -0,0 +1,26 @@ +query getProjectIssuesCount( + $projectPath: ID! + $search: String + $state: IssuableState + $assigneeId: String + $assigneeUsernames: [String!] + $authorUsername: String + $labelName: [String] + $milestoneTitle: [String] + $not: NegatedIssueFilterInput +) { + project(fullPath: $projectPath) { + issues( + search: $search + state: $state + assigneeId: $assigneeId + assigneeUsernames: $assigneeUsernames + authorUsername: $authorUsername + labelName: $labelName + milestoneTitle: $milestoneTitle + not: $not + ) { + count + } + } +} diff --git a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql index de30d8b4bf6..f7ebf64ffb8 100644 --- a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql +++ b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql @@ -11,7 +11,7 @@ fragment IssueFragment on Issue { title updatedAt upvotes - userDiscussionsCount + userDiscussionsCount @include(if: $isSignedIn) webUrl assignees { nodes { diff --git a/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql b/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql new file mode 100644 index 00000000000..11d9dcea573 --- /dev/null +++ b/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql @@ -0,0 +1,10 @@ +query searchIterations($projectPath: ID!, $search: String, $id: ID) { + project(fullPath: $projectPath) { + iterations(title: $search, id: $id) { + nodes { + id + title + } + } + } +} diff --git a/app/assets/javascripts/issues_list/queries/search_labels.query.graphql b/app/assets/javascripts/issues_list/queries/search_labels.query.graphql new file mode 100644 index 00000000000..de884e1221c --- /dev/null +++ b/app/assets/javascripts/issues_list/queries/search_labels.query.graphql @@ -0,0 +1,12 @@ +query searchLabels($projectPath: ID!, $search: String) { + project(fullPath: $projectPath) { + labels(searchTerm: $search, includeAncestorGroups: true) { + nodes { + id + color + textColor + title + } + } + } +} diff --git a/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql b/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql new file mode 100644 index 00000000000..91f74fd220b --- /dev/null +++ b/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql @@ -0,0 +1,10 @@ +query searchMilestones($projectPath: ID!, $search: String) { + project(fullPath: $projectPath) { + milestones(searchTitle: $search, includeAncestors: true) { + nodes { + id + title + } + } + } +} diff --git a/app/assets/javascripts/issues_list/queries/search_users.query.graphql b/app/assets/javascripts/issues_list/queries/search_users.query.graphql new file mode 100644 index 00000000000..953157cfe3a --- /dev/null +++ b/app/assets/javascripts/issues_list/queries/search_users.query.graphql @@ -0,0 +1,14 @@ +query searchUsers($projectPath: ID!, $search: String) { + project(fullPath: $projectPath) { + projectMembers(search: $search) { + nodes { + user { + id + avatarUrl + name + username + } + } + } + } +} diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js index b5ec44198da..49f937cc453 100644 --- a/app/assets/javascripts/issues_list/utils.js +++ b/app/assets/javascripts/issues_list/utils.js @@ -1,4 +1,5 @@ import { + API_PARAM, BLOCKING_ISSUES_DESC, CREATED_ASC, CREATED_DESC, @@ -6,29 +7,36 @@ import { DUE_DATE_DESC, DUE_DATE_VALUES, filters, + LABEL_PRIORITY_ASC, LABEL_PRIORITY_DESC, MILESTONE_DUE_ASC, MILESTONE_DUE_DESC, NORMAL_FILTER, POPULARITY_ASC, POPULARITY_DESC, + PRIORITY_ASC, PRIORITY_DESC, - RELATIVE_POSITION_DESC, + RELATIVE_POSITION_ASC, SPECIAL_FILTER, SPECIAL_FILTER_VALUES, TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_ITERATION, UPDATED_ASC, UPDATED_DESC, + URL_PARAM, urlSortParams, 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'; +import { + FILTERED_SEARCH_TERM, + OPERATOR_IS_NOT, +} from '~/vue_shared/components/filtered_search_bar/constants'; export const getSortKey = (sort) => - Object.keys(urlSortParams).find((key) => urlSortParams[key].sort === sort); + Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort); export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined); @@ -38,7 +46,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) id: 1, title: __('Priority'), sortDirection: { - ascending: PRIORITY_DESC, + ascending: PRIORITY_ASC, descending: PRIORITY_DESC, }, }, @@ -86,7 +94,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) id: 7, title: __('Label priority'), sortDirection: { - ascending: LABEL_PRIORITY_DESC, + ascending: LABEL_PRIORITY_ASC, descending: LABEL_PRIORITY_DESC, }, }, @@ -94,8 +102,8 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) id: 8, title: __('Manual'), sortDirection: { - ascending: RELATIVE_POSITION_DESC, - descending: RELATIVE_POSITION_DESC, + ascending: RELATIVE_POSITION_ASC, + descending: RELATIVE_POSITION_ASC, }, }, ]; @@ -128,7 +136,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) const tokenTypes = Object.keys(filters); const getUrlParams = (tokenType) => - Object.values(filters[tokenType].urlParam).flatMap((filterObj) => Object.values(filterObj)); + Object.values(filters[tokenType][URL_PARAM]).flatMap((filterObj) => Object.values(filterObj)); const urlParamKeys = tokenTypes.flatMap(getUrlParams); @@ -136,7 +144,7 @@ const getTokenTypeFromUrlParamKey = (urlParamKey) => tokenTypes.find((tokenType) => getUrlParams(tokenType).includes(urlParamKey)); const getOperatorFromUrlParamKey = (tokenType, urlParamKey) => - Object.entries(filters[tokenType].urlParam).find(([, filterObj]) => + Object.entries(filters[tokenType][URL_PARAM]).find(([, filterObj]) => Object.values(filterObj).includes(urlParamKey), )[0]; @@ -178,12 +186,36 @@ const getFilterType = (data, tokenType = '') => ? SPECIAL_FILTER : NORMAL_FILTER; -export const convertToParams = (filterTokens, paramType) => +const isIterationSpecialValue = (tokenType, value) => + 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 .filter((token) => token.type !== FILTERED_SEARCH_TERM) .reduce((acc, token) => { const filterType = getFilterType(token.value.data, token.type); - const param = filters[token.type][paramType][token.value.operator]?.[filterType]; + const param = filters[token.type][URL_PARAM][token.value.operator]?.[filterType]; return Object.assign(acc, { [param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data, }); |