Commit 9fc8ef00 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch 'psi-cadence-list' into 'master'

Add iteration cadences list

See merge request gitlab-org/gitlab!61096
parents 427d1b2f 704fb932
<script>
export default {
props: {
title: {
type: String,
required: true,
},
},
};
</script>
<template>
<li>
{{ title }}
</li>
</template>
<template><ul></ul></template>
<script>
import { GlAlert, GlButton, GlLoadingIcon, GlKeysetPagination, GlTab, GlTabs } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import query from '../queries/iteration_cadences_list.query.graphql';
import IterationCadence from './iteration_cadence.vue';
const pageSize = 20;
export default {
tabTitles: [__('Open'), __('Done'), __('All')],
components: {
IterationCadence,
GlAlert,
GlButton,
GlLoadingIcon,
GlKeysetPagination,
GlTab,
GlTabs,
},
apollo: {
group: {
query,
variables() {
return this.queryVariables;
},
error({ message }) {
this.error = message || s__('Iterations|Error loading iteration cadences.');
},
},
},
inject: ['groupPath', 'cadencesListPath', 'canCreateCadence'],
data() {
return {
group: {
iterationCadences: [],
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
},
},
pagination: {},
tabIndex: 0,
error: '',
};
},
computed: {
queryVariables() {
const vars = {
fullPath: this.groupPath,
};
if (this.active !== undefined) {
vars.active = this.active;
}
if (this.pagination.beforeCursor) {
vars.beforeCursor = this.pagination.beforeCursor;
vars.lastPageSize = pageSize;
} else {
vars.afterCursor = this.pagination.afterCursor;
vars.firstPageSize = pageSize;
}
return vars;
},
cadences() {
return this.group?.iterationCadences?.nodes || [];
},
pageInfo() {
return this.group?.iterationCadences?.pageInfo || {};
},
loading() {
return this.$apollo.queries.group.loading;
},
active() {
switch (this.tabIndex) {
default:
case 0:
return true;
case 1:
return false;
case 2:
return undefined;
}
},
},
methods: {
nextPage() {
this.pagination = {
afterCursor: this.pageInfo.endCursor,
};
},
previousPage() {
this.pagination = {
beforeCursor: this.pageInfo.startCursor,
};
},
handleTabChange() {
this.pagination = {};
},
},
};
</script>
<template>
<gl-tabs v-model="tabIndex" @activate-tab="handleTabChange">
<gl-tab v-for="tab in $options.tabTitles" :key="tab">
<template #title>
{{ tab }}
</template>
<gl-loading-icon v-if="loading" class="gl-my-5" size="lg" />
<gl-alert v-else-if="error" variant="danger" @dismiss="error = ''">
{{ error }}
</gl-alert>
<template v-else>
<ul v-if="cadences.length" class="content-list">
<iteration-cadence v-for="cadence in cadences" :key="cadence.id" :title="cadence.title" />
</ul>
<p v-else class="nothing-here-block">
{{ s__('Iterations|No iteration cadences to show.') }}
</p>
<div
v-if="pageInfo.hasNextPage || pageInfo.hasPreviousPage"
class="gl-display-flex gl-justify-content-center gl-mt-3"
>
<gl-keyset-pagination
:has-next-page="pageInfo.hasNextPage"
:has-previous-page="pageInfo.hasPreviousPage"
@prev="previousPage"
@next="nextPage"
/>
</div>
</template>
</gl-tab>
<template v-if="canCreateCadence" #tabs-end>
<li class="gl-ml-auto gl-display-flex gl-align-items-center">
<gl-button
variant="confirm"
data-qa-selector="create_cadence_button"
:to="{
name: 'new',
}"
>
{{ s__('Iterations|New iteration cadence') }}
</gl-button>
</li>
</template>
</gl-tabs>
</template>
......@@ -99,7 +99,12 @@ export function initCadenceApp() {
return null;
}
const { groupFullPath: groupPath, cadencesListPath } = el.dataset;
const {
groupFullPath: groupPath,
cadencesListPath,
canCreateCadence,
canEditCadence,
} = el.dataset;
const router = createRouter(cadencesListPath);
return new Vue({
......@@ -109,6 +114,8 @@ export function initCadenceApp() {
provide: {
groupPath,
cadencesListPath,
canCreateCadence: parseBoolean(canCreateCadence),
canEditCadence: parseBoolean(canEditCadence),
},
render(createElement) {
return createElement(App);
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query IterationCadences(
$fullPath: ID!
$active: Boolean
$beforeCursor: String = ""
$afterCursor: String = ""
$firstPageSize: Int
$lastPageSize: Int
) {
group(fullPath: $fullPath) {
iterationCadences(
active: $active
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
nodes {
id
title
durationInWeeks
}
pageInfo {
...PageInfo
}
}
}
}
.js-iteration-cadence-app{ data: { group_full_path: @group.full_path,
cadences_list_path: group_iteration_cadences_path(@group),
can_create_cadence: can?(current_user, :create_iteration_cadence, @group).to_s,
can_edit_cadence: can?(current_user, :admin_iteration_cadence, @group).to_s } }
- add_to_breadcrumbs _("Iteration cadences"), group_iteration_cadences_path(@group)
- add_to_breadcrumbs _('Iteration cadences'), group_iteration_cadences_path(@group)
- breadcrumb_title params[:id]
- page_title _("Edit iteration cadence")
- page_title _('Edit iteration cadence')
.js-iteration-cadence-app{ data: { group_full_path: @group.full_path,
cadence_id: params[:id],
cadences_list_path: group_iteration_cadences_path(@group) } }
= render 'js_app'
- page_title _("Iteration cadences")
- page_title _('Iteration cadences')
.js-iteration-cadence-app{ data: { group_full_path: @group.full_path, cadences_list_path: group_iteration_cadences_path(@group) } }
= render 'js_app'
- add_to_breadcrumbs _("Iteration cadences"), group_iteration_cadences_path(@group)
- breadcrumb_title _("New")
- page_title _("New iteration cadence")
- add_to_breadcrumbs _('Iteration cadences'), group_iteration_cadences_path(@group)
- breadcrumb_title _('New')
- page_title _('New iteration cadence')
.js-iteration-cadence-app{ data: { group_full_path: @group.full_path, cadences_list_path: group_iteration_cadences_path(@group) } }
= render 'js_app'
import { GlKeysetPagination, GlLoadingIcon } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import IterationCadencesList from 'ee/iterations/components/iteration_cadences_list.vue';
import cadencesListQuery from 'ee/iterations/queries/iteration_cadences_list.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended as mount } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
const push = jest.fn();
const $router = {
push,
};
const localVue = createLocalVue();
function createMockApolloProvider(requestHandlers) {
localVue.use(VueApollo);
return createMockApollo(requestHandlers);
}
describe('Iteration cadences list', () => {
let wrapper;
let apolloProvider;
const cadencesListPath = TEST_HOST;
const groupPath = 'gitlab-org';
const cadences = [
{
id: 'gid://gitlab/Iterations::Cadence/561',
title: 'A eligendi molestias temporibus maiores architecto ut facilis autem.',
durationInWeeks: 3,
},
{
id: 'gid://gitlab/Iterations::Cadence/392',
title: 'A quam repellat omnis eum veritatis voluptas voluptatem consequuntur est.',
durationInWeeks: 4,
},
{
id: 'gid://gitlab/Iterations::Cadence/152',
title: 'A repudiandae ut eligendi quae et ducimus porro nam sint perferendis.',
durationInWeeks: 1,
},
];
const startCursor = 'MQ';
const endCursor = 'MjA';
const querySuccessResponse = {
data: {
group: {
iterationCadences: {
nodes: cadences,
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
startCursor,
endCursor,
},
},
},
},
};
const queryEmptyResponse = {
data: {
group: {
iterationCadences: {
nodes: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
},
},
},
};
function createComponent({
canCreateCadence,
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse),
} = {}) {
apolloProvider = createMockApolloProvider([[cadencesListQuery, resolverMock]]);
wrapper = mount(IterationCadencesList, {
localVue,
apolloProvider,
mocks: {
$router,
},
provide: {
groupPath,
cadencesListPath,
canCreateCadence,
},
});
return nextTick();
}
const createCadenceButton = () =>
wrapper.findByRole('link', { name: 'New iteration cadence', href: cadencesListPath });
const findNextPageButton = () => wrapper.findByRole('button', { name: 'Next' });
const findPrevPageButton = () => wrapper.findByRole('button', { name: 'Prev' });
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
afterEach(() => {
wrapper.destroy();
apolloProvider = null;
});
describe('Create cadence button', () => {
it('is shown when canCreateCadence is true', async () => {
await createComponent({ canCreateCadence: true });
expect(createCadenceButton().exists()).toBe(true);
});
it('is hidden when canCreateCadence is false', async () => {
await createComponent({
canCreateCadence: false,
});
expect(createCadenceButton().exists()).toBe(false);
});
});
describe('cadences list', () => {
it('shows loading state on mount', () => {
createComponent();
expect(findLoadingIcon().exists()).toBe(true);
});
it('shows empty text when no results', async () => {
await createComponent({
resolverMock: jest.fn().mockResolvedValue(queryEmptyResponse),
});
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
expect(findPagination().exists()).toBe(false);
expect(wrapper.text()).toContain('No iteration cadences to show');
});
it('shows cadences after loading', async () => {
await createComponent();
await waitForPromises();
cadences.forEach(({ title }) => {
expect(wrapper.text()).toContain(title);
});
});
it('shows alert on query error', async () => {
await createComponent({
resolverMock: jest.fn().mockRejectedValue(queryEmptyResponse),
});
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
expect(wrapper.text()).toContain('Network error');
});
describe('pagination', () => {
let resolverMock;
beforeEach(async () => {
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse);
await createComponent({ resolverMock });
await waitForPromises();
resolverMock.mockReset();
});
it('correctly disables pagination buttons', async () => {
expect(findNextPageButton().element.disabled).toBe(false);
expect(findPrevPageButton().element.disabled).toBe(true);
});
it('updates query when next page clicked', async () => {
findPagination().vm.$emit('next');
await nextTick();
expect(resolverMock).toHaveBeenCalledWith(
expect.objectContaining({
beforeCursor: '',
afterCursor: endCursor,
}),
);
});
it('updates query when previous page clicked', async () => {
findPagination().vm.$emit('prev');
await nextTick();
expect(resolverMock).toHaveBeenCalledWith(
expect.objectContaining({
beforeCursor: startCursor,
afterCursor: '',
}),
);
});
});
});
});
......@@ -18387,6 +18387,9 @@ msgstr ""
msgid "Iterations|Duration"
msgstr ""
msgid "Iterations|Error loading iteration cadences."
msgstr ""
msgid "Iterations|Future iterations"
msgstr ""
......@@ -18396,6 +18399,9 @@ msgstr ""
msgid "Iterations|New iteration cadence"
msgstr ""
msgid "Iterations|No iteration cadences to show."
msgstr ""
msgid "Iterations|Number of future iterations you would like to have scheduled"
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