Commit 3af417f8 authored by Simon Knox's avatar Simon Knox

Merge branch '353349-prepare-iteration-components-for-manual-mode-deprecation' into 'master'

Add deprecation alert for manual cadences

See merge request gitlab-org/gitlab!82458
parents 8371dd35 5689e1e4
......@@ -12,6 +12,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Moved to GitLab Premium in 13.9.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) in GitLab 14.6. [Feature flag `group_iterations`](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) removed.
WARNING:
After [Iteration Cadences](#iteration-cadences) becomes generally available,
manual iteration scheduling will be [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) in GitLab 15.6.
To enhance the role of iterations as time boundaries, we will also deprecate the title field.
Iterations are a way to track issues over a period of time. This allows teams
to track velocity and volatility metrics. Iterations can be used with [milestones](../../project/milestones/index.md)
for tracking over different time periods.
......@@ -28,54 +33,6 @@ In GitLab, iterations are similar to milestones, with a few differences:
- Iterations require both a start and an end date.
- Iteration date ranges cannot overlap.
## Iteration cadences
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5077) in GitLab 14.1.
> - Deployed behind a [feature flag](../../feature_flags.md), disabled by default.
> - Disabled on GitLab.com.
> - Not recommended for production use.
> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-iteration-cadences).
This in-development feature might not be available for your use. There can be
[risks when enabling features still in development](../../../administration/feature_flags.md#risks-when-enabling-features-still-in-development).
Refer to this feature's version history for more details.
Iteration cadences automate some common iteration tasks. They can be used to
automatically create iterations every 1, 2, 3, 4, or 6 weeks. They can also
be configured to automatically roll over incomplete issues to the next iteration.
With iteration cadences enabled, you must first
[create an iteration cadence](#create-an-iteration-cadence) before you can
[create an iteration](#create-an-iteration).
### Create an iteration cadence
Prerequisites:
- You must have at least the Developer role for a group.
To create an iteration cadence:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select **New iteration cadence**.
1. Fill out required fields, and select **Create iteration cadence**. The cadence list page opens.
### Delete an iteration cadence
Prerequisites:
- You must have at least the Developer role for a group.
Deleting an iteration cadence also deletes all iterations within that cadence.
To delete an iteration cadence:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select the three-dot menu (**{ellipsis_v}**) > **Delete cadence** for the cadence you want to delete.
1. Select **Delete cadence** in the confirmation modal.
## View the iterations list
To view the iterations list, go to **{issues}** **Issues > Iterations**.
......@@ -94,8 +51,6 @@ Prerequisites:
- You must have at least the Developer role for a group.
For manually scheduled iteration cadences, you create and add iterations yourself.
To create an iteration:
1. On the top bar, select **Menu > Groups** and find your group.
......@@ -153,7 +108,7 @@ The report also shows a breakdown of total issues in an iteration.
Open iteration reports show a summary of completed, unstarted, and in-progress issues.
Closed iteration reports show the total number of issues completed by the due date.
To view an iteration report, go to the iterations list page and select an iteration's title.
To view an iteration report, go to the iterations list page and select an iteration's period.
### Iteration burndown and burnup charts
......@@ -212,33 +167,61 @@ To group issues by label:
You can also search for labels by typing in the search input.
1. Select any area outside the label dropdown list. The page is now grouped by the selected labels.
### Enable or disable iteration cadences **(PREMIUM SELF)**
## Iteration cadences
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5077) in GitLab 14.1.
> - Deployed behind a [feature flag](../../feature_flags.md), named `iteration_cadences`, disabled by default.
Iteration Cadences feature is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an
administrator to [enable the feature flag](../../../administration/feature_flags.md) named
`iteration_cadences` for a root group.
On GitLab.com, this feature is not available. This feature is not ready for production use.
To enable it:
Iteration cadences automate iteration scheduling. You can use them to
automate creating iterations every 1, 2, 3, 4, or 6 weeks. You can also
configure iteration cadences to automatically roll over incomplete issues to the next iteration.
### Create an iteration cadence
Prerequisites:
```ruby
Feature.enable(:iteration_cadences)
```
- You must have at least the Developer role for a group.
To disable it:
To create an iteration cadence:
```ruby
Feature.disable(:iteration_cadences)
```
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select **New iteration cadence**.
1. Fill out required fields, and select **Create iteration cadence**. The cadence list page opens.
<!-- ## Troubleshooting
### Delete an iteration cadence
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
one might have when setting this up, or when something is changed, or on upgrading, it's
important to describe those, too. Think of things that may go wrong and include them here.
This is important to minimize requests for support, and to avoid doc comments with
questions that you know someone might ask.
Prerequisites:
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
- You must have at least the Developer role for a group.
Deleting an iteration cadence also deletes all iterations within that cadence.
To delete an iteration cadence:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select the three-dot menu (**{ellipsis_v}**) > **Delete cadence** for the cadence you want to delete.
1. Select **Delete cadence** in the confirmation modal.
### Convert manual cadence to use automatic scheduling
WARNING:
The upgrade is irreversible. After it's done, manual iteration cadences cannot be created.
When you **enable** the iteration cadences feature, all iterations are added
to a default iteration cadence.
In this default iteration cadence, you can continue to add, edit, and remove iterations.
To upgrade the iteration cadence to use the automation features:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select the three-dot menu (**{ellipsis_v}**) > **Edit cadence** for the cadence you want to upgrade.
1. Fill out required fields, and select **Save changes**.
......@@ -13,6 +13,7 @@ import {
import { TYPE_ITERATIONS_CADENCE } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import createCadence from '../queries/cadence_create.mutation.graphql';
import updateCadence from '../queries/cadence_update.mutation.graphql';
import readCadence from '../queries/iteration_cadence.query.graphql';
......@@ -22,10 +23,6 @@ const i18n = Object.freeze({
label: s__('Iterations|Title'),
placeholder: s__('Iterations|Cadence name'),
},
automatedScheduling: {
label: s__('Iterations|Automated scheduling'),
description: s__('Iterations|Iteration scheduling will be handled automatically'),
},
startDate: {
label: s__('Iterations|Start date'),
placeholder: s__('Iterations|Select start date'),
......@@ -50,18 +47,27 @@ const i18n = Object.freeze({
},
edit: {
title: s__('Iterations|Edit iteration cadence'),
save: s__('Iterations|Save cadence'),
save: s__('Iterations|Save changes'),
},
new: {
title: s__('Iterations|New iteration cadence'),
save: s__('Iterations|Create cadence'),
},
createAndStartIteration: s__('Iterations|Create cadence and start iteration'),
cancel: __('Cancel'),
requiredField: __('This field is required.'),
deprecationAlert: {
title: s__('Iterations|This cadence requires an update'),
message: s__(
'Iterations|Add a duration, and number of future iterations in order to convert this cadence to automatic scheduling.',
),
primaryButtonText: s__('Iterations|Learn more about automatic scheduling'),
},
});
export default {
iterationCadencesHelpPagePath: helpPagePath('user/group/iterations/index.md', {
anchor: 'iteration-cadences',
}),
availableDurations: [{ value: 0, text: i18n.duration.placeholder }, 1, 2, 3, 4, 5, 6],
availableFutureIterations: [
{ value: 0, text: i18n.futureIterations.placeholder },
......@@ -123,9 +129,6 @@ export default {
page() {
return this.isEdit ? 'edit' : 'new';
},
showStartIteration() {
return !this.isEdit && !this.automatic;
},
mutation() {
return this.isEdit ? updateCadence : createCadence;
},
......@@ -143,7 +146,7 @@ export default {
groupPath,
id,
title: this.title,
automatic: this.automatic,
automatic: true,
rollOver: this.rollOver,
startDate: this.startDate,
durationInWeeks: this.durationInWeeks,
......@@ -191,6 +194,10 @@ export default {
this.rollOver = cadence.rollOver;
this.iterationsInAdvance = cadence.iterationsInAdvance;
this.description = cadence.description;
if (!cadence.automatic) {
this.validateAllFields();
}
},
error(error) {
this.errorMessage = error;
......@@ -202,44 +209,15 @@ export default {
this.validationState[field] = Boolean(this[field]);
},
validateAllFields() {
Object.keys(this.validationState)
.filter((field) => {
if (this.automatic) {
return true;
}
const requiredFieldsForAutomatedScheduling = [
'iterationsInAdvance',
'durationInWeeks',
'startDate',
];
return !requiredFieldsForAutomatedScheduling.includes(field);
})
.forEach((field) => {
this.validate(field);
});
Object.keys(this.validationState).forEach((field) => {
this.validate(field);
});
},
clearValidation() {
this.validationState.startDate = null;
this.validationState.durationInWeeks = null;
this.validationState.iterationsInAdvance = null;
},
updateAutomatic(value) {
this.clearValidation();
if (!value) {
this.startDate = null;
this.iterationsInAdvance = 0;
this.durationInWeeks = 0;
}
},
saveAndCreateIteration() {
return this.save()
.then((cadenceId) => {
this.$router.push({ name: 'newIteration', params: { cadenceId } });
})
.catch((error) => {
this.errorMessage = error ?? s__('Iterations|Unable to save cadence. Please try again.');
});
},
saveAndViewList() {
return this.save()
.then((cadenceId) => {
......@@ -301,6 +279,16 @@ export default {
</h3>
</div>
<gl-form>
<gl-alert
v-if="isEdit && !automatic"
:dismissible="false"
class="gl-mb-5"
variant="danger"
:title="i18n.deprecationAlert.title"
:primary-button-text="i18n.deprecationAlert.primaryButtonText"
:primary-button-link="$options.iterationCadencesHelpPagePath"
>{{ i18n.deprecationAlert.message }}</gl-alert
>
<gl-alert v-if="errorMessage" class="gl-mb-5" variant="danger" @dismiss="errorMessage = ''">{{
errorMessage
}}</gl-alert>
......@@ -326,23 +314,6 @@ export default {
/>
</gl-form-group>
<gl-form-group
:label-cols-md="2"
label-class="gl-font-weight-bold text-right-md gl-pt-3!"
label-for="cadence-automated-scheduling"
:description="i18n.automatedScheduling.description"
>
<gl-form-checkbox
id="cadence-automated-scheduling"
v-model="automatic"
data-qa-selector="iteration_cadence_automated_scheduling_checkbox"
:disabled="loadingCadence"
@change="updateAutomatic"
>
<span class="gl-font-weight-bold">{{ i18n.automatedScheduling.label }}</span>
</gl-form-checkbox>
</gl-form-group>
<gl-form-group
:label="i18n.startDate.label"
:label-cols-md="2"
......@@ -360,8 +331,7 @@ export default {
class="datepicker gl-datepicker-input"
autocomplete="off"
inputmode="none"
:required="automatic"
:disabled="loadingCadence || !automatic"
:disabled="loadingCadence"
:state="validationState.startDate"
data-qa-selector="iteration_cadence_start_date_field"
@blur="validate('startDate')"
......@@ -383,8 +353,7 @@ export default {
v-model.number="durationInWeeks"
:options="$options.availableDurations"
class="gl-form-input-md"
:required="automatic"
:disabled="loadingCadence || !automatic"
:disabled="loadingCadence"
data-qa-selector="iteration_cadence_duration_field"
@change="validate('durationInWeeks')"
/>
......@@ -403,9 +372,8 @@ export default {
<gl-form-select
id="cadence-schedule-future-iterations"
v-model.number="iterationsInAdvance"
:disabled="!automatic || loadingCadence"
:disabled="loadingCadence"
:options="$options.availableFutureIterations"
:required="automatic"
class="gl-form-input-md"
data-qa-selector="iteration_cadence_future_iterations_field"
@change="validate('iterationsInAdvance')"
......@@ -444,22 +412,11 @@ export default {
data-testid="save-cadence"
variant="confirm"
data-qa-selector="save_iteration_cadence_button"
:disabled="!valid"
@click="saveAndViewList"
>
{{ i18n[page].save }}
</gl-button>
<gl-button
v-if="showStartIteration"
:loading="loading"
class="gl-ml-3"
data-testid="save-cadence-create-iteration"
variant="confirm"
category="secondary"
data-qa-selector="save_cadence_start_iteration_button"
@click="saveAndCreateIteration"
>
{{ i18n.createAndStartIteration }}
</gl-button>
<gl-button class="gl-ml-3" data-testid="cancel-create-cadence" @click="cancel">
{{ i18n.cancel }}
</gl-button>
......
<script>
import {
GlAlert,
GlBadge,
GlButton,
GlCollapse,
GlDropdown,
......@@ -12,7 +13,7 @@ import {
} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { fetchPolicies } from '~/lib/graphql';
import { __, s__ } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import { getIterationPeriod } from '../utils';
import { Namespace } from '../constants';
import groupQuery from '../queries/group_iterations_in_cadence.query.graphql';
......@@ -27,7 +28,7 @@ const i18n = Object.freeze({
closed: s__('Iterations|No closed iterations.'),
all: s__('Iterations|No iterations in cadence.'),
},
createIteration: s__('Iterations|Create iteration'),
addIteration: s__('Iterations|Add iteration'),
error: __('Error loading iterations'),
deleteCadence: s__('Iterations|Delete cadence'),
......@@ -37,12 +38,14 @@ const i18n = Object.freeze({
),
modalConfirm: s__('Iterations|Delete cadence'),
modalCancel: __('Cancel'),
deprecationBadgeText: s__('Iterations|Requires update'),
});
export default {
i18n,
components: {
GlAlert,
GlBadge,
GlButton,
GlCollapse,
GlDropdown,
......@@ -136,6 +139,9 @@ export default {
state: this.iterationState,
};
},
deprecationNotice() {
return sprintf(i18n.deprecationNotice, { cadenceTitle: this.title });
},
pageInfo() {
return this.workspace.iterations?.pageInfo || {};
},
......@@ -164,6 +170,12 @@ export default {
},
};
},
showAddIteration() {
return !this.automatic && this.canCreateIteration;
},
showDurationBadget() {
return this.automatic && this.durationInWeeks;
},
},
created() {
if (
......@@ -219,11 +231,18 @@ export default {
focusMenu() {
this.$refs.menu.$el.focus();
},
toEditCadence() {
this.$router.push({
name: 'edit',
params: {
cadenceId: getIdFromGraphQLId(this.cadenceId),
},
});
},
getIterationPeriod,
},
};
</script>
<template>
<li class="gl-py-0!">
<div class="gl-display-flex gl-align-items-center">
......@@ -239,11 +258,22 @@ export default {
:class="{ 'gl-rotate-90': expanded }"
/><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">
<span
v-if="showDurationBadget"
class="gl-mr-5 gl-display-none gl-sm-display-inline-block"
data-testid="duration-badge"
>
<gl-icon name="clock" class="gl-mr-3" />
{{ n__('Every week', 'Every %d weeks', durationInWeeks) }}</span
>
<gl-badge
v-if="!automatic"
variant="danger"
class="gl-mr-2 gl-display-none gl-sm-display-inline-block"
>
<gl-icon name="warning" />
{{ i18n.deprecationBadgeText }}
</gl-badge>
<gl-dropdown
v-if="canEditCadence"
ref="menu"
......@@ -255,11 +285,11 @@ export default {
data-qa-selector="cadence_options_button"
>
<gl-dropdown-item
v-if="!automatic"
v-if="showAddIteration"
:to="newIteration"
data-qa-selector="new_iteration_button"
>
{{ s__('Iterations|Add iteration') }}
{{ i18n.addIteration }}
</gl-dropdown-item>
<gl-dropdown-item :to="editCadence">
......@@ -322,16 +352,6 @@ export default {
</gl-infinite-scroll>
<template v-else-if="!loading">
<p class="gl-px-7">{{ i18n.noResults[iterationState] }}</p>
<gl-button
v-if="!automatic && canCreateIteration"
variant="confirm"
category="secondary"
class="gl-mb-5 gl-ml-7"
data-qa-selector="create_cadence_cta"
:to="newIteration"
>
{{ i18n.createIteration }}
</gl-button>
</template>
</gl-collapse>
</li>
......
<script>
import { GlAlert, GlButton, GlLoadingIcon, GlKeysetPagination, GlTab, GlTabs } from '@gitlab/ui';
import produce from 'immer';
import { __, s__ } from '~/locale';
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { Namespace } from '../constants';
import destroyIterationCadence from '../queries/destroy_cadence.mutation.graphql';
import groupQuery from '../queries/group_iteration_cadences_list.query.graphql';
......@@ -11,7 +12,19 @@ import IterationCadenceListItem from './iteration_cadence_list_item.vue';
const pageSize = 20;
export default {
tabTitles: [__('Open'), __('Done'), __('All')],
iterationCadencesHelpPagePath: helpPagePath('user/group/iterations/index.md', {
anchor: 'iteration-cadences',
}),
i18n: {
deprecationAlert: {
title: s__('Iterations|Some of your cadences need to be updated'),
message: s__(
'Iterations|Iterations can no longer be scheduled manually. Convert all cadences to automatic scheduling to keep your iterations working as expected.',
),
primaryButtonText: s__('Iterations|Learn more about automatic scheduling'),
},
tabTitles: [s__('Iterations|Open'), s__('Iterations|Done'), s__('Iterations|All')],
},
components: {
IterationCadenceListItem,
GlAlert,
......@@ -77,7 +90,7 @@ export default {
return vars;
},
cadences() {
return this.workspace?.iterationCadences?.nodes || [];
return this.groupDeprecatedItems(this.workspace?.iterationCadences?.nodes) || [];
},
pageInfo() {
return this.workspace?.iterationCadences?.pageInfo || {};
......@@ -96,6 +109,9 @@ export default {
return 'all';
}
},
manualCadenceExists() {
return this.cadences.findIndex((c) => !c.automatic) > -1;
},
},
mounted() {
if (this.$router.currentRoute.query.createdCadenceId) {
......@@ -103,6 +119,9 @@ export default {
}
},
methods: {
groupDeprecatedItems(cadences) {
return [...cadences.filter((c) => !c.automatic), ...cadences.filter((c) => c.automatic)];
},
nextPage() {
this.pagination = {
afterCursor: this.pageInfo.endCursor,
......@@ -156,7 +175,7 @@ export default {
<template>
<gl-tabs v-model="tabIndex" @activate-tab="handleTabChange">
<gl-tab v-for="tab in $options.tabTitles" :key="tab">
<gl-tab v-for="tab in $options.i18n.tabTitles" :key="tab">
<template #title>
{{ tab }}
</template>
......@@ -168,6 +187,16 @@ export default {
<gl-loading-icon v-if="loading" class="gl-my-5" size="lg" />
<template v-else>
<gl-alert
v-if="manualCadenceExists"
variant="danger"
:dismissible="false"
:title="$options.i18n.deprecationAlert.title"
:primary-button-text="$options.i18n.deprecationAlert.primaryButtonText"
:primary-button-link="$options.iterationCadencesHelpPagePath"
>
{{ $options.i18n.deprecationAlert.message }}
</gl-alert>
<ul v-if="cadences.length" class="content-list">
<iteration-cadence-list-item
v-for="cadence in cadences"
......
......@@ -34,7 +34,7 @@ RSpec.describe 'User edits iteration cadence', :js do
updated_title = 'Updated cadence title'
fill_in('Title', with: updated_title)
click_button('Save cadence')
click_button('Save changes')
expect(page).to have_content(updated_title)
end
......
......@@ -10,7 +10,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { manualIterationCadence } from '../mock_data';
import { automaticIterationCadence, manualIterationCadence } from '../mock_data';
const push = jest.fn();
const $router = {
......@@ -30,7 +30,7 @@ describe('Iteration cadence form', () => {
let wrapper;
const groupPath = 'gitlab-org';
const id = 72;
const iterationCadence = manualIterationCadence;
const iterationCadence = automaticIterationCadence;
const createMutationSuccess = {
data: { result: { iterationCadence, errors: [] } },
......@@ -45,7 +45,7 @@ describe('Iteration cadence form', () => {
group: {
id: 'gid://gitlab/Group/114',
iterationCadences: {
nodes: [manualIterationCadence],
nodes: [automaticIterationCadence],
},
},
},
......@@ -80,11 +80,10 @@ describe('Iteration cadence form', () => {
});
const findTitleGroup = () => wrapper.findAllComponents(GlFormGroup).at(0);
const findAutomatedSchedulingGroup = () => wrapper.findAllComponents(GlFormGroup).at(1);
const findStartDateGroup = () => wrapper.findAllComponents(GlFormGroup).at(2);
const findDurationGroup = () => wrapper.findAllComponents(GlFormGroup).at(3);
const findFutureIterationsGroup = () => wrapper.findAllComponents(GlFormGroup).at(4);
const findRollOverGroup = () => wrapper.findAllComponents(GlFormGroup).at(5);
const findStartDateGroup = () => wrapper.findAllComponents(GlFormGroup).at(1);
const findDurationGroup = () => wrapper.findAllComponents(GlFormGroup).at(2);
const findFutureIterationsGroup = () => wrapper.findAllComponents(GlFormGroup).at(3);
const findRollOverGroup = () => wrapper.findAllComponents(GlFormGroup).at(4);
const findError = () => wrapper.findComponent(GlAlert);
......@@ -99,11 +98,6 @@ describe('Iteration cadence form', () => {
const setStartDate = (value) => findStartDate().vm.$emit('input', value);
const setFutureIterations = (value) => findFutureIterations().vm.$emit('input', value);
const setDuration = (value) => findDuration().vm.$emit('input', value);
const setAutomaticValue = (value) => {
const checkbox = findAutomatedSchedulingGroup().findComponent(GlFormCheckbox).vm;
checkbox.$emit('input', value);
checkbox.$emit('change', value);
};
const setRollOver = (value) => {
const checkbox = findRollOverGroup().findComponent(GlFormCheckbox).vm;
......@@ -119,7 +113,6 @@ describe('Iteration cadence form', () => {
];
const findSaveButton = () => wrapper.findByTestId('save-cadence');
const findSaveAndStartButton = () => wrapper.findByTestId('save-cadence-create-iteration');
const findCancelButton = () => wrapper.findByTestId('cancel-create-cadence');
const clickSave = () => findSaveButton().vm.$emit('click');
const clickCancel = () => findCancelButton().vm.$emit('click');
......@@ -228,60 +221,6 @@ describe('Iteration cadence form', () => {
expect(findSaveButton().props('loading')).toBe(false);
});
it('does not show the Create cadence and start iteration button', async () => {
expect(findSaveAndStartButton().exists()).toBe(false);
});
});
describe('automated scheduling disabled', () => {
beforeEach(() => {
setAutomaticValue(false);
});
it('disables future iterations, duration in weeks, and start date fields', () => {
expect(findFutureIterations().attributes('disabled')).toBe('disabled');
expect(findFutureIterations().attributes('required')).toBeUndefined();
expect(findDuration().attributes('disabled')).toBe('disabled');
expect(findDuration().attributes('required')).toBeUndefined();
expect(findStartDate().attributes('disabled')).toBe('disabled');
expect(findStartDate().attributes('required')).toBeUndefined();
});
it('sets future iterations and cadence duration to 0', async () => {
const title = 'Iteration 5';
setFutureIterations(10);
setDuration(2);
setAutomaticValue(false);
await nextTick();
setTitle(title);
clickSave();
await nextTick();
expect(mutationMock).toHaveBeenCalledWith({
input: {
groupPath,
title,
automatic: false,
startDate: null,
rollOver: false,
durationInWeeks: 0,
iterationsInAdvance: 0,
description: '',
active: true,
},
});
});
it('shows the Create cadence and start iteration button', () => {
expect(findSaveAndStartButton().exists()).toBe(true);
});
});
});
......@@ -319,6 +258,46 @@ describe('Iteration cadence form', () => {
});
});
it('does not show the deprecation alert for automatic cadence', async () => {
createComponent({ query, resolverMock });
await waitForPromises();
expect(wrapper.text()).not.toContain('This cadence requires an update');
});
describe('when a cadence is manually managed', () => {
beforeEach(async () => {
createComponent({
query,
resolverMock: jest.fn().mockResolvedValue({
data: {
group: {
id: 'gid://gitlab/Group/114',
iterationCadences: {
nodes: [manualIterationCadence],
},
},
},
}),
});
await waitForPromises();
await nextTick();
});
it('displays the deprecation message', async () => {
expect(wrapper.text()).toContain('This cadence requires an update');
});
it('highlights fields required for automatic scheduling', async () => {
expect(findStartDateGroup().text()).toContain('This field is required');
expect(findDurationGroup().text()).toContain('This field is required');
expect(findFutureIterationsGroup().text()).toContain('This field is required');
});
});
it('fills fields with existing cadence info after loading', async () => {
createComponent({ query, resolverMock, mutation: updateCadence });
......@@ -334,14 +313,6 @@ describe('Iteration cadence form', () => {
expect(findDescription().element.value).toBe(iterationCadence.description);
});
it('does not show the Create cadence and start iteration button', async () => {
setAutomaticValue(false);
await nextTick();
expect(findSaveAndStartButton().exists()).toBe(false);
});
it('updates roll over issues checkbox', async () => {
await waitForPromises();
const rollOver = true;
......
import { GlDropdown, GlInfiniteScroll, GlModal, GlSkeletonLoader } from '@gitlab/ui';
import { GlBadge, GlDropdown, GlInfiniteScroll, GlModal, GlSkeletonLoader } from '@gitlab/ui';
import { RouterLinkStub } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
......@@ -13,6 +13,7 @@ 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';
import { automaticIterationCadence } from '../mock_data';
const { i18n } = IterationCadenceListItem;
const push = jest.fn();
......@@ -54,12 +55,6 @@ describe('Iteration cadence list item', () => {
},
];
const cadence = {
id: 'gid://gitlab/Iterations::Cadence/561',
title: 'Weekly cadence',
durationInWeeks: 3,
};
const startCursor = 'MQ';
const endCursor = 'MjA';
const querySuccessResponse = {
......@@ -101,6 +96,7 @@ describe('Iteration cadence list item', () => {
canCreateIteration,
canEditCadence,
currentRoute,
cadence = automaticIterationCadence,
namespaceType = Namespace.Group,
query = groupIterationsInCadenceQuery,
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse),
......@@ -127,6 +123,7 @@ describe('Iteration cadence list item', () => {
propsData: {
title: cadence.title,
cadenceId: cadence.id,
automatic: true,
iterationState: 'opened',
...props,
},
......@@ -136,10 +133,12 @@ describe('Iteration cadence list item', () => {
}
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findCreateIterationButton = () =>
wrapper.findByRole('link', { text: i18n.createIteration });
const findAddIterationButton = () => wrapper.findByRole('menuitem', { name: i18n.addIteration });
const findIterationItemText = (i) => wrapper.findAllByTestId('iteration-item').at(i).text();
const expand = () => wrapper.findByRole('button', { text: cadence.title }).trigger('click');
const findDurationBadge = () => wrapper.find('[data-testid="duration-badge"]');
const findDeprecationBadge = () => wrapper.findComponent(GlBadge);
const expand = (cadence = automaticIterationCadence) =>
wrapper.findByRole('button', { text: cadence.title }).trigger('click');
afterEach(() => {
wrapper.destroy();
......@@ -175,20 +174,89 @@ describe('Iteration cadence list item', () => {
},
);
it.each([
['hides', false],
['shows', true],
])('%s Create iteration button when canCreateIteration is %s', async (_, canCreateIteration) => {
it('hides Add iteration button for automatic cadence', async () => {
await createComponent({
canCreateIteration,
resolverMock: jest.fn().mockResolvedValue(queryEmptyResponse),
canCreateIteration: true,
canEditCadence: true,
});
expand();
await waitForPromises();
expect(findCreateIterationButton().exists()).toBe(canCreateIteration);
expect(findAddIterationButton().exists()).toBe(false);
});
it.each([
['hides', false],
['shows', true],
])(
'%s Add iteration button when canCreateIteration is %s for manual cadence',
async (_, canCreateIteration) => {
await createComponent({
props: {
automatic: false,
},
canCreateIteration,
canEditCadence: true,
resolverMock: jest.fn().mockResolvedValue(queryEmptyResponse),
});
expand();
await waitForPromises();
expect(findAddIterationButton().exists()).toBe(canCreateIteration);
},
);
describe('deprecation badge', () => {
it('does not show deprecation badge for automatic cadence', async () => {
await createComponent({
props: {
automatic: true,
},
canEditCadence: true,
});
expect(findDeprecationBadge().exists()).toBe(false);
});
it('shows deprecation badge for manual cadence', async () => {
await createComponent({
props: {
automatic: false,
},
canEditCadence: true,
});
expect(findDeprecationBadge().exists()).toBe(true);
expect(findDeprecationBadge().text()).toBe('Requires update');
});
});
describe('duration badge', () => {
it('does not show duration badge for manual cadence', async () => {
await createComponent({
props: {
automatic: false,
durationInWeeks: 2,
},
});
expect(findDurationBadge().exists()).toBe(false);
});
it('shows duration badge for automatic cadence', async () => {
await createComponent({
props: {
automatic: true,
durationInWeeks: 2,
},
});
expect(findDurationBadge().exists()).toBe(true);
});
});
const expectIterationItemToHavePeriod = () => {
......@@ -211,7 +279,9 @@ describe('Iteration cadence list item', () => {
it('automatically expands for newly created cadence', async () => {
await createComponent({
currentRoute: { query: { createdCadenceId: getIdFromGraphQLId(cadence.id) } },
currentRoute: {
query: { createdCadenceId: getIdFromGraphQLId(automaticIterationCadence.id) },
},
});
await waitForPromises();
......@@ -300,7 +370,7 @@ describe('Iteration cadence list item', () => {
it('emits delete-cadence event with cadence ID', () => {
wrapper.findComponent(GlModal).vm.$emit('ok');
expect(wrapper.emitted('delete-cadence')).toEqual([[cadence.id]]);
expect(wrapper.emitted('delete-cadence')).toEqual([[automaticIterationCadence.id]]);
});
});
});
......
......@@ -51,22 +51,24 @@ describe('Iteration cadences list', () => {
const startCursor = 'MQ';
const endCursor = 'MjA';
const querySuccessResponse = {
data: {
workspace: {
id: 'id',
iterationCadences: {
nodes: cadences,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: true,
hasPreviousPage: false,
startCursor,
endCursor,
const querySuccessResponse = (nodes = cadences) => {
return {
data: {
workspace: {
id: 'id',
iterationCadences: {
nodes,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: true,
hasPreviousPage: false,
startCursor,
endCursor,
},
},
},
},
},
};
};
const queryEmptyResponse = {
......@@ -95,7 +97,7 @@ describe('Iteration cadences list', () => {
canEditCadence,
namespaceType = Namespace.Group,
query = cadencesListQuery,
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse),
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse()),
destroyMutationMock = jest
.fn()
.mockResolvedValue({ data: { iterationCadenceDestroy: { errors: [] } } }),
......@@ -127,6 +129,7 @@ describe('Iteration cadences list', () => {
const findPrevPageButton = () => wrapper.findByRole('button', { name: 'Prev' });
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findCadenceItems = () => wrapper.findAll(IterationCadenceListItem);
afterEach(() => {
wrapper.destroy();
......@@ -178,6 +181,49 @@ describe('Iteration cadences list', () => {
});
});
it('does not display deprecation alert when only automatic cadences are shown', async () => {
await createComponent();
await waitForPromises();
expect(wrapper.text()).not.toContain('Some of your cadences need to be updated');
});
describe('when manual cadence exists', () => {
beforeEach(async () => {
const mixedCadences = [
...cadences,
{
id: 'gid://gitlab/Iterations::Cadence/100',
title: 'Manual cadence',
durationInWeeks: 0,
automatic: false,
},
{
id: 'gid://gitlab/Iterations::Cadence/101',
title: 'Deprecated cadence',
durationInWeeks: 0,
automatic: false,
},
];
await createComponent({
resolverMock: jest.fn().mockResolvedValue(querySuccessResponse(mixedCadences)),
});
await waitForPromises();
});
it('displays deprecation alert when manual cadence exists', async () => {
expect(wrapper.text()).toContain('Some of your cadences need to be updated');
});
it('groups manual cadences (deprecated) and displays them first', async () => {
expect(findCadenceItems().at(0).text()).toContain('Manual cadence');
expect(findCadenceItems().at(1).text()).toContain('Deprecated cadence');
});
});
it('loads project iterations for Project namespaceType', async () => {
await createComponent({
namespaceType: Namespace.Project,
......@@ -206,7 +252,7 @@ describe('Iteration cadences list', () => {
let resolverMock;
beforeEach(async () => {
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse);
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse());
await createComponent({ resolverMock });
await waitForPromises();
......
......@@ -96,7 +96,20 @@ export const manualIterationCadence = {
__typename: 'IterationCadence',
active: true,
id: `gid://gitlab/Iterations::Cadence/72`,
title: 'A manual iteration cadence',
title: 'A manually-scheduled iteration cadence',
automatic: false,
rollOver: false,
durationInWeeks: 2,
description: null,
startDate: '2020-06-28',
iterationsInAdvance: 0,
};
export const automaticIterationCadence = {
__typename: 'IterationCadence',
active: true,
id: `gid://gitlab/Iterations::Cadence/72`,
title: 'An automatically-scheduled iteration cadence',
automatic: true,
rollOver: false,
durationInWeeks: 3,
......
......@@ -21107,10 +21107,13 @@ msgstr ""
msgid "Iterations"
msgstr ""
msgid "Iterations|Add a duration, and number of future iterations in order to convert this cadence to automatic scheduling."
msgstr ""
msgid "Iterations|Add iteration"
msgstr ""
msgid "Iterations|Automated scheduling"
msgid "Iterations|All"
msgstr ""
msgid "Iterations|Cadence configuration is invalid."
......@@ -21125,12 +21128,6 @@ msgstr ""
msgid "Iterations|Create cadence"
msgstr ""
msgid "Iterations|Create cadence and start iteration"
msgstr ""
msgid "Iterations|Create iteration"
msgstr ""
msgid "Iterations|Delete cadence"
msgstr ""
......@@ -21140,6 +21137,9 @@ msgstr ""
msgid "Iterations|Delete iteration?"
msgstr ""
msgid "Iterations|Done"
msgstr ""
msgid "Iterations|Duration"
msgstr ""
......@@ -21161,10 +21161,13 @@ msgstr ""
msgid "Iterations|Iteration cadences"
msgstr ""
msgid "Iterations|Iteration scheduling will be handled automatically"
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|Iterations are a way to track issues over a period of time, allowing teams to also track velocity and volatility metrics."
msgid "Iterations|Iterations can no longer be scheduled manually. Convert all cadences to automatic scheduling to keep your iterations working as expected."
msgstr ""
msgid "Iterations|Learn more about automatic scheduling"
msgstr ""
msgid "Iterations|Move incomplete issues to the next iteration"
......@@ -21194,10 +21197,16 @@ msgstr ""
msgid "Iterations|Number of future iterations you would like to have scheduled"
msgstr ""
msgid "Iterations|Open"
msgstr ""
msgid "Iterations|Requires update"
msgstr ""
msgid "Iterations|Roll over issues"
msgstr ""
msgid "Iterations|Save cadence"
msgid "Iterations|Save changes"
msgstr ""
msgid "Iterations|Select duration"
......@@ -21209,6 +21218,9 @@ msgstr ""
msgid "Iterations|Select start date"
msgstr ""
msgid "Iterations|Some of your cadences need to be updated"
msgstr ""
msgid "Iterations|Start date"
msgstr ""
......@@ -21221,6 +21233,9 @@ msgstr ""
msgid "Iterations|The start date of your first iteration"
msgstr ""
msgid "Iterations|This cadence requires an update"
msgstr ""
msgid "Iterations|This will delete the cadence as well as all of the iterations within it."
msgstr ""
......
......@@ -11,7 +11,6 @@ module QA
element :iteration_cadence_description_field
element :iteration_cadence_start_date_field
element :iteration_cadence_title_field, required: true
element :iteration_cadence_automated_scheduling_checkbox
element :save_iteration_cadence_button
end
......@@ -39,10 +38,6 @@ module QA
def fill_title(title)
fill_element(:iteration_cadence_title_field, title)
end
def uncheck_automatic_scheduling
uncheck_element(:iteration_cadence_automated_scheduling_checkbox, true)
end
end
end
end
......
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