Commit c801180b authored by Sean McGivern's avatar Sean McGivern

Merge branch '229404-filter-capabilities' into 'master'

Resolve "Add filter capabilities to Incident list"

Closes #229404

See merge request gitlab-org/gitlab!42377
parents bdf0f246 4b6bca3a
...@@ -8,7 +8,6 @@ import { ...@@ -8,7 +8,6 @@ import {
GlAvatar, GlAvatar,
GlTooltipDirective, GlTooltipDirective,
GlButton, GlButton,
GlSearchBoxByType,
GlIcon, GlIcon,
GlPagination, GlPagination,
GlTabs, GlTabs,
...@@ -16,16 +15,25 @@ import { ...@@ -16,16 +15,25 @@ import {
GlBadge, GlBadge,
GlEmptyState, GlEmptyState,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { debounce } from 'lodash'; import Api from '~/api';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; 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 { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__ } from '~/locale'; import { s__, __ } from '~/locale';
import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility'; 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 getIncidents from '../graphql/queries/get_incidents.query.graphql';
import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql'; import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import SeverityToken from '~/sidebar/components/severity/severity.vue'; import SeverityToken from '~/sidebar/components/severity/severity.vue';
import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants'; 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 TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
const tdClass = const tdClass =
...@@ -82,7 +90,6 @@ export default { ...@@ -82,7 +90,6 @@ export default {
GlAvatar, GlAvatar,
GlButton, GlButton,
TimeAgoTooltip, TimeAgoTooltip,
GlSearchBoxByType,
GlIcon, GlIcon,
GlPagination, GlPagination,
GlTabs, GlTabs,
...@@ -91,6 +98,7 @@ export default { ...@@ -91,6 +98,7 @@ export default {
GlBadge, GlBadge,
GlEmptyState, GlEmptyState,
SeverityToken, SeverityToken,
FilteredSearchBar,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -103,6 +111,9 @@ export default { ...@@ -103,6 +111,9 @@ export default {
'issuePath', 'issuePath',
'publishedAvailable', 'publishedAvailable',
'emptyListSvgPath', 'emptyListSvgPath',
'textQuery',
'authorUsernamesQuery',
'assigneeUsernamesQuery',
], ],
apollo: { apollo: {
incidents: { incidents: {
...@@ -118,6 +129,8 @@ export default { ...@@ -118,6 +129,8 @@ export default {
lastPageSize: this.pagination.lastPageSize, lastPageSize: this.pagination.lastPageSize,
prevPageCursor: this.pagination.prevPageCursor, prevPageCursor: this.pagination.prevPageCursor,
nextPageCursor: this.pagination.nextPageCursor, nextPageCursor: this.pagination.nextPageCursor,
authorUsername: this.authorUsername,
assigneeUsernames: this.assigneeUsernames,
}; };
}, },
update({ project: { issues: { nodes = [], pageInfo = {} } = {} } = {} }) { update({ project: { issues: { nodes = [], pageInfo = {} } = {} } = {} }) {
...@@ -135,6 +148,8 @@ export default { ...@@ -135,6 +148,8 @@ export default {
variables() { variables() {
return { return {
searchTerm: this.searchTerm, searchTerm: this.searchTerm,
authorUsername: this.authorUsername,
assigneeUsernames: this.assigneeUsernames,
projectPath: this.projectPath, projectPath: this.projectPath,
issueTypes: ['INCIDENT'], issueTypes: ['INCIDENT'],
}; };
...@@ -149,7 +164,7 @@ export default { ...@@ -149,7 +164,7 @@ export default {
errored: false, errored: false,
isErrorAlertDismissed: false, isErrorAlertDismissed: false,
redirecting: false, redirecting: false,
searchTerm: '', searchTerm: this.textQuery,
pagination: initialPaginationState, pagination: initialPaginationState,
incidents: {}, incidents: {},
sort: 'created_desc', sort: 'created_desc',
...@@ -157,6 +172,9 @@ export default { ...@@ -157,6 +172,9 @@ export default {
sortDesc: true, sortDesc: true,
statusFilter: '', statusFilter: '',
filteredByStatus: '', filteredByStatus: '',
authorUsername: this.authorUsernamesQuery,
assigneeUsernames: this.assigneeUsernamesQuery,
filterParams: {},
}; };
}, },
computed: { computed: {
...@@ -242,14 +260,57 @@ export default { ...@@ -242,14 +260,57 @@ export default {
btnText: createIncidentBtnLabel, 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: { methods: {
onInputChange: debounce(function debounceSearch(input) {
const trimmedInput = input.trim();
if (trimmedInput !== this.searchTerm) {
this.searchTerm = trimmedInput;
}
}, INCIDENT_SEARCH_DELAY),
filterIncidentsByStatus(tabIndex) { filterIncidentsByStatus(tabIndex) {
const { filters, status } = this.$options.statusTabs[tabIndex]; const { filters, status } = this.$options.statusTabs[tabIndex];
this.statusFilter = filters; this.statusFilter = filters;
...@@ -292,6 +353,61 @@ export default { ...@@ -292,6 +353,61 @@ export default {
getSeverity(severity) { getSeverity(severity) {
return INCIDENT_SEVERITY[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> </script>
...@@ -331,12 +447,16 @@ export default { ...@@ -331,12 +447,16 @@ export default {
</gl-button> </gl-button>
</div> </div>
<div class="gl-bg-gray-10 gl-p-5 gl-border-b-solid gl-border-b-1 gl-border-gray-100"> <div class="filtered-search-wrapper">
<gl-search-box-by-type <filtered-search-bar
:value="searchTerm" :namespace="projectPath"
class="gl-bg-white" :search-input-placeholder="$options.i18n.searchPlaceholder"
:placeholder="$options.i18n.searchPlaceholder" :tokens="filteredSearchTokens"
@input="onInputChange" :initial-filter-value="filteredSearchValue"
initial-sortby="created_desc"
recent-searches-storage-key="incidents"
class="row-content-block"
@onFilter="handleFilterIncidents"
/> />
</div> </div>
......
...@@ -6,7 +6,7 @@ export const I18N = { ...@@ -6,7 +6,7 @@ export const I18N = {
unassigned: s__('IncidentManagement|Unassigned'), unassigned: s__('IncidentManagement|Unassigned'),
createIncidentBtnLabel: s__('IncidentManagement|Create incident'), createIncidentBtnLabel: s__('IncidentManagement|Create incident'),
unPublished: s__('IncidentManagement|Unpublished'), unPublished: s__('IncidentManagement|Unpublished'),
searchPlaceholder: __('Search results…'), searchPlaceholder: __('Search or filter results…'),
emptyState: { emptyState: {
title: s__('IncidentManagement|Display your incidents in a dedicated view'), title: s__('IncidentManagement|Display your incidents in a dedicated view'),
emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'), emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
...@@ -34,5 +34,4 @@ export const INCIDENT_STATUS_TABS = [ ...@@ -34,5 +34,4 @@ export const INCIDENT_STATUS_TABS = [
}, },
]; ];
export const INCIDENT_SEARCH_DELAY = 300;
export const DEFAULT_PAGE_SIZE = 20; 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) { project(fullPath: $projectPath) {
issueStatusCounts(search: $searchTerm, types: $issueTypes) { issueStatusCounts(
search: $searchTerm
types: $issueTypes
authorUsername: $authorUsername
assigneeUsername: $assigneeUsernames
) {
all all
opened opened
closed closed
......
...@@ -9,7 +9,9 @@ query getIncidents( ...@@ -9,7 +9,9 @@ query getIncidents(
$lastPageSize: Int $lastPageSize: Int
$prevPageCursor: String = "" $prevPageCursor: String = ""
$nextPageCursor: String = "" $nextPageCursor: String = ""
$searchTerm: String $searchTerm: String = ""
$authorUsername: String = ""
$assigneeUsernames: [String!] = []
) { ) {
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
issues( issues(
...@@ -17,6 +19,8 @@ query getIncidents( ...@@ -17,6 +19,8 @@ query getIncidents(
types: $issueTypes types: $issueTypes
sort: $sort sort: $sort
state: $status state: $status
authorUsername: $authorUsername
assigneeUsername: $assigneeUsernames
first: $firstPageSize first: $firstPageSize
last: $lastPageSize last: $lastPageSize
after: $nextPageCursor after: $nextPageCursor
......
...@@ -16,6 +16,9 @@ export default () => { ...@@ -16,6 +16,9 @@ export default () => {
issuePath, issuePath,
publishedAvailable, publishedAvailable,
emptyListSvgPath, emptyListSvgPath,
textQuery,
authorUsernamesQuery,
assigneeUsernamesQuery,
} = domEl.dataset; } = domEl.dataset;
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
...@@ -32,6 +35,9 @@ export default () => { ...@@ -32,6 +35,9 @@ export default () => {
issuePath, issuePath,
publishedAvailable, publishedAvailable,
emptyListSvgPath, emptyListSvgPath,
textQuery,
authorUsernamesQuery,
assigneeUsernamesQuery,
}, },
apolloProvider, apolloProvider,
components: { components: {
......
...@@ -18,7 +18,10 @@ module IssueResolverArguments ...@@ -18,7 +18,10 @@ module IssueResolverArguments
argument :milestone_title, GraphQL::STRING_TYPE.to_list_type, argument :milestone_title, GraphQL::STRING_TYPE.to_list_type,
required: false, required: false,
description: 'Milestone applied to this issue' 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, required: false,
description: 'Username of a user assigned to the issue' description: 'Username of a user assigned to the issue'
argument :assignee_id, GraphQL::STRING_TYPE, argument :assignee_id, GraphQL::STRING_TYPE,
......
# frozen_string_literal: true # frozen_string_literal: true
module Projects::IncidentsHelper module Projects::IncidentsHelper
def incidents_data(project) def incidents_data(project, params)
{ {
'project-path' => project.full_path, 'project-path' => project.full_path,
'new-issue-path' => new_project_issue_path(project), 'new-issue-path' => new_project_issue_path(project),
'incident-template-name' => 'incident', 'incident-template-name' => 'incident',
'incident-type' => 'incident', 'incident-type' => 'incident',
'issue-path' => project_issues_path(project), 'issue-path' => project_issues_path(project),
'empty-list-svg-path' => image_path('illustrations/incident-empty-state.svg') 'empty-list-svg-path' => image_path('illustrations/incident-empty-state.svg'),
'text-query': params[:search],
'author-usernames-query': params[:author_username],
'assignee-usernames-query': params[:assignee_username]
} }
end end
end end
......
- page_title _('Incidents') - page_title _('Incidents')
#js-incidents{ data: incidents_data(@project) } #js-incidents{ data: incidents_data(@project, params) }
---
title: Resolve Add filter capabilities to Incident list
merge_request: 42377
author:
type: changed
...@@ -6778,7 +6778,12 @@ type Group { ...@@ -6778,7 +6778,12 @@ type Group {
""" """
Username of a user assigned to the issue Username of a user assigned to the issue
""" """
assigneeUsername: String assigneeUsername: [String!]
"""
Username of the author of the issue
"""
authorUsername: String
""" """
Returns the elements in the list that come before the specified cursor. Returns the elements in the list that come before the specified cursor.
...@@ -12248,7 +12253,12 @@ type Project { ...@@ -12248,7 +12253,12 @@ type Project {
""" """
Username of a user assigned to the issue Username of a user assigned to the issue
""" """
assigneeUsername: String assigneeUsername: [String!]
"""
Username of the author of the issue
"""
authorUsername: String
""" """
Issues closed after this date Issues closed after this date
...@@ -12338,7 +12348,12 @@ type Project { ...@@ -12338,7 +12348,12 @@ type Project {
""" """
Username of a user assigned to the issue Username of a user assigned to the issue
""" """
assigneeUsername: String assigneeUsername: [String!]
"""
Username of the author of the issue
"""
authorUsername: String
""" """
Issues closed after this date Issues closed after this date
...@@ -12418,7 +12433,12 @@ type Project { ...@@ -12418,7 +12433,12 @@ type Project {
""" """
Username of a user assigned to the issue Username of a user assigned to the issue
""" """
assigneeUsername: String assigneeUsername: [String!]
"""
Username of the author of the issue
"""
authorUsername: String
""" """
Returns the elements in the list that come before the specified cursor. Returns the elements in the list that come before the specified cursor.
......
...@@ -18849,8 +18849,8 @@ ...@@ -18849,8 +18849,8 @@
"defaultValue": null "defaultValue": null
}, },
{ {
"name": "assigneeUsername", "name": "authorUsername",
"description": "Username of a user assigned to the issue", "description": "Username of the author of the issue",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -18858,6 +18858,24 @@ ...@@ -18858,6 +18858,24 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "assigneeUsername",
"description": "Username of a user assigned to the issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "assigneeId", "name": "assigneeId",
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported", "description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
...@@ -36420,8 +36438,8 @@ ...@@ -36420,8 +36438,8 @@
"defaultValue": null "defaultValue": null
}, },
{ {
"name": "assigneeUsername", "name": "authorUsername",
"description": "Username of a user assigned to the issue", "description": "Username of the author of the issue",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -36429,6 +36447,24 @@ ...@@ -36429,6 +36447,24 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "assigneeUsername",
"description": "Username of a user assigned to the issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "assigneeId", "name": "assigneeId",
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported", "description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
...@@ -36631,8 +36667,8 @@ ...@@ -36631,8 +36667,8 @@
"defaultValue": null "defaultValue": null
}, },
{ {
"name": "assigneeUsername", "name": "authorUsername",
"description": "Username of a user assigned to the issue", "description": "Username of the author of the issue",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -36640,6 +36676,24 @@ ...@@ -36640,6 +36676,24 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "assigneeUsername",
"description": "Username of a user assigned to the issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "assigneeId", "name": "assigneeId",
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported", "description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
...@@ -36808,8 +36862,8 @@ ...@@ -36808,8 +36862,8 @@
"defaultValue": null "defaultValue": null
}, },
{ {
"name": "assigneeUsername", "name": "authorUsername",
"description": "Username of a user assigned to the issue", "description": "Username of the author of the issue",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -36817,6 +36871,24 @@ ...@@ -36817,6 +36871,24 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "assigneeUsername",
"description": "Username of a user assigned to the issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "assigneeId", "name": "assigneeId",
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported", "description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
...@@ -6,7 +6,7 @@ module EE ...@@ -6,7 +6,7 @@ module EE
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
override :incidents_data override :incidents_data
def incidents_data(project) def incidents_data(project, params)
super.merge( super.merge(
incidents_data_published_available(project) incidents_data_published_available(project)
) )
......
...@@ -9,6 +9,13 @@ RSpec.describe Projects::IncidentsHelper do ...@@ -9,6 +9,13 @@ RSpec.describe Projects::IncidentsHelper do
let(:project_path) { project.full_path } let(:project_path) { project.full_path }
let(:new_issue_path) { new_project_issue_path(project) } let(:new_issue_path) { new_project_issue_path(project) }
let(:issue_path) { project_issues_path(project) } let(:issue_path) { project_issues_path(project) }
let(:params) do
{
search: 'search text',
author_username: 'root',
assignee_username: 'max.power'
}
end
describe '#incidents_data' do describe '#incidents_data' do
let(:expected_incidents_data) do let(:expected_incidents_data) do
...@@ -18,11 +25,14 @@ RSpec.describe Projects::IncidentsHelper do ...@@ -18,11 +25,14 @@ RSpec.describe Projects::IncidentsHelper do
'incident-template-name' => 'incident', 'incident-template-name' => 'incident',
'incident-type' => 'incident', 'incident-type' => 'incident',
'issue-path' => issue_path, 'issue-path' => issue_path,
'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg') 'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg'),
'text-query': 'search text',
'author-usernames-query': 'root',
'assignee-usernames-query': 'max.power'
} }
end end
subject { helper.incidents_data(project) } subject { helper.incidents_data(project, params) }
before do before do
allow(project).to receive(:feature_available?).with(:status_page).and_return(status_page_feature_available) allow(project).to receive(:feature_available?).with(:status_page).and_return(status_page_feature_available)
......
...@@ -22242,9 +22242,6 @@ msgstr "" ...@@ -22242,9 +22242,6 @@ msgstr ""
msgid "Search requirements" msgid "Search requirements"
msgstr "" msgstr ""
msgid "Search results…"
msgstr ""
msgid "Search test cases" msgid "Search test cases"
msgstr "" msgstr ""
......
...@@ -5,7 +5,6 @@ import { ...@@ -5,7 +5,6 @@ import {
GlTable, GlTable,
GlAvatar, GlAvatar,
GlPagination, GlPagination,
GlSearchBoxByType,
GlTab, GlTab,
GlTabs, GlTabs,
GlBadge, GlBadge,
...@@ -15,13 +14,18 @@ import { visitUrl, joinPaths, mergeUrlParams } from '~/lib/utils/url_utility'; ...@@ -15,13 +14,18 @@ import { visitUrl, joinPaths, mergeUrlParams } from '~/lib/utils/url_utility';
import IncidentsList from '~/incidents/components/incidents_list.vue'; import IncidentsList from '~/incidents/components/incidents_list.vue';
import SeverityToken from '~/sidebar/components/severity/severity.vue'; import SeverityToken from '~/sidebar/components/severity/severity.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; 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 { I18N, INCIDENT_STATUS_TABS } from '~/incidents/constants'; import { I18N, INCIDENT_STATUS_TABS } from '~/incidents/constants';
import mockIncidents from '../mocks/incidents.json'; import mockIncidents from '../mocks/incidents.json';
import mockFilters from '../mocks/incidents_filter.json';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'), visitUrl: jest.fn().mockName('visitUrlMock'),
joinPaths: jest.fn().mockName('joinPaths'), joinPaths: jest.fn().mockName('joinPaths'),
mergeUrlParams: jest.fn().mockName('mergeUrlParams'), mergeUrlParams: jest.fn().mockName('mergeUrlParams'),
setUrlParams: jest.fn().mockName('setUrlParams'),
updateHistory: jest.fn().mockName('updateHistory'),
})); }));
describe('Incidents List', () => { describe('Incidents List', () => {
...@@ -43,7 +47,7 @@ describe('Incidents List', () => { ...@@ -43,7 +47,7 @@ describe('Incidents List', () => {
const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip); const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip);
const findDateColumnHeader = () => const findDateColumnHeader = () =>
wrapper.find('[data-testid="incident-management-created-at-sort"]'); wrapper.find('[data-testid="incident-management-created-at-sort"]');
const findSearch = () => wrapper.find(GlSearchBoxByType); const findSearch = () => wrapper.find(FilteredSearchBar);
const findAssingees = () => wrapper.findAll('[data-testid="incident-assignees"]'); const findAssingees = () => wrapper.findAll('[data-testid="incident-assignees"]');
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]'); const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']"); const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
...@@ -76,6 +80,9 @@ describe('Incidents List', () => { ...@@ -76,6 +80,9 @@ describe('Incidents List', () => {
issuePath: '/project/isssues', issuePath: '/project/isssues',
publishedAvailable: true, publishedAvailable: true,
emptyListSvgPath, emptyListSvgPath,
textQuery: '',
authorUsernamesQuery: '',
assigneeUsernamesQuery: '',
}, },
stubs: { stubs: {
GlButton: true, GlButton: true,
...@@ -315,7 +322,7 @@ describe('Incidents List', () => { ...@@ -315,7 +322,7 @@ describe('Incidents List', () => {
}); });
}); });
describe('Search', () => { describe('Filtered search component', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ mountComponent({
data: { data: {
...@@ -331,15 +338,62 @@ describe('Incidents List', () => { ...@@ -331,15 +338,62 @@ describe('Incidents List', () => {
}); });
it('renders the search component for incidents', () => { it('renders the search component for incidents', () => {
expect(findSearch().exists()).toBe(true); expect(findSearch().props('searchInputPlaceholder')).toBe('Search or filter results…');
expect(findSearch().props('tokens')).toEqual([
{
type: 'author_username',
icon: 'user',
title: 'Author',
unique: true,
symbol: '@',
token: AuthorToken,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchPath: '/project/path',
fetchAuthors: expect.any(Function),
},
{
type: 'assignee_username',
icon: 'user',
title: 'Assignees',
unique: true,
symbol: '@',
token: AuthorToken,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchPath: '/project/path',
fetchAuthors: expect.any(Function),
},
]);
expect(findSearch().props('recentSearchesStorageKey')).toBe('incidents');
});
it('returns correctly applied filter search values', async () => {
const searchTerm = 'foo';
wrapper.setData({
searchTerm,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.filteredSearchValue).toEqual([searchTerm]);
}); });
it('sets the `searchTerm` graphql variable', () => { it('updates props tied to getIncidents GraphQL query', () => {
const SEARCH_TERM = 'Simple Incident'; wrapper.vm.handleFilterIncidents(mockFilters);
expect(wrapper.vm.authorUsername).toBe('root');
expect(wrapper.vm.assigneeUsernames).toEqual(['root2']);
expect(wrapper.vm.searchTerm).toBe(mockFilters[2].value.data);
});
it('updates props `searchTerm` and `authorUsername` with empty values when passed filters param is empty', () => {
wrapper.setData({
authorUsername: 'foo',
searchTerm: 'bar',
});
findSearch().vm.$emit('input', SEARCH_TERM); wrapper.vm.handleFilterIncidents([]);
expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM); expect(wrapper.vm.authorUsername).toBe('');
expect(wrapper.vm.searchTerm).toBe('');
}); });
}); });
......
[
{
"type": "assignee_username",
"value": { "data": "root2" }
},
{
"type": "author_username",
"value": { "data": "root" }
},
{
"type": "filtered-search-term",
"value": { "data": "bar" }
}
]
\ No newline at end of file
...@@ -54,10 +54,21 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -54,10 +54,21 @@ RSpec.describe Resolvers::IssuesResolver do
expect(resolve_issues(assignee_id: IssuableFinder::Params::FILTER_ANY)).to contain_exactly(issue2) expect(resolve_issues(assignee_id: IssuableFinder::Params::FILTER_ANY)).to contain_exactly(issue2)
end end
it 'filters by two assignees' do
user_2 = create(:user)
issue2.update!(assignees: [assignee, user_2])
expect(resolve_issues(assignee_id: [assignee.id, user_2.id])).to contain_exactly(issue2)
end
it 'filters by no assignee' do it 'filters by no assignee' do
expect(resolve_issues(assignee_id: IssuableFinder::Params::FILTER_NONE)).to contain_exactly(issue1) expect(resolve_issues(assignee_id: IssuableFinder::Params::FILTER_NONE)).to contain_exactly(issue1)
end end
it 'filters by author' do
expect(resolve_issues(author_username: issue1.author.username)).to contain_exactly(issue1, issue2)
end
it 'filters by labels' do it 'filters by labels' do
expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2) expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2) expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
......
...@@ -9,9 +9,16 @@ RSpec.describe Projects::IncidentsHelper do ...@@ -9,9 +9,16 @@ RSpec.describe Projects::IncidentsHelper do
let(:project_path) { project.full_path } let(:project_path) { project.full_path }
let(:new_issue_path) { new_project_issue_path(project) } let(:new_issue_path) { new_project_issue_path(project) }
let(:issue_path) { project_issues_path(project) } let(:issue_path) { project_issues_path(project) }
let(:params) do
{
search: 'search text',
author_username: 'root',
assignee_username: 'max.power'
}
end
describe '#incidents_data' do describe '#incidents_data' do
subject(:data) { helper.incidents_data(project) } subject(:data) { helper.incidents_data(project, params) }
it 'returns frontend configuration' do it 'returns frontend configuration' do
expect(data).to match( expect(data).to match(
...@@ -20,7 +27,10 @@ RSpec.describe Projects::IncidentsHelper do ...@@ -20,7 +27,10 @@ RSpec.describe Projects::IncidentsHelper do
'incident-template-name' => 'incident', 'incident-template-name' => 'incident',
'incident-type' => 'incident', 'incident-type' => 'incident',
'issue-path' => issue_path, 'issue-path' => issue_path,
'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg') 'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg'),
'text-query': 'search text',
'author-usernames-query': 'root',
'assignee-usernames-query': 'max.power'
) )
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment