diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-23 06:09:40 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-23 06:09:40 +0300 |
commit | d66f7a4222f48b51c468c3e0b4f1824c9457345e (patch) | |
tree | e6fe58e902027e003d90d4fe5ffb0697b5d1c483 /app | |
parent | 814fd46dfdf3493c06007653774327ddb2f02938 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/issues_list/components/issues_list_app.vue | 141 | ||||
-rw-r--r-- | app/assets/javascripts/issues_list/constants.js | 69 | ||||
-rw-r--r-- | app/assets/javascripts/issues_list/index.js | 8 | ||||
-rw-r--r-- | app/assets/javascripts/issues_list/utils.js | 74 | ||||
-rw-r--r-- | app/helpers/issues_helper.rb | 4 | ||||
-rw-r--r-- | app/models/project_services/ewm_service.rb | 9 |
6 files changed, 244 insertions, 61 deletions
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 57c5107fcbb..4b7b12f6b99 100644 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue @@ -1,5 +1,6 @@ <script> import { GlButton, GlEmptyState, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import fuzzaldrinPlus from 'fuzzaldrin-plus'; import { toNumber } from 'lodash'; import createFlash from '~/flash'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; @@ -7,50 +8,35 @@ import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants'; import { CREATED_DESC, + i18n, + MAX_LIST_SIZE, PAGE_SIZE, RELATIVE_POSITION_ASC, sortOptions, sortParams, } from '~/issues_list/constants'; +import { + convertToApiParams, + convertToSearchQuery, + convertToUrlParams, + getFilterTokens, + getSortKey, +} from '~/issues_list/utils'; import axios from '~/lib/utils/axios_utils'; import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils'; -import { __, s__ } from '~/locale'; +import { __ } from '~/locale'; +import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; +import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import eventHub from '../eventhub'; import IssueCardTimeInfo from './issue_card_time_info.vue'; export default { CREATED_DESC, + i18n, IssuableListTabs, PAGE_SIZE, sortOptions, sortParams, - i18n: { - calendarLabel: __('Subscribe to calendar'), - jiraIntegrationMessage: s__( - 'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.', - ), - jiraIntegrationSecondaryMessage: s__('JiraService|This feature requires a Premium plan.'), - jiraIntegrationTitle: s__('JiraService|Using Jira for issue tracking?'), - newIssueLabel: __('New issue'), - noClosedIssuesTitle: __('There are no closed issues'), - noOpenIssuesDescription: __('To keep this project going, create a new issue'), - noOpenIssuesTitle: __('There are no open issues'), - noIssuesSignedInDescription: __( - 'Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.', - ), - noIssuesSignedInTitle: __( - 'The Issue Tracker is the place to add things that need to be improved or solved in a project', - ), - noIssuesSignedOutButtonText: __('Register / Sign In'), - noIssuesSignedOutDescription: __( - 'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.', - ), - noIssuesSignedOutTitle: __('There are no issues to show'), - noSearchResultsDescription: __('To widen your search, change or remove filters above'), - noSearchResultsTitle: __('Sorry, your filter produced no results'), - reorderError: __('An error occurred while reordering issues.'), - rssLabel: __('Subscribe to RSS feed'), - }, components: { CsvImportExportButtons, GlButton, @@ -66,6 +52,9 @@ export default { GlTooltip: GlTooltipDirective, }, inject: { + autocompleteUsersPath: { + default: '', + }, calendarPath: { default: '', }, @@ -81,9 +70,6 @@ export default { exportCsvPath: { default: '', }, - fullPath: { - default: '', - }, hasIssues: { default: false, }, @@ -99,6 +85,12 @@ export default { newIssuePath: { default: '', }, + projectLabelsPath: { + default: '', + }, + projectPath: { + default: '', + }, rssPath: { default: '', }, @@ -112,27 +104,15 @@ export default { data() { const orderBy = getParameterByName('order_by'); const sort = getParameterByName('sort'); - const sortKey = Object.keys(sortParams).find( - (key) => sortParams[key].order_by === orderBy && sortParams[key].sort === sort, - ); - - const search = getParameterByName('search') || ''; - const tokens = search.split(' ').map((searchWord) => ({ - type: 'filtered-search-term', - value: { - data: searchWord, - }, - })); return { exportCsvPathWithQuery: this.getExportCsvPathWithQuery(), - filters: sortParams[sortKey] || {}, - filterTokens: tokens, + filterTokens: getFilterTokens(window.location.search), isLoading: false, issues: [], page: toNumber(getParameterByName('page')) || 1, showBulkEditSidebar: false, - sortKey: sortKey || CREATED_DESC, + sortKey: getSortKey(orderBy, sort) || CREATED_DESC, state: getParameterByName('state') || IssuableStates.Opened, totalIssues: 0, }; @@ -144,13 +124,46 @@ export default { isOpenTab() { return this.state === IssuableStates.Opened; }, + apiFilterParams() { + return convertToApiParams(this.filterTokens); + }, + urlFilterParams() { + return convertToUrlParams(this.filterTokens); + }, searchQuery() { - return ( - this.filterTokens - .map((searchTerm) => searchTerm.value.data) - .filter((searchWord) => Boolean(searchWord)) - .join(' ') || undefined - ); + return convertToSearchQuery(this.filterTokens) || undefined; + }, + searchTokens() { + return [ + { + type: 'author_username', + title: __('Author'), + icon: 'pencil', + token: AuthorToken, + dataType: 'user', + unique: true, + defaultAuthors: [], + fetchAuthors: this.fetchUsers, + }, + { + type: 'assignee_username', + title: __('Assignee'), + icon: 'user', + token: AuthorToken, + dataType: 'user', + unique: true, + defaultAuthors: [], + fetchAuthors: this.fetchUsers, + }, + { + type: 'labels', + title: __('Label'), + icon: 'labels', + token: LabelToken, + defaultLabels: [], + fetchLabels: this.fetchLabels, + }, + ]; }, showPaginationControls() { return this.issues.length > 0; @@ -169,7 +182,8 @@ export default { page: this.page, search: this.searchQuery, state: this.state, - ...this.filters, + ...sortParams[this.sortKey], + ...this.urlFilterParams, }; }, }, @@ -184,6 +198,21 @@ export default { eventHub.$off('issuables:toggleBulkEdit'); }, methods: { + fetchLabels(search) { + if (this.labelsCache) { + return search + ? Promise.resolve(fuzzaldrinPlus.filter(this.labelsCache, search, { key: 'title' })) + : Promise.resolve(this.labelsCache.slice(0, MAX_LIST_SIZE)); + } + + return axios.get(this.projectLabelsPath).then(({ data }) => { + this.labelsCache = data; + return data.slice(0, MAX_LIST_SIZE); + }); + }, + fetchUsers(search) { + return axios.get(this.autocompleteUsersPath, { params: { search } }); + }, fetchIssues() { if (!this.hasIssues) { return undefined; @@ -199,7 +228,8 @@ export default { search: this.searchQuery, state: this.state, with_labels_details: true, - ...this.filters, + ...sortParams[this.sortKey], + ...this.apiFilterParams, }, }) .then(({ data, headers }) => { @@ -278,7 +308,6 @@ export default { }, handleSort(value) { this.sortKey = value; - this.filters = sortParams[value]; this.fetchIssues(); }, }, @@ -288,10 +317,10 @@ export default { <template> <issuable-list v-if="hasIssues" - :namespace="fullPath" + :namespace="projectPath" recent-searches-storage-key="issues" :search-input-placeholder="__('Search or filter results…')" - :search-tokens="[]" + :search-tokens="searchTokens" :initial-filter-value="filterTokens" :sort-options="$options.sortOptions" :initial-sort-by="sortKey" diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js index f6f23af80ba..4a460859201 100644 --- a/app/assets/javascripts/issues_list/constants.js +++ b/app/assets/javascripts/issues_list/constants.js @@ -1,4 +1,4 @@ -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; // Maps sort order as it appears in the URL query to API `order_by` and `sort` params. const PRIORITY = 'priority'; @@ -53,6 +53,34 @@ export const availableSortOptionsJira = [ }, ]; +export const i18n = { + calendarLabel: __('Subscribe to calendar'), + jiraIntegrationMessage: s__( + 'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.', + ), + jiraIntegrationSecondaryMessage: s__('JiraService|This feature requires a Premium plan.'), + jiraIntegrationTitle: s__('JiraService|Using Jira for issue tracking?'), + newIssueLabel: __('New issue'), + noClosedIssuesTitle: __('There are no closed issues'), + noOpenIssuesDescription: __('To keep this project going, create a new issue'), + noOpenIssuesTitle: __('There are no open issues'), + noIssuesSignedInDescription: __( + 'Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.', + ), + noIssuesSignedInTitle: __( + 'The Issue Tracker is the place to add things that need to be improved or solved in a project', + ), + noIssuesSignedOutButtonText: __('Register / Sign In'), + noIssuesSignedOutDescription: __( + 'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.', + ), + noIssuesSignedOutTitle: __('There are no issues to show'), + noSearchResultsDescription: __('To widen your search, change or remove filters above'), + noSearchResultsTitle: __('Sorry, your filter produced no results'), + reorderError: __('An error occurred while reordering issues.'), + rssLabel: __('Subscribe to RSS feed'), +}; + export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map'; export const BLOCKING_ISSUES_ASC = 'BLOCKING_ISSUES_ASC'; @@ -242,3 +270,42 @@ export const sortOptions = [ }, }, ]; + +export const MAX_LIST_SIZE = 10; + +export const FILTERED_SEARCH_TERM = 'filtered-search-term'; +export const OPERATOR_IS = '='; +export const OPERATOR_IS_NOT = '!='; + +export const filters = { + author_username: { + apiParam: { + [OPERATOR_IS]: 'author_username', + [OPERATOR_IS_NOT]: 'not[author_username]', + }, + urlParam: { + [OPERATOR_IS]: 'author_username', + [OPERATOR_IS_NOT]: 'not[author_username]', + }, + }, + assignee_username: { + apiParam: { + [OPERATOR_IS]: 'assignee_username', + [OPERATOR_IS_NOT]: 'not[assignee_username]', + }, + urlParam: { + [OPERATOR_IS]: 'assignee_username[]', + [OPERATOR_IS_NOT]: 'not[assignee_username][]', + }, + }, + labels: { + apiParam: { + [OPERATOR_IS]: 'labels', + [OPERATOR_IS_NOT]: 'not[labels]', + }, + urlParam: { + [OPERATOR_IS]: 'label_name[]', + [OPERATOR_IS_NOT]: 'not[label_name][]', + }, + }, +}; diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js index 0b64df50691..85b64be7718 100644 --- a/app/assets/javascripts/issues_list/index.js +++ b/app/assets/javascripts/issues_list/index.js @@ -73,6 +73,7 @@ export function initIssuesListApp() { } const { + autocompleteUsersPath, calendarPath, canBulkUpdate, canEdit, @@ -81,7 +82,6 @@ export function initIssuesListApp() { emptyStateSvgPath, endpoint, exportCsvPath, - fullPath, hasBlockedIssuesFeature, hasIssuableHealthStatusFeature, hasIssues, @@ -93,6 +93,8 @@ export function initIssuesListApp() { maxAttachmentSize, newIssuePath, projectImportJiraPath, + projectLabelsPath, + projectPath, rssPath, showNewIssueLink, signInPath, @@ -104,11 +106,11 @@ export function initIssuesListApp() { // issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153 apolloProvider: {}, provide: { + autocompleteUsersPath, calendarPath, canBulkUpdate: parseBoolean(canBulkUpdate), emptyStateSvgPath, endpoint, - fullPath, hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature), hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature), hasIssues: parseBoolean(hasIssues), @@ -117,6 +119,8 @@ export function initIssuesListApp() { issuesPath, jiraIntegrationPath, newIssuePath, + projectLabelsPath, + projectPath, rssPath, showNewIssueLink: parseBoolean(showNewIssueLink), signInPath, diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js new file mode 100644 index 00000000000..f470ccb5e06 --- /dev/null +++ b/app/assets/javascripts/issues_list/utils.js @@ -0,0 +1,74 @@ +import { FILTERED_SEARCH_TERM, filters, sortParams } from '~/issues_list/constants'; + +export const getSortKey = (orderBy, sort) => + Object.keys(sortParams).find( + (key) => sortParams[key].order_by === orderBy && sortParams[key].sort === sort, + ); + +const tokenTypes = Object.keys(filters); + +const urlParamKeys = tokenTypes.flatMap((key) => Object.values(filters[key].urlParam)); + +const getTokenTypeFromUrlParamKey = (urlParamKey) => + tokenTypes.find((key) => Object.values(filters[key].urlParam).includes(urlParamKey)); + +const getOperatorFromUrlParamKey = (tokenType, urlParamKey) => + Object.entries(filters[tokenType].urlParam).find(([, urlParam]) => urlParam === urlParamKey)[0]; + +const convertToFilteredTokens = (locationSearch) => + Array.from(new URLSearchParams(locationSearch).entries()) + .filter(([key]) => urlParamKeys.includes(key)) + .map(([key, data]) => { + const type = getTokenTypeFromUrlParamKey(key); + const operator = getOperatorFromUrlParamKey(type, key); + return { + type, + value: { data, operator }, + }; + }); + +const convertToFilteredSearchTerms = (locationSearch) => + new URLSearchParams(locationSearch) + .get('search') + ?.split(' ') + .map((word) => ({ + type: FILTERED_SEARCH_TERM, + value: { + data: word, + }, + })) || []; + +export const getFilterTokens = (locationSearch) => { + if (!locationSearch) { + return []; + } + const filterTokens = convertToFilteredTokens(locationSearch); + const searchTokens = convertToFilteredSearchTerms(locationSearch); + return filterTokens.concat(searchTokens); +}; + +export const convertToApiParams = (filterTokens) => + filterTokens + .filter((token) => token.type !== FILTERED_SEARCH_TERM) + .reduce((acc, token) => { + const apiParam = filters[token.type].apiParam[token.value.operator]; + return Object.assign(acc, { + [apiParam]: acc[apiParam] ? `${acc[apiParam]},${token.value.data}` : token.value.data, + }); + }, {}); + +export const convertToUrlParams = (filterTokens) => + filterTokens + .filter((token) => token.type !== FILTERED_SEARCH_TERM) + .reduce((acc, token) => { + const urlParam = filters[token.type].urlParam[token.value.operator]; + return Object.assign(acc, { + [urlParam]: acc[urlParam] ? acc[urlParam].concat(token.value.data) : [token.value.data], + }); + }, {}); + +export const convertToSearchQuery = (filterTokens) => + filterTokens + .filter((token) => token.type === FILTERED_SEARCH_TERM && token.value.data) + .map((token) => token.value.data) + .join(' '); diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 0a83e707412..2296fe91fba 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -165,6 +165,7 @@ module IssuesHelper def issues_list_data(project, current_user, finder) { + autocomplete_users_path: autocomplete_users_path(active: true, current_user: true, project_id: project.id, format: :json), calendar_path: url_for(safe_params.merge(calendar_url_options)), can_bulk_update: can?(current_user, :admin_issue, project).to_s, can_edit: can?(current_user, :admin_project, project).to_s, @@ -173,7 +174,6 @@ module IssuesHelper 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), - full_path: project.full_path, has_issues: project_issues(project).exists?.to_s, import_csv_issues_path: import_csv_namespace_project_issues_path, is_signed_in: current_user.present?.to_s, @@ -182,6 +182,8 @@ module IssuesHelper max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes), new_issue_path: new_project_issue_path(project, issue: { assignee_id: finder.assignee.try(:id), milestone_id: finder.milestones.first.try(:id) }), project_import_jira_path: project_import_jira_path(project), + project_labels_path: project_labels_path(project, include_ancestor_groups: true, format: :json), + project_path: project.full_path, rss_path: url_for(safe_params.merge(rss_url_options)), show_new_issue_link: show_new_issue_link?(project).to_s, sign_in_path: new_user_session_path diff --git a/app/models/project_services/ewm_service.rb b/app/models/project_services/ewm_service.rb index af402e50292..90fcbb10d2b 100644 --- a/app/models/project_services/ewm_service.rb +++ b/app/models/project_services/ewm_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class EwmService < IssueTrackerService + include ActionView::Helpers::UrlHelper + validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? def self.reference_pattern(only_long: true) @@ -12,7 +14,12 @@ class EwmService < IssueTrackerService end def description - s_('IssueTracker|EWM work items tracker') + s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.") + end + + def help + docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/ewm'), target: '_blank', rel: 'noopener noreferrer' + s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } end def self.to_param |