Commit 4deef2df authored by Natalia Tepluhina's avatar Natalia Tepluhina

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

Improve Add new cadence experience

See merge request gitlab-org/gitlab!68924
parents cb77213e 5955cf4b
......@@ -11,7 +11,7 @@ import {
GlFormTextarea,
} from '@gitlab/ui';
import { TYPE_ITERATIONS_CADENCE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale';
import createCadence from '../queries/cadence_create.mutation.graphql';
import updateCadence from '../queries/cadence_update.mutation.graphql';
......@@ -164,12 +164,15 @@ export default {
id: this.cadenceId,
};
},
result({ data: { group, errors } }) {
result({ data: { group, errors }, error }) {
if (error) {
return;
}
if (errors?.length) {
[this.errorMessage] = errors;
return;
}
const cadence = group?.iterationCadences?.nodes?.[0];
if (!cadence) {
......@@ -244,14 +247,17 @@ export default {
return;
}
const { errors } = data?.result || {};
const { iterationCadence, errors } = data?.result || {};
if (errors?.length > 0) {
[this.errorMessage] = errors;
return;
}
this.$router.push({ name: 'index' });
this.$router.push({
name: 'index',
query: { createdCadenceId: getIdFromGraphQLId(iterationCadence.id) },
});
})
.catch((e) => {
this.errorMessage = __('Unable to save cadence. Please try again');
......
......@@ -15,11 +15,13 @@ import { __, s__ } from '~/locale';
import { Namespace } from '../constants';
import groupQuery from '../queries/group_iterations_in_cadence.query.graphql';
import projectQuery from '../queries/project_iterations_in_cadence.query.graphql';
import TimeboxStatusBadge from './timebox_status_badge.vue';
const pageSize = 20;
const i18n = Object.freeze({
noResults: s__('Iterations|No iterations in cadence.'),
createFirstIteration: s__('Iterations|Create your first iteration'),
error: __('Error loading iterations'),
deleteCadence: s__('Iterations|Delete cadence'),
......@@ -43,6 +45,7 @@ export default {
GlInfiniteScroll,
GlModal,
GlSkeletonLoader,
TimeboxStatusBadge,
},
apollo: {
workspace: {
......@@ -84,6 +87,11 @@ export default {
type: String,
required: true,
},
showStateBadge: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -150,6 +158,14 @@ export default {
};
},
},
created() {
if (
`${this.$router.currentRoute?.query.createdCadenceId}` ===
`${getIdFromGraphQLId(this.cadenceId)}`
) {
this.expanded = true;
}
},
methods: {
fetchMore() {
if (this.iterations.length === 0 || !this.hasNextPage || this.loading) {
......@@ -213,8 +229,7 @@ export default {
name="chevron-right"
class="gl-transition-medium"
:class="{ 'gl-rotate-90': expanded }"
/>
{{ title }}
/><span class="gl-ml-2">{{ title }}</span>
</gl-button>
<span v-if="durationInWeeks" class="gl-mr-5 gl-display-none gl-sm-display-inline-block">
......@@ -279,6 +294,7 @@ export default {
<router-link :to="path(iteration.id)">
{{ iteration.title }}
</router-link>
<timebox-status-badge v-if="showStateBadge" :state="iteration.state" />
</li>
</ol>
<div v-if="loading" class="gl-p-5">
......@@ -286,9 +302,19 @@ export default {
</div>
</template>
</gl-infinite-scroll>
<p v-else-if="!loading" class="gl-px-5">
{{ i18n.noResults }}
</p>
<template v-else-if="!loading">
<p class="gl-px-7">{{ i18n.noResults }}</p>
<gl-button
v-if="!automatic"
variant="confirm"
category="secondary"
class="gl-mb-5 gl-ml-7"
data-qa-selector="create_cadence_cta"
:to="newIteration"
>
{{ i18n.createFirstIteration }}
</gl-button>
</template>
</gl-collapse>
</li>
</template>
......@@ -97,6 +97,11 @@ export default {
}
},
},
mounted() {
if (this.$router.currentRoute.query.createdCadenceId) {
this.$apollo.queries.workspace.refetch();
}
},
methods: {
nextPage() {
this.pagination = {
......@@ -172,6 +177,7 @@ export default {
:automatic="cadence.automatic"
:title="cadence.title"
:iteration-state="state"
:show-state-badge="tabIndex === 2"
@delete-cadence="deleteCadence"
/>
</ul>
......
......@@ -2,7 +2,6 @@
/* eslint-disable vue/no-v-html */
import {
GlAlert,
GlBadge,
GlDropdown,
GlDropdownItem,
GlEmptyState,
......@@ -13,29 +12,24 @@ import BurnCharts from 'ee/burndown_chart/components/burn_charts.vue';
import { TYPE_ITERATION } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { Namespace } from '../constants';
import query from '../queries/iteration.query.graphql';
import IterationReportTabs from './iteration_report_tabs.vue';
const iterationStates = {
closed: 'closed',
upcoming: 'upcoming',
expired: 'expired',
};
import TimeboxStatusBadge from './timebox_status_badge.vue';
export default {
components: {
BurnCharts,
GlAlert,
GlBadge,
GlIcon,
GlDropdown,
GlDropdownItem,
GlEmptyState,
GlLoadingIcon,
IterationReportTabs,
TimeboxStatusBadge,
},
apollo: {
iteration: {
......@@ -85,21 +79,6 @@ export default {
showEmptyState() {
return !this.loading && this.iteration && !this.iteration.title;
},
status() {
switch (this.iteration.state) {
case iterationStates.closed:
return {
text: __('Closed'),
variant: 'danger',
};
case iterationStates.expired:
return { text: __('Past due'), variant: 'warning' };
case iterationStates.upcoming:
return { text: __('Upcoming'), variant: 'neutral' };
default:
return { text: __('Open'), variant: 'success' };
}
},
editPage() {
return {
name: 'editIteration',
......@@ -130,9 +109,7 @@ export default {
ref="topbar"
class="gl-display-flex gl-justify-items-center gl-align-items-center gl-py-3 gl-border-1 gl-border-b-solid gl-border-gray-100"
>
<gl-badge :variant="status.variant">
{{ status.text }}
</gl-badge>
<timebox-status-badge :state="iteration.state" />
<span class="gl-ml-4"
>{{ formatDate(iteration.startDate) }}{{ formatDate(iteration.dueDate) }}</span
>
......
<script>
import { GlBadge } from '@gitlab/ui';
import { __ } from '~/locale';
const iterationStates = {
closed: 'closed',
upcoming: 'upcoming',
expired: 'expired',
};
export default {
components: {
GlBadge,
},
props: {
state: {
type: String,
required: false,
default: '',
},
},
computed: {
status() {
switch (this.state) {
case iterationStates.closed:
return {
text: __('Closed'),
variant: 'danger',
};
case iterationStates.expired:
return { text: __('Past due'), variant: 'warning' };
case iterationStates.upcoming:
return { text: __('Upcoming'), variant: 'neutral' };
default:
return { text: __('Open'), variant: 'success' };
}
},
},
};
</script>
<template>
<gl-badge :variant="status.variant">
{{ status.text }}
</gl-badge>
</template>
......@@ -168,7 +168,12 @@ describe('Iteration cadence form', () => {
await waitForPromises();
expect(push).toHaveBeenCalledWith({ name: 'index' });
expect(push).toHaveBeenCalledWith({
name: 'index',
query: {
createdCadenceId: id,
},
});
});
it('does not submit if required fields missing', () => {
......
......@@ -3,12 +3,14 @@ import { createLocalVue, RouterLinkStub } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import IterationCadenceListItem from 'ee/iterations/components/iteration_cadence_list_item.vue';
import TimeboxStatusBadge from 'ee/iterations/components/timebox_status_badge.vue';
import { Namespace } from 'ee/iterations/constants';
import groupIterationsInCadenceQuery from 'ee/iterations/queries/group_iterations_in_cadence.query.graphql';
import projectIterationsInCadenceQuery from 'ee/iterations/queries/project_iterations_in_cadence.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended as mount } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
const push = jest.fn();
const $router = {
......@@ -82,11 +84,11 @@ describe('Iteration cadence list item', () => {
},
},
};
function createComponent({
props = {},
canCreateCadence,
canEditCadence,
currentRoute,
namespaceType = Namespace.Group,
query = groupIterationsInCadenceQuery,
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse),
......@@ -97,7 +99,10 @@ describe('Iteration cadence list item', () => {
localVue,
apolloProvider,
mocks: {
$router,
$router: {
...$router,
currentRoute,
},
},
stubs: {
RouterLink: RouterLinkStub,
......@@ -137,7 +142,7 @@ describe('Iteration cadence list item', () => {
expect(resolverMock).not.toHaveBeenCalled();
});
it('shows empty text when no results', async () => {
it('shows empty text and CTA when no results', async () => {
await createComponent({
resolverMock: jest.fn().mockResolvedValue(queryEmptyResponse),
});
......@@ -148,6 +153,7 @@ describe('Iteration cadence list item', () => {
expect(findLoader().exists()).toBe(false);
expect(wrapper.text()).toContain(IterationCadenceListItem.i18n.noResults);
expect(wrapper.text()).toContain(IterationCadenceListItem.i18n.createFirstIteration);
});
it('shows iterations after loading', async () => {
......@@ -162,6 +168,18 @@ describe('Iteration cadence list item', () => {
});
});
it('automatically expands for newly created cadence', async () => {
await createComponent({
currentRoute: { query: { createdCadenceId: getIdFromGraphQLId(cadence.id) } },
});
await waitForPromises();
iterations.forEach(({ title }) => {
expect(wrapper.text()).toContain(title);
});
});
it('loads project iterations for Project namespaceType', async () => {
await createComponent({
namespaceType: Namespace.Project,
......@@ -261,4 +279,17 @@ describe('Iteration cadence list item', () => {
expect(wrapper.find(GlDropdown).exists()).toBe(true);
});
it.each([
['hides', false],
['shows', true],
])('%s status badge when showStateBadge is %s', async (_, showStateBadge) => {
await createComponent({ props: { showStateBadge } });
expand();
await waitForPromises();
expect(wrapper.findComponent(TimeboxStatusBadge).exists()).toBe(showStateBadge);
});
});
......@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import IterationReport from 'ee/iterations/components/iteration_report.vue';
import IterationReportTabs from 'ee/iterations/components/iteration_report_tabs.vue';
import TimeboxStatusBadge from 'ee/iterations/components/timebox_status_badge.vue';
import { Namespace } from 'ee/iterations/constants';
import query from 'ee/iterations/queries/iteration.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
......@@ -168,7 +169,9 @@ describe('Iterations report', () => {
it('shows status and date in header', () => {
const startDate = IterationReport.methods.formatDate(mockIterationNode.startDate);
const dueDate = IterationReport.methods.formatDate(mockIterationNode.startDate);
expect(findTopbar().text().toLowerCase()).toContain(mockIterationNode.state);
expect(wrapper.findComponent(TimeboxStatusBadge).props('state')).toContain(
mockIterationNode.state,
);
expect(findTopbar().text()).toContain(startDate);
expect(findTopbar().text()).toContain(dueDate);
});
......
......@@ -18815,6 +18815,9 @@ msgstr ""
msgid "Iterations|Create cadence"
msgstr ""
msgid "Iterations|Create your first iteration"
msgstr ""
msgid "Iterations|Delete cadence"
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