Commit 30edb799 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '268372-epics-list-using-issuable-list' into 'master'

Epics list using `issuable_list` [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!46769
parents 4ccb852e 8d660da8
---
name: vue_epics_list
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46769
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276189
milestone: '13.9'
type: development
group: group::product planning
default_enabled: false
<script>
import { GlEmptyState } from '@gitlab/ui';
import { __ } from '~/locale';
import { IssuableStates } from '~/issuable_list/constants';
import { FilterStateEmptyMessage } from '../constants';
export default {
components: {
GlEmptyState,
},
inject: ['emptyStatePath'],
props: {
currentState: {
type: String,
required: true,
},
epicsCount: {
type: Object,
required: true,
},
},
computed: {
emptyStateTitle() {
return this.epicsCount[IssuableStates.All]
? FilterStateEmptyMessage[this.currentState]
: __(
'Epics let you manage your portfolio of projects more efficiently and with less effort',
);
},
showDescription() {
return !this.epicsCount[IssuableStates.All];
},
},
};
</script>
<template>
<gl-empty-state :svg-path="emptyStatePath" :title="emptyStateTitle">
<template v-if="showDescription" #description>
{{ __('Track groups of issues that share a theme, across projects and milestones') }}
</template>
</gl-empty-state>
</template>
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
import EpicsFilteredSearchMixin from 'ee/roadmap/mixins/filtered_search_mixin';
import { s__, sprintf } from '~/locale';
import createFlash from '~/flash';
import { parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs, DEFAULT_PAGE_SIZE } from '~/issuable_list/constants';
import groupEpics from '../queries/group_epics.query.graphql';
import { EpicsSortOptions } from '../constants';
import EpicsListEmptyState from './epics_list_empty_state.vue';
export default {
IssuableListTabs,
EpicsSortOptions,
defaultPageSize: DEFAULT_PAGE_SIZE,
epicSymbol: '&',
components: {
GlButton,
GlIcon,
IssuableList,
EpicsListEmptyState,
},
mixins: [EpicsFilteredSearchMixin],
inject: [
'canCreateEpic',
'canBulkEditEpics',
'page',
'prev',
'next',
'initialState',
'initialSortBy',
'epicsCount',
'epicNewPath',
'groupFullPath',
'groupLabelsPath',
'groupMilestonesPath',
'emptyStatePath',
],
apollo: {
epics: {
query: groupEpics,
variables() {
const queryVariables = {
groupPath: this.groupFullPath,
state: this.currentState,
};
if (this.prevPageCursor) {
queryVariables.prevPageCursor = this.prevPageCursor;
queryVariables.lastPageSize = this.$options.defaultPageSize;
} else if (this.nextPageCursor) {
queryVariables.nextPageCursor = this.nextPageCursor;
queryVariables.firstPageSize = this.$options.defaultPageSize;
} else {
queryVariables.firstPageSize = this.$options.defaultPageSize;
}
if (this.sortedBy) {
queryVariables.sortBy = this.sortedBy;
}
if (Object.keys(this.filterParams).length) {
Object.assign(queryVariables, {
...this.filterParams,
});
}
return queryVariables;
},
update(data) {
const epicsRoot = data.group?.epics;
return {
list: epicsRoot?.nodes || [],
pageInfo: epicsRoot?.pageInfo || {},
};
},
error(error) {
createFlash({
message: s__('Epics|Something went wrong while fetching epics list.'),
captureError: true,
error,
});
},
},
},
props: {
initialFilterParams: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
currentState: this.initialState,
currentPage: this.page,
prevPageCursor: this.prev,
nextPageCursor: this.next,
filterParams: this.initialFilterParams,
sortedBy: this.initialSortBy,
epics: {
list: [],
pageInfo: {},
},
};
},
computed: {
epicsListLoading() {
return this.$apollo.queries.epics.loading;
},
epicsListEmpty() {
return !this.$apollo.queries.epics.loading && !this.epics.list.length;
},
showPaginationControls() {
const { hasPreviousPage, hasNextPage } = this.epics.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.epicsListEmpty;
},
previousPage() {
return Math.max(this.currentPage - 1, 0);
},
nextPage() {
const nextPage = this.currentPage + 1;
return nextPage >
Math.ceil(this.epicsCount[this.currentState] / this.$options.defaultPageSize)
? null
: nextPage;
},
},
methods: {
epicTimeframe({ startDate, dueDate }) {
const start = startDate ? parsePikadayDate(startDate) : null;
const due = dueDate ? parsePikadayDate(dueDate) : null;
if (startDate && dueDate) {
const startDateInWords = dateInWords(
start,
true,
start.getFullYear() === due.getFullYear(),
);
const dueDateInWords = dateInWords(due, true);
return sprintf(s__('Epics|%{startDate} – %{dueDate}'), {
startDate: startDateInWords,
dueDate: dueDateInWords,
});
} else if (startDate && !dueDate) {
return sprintf(s__('Epics|%{startDate} – No due date'), {
startDate: dateInWords(start, true, false),
});
} else if (!startDate && dueDate) {
return sprintf(s__('Epics|No start date – %{dueDate}'), {
dueDate: dateInWords(due, true, false),
});
}
return '';
},
fetchEpicsBy(propsName, propValue) {
if (propsName === 'currentPage') {
const { startCursor, endCursor } = this.epics.pageInfo;
if (propValue > this.currentPage) {
this.prevPageCursor = '';
this.nextPageCursor = endCursor;
} else {
this.prevPageCursor = startCursor;
this.nextPageCursor = '';
}
} else if (propsName === 'currentState') {
this.currentPage = 1;
this.prevPageCursor = '';
this.nextPageCursor = '';
}
this[propsName] = propValue;
},
handleFilterEpics(filters) {
this.filterParams = this.getFilterParams(filters);
},
},
};
</script>
<template>
<issuable-list
:namespace="groupFullPath"
:tabs="$options.IssuableListTabs"
:current-tab="currentState"
:tab-counts="epicsCount"
:search-input-placeholder="__('Search or filter results...')"
:search-tokens="getFilteredSearchTokens()"
:sort-options="$options.EpicsSortOptions"
:initial-filter-value="getFilteredSearchValue()"
:initial-sort-by="sortedBy"
:issuables="epics.list"
:issuables-loading="epicsListLoading"
:show-pagination-controls="showPaginationControls"
:show-discussions="true"
:default-page-size="$options.defaultPageSize"
:current-page="currentPage"
:previous-page="previousPage"
:next-page="nextPage"
:url-params="urlParams"
:issuable-symbol="$options.epicSymbol"
recent-searches-storage-key="epics"
@click-tab="fetchEpicsBy('currentState', $event)"
@page-change="fetchEpicsBy('currentPage', $event)"
@sort="fetchEpicsBy('sortedBy', $event)"
@filter="handleFilterEpics"
>
<template v-if="canCreateEpic || canBulkEditEpics" #nav-actions>
<gl-button v-if="canCreateEpic" category="primary" variant="success" :href="epicNewPath">{{
__('New epic')
}}</gl-button>
</template>
<template #timeframe="{ issuable }">
<gl-icon name="calendar" />
{{ epicTimeframe(issuable) }}
</template>
<template #empty-state>
<epics-list-empty-state :current-state="currentState" :epics-count="epicsCount" />
</template>
</issuable-list>
</template>
import { __ } from '~/locale';
import { AvailableSortOptions } from '~/issuable_list/constants';
export const EpicsSortOptions = [
{
id: AvailableSortOptions.length + 10,
title: __('Start date'),
sortDirection: {
descending: 'start_date_desc',
ascending: 'start_date_asc',
},
},
{
id: AvailableSortOptions.length + 20,
title: __('Due date'),
sortDirection: {
descending: 'end_date_desc',
ascending: 'end_date_asc',
},
},
];
export const FilterStateEmptyMessage = {
opened: __('There are no open epics'),
closed: __('There are no closed epics'),
};
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import {
urlParamsToObject,
parseBoolean,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import { IssuableStates } from '~/issuable_list/constants';
import EpicsListApp from './components/epics_list_root.vue';
Vue.use(VueApollo);
export default function initEpicsList({ mountPointSelector }) {
const mountPointEl = document.querySelector(mountPointSelector);
if (!mountPointEl) {
return null;
}
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const {
page = 1,
prev = '',
next = '',
initialState = IssuableStates.Opened,
initialSortBy = 'start_date_desc',
canCreateEpic,
canBulkEditEpics,
epicsCountOpened,
epicsCountClosed,
epicsCountAll,
epicNewPath,
groupFullPath,
groupLabelsPath,
groupMilestonesPath,
emptyStatePath,
} = mountPointEl.dataset;
const rawFilterParams = urlParamsToObject(window.location.search);
const initialFilterParams = {
...convertObjectPropsToCamelCase(rawFilterParams, {
dropKeys: ['scope', 'utf8', 'state', 'sort'], // These keys are unsupported/unnecessary
}),
// We shall put parsed value of `confidential` only
// when it is defined.
...(rawFilterParams.confidential && {
confidential: parseBoolean(rawFilterParams.confidential),
}),
};
return new Vue({
el: mountPointEl,
apolloProvider,
provide: {
initialState,
initialSortBy,
prev,
next,
page: parseInt(page, 10),
canCreateEpic: parseBoolean(canCreateEpic),
canBulkEditEpics: parseBoolean(canBulkEditEpics),
epicsCount: {
[IssuableStates.Opened]: parseInt(epicsCountOpened, 10),
[IssuableStates.Closed]: parseInt(epicsCountClosed, 10),
[IssuableStates.All]: parseInt(epicsCountAll, 10),
},
epicNewPath,
groupFullPath,
groupLabelsPath,
groupMilestonesPath,
emptyStatePath,
},
render: (createElement) =>
createElement(EpicsListApp, {
props: {
initialFilterParams,
},
}),
});
}
#import "~/graphql_shared/fragments/author.fragment.graphql"
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query groupEpics(
$groupPath: ID!
$state: EpicState
$authorUsername: String
$labelName: [String!]
$milestoneTitle: String = ""
$confidential: Boolean
$sortBy: EpicSort
$firstPageSize: Int
$lastPageSize: Int
$prevPageCursor: String = ""
$nextPageCursor: String = ""
) {
group(fullPath: $groupPath) {
epics(
state: $state
authorUsername: $authorUsername
labelName: $labelName
milestoneTitle: $milestoneTitle
confidential: $confidential
sort: $sortBy
first: $firstPageSize
last: $lastPageSize
after: $nextPageCursor
before: $prevPageCursor
) {
nodes {
id
iid
title
createdAt
updatedAt
startDate
dueDate
webUrl
userDiscussionsCount
confidential
author {
...Author
}
labels {
nodes {
...Label
}
}
}
pageInfo {
...PageInfo
}
}
}
}
import FilteredSearchTokenKeysEpics from 'ee/filtered_search/filtered_search_token_keys_epics';
import initEpicCreateApp from 'ee/epic/epic_bundle';
import initEpicsList from 'ee/epics_list/epics_list_bundle';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
const EPIC_BULK_UPDATE_PREFIX = 'epic_';
initFilteredSearch({
page: 'epics',
isGroup: true,
isGroupDecendent: true,
useDefaultState: true,
filteredSearchTokenKeys: FilteredSearchTokenKeysEpics,
stateFiltersSelector: '.epics-state-filters',
});
if (gon.features.vueEpicsList) {
initEpicsList({
mountPointSelector: '#js-epics-list',
});
} else {
initFilteredSearch({
page: 'epics',
isGroup: true,
isGroupDecendent: true,
useDefaultState: true,
filteredSearchTokenKeys: FilteredSearchTokenKeysEpics,
stateFiltersSelector: '.epics-state-filters',
});
initEpicCreateApp(true);
initEpicCreateApp(true);
issuableInitBulkUpdateSidebar.init(EPIC_BULK_UPDATE_PREFIX);
issuableInitBulkUpdateSidebar.init(EPIC_BULK_UPDATE_PREFIX);
}
......@@ -18,6 +18,10 @@ class Groups::EpicsController < Groups::ApplicationController
before_action :verify_group_bulk_edit_enabled!, only: [:bulk_update]
after_action :log_epic_show, only: :show
before_action do
push_frontend_feature_flag(:vue_epics_list, @group, type: :development, default_enabled: :yaml)
end
feature_category :epics
def new
......
......@@ -2,20 +2,37 @@
- page_title _("Epics")
.top-area
= render 'shared/issuable/epic_nav', type: :epics
.nav-controls
- if @can_bulk_update
= render_if_exists 'shared/issuable/bulk_update_button', type: :epics
- if can?(current_user, :create_epic, @group)
= link_to _('New epic'), new_group_epic_path(@group), class: 'btn btn-success gl-button', data: { qa_selector: 'new_epic_button' }
- if Feature.enabled?(:vue_epics_list, @group)
#js-epics-list{ data: { can_create_epic: can?(current_user, :create_epic, @group).to_s,
can_bulk_edit_epics: @can_bulk_update.to_s,
page: params[:page],
prev: params[:prev],
next: params[:next],
initial_state: params[:state],
initial_sort_by: params[:sort],
epics_count: { opened: issuables_count_for_state(:epic, :opened),
closed: issuables_count_for_state(:epic, :closed),
all: issuables_count_for_state(:epic, :all) },
epic_new_path: new_group_epic_url(@group),
group_full_path: @group.full_path,
group_labels_path: group_labels_path(@group, format: :json),
group_milestones_path: group_milestones_path(@group, format: :json),
empty_state_path: image_path('illustrations/epics/list.svg') } }
- else
.top-area
= render 'shared/issuable/epic_nav', type: :epics
.nav-controls
- if @can_bulk_update
= render_if_exists 'shared/issuable/bulk_update_button', type: :epics
- if can?(current_user, :create_epic, @group)
= link_to _('New epic'), new_group_epic_path(@group), class: 'btn btn-success gl-button', data: { qa_selector: 'new_epic_button' }
= render 'shared/epic/search_bar', type: :epics
= render 'shared/epic/search_bar', type: :epics
- if @can_bulk_update
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :epics
- if @can_bulk_update
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :epics
- if @epics.to_a.any?
= render 'shared/epics'
- else
= render 'shared/empty_states/epics'
- if @epics.to_a.any?
= render 'shared/epics'
- else
= render 'shared/empty_states/epics'
......@@ -10,6 +10,7 @@ RSpec.describe 'epics list', :js do
stub_licensed_features(epics: true)
stub_feature_flags(unfiltered_epic_aggregates: false)
stub_feature_flags(async_filtering: false)
stub_feature_flags(vue_epics_list: false)
sign_in(user)
end
......@@ -212,4 +213,45 @@ RSpec.describe 'epics list', :js do
end
end
end
context 'vue epics list' do
let!(:epic1) { create(:epic, group: group, start_date: '2020-12-15', end_date: '2021-1-15') }
let!(:epic2) { create(:epic, group: group, start_date: '2020-12-15') }
let!(:epic3) { create(:epic, group: group, end_date: '2021-1-15') }
before do
stub_feature_flags(vue_epics_list: true)
group.add_developer(user)
visit group_epics_path(group)
wait_for_requests
end
it 'renders epics list' do
page.within('.issuable-list-container') do
expect(page).to have_selector('.gl-tabs')
expect(page).to have_link('New epic')
expect(page).to have_selector('.vue-filtered-search-bar-container')
expect(page.find('.issuable-list')).to have_selector('li.issue', count: 3)
end
end
it 'renders epics item with metadata' do
page.within('.issuable-list-container .issuable-list') do
expect(page.all('.issuable-info-container')[0].find('.issue-title')).to have_content(epic2.title)
expect(page.all('.issuable-info-container')[0].find('.issuable-reference')).to have_content("&#{epic2.iid}")
expect(page.all('.issuable-info-container')[0].find('.issuable-authored')).to have_content('created')
expect(page.all('.issuable-info-container')[0].find('.issuable-authored')).to have_content("by #{epic2.author.name}")
end
end
it 'renders epic item timeframe' do
page.within('.issuable-list-container .issuable-list') do
expect(page.all('.issuable-info-container')[0].find('.issuable-info')).to have_content('Dec 15, 2020 – No due date')
expect(page.all('.issuable-info-container')[1].find('.issuable-info')).to have_content('Dec 15, 2020 – Jan 15, 2021')
expect(page.all('.issuable-info-container')[2].find('.issuable-info')).to have_content('No start date – Jan 15, 2021')
end
end
end
end
......@@ -16,6 +16,7 @@ RSpec.describe 'epics list', :js do
before do
stub_licensed_features(epics: true)
stub_feature_flags(vue_epics_list: false)
sign_in(user)
......
import { GlEmptyState } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import EpicsListEmptyState from 'ee/epics_list/components/epics_list_empty_state.vue';
const createComponent = (props = {}) =>
mount(EpicsListEmptyState, {
provide: {
emptyStatePath: '/assets/illustrations/empty-state/epics.svg',
},
propsData: {
currentState: 'opened',
epicsCount: {
opened: 0,
closed: 0,
all: 0,
},
...props,
},
});
describe('EpicsListEmptyState', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('renders gl-empty-state component', () => {
const emptyStateEl = wrapper.find(GlEmptyState);
expect(emptyStateEl.exists()).toBe(true);
expect(emptyStateEl.props('svgPath')).toBe('/assets/illustrations/empty-state/epics.svg');
});
it('returns string "There are no open epics" when value of `currentState` prop is "opened" and group has some epics', async () => {
wrapper.setProps({
epicsCount: {
opened: 0,
closed: 2,
all: 2,
},
});
await wrapper.vm.$nextTick();
expect(wrapper.find('h1').text()).toBe('There are no open epics');
});
it('returns string "There are no archived epics" when value of `currenState` prop is "closed" and group has some epics', async () => {
wrapper.setProps({
currentState: 'closed',
epicsCount: {
opened: 2,
closed: 0,
all: 2,
},
});
await wrapper.vm.$nextTick();
expect(wrapper.find('h1').text()).toBe('There are no closed epics');
});
it('returns a generic string when group has no epics', () => {
expect(wrapper.find('h1').text()).toBe(
'Epics let you manage your portfolio of projects more efficiently and with less effort',
);
});
it('renders empty state description with default description when all epics count is not zero', async () => {
wrapper.setProps({
epicsCount: {
opened: 0,
closed: 0,
all: 0,
},
});
await wrapper.vm.$nextTick();
expect(wrapper.find('p').exists()).toBe(true);
expect(wrapper.find('p').text()).toContain(
'Track groups of issues that share a theme, across projects and milestones',
);
});
it('does not render empty state description when all epics count is zero', async () => {
wrapper.setProps({
epicsCount: {
opened: 1,
closed: 0,
all: 1,
},
});
await wrapper.vm.$nextTick();
expect(wrapper.find('p').exists()).toBe(false);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { pick } from 'lodash';
import { stubComponent } from 'helpers/stub_component';
import EpicsListRoot from 'ee/epics_list/components/epics_list_root.vue';
import { EpicsSortOptions } from 'ee/epics_list/constants';
import { mockFormattedEpic } from 'ee_jest/roadmap/mock_data';
import { mockAuthor, mockLabels } from 'jest/issuable_list/mock_data';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs } from '~/issuable_list/constants';
jest.mock('~/issuable_list/constants', () => ({
DEFAULT_PAGE_SIZE: 2,
IssuableListTabs: jest.requireActual('~/issuable_list/constants').IssuableListTabs,
AvailableSortOptions: jest.requireActual('~/issuable_list/constants').AvailableSortOptions,
}));
const mockRawEpic = {
...pick(mockFormattedEpic, [
'title',
'createdAt',
'updatedAt',
'webUrl',
'userDiscussionsCount',
'confidential',
]),
author: mockAuthor,
labels: {
nodes: mockLabels,
},
startDate: '2021-04-01',
dueDate: '2021-06-30',
};
const mockEpics = new Array(5)
.fill()
.map((_, i) => ({ ...mockRawEpic, id: i + 1, iid: (i + 1) * 10 }));
const mockProvide = {
canCreateEpic: true,
canBulkEditEpics: true,
page: 1,
prev: '',
next: '',
initialState: 'opened',
initialSortBy: 'created_desc',
epicsCount: {
opened: 5,
closed: 0,
all: 5,
},
epicNewPath: '/groups/gitlab-org/-/epics/new',
groupFullPath: 'gitlab-org',
groupLabelsPath: '/gitlab-org/-/labels.json',
groupMilestonesPath: '/gitlab-org/-/milestone.json',
emptyStatePath: '/assets/illustrations/empty-state/epics.svg',
};
const mockPageInfo = {
startCursor: 'eyJpZCI6IjI1IiwiY3JlYXRlZF9hdCI6IjIwMjAtMDMtMzEgMTM6MzI6MTQgVVRDIn0',
endCursor: 'eyJpZCI6IjIxIiwiY3JlYXRlZF9hdCI6IjIwMjAtMDMtMzEgMTM6MzE6MTUgVVRDIn0',
};
const createComponent = ({
provide = mockProvide,
initialFilterParams = {},
epicsLoading = false,
epicsList = mockEpics,
} = {}) =>
shallowMount(EpicsListRoot, {
propsData: {
initialFilterParams,
},
provide,
mocks: {
$apollo: {
queries: {
epics: {
loading: epicsLoading,
list: epicsList,
pageInfo: mockPageInfo,
},
},
},
},
stubs: {
IssuableList: stubComponent(IssuableList),
},
});
describe('EpicsListRoot', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('methods', () => {
describe('epicTimeframe', () => {
it.each`
startDate | dueDate | returnValue
${'2021-1-1'} | ${'2021-2-28'} | ${'Jan 1 – Feb 28, 2021'}
${'2021-1-1'} | ${'2022-2-28'} | ${'Jan 1, 2021 – Feb 28, 2022'}
${'2021-1-1'} | ${null} | ${'Jan 1, 2021 – No due date'}
${null} | ${'2021-2-28'} | ${'No start date – Feb 28, 2021'}
`(
'returns string "$returnValue" when startDate is $startDate and dueDate is $dueDate',
({ startDate, dueDate, returnValue }) => {
expect(
wrapper.vm.epicTimeframe({
startDate,
dueDate,
}),
).toBe(returnValue);
},
);
});
describe('fetchEpicsBy', () => {
it('updates prevPageCursor and nextPageCursor values when provided propsName param is "currentPage"', async () => {
wrapper.setData({
epics: {
pageInfo: mockPageInfo,
},
});
wrapper.vm.fetchEpicsBy('currentPage', 2);
await wrapper.vm.$nextTick();
expect(wrapper.vm.prevPageCursor).toBe('');
expect(wrapper.vm.nextPageCursor).toBe(mockPageInfo.endCursor);
expect(wrapper.vm.currentPage).toBe(2);
});
});
});
describe('template', () => {
const getIssuableList = () => wrapper.find(IssuableList);
it('renders issuable-list component', async () => {
wrapper.setData({
filterParams: {
search: 'foo',
},
});
await wrapper.vm.$nextTick();
expect(getIssuableList().exists()).toBe(true);
expect(getIssuableList().props()).toMatchObject({
namespace: mockProvide.groupFullPath,
tabs: IssuableListTabs,
currentTab: 'opened',
tabCounts: mockProvide.epicsCount,
searchInputPlaceholder: 'Search or filter results...',
sortOptions: EpicsSortOptions,
initialFilterValue: ['foo'],
initialSortBy: 'created_desc',
urlParams: wrapper.vm.urlParams,
issuableSymbol: '&',
recentSearchesStorageKey: 'epics',
});
});
it.each`
hasPreviousPage | hasNextPage | returnValue
${true} | ${undefined} | ${true}
${undefined} | ${true} | ${true}
${false} | ${undefined} | ${false}
${undefined} | ${false} | ${false}
${false} | ${false} | ${false}
${true} | ${true} | ${true}
`(
'sets showPaginationControls prop value as $returnValue when hasPreviousPage is $hasPreviousPage and hasNextPage is $hasNextPage within `epics.pageInfo`',
async ({ hasPreviousPage, hasNextPage, returnValue }) => {
wrapper.setData({
epics: {
pageInfo: {
hasPreviousPage,
hasNextPage,
},
},
});
await wrapper.vm.$nextTick();
expect(getIssuableList().props('showPaginationControls')).toBe(returnValue);
},
);
it('sets previousPage prop value a number representing previous page based on currentPage value', async () => {
wrapper.setData({
currentPage: 3,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.previousPage).toBe(2);
});
it('sets nextPage prop value a number representing next page based on currentPage value', async () => {
wrapper.setData({
currentPage: 1,
epicsCount: {
opened: 5,
closed: 0,
all: 5,
},
});
await wrapper.vm.$nextTick();
expect(getIssuableList().props('nextPage')).toBe(2);
});
it('sets nextPage prop value as `null` when currentPage is already last page', async () => {
wrapper.setData({
currentPage: 3,
epicsCount: {
opened: 5,
closed: 0,
all: 5,
},
});
await wrapper.vm.$nextTick();
expect(getIssuableList().props('nextPage')).toBeNull();
});
});
});
......@@ -179,6 +179,10 @@ export const mockRawEpic = {
start_date: '2017-6-26',
end_date: '2018-03-10',
web_url: '/groups/gitlab-org/marketing/-/epics/2',
created_at: '2021-01-22T13:51:19Z',
updated_at: '2021-01-27T05:40:19Z',
user_discussions_count: 2,
confidential: false,
descendantCounts: {
openedEpics: 3,
closedEpics: 2,
......@@ -251,6 +255,10 @@ export const mockFormattedEpic = {
endDateOutOfRange: false,
webUrl: '/groups/gitlab-org/marketing/-/epics/2',
newEpic: undefined,
createdAt: '2021-01-22T13:51:19Z',
updatedAt: '2021-01-27T05:40:19Z',
userDiscussionsCount: 2,
confidential: false,
descendantWeightSum: {
closedIssues: 3,
openedIssues: 2,
......
......@@ -11544,6 +11544,12 @@ msgstr ""
msgid "Epics, Issues, and Merge Requests"
msgstr ""
msgid "Epics|%{startDate} – %{dueDate}"
msgstr ""
msgid "Epics|%{startDate} – No due date"
msgstr ""
msgid "Epics|Add a new epic"
msgstr ""
......@@ -11571,6 +11577,9 @@ msgstr ""
msgid "Epics|Leave empty to inherit from milestone dates"
msgstr ""
msgid "Epics|No start date – %{dueDate}"
msgstr ""
msgid "Epics|Remove epic"
msgstr ""
......@@ -11598,6 +11607,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching child epics."
msgstr ""
msgid "Epics|Something went wrong while fetching epics list."
msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
......@@ -29413,6 +29425,9 @@ msgstr ""
msgid "There are no charts configured for this page"
msgstr ""
msgid "There are no closed epics"
msgstr ""
msgid "There are no closed issues"
msgstr ""
......@@ -29437,6 +29452,9 @@ msgstr ""
msgid "There are no matching files"
msgstr ""
msgid "There are no open epics"
msgstr ""
msgid "There are no open issues"
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