Commit 5689e1e4 authored by Eulyeon Ko's avatar Eulyeon Ko

Add deprecation alert for manual cadences

- Group manual cadences in the top in the cadence list
- Show a deprecation warning for manual cadences (in the cadence list and in the update form)
- Remove "automatic" checkbox from the cadence new/edit form
parent 13a7ea6f
...@@ -12,6 +12,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -12,6 +12,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Moved to GitLab Premium in 13.9. > - 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. > - [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 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) to track velocity and volatility metrics. Iterations can be used with [milestones](../../project/milestones/index.md)
for tracking over different time periods. for tracking over different time periods.
...@@ -28,54 +33,6 @@ In GitLab, iterations are similar to milestones, with a few differences: ...@@ -28,54 +33,6 @@ In GitLab, iterations are similar to milestones, with a few differences:
- Iterations require both a start and an end date. - Iterations require both a start and an end date.
- Iteration date ranges cannot overlap. - 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 ## View the iterations list
To view the iterations list, go to **{issues}** **Issues > Iterations**. To view the iterations list, go to **{issues}** **Issues > Iterations**.
...@@ -88,8 +45,6 @@ Prerequisites: ...@@ -88,8 +45,6 @@ Prerequisites:
- You must have at least the Developer role for a group. - 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: To create an iteration:
1. On the top bar, select **Menu > Groups** and find your group. 1. On the top bar, select **Menu > Groups** and find your group.
...@@ -136,7 +91,7 @@ The report also shows a breakdown of total issues in an iteration. ...@@ -136,7 +91,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. 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. 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 ### Iteration burndown and burnup charts
...@@ -195,33 +150,61 @@ To group issues by label: ...@@ -195,33 +150,61 @@ To group issues by label:
You can also search for labels by typing in the search input. 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. 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.
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.
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:
- 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
Iteration Cadences feature is under development and not ready for production use. It is Prerequisites:
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.
To enable it: - You must have at least the Developer role for a group.
```ruby Deleting an iteration cadence also deletes all iterations within that cadence.
Feature.enable(:iteration_cadences)
```
To disable it: To delete an iteration cadence:
```ruby 1. On the top bar, select **Menu > Groups** and find your group.
Feature.disable(:iteration_cadences) 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.
<!-- ## Troubleshooting ### Convert manual cadence to use automatic scheduling
Include any troubleshooting steps that you can foresee. If you know beforehand what issues WARNING:
one might have when setting this up, or when something is changed, or on upgrading, it's The upgrade is irreversible. After it's done, manual iteration cadences cannot be created.
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.
Each scenario can be a third-level heading, e.g. `### Getting error message X`. When you **enable** the iteration cadences feature, all iterations are added
If you have none to add when creating a doc, leave this section in place to a default iteration cadence.
but commented out to help encourage others to add to it in the future. --> 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 { ...@@ -13,6 +13,7 @@ import {
import { TYPE_ITERATIONS_CADENCE } from '~/graphql_shared/constants'; import { TYPE_ITERATIONS_CADENCE } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
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';
import readCadence from '../queries/iteration_cadence.query.graphql'; import readCadence from '../queries/iteration_cadence.query.graphql';
...@@ -22,10 +23,6 @@ const i18n = Object.freeze({ ...@@ -22,10 +23,6 @@ const i18n = Object.freeze({
label: s__('Iterations|Title'), label: s__('Iterations|Title'),
placeholder: s__('Iterations|Cadence name'), placeholder: s__('Iterations|Cadence name'),
}, },
automatedScheduling: {
label: s__('Iterations|Automated scheduling'),
description: s__('Iterations|Iteration scheduling will be handled automatically'),
},
startDate: { startDate: {
label: s__('Iterations|Start date'), label: s__('Iterations|Start date'),
placeholder: s__('Iterations|Select start date'), placeholder: s__('Iterations|Select start date'),
...@@ -50,18 +47,27 @@ const i18n = Object.freeze({ ...@@ -50,18 +47,27 @@ const i18n = Object.freeze({
}, },
edit: { edit: {
title: s__('Iterations|Edit iteration cadence'), title: s__('Iterations|Edit iteration cadence'),
save: s__('Iterations|Save cadence'), save: s__('Iterations|Save changes'),
}, },
new: { new: {
title: s__('Iterations|New iteration cadence'), title: s__('Iterations|New iteration cadence'),
save: s__('Iterations|Create cadence'), save: s__('Iterations|Create cadence'),
}, },
createAndStartIteration: s__('Iterations|Create cadence and start iteration'),
cancel: __('Cancel'), cancel: __('Cancel'),
requiredField: __('This field is required.'), 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 { 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], availableDurations: [{ value: 0, text: i18n.duration.placeholder }, 1, 2, 3, 4, 5, 6],
availableFutureIterations: [ availableFutureIterations: [
{ value: 0, text: i18n.futureIterations.placeholder }, { value: 0, text: i18n.futureIterations.placeholder },
...@@ -123,9 +129,6 @@ export default { ...@@ -123,9 +129,6 @@ export default {
page() { page() {
return this.isEdit ? 'edit' : 'new'; return this.isEdit ? 'edit' : 'new';
}, },
showStartIteration() {
return !this.isEdit && !this.automatic;
},
mutation() { mutation() {
return this.isEdit ? updateCadence : createCadence; return this.isEdit ? updateCadence : createCadence;
}, },
...@@ -143,7 +146,7 @@ export default { ...@@ -143,7 +146,7 @@ export default {
groupPath, groupPath,
id, id,
title: this.title, title: this.title,
automatic: this.automatic, automatic: true,
rollOver: this.rollOver, rollOver: this.rollOver,
startDate: this.startDate, startDate: this.startDate,
durationInWeeks: this.durationInWeeks, durationInWeeks: this.durationInWeeks,
...@@ -191,6 +194,10 @@ export default { ...@@ -191,6 +194,10 @@ export default {
this.rollOver = cadence.rollOver; this.rollOver = cadence.rollOver;
this.iterationsInAdvance = cadence.iterationsInAdvance; this.iterationsInAdvance = cadence.iterationsInAdvance;
this.description = cadence.description; this.description = cadence.description;
if (!cadence.automatic) {
this.validateAllFields();
}
}, },
error(error) { error(error) {
this.errorMessage = error; this.errorMessage = error;
...@@ -202,44 +209,15 @@ export default { ...@@ -202,44 +209,15 @@ export default {
this.validationState[field] = Boolean(this[field]); this.validationState[field] = Boolean(this[field]);
}, },
validateAllFields() { validateAllFields() {
Object.keys(this.validationState) Object.keys(this.validationState).forEach((field) => {
.filter((field) => { this.validate(field);
if (this.automatic) { });
return true;
}
const requiredFieldsForAutomatedScheduling = [
'iterationsInAdvance',
'durationInWeeks',
'startDate',
];
return !requiredFieldsForAutomatedScheduling.includes(field);
})
.forEach((field) => {
this.validate(field);
});
}, },
clearValidation() { clearValidation() {
this.validationState.startDate = null; this.validationState.startDate = null;
this.validationState.durationInWeeks = null; this.validationState.durationInWeeks = null;
this.validationState.iterationsInAdvance = 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() { saveAndViewList() {
return this.save() return this.save()
.then((cadenceId) => { .then((cadenceId) => {
...@@ -301,6 +279,16 @@ export default { ...@@ -301,6 +279,16 @@ export default {
</h3> </h3>
</div> </div>
<gl-form> <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 = ''">{{ <gl-alert v-if="errorMessage" class="gl-mb-5" variant="danger" @dismiss="errorMessage = ''">{{
errorMessage errorMessage
}}</gl-alert> }}</gl-alert>
...@@ -326,23 +314,6 @@ export default { ...@@ -326,23 +314,6 @@ export default {
/> />
</gl-form-group> </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 <gl-form-group
:label="i18n.startDate.label" :label="i18n.startDate.label"
:label-cols-md="2" :label-cols-md="2"
...@@ -360,8 +331,7 @@ export default { ...@@ -360,8 +331,7 @@ export default {
class="datepicker gl-datepicker-input" class="datepicker gl-datepicker-input"
autocomplete="off" autocomplete="off"
inputmode="none" inputmode="none"
:required="automatic" :disabled="loadingCadence"
:disabled="loadingCadence || !automatic"
:state="validationState.startDate" :state="validationState.startDate"
data-qa-selector="iteration_cadence_start_date_field" data-qa-selector="iteration_cadence_start_date_field"
@blur="validate('startDate')" @blur="validate('startDate')"
...@@ -383,8 +353,7 @@ export default { ...@@ -383,8 +353,7 @@ export default {
v-model.number="durationInWeeks" v-model.number="durationInWeeks"
:options="$options.availableDurations" :options="$options.availableDurations"
class="gl-form-input-md" class="gl-form-input-md"
:required="automatic" :disabled="loadingCadence"
:disabled="loadingCadence || !automatic"
data-qa-selector="iteration_cadence_duration_field" data-qa-selector="iteration_cadence_duration_field"
@change="validate('durationInWeeks')" @change="validate('durationInWeeks')"
/> />
...@@ -403,9 +372,8 @@ export default { ...@@ -403,9 +372,8 @@ export default {
<gl-form-select <gl-form-select
id="cadence-schedule-future-iterations" id="cadence-schedule-future-iterations"
v-model.number="iterationsInAdvance" v-model.number="iterationsInAdvance"
:disabled="!automatic || loadingCadence" :disabled="loadingCadence"
:options="$options.availableFutureIterations" :options="$options.availableFutureIterations"
:required="automatic"
class="gl-form-input-md" class="gl-form-input-md"
data-qa-selector="iteration_cadence_future_iterations_field" data-qa-selector="iteration_cadence_future_iterations_field"
@change="validate('iterationsInAdvance')" @change="validate('iterationsInAdvance')"
...@@ -444,22 +412,11 @@ export default { ...@@ -444,22 +412,11 @@ export default {
data-testid="save-cadence" data-testid="save-cadence"
variant="confirm" variant="confirm"
data-qa-selector="save_iteration_cadence_button" data-qa-selector="save_iteration_cadence_button"
:disabled="!valid"
@click="saveAndViewList" @click="saveAndViewList"
> >
{{ i18n[page].save }} {{ i18n[page].save }}
</gl-button> </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"> <gl-button class="gl-ml-3" data-testid="cancel-create-cadence" @click="cancel">
{{ i18n.cancel }} {{ i18n.cancel }}
</gl-button> </gl-button>
......
<script> <script>
import { import {
GlAlert, GlAlert,
GlBadge,
GlButton, GlButton,
GlCollapse, GlCollapse,
GlDropdown, GlDropdown,
...@@ -12,7 +13,7 @@ import { ...@@ -12,7 +13,7 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import { __, s__ } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { getIterationPeriod } from '../utils'; import { getIterationPeriod } from '../utils';
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';
...@@ -27,7 +28,7 @@ const i18n = Object.freeze({ ...@@ -27,7 +28,7 @@ const i18n = Object.freeze({
closed: s__('Iterations|No closed iterations.'), closed: s__('Iterations|No closed iterations.'),
all: s__('Iterations|No iterations in cadence.'), all: s__('Iterations|No iterations in cadence.'),
}, },
createIteration: s__('Iterations|Create iteration'), addIteration: s__('Iterations|Add iteration'),
error: __('Error loading iterations'), error: __('Error loading iterations'),
deleteCadence: s__('Iterations|Delete cadence'), deleteCadence: s__('Iterations|Delete cadence'),
...@@ -37,12 +38,14 @@ const i18n = Object.freeze({ ...@@ -37,12 +38,14 @@ const i18n = Object.freeze({
), ),
modalConfirm: s__('Iterations|Delete cadence'), modalConfirm: s__('Iterations|Delete cadence'),
modalCancel: __('Cancel'), modalCancel: __('Cancel'),
deprecationBadgeText: s__('Iterations|Requires update'),
}); });
export default { export default {
i18n, i18n,
components: { components: {
GlAlert, GlAlert,
GlBadge,
GlButton, GlButton,
GlCollapse, GlCollapse,
GlDropdown, GlDropdown,
...@@ -136,6 +139,9 @@ export default { ...@@ -136,6 +139,9 @@ export default {
state: this.iterationState, state: this.iterationState,
}; };
}, },
deprecationNotice() {
return sprintf(i18n.deprecationNotice, { cadenceTitle: this.title });
},
pageInfo() { pageInfo() {
return this.workspace.iterations?.pageInfo || {}; return this.workspace.iterations?.pageInfo || {};
}, },
...@@ -164,6 +170,12 @@ export default { ...@@ -164,6 +170,12 @@ export default {
}, },
}; };
}, },
showAddIteration() {
return !this.automatic && this.canCreateIteration;
},
showDurationBadget() {
return this.automatic && this.durationInWeeks;
},
}, },
created() { created() {
if ( if (
...@@ -219,11 +231,18 @@ export default { ...@@ -219,11 +231,18 @@ export default {
focusMenu() { focusMenu() {
this.$refs.menu.$el.focus(); this.$refs.menu.$el.focus();
}, },
toEditCadence() {
this.$router.push({
name: 'edit',
params: {
cadenceId: getIdFromGraphQLId(this.cadenceId),
},
});
},
getIterationPeriod, getIterationPeriod,
}, },
}; };
</script> </script>
<template> <template>
<li class="gl-py-0!"> <li class="gl-py-0!">
<div class="gl-display-flex gl-align-items-center"> <div class="gl-display-flex gl-align-items-center">
...@@ -239,11 +258,22 @@ export default { ...@@ -239,11 +258,22 @@ export default {
:class="{ 'gl-rotate-90': expanded }" :class="{ 'gl-rotate-90': expanded }"
/><span class="gl-ml-2">{{ title }}</span> /><span class="gl-ml-2">{{ title }}</span>
</gl-button> </gl-button>
<span
<span v-if="durationInWeeks" class="gl-mr-5 gl-display-none gl-sm-display-inline-block"> 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" /> <gl-icon name="clock" class="gl-mr-3" />
{{ n__('Every week', 'Every %d weeks', durationInWeeks) }}</span {{ 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 <gl-dropdown
v-if="canEditCadence" v-if="canEditCadence"
ref="menu" ref="menu"
...@@ -255,11 +285,11 @@ export default { ...@@ -255,11 +285,11 @@ export default {
data-qa-selector="cadence_options_button" data-qa-selector="cadence_options_button"
> >
<gl-dropdown-item <gl-dropdown-item
v-if="!automatic" v-if="showAddIteration"
:to="newIteration" :to="newIteration"
data-qa-selector="new_iteration_button" data-qa-selector="new_iteration_button"
> >
{{ s__('Iterations|Add iteration') }} {{ i18n.addIteration }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item :to="editCadence"> <gl-dropdown-item :to="editCadence">
...@@ -322,16 +352,6 @@ export default { ...@@ -322,16 +352,6 @@ export default {
</gl-infinite-scroll> </gl-infinite-scroll>
<template v-else-if="!loading"> <template v-else-if="!loading">
<p class="gl-px-7">{{ i18n.noResults[iterationState] }}</p> <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> </template>
</gl-collapse> </gl-collapse>
</li> </li>
......
<script> <script>
import { GlAlert, GlButton, GlLoadingIcon, GlKeysetPagination, GlTab, GlTabs } from '@gitlab/ui'; import { GlAlert, GlButton, GlLoadingIcon, GlKeysetPagination, GlTab, GlTabs } from '@gitlab/ui';
import produce from 'immer'; import produce from 'immer';
import { __, s__ } from '~/locale'; import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { Namespace } from '../constants'; import { Namespace } from '../constants';
import destroyIterationCadence from '../queries/destroy_cadence.mutation.graphql'; import destroyIterationCadence from '../queries/destroy_cadence.mutation.graphql';
import groupQuery from '../queries/group_iteration_cadences_list.query.graphql'; import groupQuery from '../queries/group_iteration_cadences_list.query.graphql';
...@@ -11,7 +12,19 @@ import IterationCadenceListItem from './iteration_cadence_list_item.vue'; ...@@ -11,7 +12,19 @@ import IterationCadenceListItem from './iteration_cadence_list_item.vue';
const pageSize = 20; const pageSize = 20;
export default { 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: { components: {
IterationCadenceListItem, IterationCadenceListItem,
GlAlert, GlAlert,
...@@ -77,7 +90,7 @@ export default { ...@@ -77,7 +90,7 @@ export default {
return vars; return vars;
}, },
cadences() { cadences() {
return this.workspace?.iterationCadences?.nodes || []; return this.groupDeprecatedItems(this.workspace?.iterationCadences?.nodes) || [];
}, },
pageInfo() { pageInfo() {
return this.workspace?.iterationCadences?.pageInfo || {}; return this.workspace?.iterationCadences?.pageInfo || {};
...@@ -96,6 +109,9 @@ export default { ...@@ -96,6 +109,9 @@ export default {
return 'all'; return 'all';
} }
}, },
manualCadenceExists() {
return this.cadences.findIndex((c) => !c.automatic) > -1;
},
}, },
mounted() { mounted() {
if (this.$router.currentRoute.query.createdCadenceId) { if (this.$router.currentRoute.query.createdCadenceId) {
...@@ -103,6 +119,9 @@ export default { ...@@ -103,6 +119,9 @@ export default {
} }
}, },
methods: { methods: {
groupDeprecatedItems(cadences) {
return [...cadences.filter((c) => !c.automatic), ...cadences.filter((c) => c.automatic)];
},
nextPage() { nextPage() {
this.pagination = { this.pagination = {
afterCursor: this.pageInfo.endCursor, afterCursor: this.pageInfo.endCursor,
...@@ -156,7 +175,7 @@ export default { ...@@ -156,7 +175,7 @@ export default {
<template> <template>
<gl-tabs v-model="tabIndex" @activate-tab="handleTabChange"> <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> <template #title>
{{ tab }} {{ tab }}
</template> </template>
...@@ -168,6 +187,16 @@ export default { ...@@ -168,6 +187,16 @@ export default {
<gl-loading-icon v-if="loading" class="gl-my-5" size="lg" /> <gl-loading-icon v-if="loading" class="gl-my-5" size="lg" />
<template v-else> <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"> <ul v-if="cadences.length" class="content-list">
<iteration-cadence-list-item <iteration-cadence-list-item
v-for="cadence in cadences" v-for="cadence in cadences"
......
...@@ -34,7 +34,7 @@ RSpec.describe 'User edits iteration cadence', :js do ...@@ -34,7 +34,7 @@ RSpec.describe 'User edits iteration cadence', :js do
updated_title = 'Updated cadence title' updated_title = 'Updated cadence title'
fill_in('Title', with: updated_title) fill_in('Title', with: updated_title)
click_button('Save cadence') click_button('Save changes')
expect(page).to have_content(updated_title) expect(page).to have_content(updated_title)
end end
......
...@@ -10,7 +10,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; ...@@ -10,7 +10,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { manualIterationCadence } from '../mock_data'; import { automaticIterationCadence, manualIterationCadence } from '../mock_data';
const push = jest.fn(); const push = jest.fn();
const $router = { const $router = {
...@@ -30,7 +30,7 @@ describe('Iteration cadence form', () => { ...@@ -30,7 +30,7 @@ describe('Iteration cadence form', () => {
let wrapper; let wrapper;
const groupPath = 'gitlab-org'; const groupPath = 'gitlab-org';
const id = 72; const id = 72;
const iterationCadence = manualIterationCadence; const iterationCadence = automaticIterationCadence;
const createMutationSuccess = { const createMutationSuccess = {
data: { result: { iterationCadence, errors: [] } }, data: { result: { iterationCadence, errors: [] } },
...@@ -45,7 +45,7 @@ describe('Iteration cadence form', () => { ...@@ -45,7 +45,7 @@ describe('Iteration cadence form', () => {
group: { group: {
id: 'gid://gitlab/Group/114', id: 'gid://gitlab/Group/114',
iterationCadences: { iterationCadences: {
nodes: [manualIterationCadence], nodes: [automaticIterationCadence],
}, },
}, },
}, },
...@@ -80,11 +80,10 @@ describe('Iteration cadence form', () => { ...@@ -80,11 +80,10 @@ describe('Iteration cadence form', () => {
}); });
const findTitleGroup = () => wrapper.findAllComponents(GlFormGroup).at(0); const findTitleGroup = () => wrapper.findAllComponents(GlFormGroup).at(0);
const findAutomatedSchedulingGroup = () => wrapper.findAllComponents(GlFormGroup).at(1); const findStartDateGroup = () => wrapper.findAllComponents(GlFormGroup).at(1);
const findStartDateGroup = () => wrapper.findAllComponents(GlFormGroup).at(2); const findDurationGroup = () => wrapper.findAllComponents(GlFormGroup).at(2);
const findDurationGroup = () => wrapper.findAllComponents(GlFormGroup).at(3); const findFutureIterationsGroup = () => wrapper.findAllComponents(GlFormGroup).at(3);
const findFutureIterationsGroup = () => wrapper.findAllComponents(GlFormGroup).at(4); const findRollOverGroup = () => wrapper.findAllComponents(GlFormGroup).at(4);
const findRollOverGroup = () => wrapper.findAllComponents(GlFormGroup).at(5);
const findError = () => wrapper.findComponent(GlAlert); const findError = () => wrapper.findComponent(GlAlert);
...@@ -99,11 +98,6 @@ describe('Iteration cadence form', () => { ...@@ -99,11 +98,6 @@ describe('Iteration cadence form', () => {
const setStartDate = (value) => findStartDate().vm.$emit('input', value); const setStartDate = (value) => findStartDate().vm.$emit('input', value);
const setFutureIterations = (value) => findFutureIterations().vm.$emit('input', value); const setFutureIterations = (value) => findFutureIterations().vm.$emit('input', value);
const setDuration = (value) => findDuration().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 setRollOver = (value) => {
const checkbox = findRollOverGroup().findComponent(GlFormCheckbox).vm; const checkbox = findRollOverGroup().findComponent(GlFormCheckbox).vm;
...@@ -119,7 +113,6 @@ describe('Iteration cadence form', () => { ...@@ -119,7 +113,6 @@ describe('Iteration cadence form', () => {
]; ];
const findSaveButton = () => wrapper.findByTestId('save-cadence'); const findSaveButton = () => wrapper.findByTestId('save-cadence');
const findSaveAndStartButton = () => wrapper.findByTestId('save-cadence-create-iteration');
const findCancelButton = () => wrapper.findByTestId('cancel-create-cadence'); const findCancelButton = () => wrapper.findByTestId('cancel-create-cadence');
const clickSave = () => findSaveButton().vm.$emit('click'); const clickSave = () => findSaveButton().vm.$emit('click');
const clickCancel = () => findCancelButton().vm.$emit('click'); const clickCancel = () => findCancelButton().vm.$emit('click');
...@@ -228,60 +221,6 @@ describe('Iteration cadence form', () => { ...@@ -228,60 +221,6 @@ describe('Iteration cadence form', () => {
expect(findSaveButton().props('loading')).toBe(false); 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', () => { ...@@ -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 () => { it('fills fields with existing cadence info after loading', async () => {
createComponent({ query, resolverMock, mutation: updateCadence }); createComponent({ query, resolverMock, mutation: updateCadence });
...@@ -334,14 +313,6 @@ describe('Iteration cadence form', () => { ...@@ -334,14 +313,6 @@ describe('Iteration cadence form', () => {
expect(findDescription().element.value).toBe(iterationCadence.description); 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 () => { it('updates roll over issues checkbox', async () => {
await waitForPromises(); await waitForPromises();
const rollOver = true; 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 { RouterLinkStub } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
...@@ -13,6 +13,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; ...@@ -13,6 +13,7 @@ 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'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { automaticIterationCadence } from '../mock_data';
const { i18n } = IterationCadenceListItem; const { i18n } = IterationCadenceListItem;
const push = jest.fn(); const push = jest.fn();
...@@ -54,12 +55,6 @@ describe('Iteration cadence list item', () => { ...@@ -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 startCursor = 'MQ';
const endCursor = 'MjA'; const endCursor = 'MjA';
const querySuccessResponse = { const querySuccessResponse = {
...@@ -101,6 +96,7 @@ describe('Iteration cadence list item', () => { ...@@ -101,6 +96,7 @@ describe('Iteration cadence list item', () => {
canCreateIteration, canCreateIteration,
canEditCadence, canEditCadence,
currentRoute, currentRoute,
cadence = automaticIterationCadence,
namespaceType = Namespace.Group, namespaceType = Namespace.Group,
query = groupIterationsInCadenceQuery, query = groupIterationsInCadenceQuery,
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse), resolverMock = jest.fn().mockResolvedValue(querySuccessResponse),
...@@ -127,6 +123,7 @@ describe('Iteration cadence list item', () => { ...@@ -127,6 +123,7 @@ describe('Iteration cadence list item', () => {
propsData: { propsData: {
title: cadence.title, title: cadence.title,
cadenceId: cadence.id, cadenceId: cadence.id,
automatic: true,
iterationState: 'opened', iterationState: 'opened',
...props, ...props,
}, },
...@@ -136,10 +133,12 @@ describe('Iteration cadence list item', () => { ...@@ -136,10 +133,12 @@ describe('Iteration cadence list item', () => {
} }
const findLoader = () => wrapper.findComponent(GlSkeletonLoader); const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findCreateIterationButton = () => const findAddIterationButton = () => wrapper.findByRole('menuitem', { name: i18n.addIteration });
wrapper.findByRole('link', { text: i18n.createIteration });
const findIterationItemText = (i) => wrapper.findAllByTestId('iteration-item').at(i).text(); 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(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -175,20 +174,89 @@ describe('Iteration cadence list item', () => { ...@@ -175,20 +174,89 @@ describe('Iteration cadence list item', () => {
}, },
); );
it.each([ it('hides Add iteration button for automatic cadence', async () => {
['hides', false],
['shows', true],
])('%s Create iteration button when canCreateIteration is %s', async (_, canCreateIteration) => {
await createComponent({ await createComponent({
canCreateIteration, canCreateIteration: true,
resolverMock: jest.fn().mockResolvedValue(queryEmptyResponse), canEditCadence: true,
}); });
expand(); expand();
await waitForPromises(); 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 = () => { const expectIterationItemToHavePeriod = () => {
...@@ -211,7 +279,9 @@ describe('Iteration cadence list item', () => { ...@@ -211,7 +279,9 @@ describe('Iteration cadence list item', () => {
it('automatically expands for newly created cadence', async () => { it('automatically expands for newly created cadence', async () => {
await createComponent({ await createComponent({
currentRoute: { query: { createdCadenceId: getIdFromGraphQLId(cadence.id) } }, currentRoute: {
query: { createdCadenceId: getIdFromGraphQLId(automaticIterationCadence.id) },
},
}); });
await waitForPromises(); await waitForPromises();
...@@ -300,7 +370,7 @@ describe('Iteration cadence list item', () => { ...@@ -300,7 +370,7 @@ describe('Iteration cadence list item', () => {
it('emits delete-cadence event with cadence ID', () => { it('emits delete-cadence event with cadence ID', () => {
wrapper.findComponent(GlModal).vm.$emit('ok'); 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', () => { ...@@ -51,22 +51,24 @@ describe('Iteration cadences list', () => {
const startCursor = 'MQ'; const startCursor = 'MQ';
const endCursor = 'MjA'; const endCursor = 'MjA';
const querySuccessResponse = { const querySuccessResponse = (nodes = cadences) => {
data: { return {
workspace: { data: {
id: 'id', workspace: {
iterationCadences: { id: 'id',
nodes: cadences, iterationCadences: {
pageInfo: { nodes,
__typename: 'PageInfo', pageInfo: {
hasNextPage: true, __typename: 'PageInfo',
hasPreviousPage: false, hasNextPage: true,
startCursor, hasPreviousPage: false,
endCursor, startCursor,
endCursor,
},
}, },
}, },
}, },
}, };
}; };
const queryEmptyResponse = { const queryEmptyResponse = {
...@@ -95,7 +97,7 @@ describe('Iteration cadences list', () => { ...@@ -95,7 +97,7 @@ describe('Iteration cadences list', () => {
canEditCadence, canEditCadence,
namespaceType = Namespace.Group, namespaceType = Namespace.Group,
query = cadencesListQuery, query = cadencesListQuery,
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse), resolverMock = jest.fn().mockResolvedValue(querySuccessResponse()),
destroyMutationMock = jest destroyMutationMock = jest
.fn() .fn()
.mockResolvedValue({ data: { iterationCadenceDestroy: { errors: [] } } }), .mockResolvedValue({ data: { iterationCadenceDestroy: { errors: [] } } }),
...@@ -127,6 +129,7 @@ describe('Iteration cadences list', () => { ...@@ -127,6 +129,7 @@ describe('Iteration cadences list', () => {
const findPrevPageButton = () => wrapper.findByRole('button', { name: 'Prev' }); const findPrevPageButton = () => wrapper.findByRole('button', { name: 'Prev' });
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPagination = () => wrapper.findComponent(GlKeysetPagination); const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findCadenceItems = () => wrapper.findAll(IterationCadenceListItem);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -178,6 +181,49 @@ describe('Iteration cadences list', () => { ...@@ -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 () => { it('loads project iterations for Project namespaceType', async () => {
await createComponent({ await createComponent({
namespaceType: Namespace.Project, namespaceType: Namespace.Project,
...@@ -206,7 +252,7 @@ describe('Iteration cadences list', () => { ...@@ -206,7 +252,7 @@ describe('Iteration cadences list', () => {
let resolverMock; let resolverMock;
beforeEach(async () => { beforeEach(async () => {
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse); resolverMock = jest.fn().mockResolvedValue(querySuccessResponse());
await createComponent({ resolverMock }); await createComponent({ resolverMock });
await waitForPromises(); await waitForPromises();
......
...@@ -96,7 +96,20 @@ export const manualIterationCadence = { ...@@ -96,7 +96,20 @@ export const manualIterationCadence = {
__typename: 'IterationCadence', __typename: 'IterationCadence',
active: true, active: true,
id: `gid://gitlab/Iterations::Cadence/72`, 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, automatic: true,
rollOver: false, rollOver: false,
durationInWeeks: 3, durationInWeeks: 3,
......
...@@ -20974,10 +20974,13 @@ msgstr "" ...@@ -20974,10 +20974,13 @@ msgstr ""
msgid "Iterations" msgid "Iterations"
msgstr "" 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" msgid "Iterations|Add iteration"
msgstr "" msgstr ""
msgid "Iterations|Automated scheduling" msgid "Iterations|All"
msgstr "" msgstr ""
msgid "Iterations|Cadence configuration is invalid." msgid "Iterations|Cadence configuration is invalid."
...@@ -20992,12 +20995,6 @@ msgstr "" ...@@ -20992,12 +20995,6 @@ msgstr ""
msgid "Iterations|Create cadence" msgid "Iterations|Create cadence"
msgstr "" msgstr ""
msgid "Iterations|Create cadence and start iteration"
msgstr ""
msgid "Iterations|Create iteration"
msgstr ""
msgid "Iterations|Delete cadence" msgid "Iterations|Delete cadence"
msgstr "" msgstr ""
...@@ -21007,6 +21004,9 @@ msgstr "" ...@@ -21007,6 +21004,9 @@ msgstr ""
msgid "Iterations|Delete iteration?" msgid "Iterations|Delete iteration?"
msgstr "" msgstr ""
msgid "Iterations|Done"
msgstr ""
msgid "Iterations|Duration" msgid "Iterations|Duration"
msgstr "" msgstr ""
...@@ -21028,10 +21028,13 @@ msgstr "" ...@@ -21028,10 +21028,13 @@ msgstr ""
msgid "Iterations|Iteration cadences" msgid "Iterations|Iteration cadences"
msgstr "" 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 "" 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 "" msgstr ""
msgid "Iterations|Move incomplete issues to the next iteration" msgid "Iterations|Move incomplete issues to the next iteration"
...@@ -21061,10 +21064,16 @@ msgstr "" ...@@ -21061,10 +21064,16 @@ msgstr ""
msgid "Iterations|Number of future iterations you would like to have scheduled" msgid "Iterations|Number of future iterations you would like to have scheduled"
msgstr "" msgstr ""
msgid "Iterations|Open"
msgstr ""
msgid "Iterations|Requires update"
msgstr ""
msgid "Iterations|Roll over issues" msgid "Iterations|Roll over issues"
msgstr "" msgstr ""
msgid "Iterations|Save cadence" msgid "Iterations|Save changes"
msgstr "" msgstr ""
msgid "Iterations|Select duration" msgid "Iterations|Select duration"
...@@ -21076,6 +21085,9 @@ msgstr "" ...@@ -21076,6 +21085,9 @@ msgstr ""
msgid "Iterations|Select start date" msgid "Iterations|Select start date"
msgstr "" msgstr ""
msgid "Iterations|Some of your cadences need to be updated"
msgstr ""
msgid "Iterations|Start date" msgid "Iterations|Start date"
msgstr "" msgstr ""
...@@ -21088,6 +21100,9 @@ msgstr "" ...@@ -21088,6 +21100,9 @@ msgstr ""
msgid "Iterations|The start date of your first iteration" msgid "Iterations|The start date of your first iteration"
msgstr "" 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." msgid "Iterations|This will delete the cadence as well as all of the iterations within it."
msgstr "" msgstr ""
......
...@@ -11,7 +11,6 @@ module QA ...@@ -11,7 +11,6 @@ module QA
element :iteration_cadence_description_field element :iteration_cadence_description_field
element :iteration_cadence_start_date_field element :iteration_cadence_start_date_field
element :iteration_cadence_title_field, required: true element :iteration_cadence_title_field, required: true
element :iteration_cadence_automated_scheduling_checkbox
element :save_iteration_cadence_button element :save_iteration_cadence_button
end end
...@@ -39,10 +38,6 @@ module QA ...@@ -39,10 +38,6 @@ module QA
def fill_title(title) def fill_title(title)
fill_element(:iteration_cadence_title_field, title) fill_element(:iteration_cadence_title_field, title)
end end
def uncheck_automatic_scheduling
uncheck_element(:iteration_cadence_automated_scheduling_checkbox, true)
end
end end
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