Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-23 06:09:40 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-23 06:09:40 +0300
commitd66f7a4222f48b51c468c3e0b4f1824c9457345e (patch)
treee6fe58e902027e003d90d4fe5ffb0697b5d1c483 /app
parent814fd46dfdf3493c06007653774327ddb2f02938 (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.vue141
-rw-r--r--app/assets/javascripts/issues_list/constants.js69
-rw-r--r--app/assets/javascripts/issues_list/index.js8
-rw-r--r--app/assets/javascripts/issues_list/utils.js74
-rw-r--r--app/helpers/issues_helper.rb4
-rw-r--r--app/models/project_services/ewm_service.rb9
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