Commit 0890bda6 authored by Illya Klymov's avatar Illya Klymov

Merge branch '212543-frontend-filtered-search-requirements' into 'master'

Add frontend for Filtered Search & Sort in Requirements page

Closes #212543

See merge request gitlab-org/gitlab!33484
parents 14c9a9af 5254b3d0
...@@ -3,4 +3,5 @@ import recentSearchesStorageKeysCE from '~/filtered_search/recent_searches_stora ...@@ -3,4 +3,5 @@ import recentSearchesStorageKeysCE from '~/filtered_search/recent_searches_stora
export default { export default {
...recentSearchesStorageKeysCE, ...recentSearchesStorageKeysCE,
epics: 'epics-recent-searches', epics: 'epics-recent-searches',
requirements: 'requirements-recent-searches',
}; };
...@@ -2,10 +2,15 @@ ...@@ -2,10 +2,15 @@
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { GlPagination } from '@gitlab/ui'; import { GlPagination } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import Api from '~/api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { urlParamsToObject } from '~/lib/utils/common_utils'; import { urlParamsToObject } from '~/lib/utils/common_utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
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 { ANY_AUTHOR } from '~/vue_shared/components/filtered_search_bar/constants';
import RequirementsTabs from './requirements_tabs.vue'; import RequirementsTabs from './requirements_tabs.vue';
import RequirementsLoading from './requirements_loading.vue'; import RequirementsLoading from './requirements_loading.vue';
import RequirementsEmptyState from './requirements_empty_state.vue'; import RequirementsEmptyState from './requirements_empty_state.vue';
...@@ -17,13 +22,15 @@ import projectRequirementsCount from '../queries/projectRequirementsCount.query. ...@@ -17,13 +22,15 @@ import projectRequirementsCount from '../queries/projectRequirementsCount.query.
import createRequirement from '../queries/createRequirement.mutation.graphql'; import createRequirement from '../queries/createRequirement.mutation.graphql';
import updateRequirement from '../queries/updateRequirement.mutation.graphql'; import updateRequirement from '../queries/updateRequirement.mutation.graphql';
import { FilterState, DEFAULT_PAGE_SIZE } from '../constants'; import { FilterState, AvailableSortOptions, DEFAULT_PAGE_SIZE } from '../constants';
export default { export default {
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
AvailableSortOptions,
components: { components: {
RequirementsTabs,
GlPagination, GlPagination,
FilteredSearchBar,
RequirementsTabs,
RequirementsLoading, RequirementsLoading,
RequirementsEmptyState, RequirementsEmptyState,
RequirementItem, RequirementItem,
...@@ -38,6 +45,21 @@ export default { ...@@ -38,6 +45,21 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
initialTextSearch: {
type: String,
required: false,
default: '',
},
initialSortBy: {
type: String,
required: false,
default: 'created_desc',
},
initialAuthorUsernames: {
type: Array,
required: false,
default: () => [],
},
initialRequirementsCount: { initialRequirementsCount: {
type: Object, type: Object,
required: true, required: true,
...@@ -96,6 +118,18 @@ export default { ...@@ -96,6 +118,18 @@ export default {
queryVariables.state = this.filterBy; queryVariables.state = this.filterBy;
} }
if (this.textSearch) {
queryVariables.search = this.textSearch;
}
if (this.authorUsernames.length) {
queryVariables.authorUsernames = this.authorUsernames;
}
if (this.sortBy) {
queryVariables.sortBy = this.sortBy;
}
return queryVariables; return queryVariables;
}, },
update(data) { update(data) {
...@@ -136,6 +170,9 @@ export default { ...@@ -136,6 +170,9 @@ export default {
data() { data() {
return { return {
filterBy: this.initialFilterBy, filterBy: this.initialFilterBy,
textSearch: this.initialTextSearch,
authorUsernames: this.initialAuthorUsernames,
sortBy: this.initialSortBy,
showCreateForm: false, showCreateForm: false,
showUpdateFormForRequirement: 0, showUpdateFormForRequirement: 0,
createRequirementRequestActive: false, createRequirementRequestActive: false,
...@@ -177,6 +214,13 @@ export default { ...@@ -177,6 +214,13 @@ export default {
return this.requirementsListEmpty && !this.showCreateForm; return this.requirementsListEmpty && !this.showCreateForm;
}, },
showPaginationControls() { showPaginationControls() {
const { hasPreviousPage, hasNextPage } = this.requirements.pageInfo;
// This explicit check is necessary as both the variables
// can also be `false` and we just want to ensure that they're present.
if (hasPreviousPage !== undefined || hasNextPage !== undefined) {
return Boolean(hasPreviousPage || hasNextPage);
}
return this.totalRequirementsForCurrentTab > DEFAULT_PAGE_SIZE && !this.requirementsListEmpty; return this.totalRequirementsForCurrentTab > DEFAULT_PAGE_SIZE && !this.requirementsListEmpty;
}, },
prevPage() { prevPage() {
...@@ -190,27 +234,87 @@ export default { ...@@ -190,27 +234,87 @@ export default {
}, },
}, },
methods: { methods: {
getFilteredSearchTokens() {
return [
{
type: 'author_username',
icon: 'user',
title: __('Author'),
unique: false,
symbol: '@',
token: AuthorToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
fetchPath: this.projectPath,
fetchAuthors: Api.projectUsers.bind(Api),
},
];
},
getFilteredSearchValue() {
const value = this.authorUsernames.map(author => ({
type: 'author_username',
value: { data: author },
}));
if (this.textSearch) {
value.push(this.textSearch);
}
return value;
},
/** /**
* Update browser URL with updated query-param values * Update browser URL with updated query-param values
* based on current page details. * based on current page details.
*/ */
updateUrl({ page, prev, next }) { updateUrl() {
const { href, search } = window.location; const { href, search } = window.location;
const queryParams = urlParamsToObject(search); const queryParams = urlParamsToObject(search);
const {
filterBy,
currentPage,
prevPageCursor,
nextPageCursor,
textSearch,
authorUsernames,
sortBy,
} = this;
queryParams.page = page || 1; queryParams.page = currentPage || 1;
// Only keep params that have any values. // Only keep params that have any values.
if (prev) { if (prevPageCursor) {
queryParams.prev = prev; queryParams.prev = prevPageCursor;
} else { } else {
delete queryParams.prev; delete queryParams.prev;
} }
if (next) {
queryParams.next = next; if (nextPageCursor) {
queryParams.next = nextPageCursor;
} else { } else {
delete queryParams.next; delete queryParams.next;
} }
if (filterBy) {
queryParams.state = filterBy.toLowerCase();
} else {
delete queryParams.state;
}
if (textSearch) {
queryParams.search = textSearch;
} else {
delete queryParams.search;
}
if (sortBy) {
queryParams.sort = sortBy;
} else {
delete queryParams.sort;
}
delete queryParams.author_username;
if (authorUsernames.length) {
queryParams['author_username[]'] = authorUsernames;
}
// We want to replace the history state so that back button // We want to replace the history state so that back button
// correctly reloads the page with previous URL. // correctly reloads the page with previous URL.
updateHistory({ updateHistory({
...@@ -356,6 +460,32 @@ export default { ...@@ -356,6 +460,32 @@ export default {
handleUpdateRequirementCancel() { handleUpdateRequirementCancel() {
this.showUpdateFormForRequirement = 0; this.showUpdateFormForRequirement = 0;
}, },
handleFilterRequirements(filters = []) {
const authors = [];
filters.forEach(filter => {
if (typeof filter === 'string') {
this.textSearch = filter;
} else if (filter.value.data !== ANY_AUTHOR) {
authors.push(filter.value.data);
}
});
this.authorUsernames = [...authors];
this.currentPage = 1;
this.prevPageCursor = '';
this.nextPageCursor = '';
this.updateUrl();
},
handleSortRequirements(sortBy) {
this.sortBy = sortBy;
this.currentPage = 1;
this.prevPageCursor = '';
this.nextPageCursor = '';
this.updateUrl();
},
handlePageChange(page) { handlePageChange(page) {
const { startCursor, endCursor } = this.requirements.pageInfo; const { startCursor, endCursor } = this.requirements.pageInfo;
...@@ -369,11 +499,7 @@ export default { ...@@ -369,11 +499,7 @@ export default {
this.currentPage = page; this.currentPage = page;
this.updateUrl({ this.updateUrl();
page,
prev: this.prevPageCursor,
next: this.nextPageCursor,
});
}, },
}, },
}; };
...@@ -389,6 +515,17 @@ export default { ...@@ -389,6 +515,17 @@ export default {
@clickTab="handleTabClick" @clickTab="handleTabClick"
@clickNewRequirement="handleNewRequirementClick" @clickNewRequirement="handleNewRequirementClick"
/> />
<filtered-search-bar
:namespace="projectPath"
:search-input-placeholder="__('Search requirements')"
:tokens="getFilteredSearchTokens()"
:sort-options="$options.AvailableSortOptions"
:initial-filter-value="getFilteredSearchValue()"
:initial-sort-by="sortBy"
class="row-content-block"
@onFilter="handleFilterRequirements"
@onSort="handleSortRequirements"
/>
<requirement-form <requirement-form
v-if="showCreateForm" v-if="showCreateForm"
:requirement-request-active="createRequirementRequestActive" :requirement-request-active="createRequirementRequestActive"
......
...@@ -11,6 +11,25 @@ export const FilterStateEmptyMessage = { ...@@ -11,6 +11,25 @@ export const FilterStateEmptyMessage = {
ARCHIVED: __('There are no archived requirements'), ARCHIVED: __('There are no archived requirements'),
}; };
export const AvailableSortOptions = [
{
id: 1,
title: __('Created date'),
sortDirection: {
descending: 'created_desc',
ascending: 'created_asc',
},
},
{
id: 2,
title: __('Last updated'),
sortDirection: {
descending: 'updated_desc',
ascending: 'updated_asc',
},
},
];
export const DEFAULT_PAGE_SIZE = 20; export const DEFAULT_PAGE_SIZE = 20;
export const MAX_TITLE_LENGTH = 255; export const MAX_TITLE_LENGTH = 255;
...@@ -5,6 +5,9 @@ query projectRequirements( ...@@ -5,6 +5,9 @@ query projectRequirements(
$lastPageSize: Int $lastPageSize: Int
$prevPageCursor: String = "" $prevPageCursor: String = ""
$nextPageCursor: String = "" $nextPageCursor: String = ""
$authorUsernames: [String!] = []
$search: String = ""
$sortBy: Sort = created_desc
) { ) {
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
requirements( requirements(
...@@ -12,8 +15,10 @@ query projectRequirements( ...@@ -12,8 +15,10 @@ query projectRequirements(
last: $lastPageSize last: $lastPageSize
after: $nextPageCursor after: $nextPageCursor
before: $prevPageCursor before: $prevPageCursor
sort: created_desc
state: $state state: $state
authorUsername: $authorUsernames
search: $search
sort: $sortBy
) { ) {
nodes { nodes {
iid iid
...@@ -33,6 +38,8 @@ query projectRequirements( ...@@ -33,6 +38,8 @@ query projectRequirements(
} }
} }
pageInfo { pageInfo {
hasPreviousPage
hasNextPage
startCursor startCursor
endCursor endCursor
} }
......
...@@ -44,6 +44,9 @@ export default () => { ...@@ -44,6 +44,9 @@ export default () => {
page, page,
next, next,
prev, prev,
textSearch,
authorUsernames,
sortBy,
projectPath, projectPath,
emptyStatePath, emptyStatePath,
opened, opened,
...@@ -60,6 +63,9 @@ export default () => { ...@@ -60,6 +63,9 @@ export default () => {
return { return {
initialFilterBy: stateFilterBy, initialFilterBy: stateFilterBy,
initialTextSearch: textSearch,
initialAuthorUsernames: authorUsernames ? JSON.parse(authorUsernames) : [],
initialSortBy: sortBy,
initialRequirementsCount: { initialRequirementsCount: {
OPENED, OPENED,
ARCHIVED, ARCHIVED,
...@@ -79,6 +85,9 @@ export default () => { ...@@ -79,6 +85,9 @@ export default () => {
props: { props: {
projectPath: this.projectPath, projectPath: this.projectPath,
initialFilterBy: this.initialFilterBy, initialFilterBy: this.initialFilterBy,
initialTextSearch: this.initialTextSearch,
initialAuthorUsernames: this.initialAuthorUsernames,
initialSortBy: this.initialSortBy,
initialRequirementsCount: this.initialRequirementsCount, initialRequirementsCount: this.initialRequirementsCount,
page: parseInt(this.page, 10) || 1, page: parseInt(this.page, 10) || 1,
prev: this.prev, prev: this.prev,
......
...@@ -20,6 +20,9 @@ ...@@ -20,6 +20,9 @@
page: params[:page], page: params[:page],
prev: params[:prev], prev: params[:prev],
next: params[:next], next: params[:next],
text_search: params[:search],
author_usernames: params[:author_username],
sort_by: params[:sort],
project_path: @project.full_path, project_path: @project.full_path,
opened: requirements_count['opened'], opened: requirements_count['opened'],
archived: requirements_count['archived'], archived: requirements_count['archived'],
......
...@@ -236,6 +236,21 @@ RSpec.describe 'Requirements list', :js do ...@@ -236,6 +236,21 @@ RSpec.describe 'Requirements list', :js do
end end
end end
end end
context 'filtered search' do
it 'shows filtered search input field' do
page.within('.vue-filtered-search-bar-container') do
expect(page).to have_selector('input.gl-filtered-search-term-input')
end
end
it 'shows sort dropdown' do
page.within('.vue-filtered-search-bar-container') do
expect(page).to have_selector('.gl-new-dropdown button.gl-dropdown-toggle')
expect(page).to have_selector('.gl-new-dropdown ul.dropdown-menu', visible: false)
end
end
end
end end
context 'when accessing project as guest user' do context 'when accessing project as guest user' do
......
...@@ -4,6 +4,9 @@ import { GlPagination } from '@gitlab/ui'; ...@@ -4,6 +4,9 @@ import { GlPagination } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import createFlash from '~/flash'; import createFlash from '~/flash';
import FilteredSearchBarRoot 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 RequirementsRoot from 'ee/requirements/components/requirements_root.vue'; import RequirementsRoot from 'ee/requirements/components/requirements_root.vue';
import RequirementsTabs from 'ee/requirements/components/requirements_tabs.vue'; import RequirementsTabs from 'ee/requirements/components/requirements_tabs.vue';
import RequirementsLoading from 'ee/requirements/components/requirements_loading.vue'; import RequirementsLoading from 'ee/requirements/components/requirements_loading.vue';
...@@ -19,15 +22,13 @@ import { ...@@ -19,15 +22,13 @@ import {
mockRequirementsOpen, mockRequirementsOpen,
mockRequirementsCount, mockRequirementsCount,
mockPageInfo, mockPageInfo,
mockFilters,
} from '../mock_data'; } from '../mock_data';
jest.mock('ee/requirements/constants', () => ({ jest.mock('ee/requirements/constants', () => ({
DEFAULT_PAGE_SIZE: 2, DEFAULT_PAGE_SIZE: 2,
FilterState: { FilterState: jest.requireActual('ee/requirements/constants').FilterState,
opened: 'OPENED', AvailableSortOptions: jest.requireActual('ee/requirements/constants').AvailableSortOptions,
archived: 'ARCHIVED',
all: 'ALL',
},
})); }));
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -181,6 +182,32 @@ describe('RequirementsRoot', () => { ...@@ -181,6 +182,32 @@ describe('RequirementsRoot', () => {
expect(wrapper.vm.showPaginationControls).toBe(false); expect(wrapper.vm.showPaginationControls).toBe(false);
}); });
}); });
it.each`
hasPreviousPage | hasNextPage | isVisible
${true} | ${undefined} | ${true}
${undefined} | ${true} | ${true}
${false} | ${undefined} | ${false}
${undefined} | ${false} | ${false}
${false} | ${false} | ${false}
${true} | ${true} | ${true}
`(
'returns $isVisible when hasPreviousPage is $hasPreviousPage and hasNextPage is $hasNextPage within `requirements.pageInfo`',
({ hasPreviousPage, hasNextPage, isVisible }) => {
wrapper.setData({
requirements: {
pageInfo: {
hasPreviousPage,
hasNextPage,
},
},
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.showPaginationControls).toBe(isVisible);
});
},
);
}); });
describe('prevPage', () => { describe('prevPage', () => {
...@@ -225,23 +252,37 @@ describe('RequirementsRoot', () => { ...@@ -225,23 +252,37 @@ describe('RequirementsRoot', () => {
}, },
}; };
describe('updateUrl', () => { describe('getFilteredSearchValue', () => {
it('updates window URL with query params `page` and `prev`', () => { it('returns array containing applied filter search values', () => {
wrapper.vm.updateUrl({ wrapper.setData({
page: 2, authorUsernames: ['root', 'john.doe'],
prev: mockPageInfo.startCursor, textSearch: 'foo',
}); });
expect(global.window.location.href).toContain(`?page=2&prev=${mockPageInfo.startCursor}`); return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.getFilteredSearchValue()).toEqual(mockFilters);
});
}); });
});
it('updates window URL with query params `page` and `next`', () => { describe('updateUrl', () => {
wrapper.vm.updateUrl({ it('updates window URL based on presence of props for filtered search and sort criteria', () => {
page: 1, wrapper.setData({
next: mockPageInfo.endCursor, filterBy: FilterState.all,
currentPage: 2,
nextPageCursor: mockPageInfo.endCursor,
authorUsernames: ['root', 'john.doe'],
textSearch: 'foo',
sortBy: 'updated_asc',
}); });
expect(global.window.location.href).toContain(`?page=1&next=${mockPageInfo.endCursor}`); return wrapper.vm.$nextTick(() => {
wrapper.vm.updateUrl();
expect(global.window.location.href).toBe(
`http://localhost/?page=2&next=${mockPageInfo.endCursor}&state=all&search=foo&sort=updated_asc&author_username%5B%5D=root&author_username%5B%5D=john.doe`,
);
});
}); });
}); });
...@@ -613,45 +654,72 @@ describe('RequirementsRoot', () => { ...@@ -613,45 +654,72 @@ describe('RequirementsRoot', () => {
}); });
}); });
describe('handlePageChange', () => { describe('handleFilterRequirements', () => {
beforeEach(() => { it('updates props tied to requirements Graph query', () => {
jest.spyOn(wrapper.vm, 'updateUrl').mockImplementation(jest.fn()); wrapper.vm.handleFilterRequirements(mockFilters);
expect(wrapper.vm.authorUsernames).toEqual(['root', 'john.doe']);
expect(wrapper.vm.textSearch).toBe('foo');
expect(wrapper.vm.currentPage).toBe(1);
expect(wrapper.vm.prevPageCursor).toBe('');
expect(wrapper.vm.nextPageCursor).toBe('');
expect(global.window.location.href).toBe(
`http://localhost/?page=1&state=opened&search=foo&sort=created_desc&author_username%5B%5D=root&author_username%5B%5D=john.doe`,
);
});
});
describe('handleSortRequirements', () => {
it('updates props tied to requirements Graph query', () => {
wrapper.vm.handleSortRequirements('updated_desc');
expect(wrapper.vm.sortBy).toBe('updated_desc');
expect(wrapper.vm.currentPage).toBe(1);
expect(wrapper.vm.prevPageCursor).toBe('');
expect(wrapper.vm.nextPageCursor).toBe('');
expect(global.window.location.href).toBe(
`http://localhost/?page=1&state=opened&sort=updated_desc`,
);
});
});
describe('handlePageChange', () => {
it('sets data prop `prevPageCursor` to empty string and `nextPageCursor` to `requirements.pageInfo.endCursor` when provided page param is greater than currentPage', () => {
wrapper.setData({ wrapper.setData({
requirements: { requirements: {
list: mockRequirementsOpen, list: mockRequirementsOpen,
pageInfo: mockPageInfo, pageInfo: mockPageInfo,
}, },
currentPage: 1,
requirementsCount: mockRequirementsCount, requirementsCount: mockRequirementsCount,
}); });
return wrapper.vm.$nextTick();
});
it('calls `updateUrl` with `page` and `next` params when value of page is `2`', () => {
wrapper.vm.handlePageChange(2); wrapper.vm.handlePageChange(2);
expect(wrapper.vm.updateUrl).toHaveBeenCalledWith({ expect(wrapper.vm.prevPageCursor).toBe('');
page: 2, expect(wrapper.vm.nextPageCursor).toBe(mockPageInfo.endCursor);
prev: '', expect(global.window.location.href).toBe(
next: mockPageInfo.endCursor, `http://localhost/?page=2&state=opened&sort=created_desc&next=${mockPageInfo.endCursor}`,
}); );
}); });
it('calls `updateUrl` with `page` and `next` params when value of page is `1`', () => { it('sets data prop `nextPageCursor` to empty string and `prevPageCursor` to `requirements.pageInfo.startCursor` when provided page param is less than currentPage', () => {
wrapper.setData({ wrapper.setData({
requirements: {
list: mockRequirementsOpen,
pageInfo: mockPageInfo,
},
currentPage: 2, currentPage: 2,
requirementsCount: mockRequirementsCount,
}); });
return wrapper.vm.$nextTick(() => { wrapper.vm.handlePageChange(1);
wrapper.vm.handlePageChange(1);
expect(wrapper.vm.updateUrl).toHaveBeenCalledWith({ expect(wrapper.vm.prevPageCursor).toBe(mockPageInfo.startCursor);
page: 1, expect(wrapper.vm.nextPageCursor).toBe('');
prev: mockPageInfo.startCursor, expect(global.window.location.href).toBe(
next: '', `http://localhost/?page=1&state=opened&sort=created_desc&prev=${mockPageInfo.startCursor}`,
}); );
});
}); });
}); });
}); });
...@@ -662,7 +730,27 @@ describe('RequirementsRoot', () => { ...@@ -662,7 +730,27 @@ describe('RequirementsRoot', () => {
}); });
it('renders requirements-tabs component', () => { it('renders requirements-tabs component', () => {
expect(wrapper.find(RequirementsTabs).exists()).toBe(true); expect(wrapper.contains(RequirementsTabs)).toBe(true);
});
it('renders filtered-search-bar component', () => {
expect(wrapper.contains(FilteredSearchBarRoot)).toBe(true);
expect(wrapper.find(FilteredSearchBarRoot).props('searchInputPlaceholder')).toBe(
'Search requirements',
);
expect(wrapper.find(FilteredSearchBarRoot).props('tokens')).toEqual([
{
type: 'author_username',
icon: 'user',
title: 'Author',
unique: false,
symbol: '@',
token: AuthorToken,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchPath: 'gitlab-org/gitlab-shell',
fetchAuthors: expect.any(Function),
},
]);
}); });
it('renders empty state when query results are empty', () => { it('renders empty state when query results are empty', () => {
...@@ -676,7 +764,7 @@ describe('RequirementsRoot', () => { ...@@ -676,7 +764,7 @@ describe('RequirementsRoot', () => {
}); });
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(wrapper.find(RequirementsEmptyState).exists()).toBe(true); expect(wrapper.contains(RequirementsEmptyState)).toBe(true);
}); });
}); });
...@@ -694,7 +782,7 @@ describe('RequirementsRoot', () => { ...@@ -694,7 +782,7 @@ describe('RequirementsRoot', () => {
}); });
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(wrapper.find(RequirementForm).exists()).toBe(true); expect(wrapper.contains(RequirementForm)).toBe(true);
}); });
}); });
...@@ -704,7 +792,7 @@ describe('RequirementsRoot', () => { ...@@ -704,7 +792,7 @@ describe('RequirementsRoot', () => {
}); });
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(wrapper.find(RequirementsEmptyState).exists()).toBe(false); expect(wrapper.contains(RequirementsEmptyState)).toBe(false);
}); });
}); });
......
...@@ -72,3 +72,15 @@ export const mockPageInfo = { ...@@ -72,3 +72,15 @@ export const mockPageInfo = {
startCursor: 'eyJpZCI6IjI1IiwiY3JlYXRlZF9hdCI6IjIwMjAtMDMtMzEgMTM6MzI6MTQgVVRDIn0', startCursor: 'eyJpZCI6IjI1IiwiY3JlYXRlZF9hdCI6IjIwMjAtMDMtMzEgMTM6MzI6MTQgVVRDIn0',
endCursor: 'eyJpZCI6IjIxIiwiY3JlYXRlZF9hdCI6IjIwMjAtMDMtMzEgMTM6MzE6MTUgVVRDIn0', endCursor: 'eyJpZCI6IjIxIiwiY3JlYXRlZF9hdCI6IjIwMjAtMDMtMzEgMTM6MzE6MTUgVVRDIn0',
}; };
export const mockFilters = [
{
type: 'author_username',
value: { data: 'root' },
},
{
type: 'author_username',
value: { data: 'john.doe' },
},
'foo',
];
...@@ -6575,6 +6575,9 @@ msgstr "" ...@@ -6575,6 +6575,9 @@ msgstr ""
msgid "Created by me" msgid "Created by me"
msgstr "" msgstr ""
msgid "Created date"
msgstr ""
msgid "Created issue %{issueLink}" msgid "Created issue %{issueLink}"
msgstr "" msgstr ""
...@@ -19170,6 +19173,9 @@ msgstr "" ...@@ -19170,6 +19173,9 @@ msgstr ""
msgid "Search projects..." msgid "Search projects..."
msgstr "" msgstr ""
msgid "Search requirements"
msgstr ""
msgid "Search users" msgid "Search users"
msgstr "" msgstr ""
......
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