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