Commit 79850719 authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖

Add latest changes from gitlab-org/[email protected]

parent d05604c9
......@@ -1139,9 +1139,6 @@ Rails/SaveBang:
- 'spec/services/emails/confirm_service_spec.rb'
- 'spec/services/groups/destroy_service_spec.rb'
- 'spec/services/groups/import_export/import_service_spec.rb'
- 'spec/services/issuable/bulk_update_service_spec.rb'
- 'spec/services/issuable/clone/attributes_rewriter_spec.rb'
- 'spec/services/issuable/common_system_notes_service_spec.rb'
- 'spec/services/labels/promote_service_spec.rb'
- 'spec/services/notes/create_service_spec.rb'
- 'spec/services/notification_recipients/build_service_spec.rb'
......@@ -1160,7 +1157,6 @@ Rails/SaveBang:
- 'spec/services/projects/unlink_fork_service_spec.rb'
- 'spec/services/projects/update_pages_service_spec.rb'
- 'spec/services/projects/update_service_spec.rb'
- 'spec/services/quick_actions/interpret_service_spec.rb'
- 'spec/services/reset_project_cache_service_spec.rb'
- 'spec/services/resource_events/change_milestone_service_spec.rb'
- 'spec/services/system_hooks_service_spec.rb'
......
2f16d97afa2e8accb4144f04e2e1e90bf4d1e9fb
40de6ac3d3e6db6a5fd85b63ff3ae8f60aece271
......@@ -209,15 +209,14 @@ export default {
<div v-safe-html="errorMessage" class="nothing-here-block"></div>
</div>
<template v-else>
<div v-show="isCollapsed" class="gl-p-7 gl-text-center collapsed-file-warning">
<div
v-show="isCollapsed"
class="gl-p-7 gl-bg-gray-10 gl-text-center collapsed-file-warning"
>
<p class="gl-mb-8 gl-mt-5">
{{ $options.i18n.collapsed }}
</p>
<gl-button
class="gl-alert-action gl-mb-5"
data-testid="expandButton"
@click="handleToggle"
>
<gl-button class="gl-mb-5" data-testid="expandButton" @click="handleToggle">
{{ $options.i18n.expand }}
</gl-button>
</div>
......
......@@ -8,7 +8,6 @@ import {
GlAvatar,
GlTooltipDirective,
GlButton,
GlSearchBoxByType,
GlIcon,
GlPagination,
GlTabs,
......@@ -16,16 +15,25 @@ import {
GlBadge,
GlEmptyState,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import {
visitUrl,
mergeUrlParams,
joinPaths,
updateHistory,
setUrlParams,
} from '~/lib/utils/url_utility';
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATUS_TABS } from '../constants';
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_STATUS_TABS } from '../constants';
const TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
const tdClass =
......@@ -82,7 +90,6 @@ export default {
GlAvatar,
GlButton,
TimeAgoTooltip,
GlSearchBoxByType,
GlIcon,
GlPagination,
GlTabs,
......@@ -91,6 +98,7 @@ export default {
GlBadge,
GlEmptyState,
SeverityToken,
FilteredSearchBar,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -103,6 +111,9 @@ export default {
'issuePath',
'publishedAvailable',
'emptyListSvgPath',
'textQuery',
'authorUsernamesQuery',
'assigneeUsernamesQuery',
],
apollo: {
incidents: {
......@@ -118,6 +129,8 @@ export default {
lastPageSize: this.pagination.lastPageSize,
prevPageCursor: this.pagination.prevPageCursor,
nextPageCursor: this.pagination.nextPageCursor,
authorUsername: this.authorUsername,
assigneeUsernames: this.assigneeUsernames,
};
},
update({ project: { issues: { nodes = [], pageInfo = {} } = {} } = {} }) {
......@@ -135,6 +148,8 @@ export default {
variables() {
return {
searchTerm: this.searchTerm,
authorUsername: this.authorUsername,
assigneeUsernames: this.assigneeUsernames,
projectPath: this.projectPath,
issueTypes: ['INCIDENT'],
};
......@@ -149,7 +164,7 @@ export default {
errored: false,
isErrorAlertDismissed: false,
redirecting: false,
searchTerm: '',
searchTerm: this.textQuery,
pagination: initialPaginationState,
incidents: {},
sort: 'created_desc',
......@@ -157,6 +172,9 @@ export default {
sortDesc: true,
statusFilter: '',
filteredByStatus: '',
authorUsername: this.authorUsernamesQuery,
assigneeUsernames: this.assigneeUsernamesQuery,
filterParams: {},
};
},
computed: {
......@@ -242,14 +260,57 @@ export default {
btnText: createIncidentBtnLabel,
};
},
filteredSearchTokens() {
return [
{
type: 'author_username',
icon: 'user',
title: __('Author'),
unique: true,
symbol: '@',
token: AuthorToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
fetchPath: this.projectPath,
fetchAuthors: Api.projectUsers.bind(Api),
},
{
type: 'assignee_username',
icon: 'user',
title: __('Assignees'),
unique: true,
symbol: '@',
token: AuthorToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
fetchPath: this.projectPath,
fetchAuthors: Api.projectUsers.bind(Api),
},
];
},
filteredSearchValue() {
const value = [];
if (this.authorUsername) {
value.push({
type: 'author_username',
value: { data: this.authorUsername },
});
}
if (this.assigneeUsernames) {
value.push({
type: 'assignee_username',
value: { data: this.assigneeUsernames },
});
}
if (this.searchTerm) {
value.push(this.searchTerm);
}
return value;
},
},
methods: {
onInputChange: debounce(function debounceSearch(input) {
const trimmedInput = input.trim();
if (trimmedInput !== this.searchTerm) {
this.searchTerm = trimmedInput;
}
}, INCIDENT_SEARCH_DELAY),
filterIncidentsByStatus(tabIndex) {
const { filters, status } = this.$options.statusTabs[tabIndex];
this.statusFilter = filters;
......@@ -292,6 +353,61 @@ export default {
getSeverity(severity) {
return INCIDENT_SEVERITY[severity];
},
handleFilterIncidents(filters) {
const filterParams = { authorUsername: '', assigneeUsername: [], search: '' };
filters.forEach(filter => {
if (typeof filter === 'object') {
switch (filter.type) {
case 'author_username':
filterParams.authorUsername = filter.value.data;
break;
case 'assignee_username':
filterParams.assigneeUsername.push(filter.value.data);
break;
case 'filtered-search-term':
if (filter.value.data !== '') filterParams.search = filter.value.data;
break;
default:
break;
}
}
});
this.filterParams = filterParams;
this.updateUrl();
this.searchTerm = filterParams?.search;
this.authorUsername = filterParams?.authorUsername;
this.assigneeUsernames = filterParams?.assigneeUsername;
},
updateUrl() {
const queryParams = urlParamsToObject(window.location.search);
const { authorUsername, assigneeUsername, search } = this.filterParams || {};
if (authorUsername) {
queryParams.author_username = authorUsername;
} else {
delete queryParams.author_username;
}
if (assigneeUsername) {
queryParams.assignee_username = assigneeUsername;
} else {
delete queryParams.assignee_username;
}
if (search) {
queryParams.search = search;
} else {
delete queryParams.search;
}
updateHistory({
url: setUrlParams(queryParams, window.location.href, true),
title: document.title,
replace: true,
});
},
},
};
</script>
......@@ -331,12 +447,16 @@ export default {
</gl-button>
</div>
<div class="gl-bg-gray-10 gl-p-5 gl-border-b-solid gl-border-b-1 gl-border-gray-100">
<gl-search-box-by-type
:value="searchTerm"
class="gl-bg-white"
:placeholder="$options.i18n.searchPlaceholder"
@input="onInputChange"
<div class="filtered-search-wrapper">
<filtered-search-bar
:namespace="projectPath"
:search-input-placeholder="$options.i18n.searchPlaceholder"
:tokens="filteredSearchTokens"
:initial-filter-value="filteredSearchValue"
initial-sortby="created_desc"
recent-searches-storage-key="incidents"
class="row-content-block"
@onFilter="handleFilterIncidents"
/>
</div>
......
......@@ -6,7 +6,7 @@ export const I18N = {
unassigned: s__('IncidentManagement|Unassigned'),
createIncidentBtnLabel: s__('IncidentManagement|Create incident'),
unPublished: s__('IncidentManagement|Unpublished'),
searchPlaceholder: __('Search results…'),
searchPlaceholder: __('Search or filter results…'),
emptyState: {
title: s__('IncidentManagement|Display your incidents in a dedicated view'),
emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
......@@ -34,5 +34,4 @@ export const INCIDENT_STATUS_TABS = [
},
];
export const INCIDENT_SEARCH_DELAY = 300;
export const DEFAULT_PAGE_SIZE = 20;
query getIncidentsCountByStatus($searchTerm: String, $projectPath: ID!, $issueTypes: [IssueType!]) {
query getIncidentsCountByStatus(
$searchTerm: String
$projectPath: ID!
$issueTypes: [IssueType!]
$authorUsername: String = ""
$assigneeUsernames: [String!] = []
) {
project(fullPath: $projectPath) {
issueStatusCounts(search: $searchTerm, types: $issueTypes) {
issueStatusCounts(
search: $searchTerm
types: $issueTypes
authorUsername: $authorUsername
assigneeUsername: $assigneeUsernames
) {
all
opened
closed
......
......@@ -9,7 +9,9 @@ query getIncidents(
$lastPageSize: Int
$prevPageCursor: String = ""
$nextPageCursor: String = ""
$searchTerm: String
$searchTerm: String = ""
$authorUsername: String = ""
$assigneeUsernames: [String!] = []
) {
project(fullPath: $projectPath) {
issues(
......@@ -17,6 +19,8 @@ query getIncidents(
types: $issueTypes
sort: $sort
state: $status
authorUsername: $authorUsername
assigneeUsername: $assigneeUsernames
first: $firstPageSize
last: $lastPageSize
after: $nextPageCursor
......
......@@ -16,6 +16,9 @@ export default () => {
issuePath,
publishedAvailable,
emptyListSvgPath,
textQuery,
authorUsernamesQuery,
assigneeUsernamesQuery,
} = domEl.dataset;
const apolloProvider = new VueApollo({
......@@ -32,6 +35,9 @@ export default () => {
issuePath,
publishedAvailable,
emptyListSvgPath,
textQuery,
authorUsernamesQuery,
assigneeUsernamesQuery,
},
apolloProvider,
components: {
......
......@@ -13,9 +13,9 @@ import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptio
import SidebarSeverity from './components/severity/sidebar_severity.vue';
import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
import { store } from '~/notes/stores';
import { isInIssuePage, parseBoolean } from '~/lib/utils/common_utils';
import mergeRequestStore from '~/mr_notes/stores';
import createFlash from '~/flash';
import { __ } from '~/locale';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
Vue.use(Translate);
......@@ -89,47 +89,72 @@ function mountConfidentialComponent(mediator) {
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
// eslint-disable-next-line no-new
new Vue({
el,
store,
components: {
ConfidentialIssueSidebar,
},
render: createElement =>
createElement('confidential-issue-sidebar', {
props: {
iid: String(iid),
fullPath,
isEditable: initialData.is_editable,
service: mediator.service,
},
}),
});
import(/* webpackChunkName: 'notesStore' */ '~/notes/stores')
.then(
({ store }) =>
new Vue({
el,
store,
components: {
ConfidentialIssueSidebar,
},
render: createElement =>
createElement('confidential-issue-sidebar', {
props: {
iid: String(iid),
fullPath,
isEditable: initialData.is_editable,
service: mediator.service,
},
}),
}),
)
.catch(() => {
createFlash({ message: __('Failed to load sidebar confidential toggle') });
});
}
function mountLockComponent() {
const el = document.getElementById('js-lock-entry-point');
if (!el) {
return;
}
const { fullPath } = getSidebarOptions();
const dataNode = document.getElementById('js-lock-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
return el
? new Vue({
el,
store: isInIssuePage() ? store : mergeRequestStore,
provide: {
fullPath,
},
render: createElement =>
createElement(IssuableLockForm, {
props: {
isEditable: initialData.is_editable,
},
}),
})
: undefined;
let importStore;
if (isInIssuePage()) {
importStore = import(/* webpackChunkName: 'notesStore' */ '~/notes/stores').then(
({ store }) => store,
);
} else {
importStore = import(/* webpackChunkName: 'mrNotesStore' */ '~/mr_notes/stores');
}
importStore
.then(
store =>
new Vue({
el,
store,
provide: {
fullPath,
},
render: createElement =>
createElement(IssuableLockForm, {
props: {
isEditable: initialData.is_editable,
},
}),
}),
)
.catch(() => {
createFlash({ message: __('Failed to load sidebar lock status') });
});
}
function mountParticipantsComponent(mediator) {
......@@ -219,7 +244,7 @@ function mountSeverityComponent() {
export function mountSidebar(mediator) {
mountAssigneesComponent(mediator);
mountConfidentialComponent(mediator);
mountLockComponent(mediator);
mountLockComponent();
mountParticipantsComponent(mediator);
mountSubscriptionsComponent(mediator);
......
......@@ -56,7 +56,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
before_action only: :index do
push_frontend_feature_flag(:scoped_labels, @project)
push_frontend_feature_flag(:scoped_labels, @project, type: :licensed)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
......
......@@ -12,7 +12,7 @@ class Projects::ServicesController < Projects::ApplicationController
before_action :set_deprecation_notice_for_prometheus_service, only: [:edit, :update]
before_action :redirect_deprecated_prometheus_service, only: [:update]
before_action only: :edit do
push_frontend_feature_flag(:jira_issues_integration, @project, { default_enabled: true })
push_frontend_feature_flag(:jira_issues_integration, @project, type: :licensed, default_enabled: true)
end
respond_to :html
......
......@@ -18,7 +18,10 @@ module IssueResolverArguments
argument :milestone_title, GraphQL::STRING_TYPE.to_list_type,
required: false,
description: 'Milestone applied to this issue'
argument :assignee_username, GraphQL::STRING_TYPE,
argument :author_username, GraphQL::STRING_TYPE,
required: false,
description: 'Username of the author of the issue'
argument :assignee_username, [GraphQL::STRING_TYPE],
required: false,
description: 'Username of a user assigned to the issue'
argument :assignee_id, GraphQL::STRING_TYPE,
......
......@@ -218,8 +218,28 @@ def contact_your_administrator_text
_('Please contact your administrator with any questions.')
end
def change_reviewer_notification_text(new_reviewers, previous_reviewers, html_tag = nil)
new = new_reviewers.any? ? users_to_sentence(new_reviewers) : s_('ChangeReviewer|Unassigned')
old = previous_reviewers.any? ? users_to_sentence(previous_reviewers) : nil
if html_tag.present?
new = content_tag(html_tag, new)
old = content_tag(html_tag, old) if old.present?
end
if old.present?
s_('ChangeReviewer|Reviewer changed from %{old} to %{new}').html_safe % { old: old, new: new }
else
s_('ChangeReviewer|Reviewer changed to %{new}').html_safe % { new: new }
end