Commit 011d95ed authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'ss/add-empty-state-iterations' into 'master'

When no iterations are present show empty state

See merge request gitlab-org/gitlab!79067
parents a582a43f 52aeccae
<script> <script>
import { GlAlert, GlButton, GlLoadingIcon, GlPagination, GlTab, GlTabs } from '@gitlab/ui'; import emptyStateSvg from '@gitlab/svgs/dist/illustrations/issues.svg';
import { __ } from '~/locale'; import {
GlAlert,
GlButton,
GlLoadingIcon,
GlPagination,
GlTab,
GlTabs,
GlEmptyState,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { Namespace } from '../constants'; import { Namespace } from '../constants';
import IterationsQuery from '../queries/iterations.query.graphql'; import IterationsQuery from '../queries/iterations.query.graphql';
import IterationsList from './iterations_list.vue'; import IterationsList from './iterations_list.vue';
...@@ -8,6 +17,14 @@ import IterationsList from './iterations_list.vue'; ...@@ -8,6 +17,14 @@ import IterationsList from './iterations_list.vue';
const pageSize = 20; const pageSize = 20;
export default { export default {
i18n: {
emptyStateDescription: s__(
'Iterations|Iterations are a way to track issues over a period of time, allowing teams to also track velocity and volatility metrics.',
),
newIteration: s__('Iterations|New iteration'),
noIterationsFound: s__('Iterations|No iterations found'),
},
emptySvgPath: `data:image/svg+xml;utf8,${encodeURIComponent(emptyStateSvg)}`,
components: { components: {
IterationsList, IterationsList,
GlAlert, GlAlert,
...@@ -16,6 +33,7 @@ export default { ...@@ -16,6 +33,7 @@ export default {
GlPagination, GlPagination,
GlTab, GlTab,
GlTabs, GlTabs,
GlEmptyState,
}, },
props: { props: {
fullPath: { fullPath: {
...@@ -113,6 +131,9 @@ export default { ...@@ -113,6 +131,9 @@ export default {
nextPage() { nextPage() {
return Number(this.namespace.pageInfo.hasNextPage); return Number(this.namespace.pageInfo.hasNextPage);
}, },
showEmptyState() {
return this.iterations.length === 0 && !this.loading;
},
}, },
methods: { methods: {
handlePageChange(page) { handlePageChange(page) {
...@@ -151,6 +172,15 @@ export default { ...@@ -151,6 +172,15 @@ export default {
{{ error }} {{ error }}
</gl-alert> </gl-alert>
</div> </div>
<gl-empty-state
v-else-if="showEmptyState"
:svg-path="$options.emptySvgPath"
:title="$options.i18n.noIterationsFound"
:primary-button-text="$options.i18n.newIteration"
:primary-button-link="newIterationPath"
:description="$options.i18n.emptyStateDescription"
/>
<div v-else> <div v-else>
<iterations-list :iterations="iterations" :namespace-type="namespaceType" /> <iterations-list :iterations="iterations" :namespace-type="namespaceType" />
<gl-pagination <gl-pagination
......
...@@ -48,7 +48,7 @@ export const iterationSelectTextMap = { ...@@ -48,7 +48,7 @@ export const iterationSelectTextMap = {
iterationSelectFail: __('Failed to set iteration on this issue. Please try again.'), iterationSelectFail: __('Failed to set iteration on this issue. Please try again.'),
currentIterationFetchError: __('Failed to fetch the iteration for this issue. Please try again.'), currentIterationFetchError: __('Failed to fetch the iteration for this issue. Please try again.'),
iterationsFetchError: __('Failed to fetch the iterations for the group. Please try again.'), iterationsFetchError: __('Failed to fetch the iterations for the group. Please try again.'),
noIterationsFound: __('No iterations found'), noIterationsFound: s__('Iterations|No iterations found'),
}; };
export const noIteration = null; export const noIteration = null;
......
- page_title _("Iterations") - page_title _("Iterations")
- iterations_path = @project&.group ? new_group_iteration_path(@project&.group) : ''
.js-iterations-list{ data: { full_path: @project.full_path } } .js-iterations-list{ data: { full_path: @project.full_path, new_iteration_path: iterations_path } }
...@@ -92,7 +92,7 @@ RSpec.describe 'User views iteration' do ...@@ -92,7 +92,7 @@ RSpec.describe 'User views iteration' do
wait_for_requests wait_for_requests
expect(page).to have_content('No iterations to show') expect(page).to have_content('No iterations found')
expect(page).not_to have_content(iteration.title) expect(page).not_to have_content(iteration.title)
end end
end end
......
import { GlAlert, GlLoadingIcon, GlPagination, GlTab, GlTabs } from '@gitlab/ui'; import { GlAlert, GlEmptyState, GlLoadingIcon, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Iterations from 'ee/iterations/components/iterations.vue'; import Iterations from 'ee/iterations/components/iterations.vue';
import IterationsList from 'ee/iterations/components/iterations_list.vue'; import IterationsList from 'ee/iterations/components/iterations_list.vue';
import { Namespace } from 'ee/iterations/constants'; import { Namespace } from 'ee/iterations/constants';
import query from 'ee/iterations/queries/iterations.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mockGroupIterations, mockGroupIterationsEmpty } from '../mock_data';
describe('Iterations', () => { describe('Iterations', () => {
let wrapper; let wrapper;
let mockApollo;
const defaultProps = { const defaultProps = {
fullPath: 'gitlab-org', fullPath: 'gitlab-org',
}; };
const mountComponent = ({ props = defaultProps, loading = false } = {}) => { const mountComponent = ({
props = defaultProps,
queryResponse = mockGroupIterations,
queryHandler = jest.fn().mockResolvedValue(queryResponse),
} = {}) => {
Vue.use(VueApollo);
mockApollo = createMockApollo([[query, queryHandler]]);
wrapper = shallowMount(Iterations, { wrapper = shallowMount(Iterations, {
apolloProvider: mockApollo,
propsData: props, propsData: props,
mocks: {
$apollo: {
queries: { namespace: { loading } },
},
},
stubs: { stubs: {
GlLoadingIcon, GlLoadingIcon,
GlTab, GlTab,
...@@ -29,23 +39,22 @@ describe('Iterations', () => { ...@@ -29,23 +39,22 @@ describe('Iterations', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
it('hides list while loading', () => { it('hides list while loading', () => {
mountComponent({ mountComponent();
loading: true,
});
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBeTruthy(); expect(wrapper.findComponent(GlLoadingIcon).exists()).toBeTruthy();
expect(wrapper.findComponent(IterationsList).exists()).toBeFalsy(); expect(wrapper.findComponent(IterationsList).exists()).toBeFalsy();
}); });
it('shows iterations list when not loading', () => { it('shows iterations list after loading', async () => {
mountComponent({ mountComponent({
loading: false, props: { ...defaultProps, newIterationPath: 'iterations' },
}); });
await waitForPromises();
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBeFalsy(); expect(wrapper.findComponent(GlLoadingIcon).exists()).toBeFalsy();
expect(wrapper.findComponent(IterationsList).exists()).toBeTruthy(); expect(wrapper.findComponent(IterationsList).exists()).toBeTruthy();
}); });
...@@ -64,6 +73,26 @@ describe('Iterations', () => { ...@@ -64,6 +73,26 @@ describe('Iterations', () => {
expect(wrapper.vm.state).toEqual('all'); expect(wrapper.vm.state).toEqual('all');
}); });
describe('when loading is false and iterations are empty', () => {
beforeEach(async () => {
mountComponent({
props: {
...defaultProps,
newIterationPath: 'iterations',
},
queryResponse: mockGroupIterationsEmpty,
});
await waitForPromises();
});
it('renders GlEmptyState with the correct props', () => {
expect(wrapper.findComponent(GlEmptyState).props()).toEqual(
expect.objectContaining({ primaryButtonLink: 'iterations' }),
);
});
});
describe('pagination', () => { describe('pagination', () => {
const findPagination = () => wrapper.findComponent(GlPagination); const findPagination = () => wrapper.findComponent(GlPagination);
const setPage = async (page) => { const setPage = async (page) => {
...@@ -71,22 +100,12 @@ describe('Iterations', () => { ...@@ -71,22 +100,12 @@ describe('Iterations', () => {
await nextTick(); await nextTick();
}; };
beforeEach(() => { beforeEach(async () => {
mountComponent({ mountComponent({
loading: false, queryResponse: mockGroupIterations,
});
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
namespace: {
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'first-item',
endCursor: 'last-item',
},
},
}); });
await waitForPromises();
}); });
it('passes prev, next, and current page props', () => { it('passes prev, next, and current page props', () => {
...@@ -184,19 +203,22 @@ describe('Iterations', () => { ...@@ -184,19 +203,22 @@ describe('Iterations', () => {
}); });
describe('error', () => { describe('error', () => {
beforeEach(() => { beforeEach(async () => {
mountComponent({ mountComponent({
loading: false, queryHandler: jest.fn().mockRejectedValue({
}); data: {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details group: {
// eslint-disable-next-line no-restricted-syntax errors: ['oh no'],
wrapper.setData({ },
error: 'Oh no!', },
}),
}); });
await waitForPromises();
}); });
it('tab shows error in alert', () => { it('tab shows error in alert', () => {
expect(wrapper.findComponent(GlAlert).text()).toContain('Oh no!'); expect(wrapper.findComponent(GlAlert).text()).toContain('Error loading iterations');
}); });
}); });
}); });
...@@ -10,6 +10,7 @@ export const mockIterationNode = { ...@@ -10,6 +10,7 @@ export const mockIterationNode = {
state: iterationStates.upcoming, state: iterationStates.upcoming,
title: 'top-level-iteration', title: 'top-level-iteration',
webPath: '/groups/top-level-group/-/iterations/4', webPath: '/groups/top-level-group/-/iterations/4',
scopedPath: '/groups/top-level-group/-/iterations/4',
__typename: 'Iteration', __typename: 'Iteration',
}; };
...@@ -29,6 +30,33 @@ export const mockGroupIterations = { ...@@ -29,6 +30,33 @@ export const mockGroupIterations = {
id: 'gid://gitlab/Group/114', id: 'gid://gitlab/Group/114',
iterations: { iterations: {
nodes: [mockIterationNode], nodes: [mockIterationNode],
pageInfo: {
hasNextPage: true,
hasPreviousPage: true,
startCursor: 'first-item',
endCursor: 'last-item',
__typename: 'PageInfo',
},
__typename: 'IterationConnection',
},
__typename: 'Group',
},
},
};
export const mockGroupIterationsEmpty = {
data: {
group: {
id: 'gid://gitlab/Group/114',
iterations: {
nodes: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
__typename: 'PageInfo',
},
__typename: 'IterationConnection', __typename: 'IterationConnection',
}, },
__typename: 'Group', __typename: 'Group',
......
...@@ -21000,6 +21000,9 @@ msgstr "" ...@@ -21000,6 +21000,9 @@ msgstr ""
msgid "Iterations|Iteration scheduling will be handled automatically" msgid "Iterations|Iteration scheduling will be handled automatically"
msgstr "" msgstr ""
msgid "Iterations|Iterations are a way to track issues over a period of time, allowing teams to also track velocity and volatility metrics."
msgstr ""
msgid "Iterations|Move incomplete issues to the next iteration" msgid "Iterations|Move incomplete issues to the next iteration"
msgstr "" msgstr ""
...@@ -21015,6 +21018,9 @@ msgstr "" ...@@ -21015,6 +21018,9 @@ msgstr ""
msgid "Iterations|No iteration cadences to show." msgid "Iterations|No iteration cadences to show."
msgstr "" msgstr ""
msgid "Iterations|No iterations found"
msgstr ""
msgid "Iterations|No iterations in cadence." msgid "Iterations|No iterations in cadence."
msgstr "" msgstr ""
...@@ -25166,9 +25172,6 @@ msgstr "" ...@@ -25166,9 +25172,6 @@ msgstr ""
msgid "No iteration" msgid "No iteration"
msgstr "" msgstr ""
msgid "No iterations found"
msgstr ""
msgid "No iterations to show" msgid "No iterations to show"
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