Commit b8e814ed authored by Eulyeon Ko's avatar Eulyeon Ko Committed by Kushal Pandya

Update iteration form and report

Iteration form:
- For manual cadences, allow updating all fields.
- For automatic cadences, only allow updating description.

Iteration report:
- Only allow deleting iterations for manual cadences.

Fix lint errors

Fix mock data

Apply 3 suggestion(s) to 1 file(s)
parent 296e97cb
<script> <script>
import { GlAlert, GlButton, GlForm, GlFormInput } from '@gitlab/ui'; import { GlAlert, GlButton, GlDatepicker, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import initDatePicker from '~/behaviors/date_picker';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { dayAfter, formatDate } from '~/lib/utils/datetime_utility'; import { dayAfter, formatDate } from '~/lib/utils/datetime_utility';
import { TYPE_ITERATIONS_CADENCE } from '~/graphql_shared/constants'; import { TYPE_ITERATION, 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 MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
...@@ -14,14 +13,33 @@ import updateIteration from '../queries/update_iteration.mutation.graphql'; ...@@ -14,14 +13,33 @@ import updateIteration from '../queries/update_iteration.mutation.graphql';
import iterationsInCadence from '../queries/group_iterations_in_cadence.query.graphql'; import iterationsInCadence from '../queries/group_iterations_in_cadence.query.graphql';
export default { export default {
i18n: {
title: {
edit: s__('Iterations|Edit iteration'),
new: s__('Iterations|New iteration'),
},
form: {
title: s__('Iterations|Title'),
startDate: s__('Iterations|Start date'),
dueDate: s__('Iterations|Due date'),
description: s__('Iterations|Description'),
},
submitButton: {
save: s__('Iterations|Save changes'),
create: s__('Iterations|Create iteration'),
},
cancelButton: s__('Iterations|Cancel'),
},
cadencesList: { cadencesList: {
name: 'index', name: 'index',
}, },
components: { components: {
GlAlert, GlAlert,
GlDatepicker,
GlButton, GlButton,
GlForm, GlForm,
GlFormInput, GlFormInput,
GlFormGroup,
MarkdownField, MarkdownField,
}, },
apollo: { apollo: {
...@@ -30,15 +48,13 @@ export default { ...@@ -30,15 +48,13 @@ export default {
skip() { skip() {
return !this.iterationId; return !this.iterationId;
}, },
/* eslint-disable @gitlab/require-i18n-strings */
variables() { variables() {
return { return {
fullPath: this.fullPath, fullPath: this.fullPath,
id: convertToGraphQLId('Iteration', this.iterationId), id: convertToGraphQLId(TYPE_ITERATION, this.iterationId),
isGroup: true, isGroup: true,
}; };
}, },
/* eslint-enable @gitlab/require-i18n-strings */
result({ data }) { result({ data }) {
const iteration = data.group.iterations?.nodes[0]; const iteration = data.group.iterations?.nodes[0];
...@@ -49,8 +65,9 @@ export default { ...@@ -49,8 +65,9 @@ export default {
this.title = iteration.title; this.title = iteration.title;
this.description = iteration.description; this.description = iteration.description;
this.startDate = iteration.startDate; this.startDate = new Date(iteration.startDate);
this.dueDate = iteration.dueDate; this.dueDate = new Date(iteration.dueDate);
this.automatic = iteration.iterationCadence.automatic;
return iteration; return iteration;
}, },
...@@ -68,8 +85,9 @@ export default { ...@@ -68,8 +85,9 @@ export default {
cadence: {}, cadence: {},
title: '', title: '',
description: '', description: '',
startDate: '', startDate: null,
dueDate: '', dueDate: null,
automatic: null,
}; };
}, },
computed: { computed: {
...@@ -82,20 +100,42 @@ export default { ...@@ -82,20 +100,42 @@ export default {
isEditing() { isEditing() {
return Boolean(this.iterationId); return Boolean(this.iterationId);
}, },
variables() { isAutoModeEdit() {
return this.isEditing && this.automatic;
},
formattedStartDate() {
return formatDate(this.startDate, 'yyyy-mm-dd');
},
formattedDueDate() {
return formatDate(this.dueDate, 'yyyy-mm-dd');
},
formattedDates() {
return {
startDate: this.formattedStartDate,
dueDate: this.formattedDueDate,
};
},
createVariables() {
return { return {
groupPath: this.fullPath, groupPath: this.fullPath,
title: this.title, title: this.title,
description: this.description, description: this.description,
startDate: this.startDate, ...this.formattedDates,
dueDate: this.dueDate,
}; };
}, },
updateVariables() {
const baseVariables = {
id: this.iterationId,
groupPath: this.fullPath,
description: this.description,
};
return this.automatic
? baseVariables
: { ...baseVariables, title: this.title, ...this.formattedDates };
},
}, },
async mounted() { async mounted() {
// TODO: utilize GlDatepicker instead of relying on this jQuery behavior
initDatePicker();
// prefill start date for the New cadence form // prefill start date for the New cadence form
// if there's iterations in the cadence, use last end_date + 1 // if there's iterations in the cadence, use last end_date + 1
// else use cadence startDate // else use cadence startDate
...@@ -112,10 +152,7 @@ export default { ...@@ -112,10 +152,7 @@ export default {
const iteration = data.workspace.iterations?.nodes[0]; const iteration = data.workspace.iterations?.nodes[0];
if (iteration) { if (iteration) {
this.startDate = formatDate( this.startDate = dayAfter(new Date(iteration.dueDate), { utc: true });
dayAfter(new Date(iteration.dueDate), { utc: true }),
'yyyy-mm-dd',
);
} else { } else {
const { data: cadenceData } = await this.$apollo.query({ const { data: cadenceData } = await this.$apollo.query({
query: readCadence, query: readCadence,
...@@ -133,7 +170,7 @@ export default { ...@@ -133,7 +170,7 @@ export default {
return; return;
} }
this.startDate = cadence.startDate; this.startDate = cadence.startDate ? new Date(cadence.startDate) : null;
} }
} }
} }
...@@ -149,8 +186,8 @@ export default { ...@@ -149,8 +186,8 @@ export default {
mutation: createIteration, mutation: createIteration,
variables: { variables: {
input: { input: {
...this.variables, ...this.createVariables,
iterationsCadenceId: convertToGraphQLId('Iterations::Cadence', this.cadenceId), iterationsCadenceId: convertToGraphQLId(TYPE_ITERATIONS_CADENCE, this.cadenceId),
}, },
}, },
}) })
...@@ -185,10 +222,7 @@ export default { ...@@ -185,10 +222,7 @@ export default {
.mutate({ .mutate({
mutation: updateIteration, mutation: updateIteration,
variables: { variables: {
input: { input: this.updateVariables,
...this.variables,
id: this.iterationId,
},
}, },
}) })
.then(({ data }) => { .then(({ data }) => {
...@@ -231,7 +265,7 @@ export default { ...@@ -231,7 +265,7 @@ export default {
<div> <div>
<div class="gl-display-flex"> <div class="gl-display-flex">
<h3 ref="pageTitle" class="page-title"> <h3 ref="pageTitle" class="page-title">
{{ isEditing ? s__('Iterations|Edit iteration') : s__('Iterations|New iteration') }} {{ isEditing ? $options.i18n.title.edit : $options.i18n.title.new }}
</h3> </h3>
</div> </div>
<hr class="gl-mt-0" /> <hr class="gl-mt-0" />
...@@ -239,27 +273,54 @@ export default { ...@@ -239,27 +273,54 @@ export default {
<gl-alert v-if="error" class="gl-mb-5" variant="danger" @dismiss="error = ''">{{ <gl-alert v-if="error" class="gl-mb-5" variant="danger" @dismiss="error = ''">{{
error error
}}</gl-alert> }}</gl-alert>
<gl-form class="row common-note-form"> <gl-form class="common-note-form">
<div class="col-md-6"> <p v-if="isAutoModeEdit && title">
<div class="form-group row"> <span class="gl-font-weight-bold gl-display-block gl-pb-3">{{
<div class="col-form-label col-sm-2"> $options.i18n.form.title
<label for="iteration-title">{{ __('Title') }}</label> }}</span>
</div> <span>{{ title }}</span>
<div class="col-sm-10"> </p>
<gl-form-group
v-else-if="!automatic || !isEditing"
:label="$options.i18n.form.title"
label-for="iteration-title"
>
<gl-form-input <gl-form-input
id="iteration-title" id="iteration-title"
v-model="title" v-model="title"
autocomplete="off" autocomplete="off"
data-qa-selector="iteration_title_field" data-qa-selector="iteration_title_field"
/> />
</div> </gl-form-group>
</div> <gl-form-group :label="$options.i18n.form.startDate" label-for="iteration-start-date">
<span v-if="isAutoModeEdit">{{ formattedStartDate }}</span>
<div class="form-group row"> <gl-datepicker
<div class="col-form-label col-sm-2"> v-else
<label for="iteration-description">{{ __('Description') }}</label> input-id="iteration-start-date"
</div> data-testid="start-date"
<div class="col-sm-10"> :value="startDate"
show-clear-button
autocomplete="off"
data-qa-selector="iteration_start_date_field"
@input="updateStartDate"
@clear="updateStartDate(null)"
/>
</gl-form-group>
<gl-form-group :label="$options.i18n.form.dueDate" label-for="iteration-due-date">
<span v-if="isAutoModeEdit">{{ formattedDueDate }}</span>
<gl-datepicker
v-else
input-id="iteration-due-date"
data-testid="due-date"
:value="dueDate"
show-clear-button
autocomplete="off"
data-qa-selector="iteration_due_date_field"
@input="updateDueDate"
@clear="updateDueDate(null)"
/>
</gl-form-group>
<gl-form-group :label="$options.i18n.form.description" label-for="iteration-description">
<markdown-field <markdown-field
:markdown-preview-path="previewMarkdownPath" :markdown-preview-path="previewMarkdownPath"
:can-attach-file="false" :can-attach-file="false"
...@@ -277,58 +338,14 @@ export default { ...@@ -277,58 +338,14 @@ export default {
class="note-textarea js-gfm-input js-autosize markdown-area" class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto" dir="auto"
data-supports-quick-actions="false" data-supports-quick-actions="false"
:aria-label="__('Description')" :aria-label="$options.i18n.form.description"
data-qa-selector="iteration_description_field" data-qa-selector="iteration_description_field"
> >
</textarea> </textarea>
</template> </template>
</markdown-field> </markdown-field>
</div> </gl-form-group>
</div>
</div>
<div class="col-md-6">
<div class="form-group row">
<div class="col-form-label col-sm-2">
<label for="iteration-start-date">{{ __('Start date') }}</label>
</div>
<div class="col-sm-10">
<gl-form-input
id="iteration-start-date"
v-model="startDate"
class="datepicker form-control"
:placeholder="__('Select start date')"
autocomplete="off"
data-qa-selector="iteration_start_date_field"
@change="updateStartDate"
/>
<a class="inline float-right gl-mt-2 js-clear-start-date" href="#">{{
__('Clear start date')
}}</a>
</div>
</div>
<div class="form-group row">
<div class="col-form-label col-sm-2">
<label for="iteration-due-date">{{ __('Due date') }}</label>
</div>
<div class="col-sm-10">
<gl-form-input
id="iteration-due-date"
v-model="dueDate"
class="datepicker form-control"
:placeholder="__('Select due date')"
autocomplete="off"
data-qa-selector="iteration_due_date_field"
@change="updateDueDate"
/>
<a class="inline float-right gl-mt-2 js-clear-due-date" href="#">{{
__('Clear due date')
}}</a>
</div>
</div>
</div>
</gl-form> </gl-form>
<div class="form-actions d-flex"> <div class="form-actions d-flex">
<gl-button <gl-button
:loading="loading" :loading="loading"
...@@ -337,10 +354,10 @@ export default { ...@@ -337,10 +354,10 @@ export default {
data-qa-selector="save_iteration_button" data-qa-selector="save_iteration_button"
@click="save" @click="save"
> >
{{ isEditing ? __('Update iteration') : __('Create iteration') }} {{ isEditing ? $options.i18n.submitButton.save : $options.i18n.submitButton.create }}
</gl-button> </gl-button>
<gl-button class="gl-ml-3" data-testid="cancel-iteration" :to="$options.cadencesList"> <gl-button class="gl-ml-3" data-testid="cancel-iteration" :to="$options.cadencesList">
{{ __('Cancel') }} {{ $options.i18n.cancelButton }}
</gl-button> </gl-button>
</div> </div>
</div> </div>
......
...@@ -268,7 +268,7 @@ export default { ...@@ -268,7 +268,7 @@ export default {
data-qa-selector="save_iteration_button" data-qa-selector="save_iteration_button"
@click="save" @click="save"
> >
{{ isEditing ? __('Update iteration') : __('Create iteration') }} {{ isEditing ? __('Save changes') : __('Create iteration') }}
</gl-button> </gl-button>
<gl-button class="ml-auto" data-testid="cancel-iteration" @click="cancel"> <gl-button class="ml-auto" data-testid="cancel-iteration" @click="cancel">
{{ __('Cancel') }} {{ __('Cancel') }}
......
...@@ -14,7 +14,7 @@ import { TYPE_ITERATION } from '~/graphql_shared/constants'; ...@@ -14,7 +14,7 @@ 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 { Namespace, iterationStates } from '../constants'; import { Namespace } from '../constants';
import deleteIteration from '../queries/destroy_iteration.mutation.graphql'; import deleteIteration from '../queries/destroy_iteration.mutation.graphql';
import query from '../queries/iteration.query.graphql'; import query from '../queries/iteration.query.graphql';
import { getIterationPeriod } from '../utils'; import { getIterationPeriod } from '../utils';
...@@ -95,7 +95,8 @@ export default { ...@@ -95,7 +95,8 @@ export default {
return getIterationPeriod(this.iteration); return getIterationPeriod(this.iteration);
}, },
showDelete() { showDelete() {
return this.iteration.state !== iterationStates.closed; // We only support deleting iterations for manual cadences.
return !this.iteration.iterationCadence.automatic;
}, },
}, },
methods: { methods: {
......
...@@ -8,4 +8,8 @@ fragment IterationReport on Iteration { ...@@ -8,4 +8,8 @@ fragment IterationReport on Iteration {
state state
title title
webPath webPath
iterationCadence {
id
automatic
}
} }
...@@ -8,7 +8,7 @@ RSpec.describe 'User edits iteration' do ...@@ -8,7 +8,7 @@ RSpec.describe 'User edits iteration' do
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user } let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
let_it_be(:guest_user) { create(:group_member, :guest, user: create(:user), group: group ).user } let_it_be(:guest_user) { create(:group_member, :guest, user: create(:user), group: group ).user }
let_it_be(:cadence) { create(:iterations_cadence, group: group) } let_it_be(:cadence) { create(:iterations_cadence, group: group) }
let_it_be(:iteration) { create(:iteration, :skip_future_date_validation, group: group, title: 'Correct Iteration', description: 'Iteration description', start_date: now - 1.day, due_date: now, iterations_cadence: cadence) } let_it_be(:iteration) { create(:iteration, :skip_future_date_validation, description: 'Iteration description', start_date: now - 1.day, due_date: now, iterations_cadence: cadence) }
let_it_be(:new_start_date) { now + 4.days } let_it_be(:new_start_date) { now + 4.days }
let_it_be(:new_due_date) { now + 5.days } let_it_be(:new_due_date) { now + 5.days }
...@@ -26,62 +26,88 @@ RSpec.describe 'User edits iteration' do ...@@ -26,62 +26,88 @@ RSpec.describe 'User edits iteration' do
sign_in(user) sign_in(user)
end end
let(:start_date_with_cadences_input) do shared_examples 'manually managed iteration' do
page.find('#iteration-start-date') let_it_be(:manual_cadence) { build(:iterations_cadence, group: group, automatic: false).tap { |cadence| cadence.save!(validate: false) } }
end let_it_be(:iteration) { create(:iteration, :skip_future_date_validation, title: 'Correct Iteration', description: 'Iteration description', start_date: now - 1.day, due_date: now, iterations_cadence: manual_cadence) }
let(:due_date_with_cadences_input) do context 'load edit page directly', :js do
page.find('#iteration-due-date') before do
end visit edit_iteration_page
let(:start_date_without_cadences_input) do wait_for_requests
input = page.first('[data-testid="gl-datepicker-input"]')
input.set(now - 1.day)
input
end end
let(:due_date_without_cadences_input) do it 'prefills fields and allows updating all values' do
input = all('[data-testid="gl-datepicker-input"]').last aggregate_failures do
input.set(now) expect(find_field('Title').value).to eq(iteration.title)
input expect(find_field('Description').value).to eq(iteration.description)
expect(start_date_field.value).to have_content(iteration.start_date)
expect(due_date_field.value).to have_content(iteration.due_date)
end end
let(:updated_start_date_with_cadences) do updated_title = 'Updated iteration title'
fill_in('Start date', with: new_start_date.strftime('%Y-%m-%d')) updated_desc = 'Updated iteration desc'
new_start_date.strftime('%b %-d, %Y')
end fill_in('Title', with: updated_title)
fill_in('Description', with: updated_desc)
start_date_input
due_date_input
let(:updated_due_date_with_cadences) do click_button('Save changes')
fill_in('Due date', with: new_due_date.strftime('%Y-%m-%d'))
new_due_date.strftime('%b %-d, %Y') aggregate_failures do
expect(page).to have_content(updated_title)
expect(page).to have_content(updated_desc)
expect(page).to have_content(new_start_date.strftime('%b %-d, %Y'))
expect(page).to have_content(new_due_date.strftime('%b %-d, %Y'))
expect(page).to have_current_path(iteration_page)
end
end
end end
let(:updated_start_date_without_cadences) do context 'load edit page from report', :js do
start_date_without_cadences_input.set(new_start_date) before do
new_start_date.strftime('%b %-d, %Y') visit iteration_page
end end
let(:updated_due_date_without_cadences) do it 'prefills fields and updates URL' do
# TODO: Reported issue with Capybara find(dropdown_selector).click
# Use fill_in instead, update datepicker to have labels click_link_or_button('Edit')
due_date_without_cadences_input.set('')
due_date_without_cadences_input.set(new_due_date) aggregate_failures do
new_due_date.strftime('%b %-d, %Y') expect(find_field('Title').value).to eq(iteration.title)
expect(find_field('Description').value).to eq(iteration.description)
expect(start_date_field.value).to have_content(iteration.start_date)
expect(due_date_field.value).to have_content(iteration.due_date)
expect(page).to have_current_path(edit_iteration_page)
end
end
end end
end
context 'using manual iteration cadences' do
let(:iteration_page) { group_iteration_cadence_iteration_path(group, iteration_cadence_id: manual_cadence.id, id: iteration.id) }
let(:edit_iteration_page) { edit_group_iteration_cadence_iteration_path(group, iteration_cadence_id: manual_cadence.id, id: iteration.id) }
let(:start_date_field) { find_field('Start date') }
let(:due_date_field) { find_field('Due date') }
let(:iteration_with_cadences_page) { group_iteration_cadence_iteration_path(group, iteration_cadence_id: cadence.id, id: iteration.id) } let(:start_date_input) do
let(:iteration_without_cadences_page) { group_iteration_path(iteration.group, iteration.id) } fill_in 'Start date', with: new_start_date
start_date_field.native.send_keys :enter
end
let(:edit_iteration_with_cadences_page) { edit_group_iteration_cadence_iteration_path(group, iteration_cadence_id: cadence.id, id: iteration.id) } let(:due_date_input) do
let(:edit_iteration_without_cadences_page) { edit_group_iteration_path(iteration.group, iteration.id) } fill_in 'Due date', with: new_due_date
due_date_field.native.send_keys :enter
end
where(:using_cadences, :start_date_input, :due_date_input, :updated_start_date, :updated_due_date, :iteration_page, :edit_iteration_page) do it_behaves_like 'manually managed iteration'
true | ref(:start_date_with_cadences_input) | ref(:due_date_with_cadences_input) | ref(:updated_start_date_with_cadences) | ref(:updated_due_date_with_cadences) | ref(:iteration_with_cadences_page) | ref(:edit_iteration_with_cadences_page)
false | ref(:start_date_without_cadences_input) | ref(:due_date_without_cadences_input) | ref(:updated_start_date_without_cadences) | ref(:updated_due_date_without_cadences) | ref(:iteration_without_cadences_page) | ref(:edit_iteration_without_cadences_page)
end end
with_them do context 'using automatic iteration cadences' do
let(:iteration_page) { group_iteration_cadence_iteration_path(group, iteration_cadence_id: cadence.id, id: iteration.id) }
let(:edit_iteration_page) { edit_group_iteration_cadence_iteration_path(group, iteration_cadence_id: cadence.id, id: iteration.id) }
context 'load edit page directly', :js do context 'load edit page directly', :js do
before do before do
visit edit_iteration_page visit edit_iteration_page
...@@ -89,31 +115,15 @@ RSpec.describe 'User edits iteration' do ...@@ -89,31 +115,15 @@ RSpec.describe 'User edits iteration' do
wait_for_requests wait_for_requests
end end
it 'prefills fields and allows updating all values' do it 'prefills and allows updating description', :aggregate_failures do
aggregate_failures do expect(find_field("Description").value).to eq(iteration.description)
expect(title_input.value).to eq(iteration.title)
expect(description_input.value).to eq(iteration.description)
expect(start_date_input.value).to have_content(iteration.start_date)
expect(due_date_input.value).to have_content(iteration.due_date)
end
updated_title = 'Updated iteration title'
updated_desc = 'Updated iteration desc' updated_desc = 'Updated iteration desc'
fill_in('Title', with: updated_title)
fill_in('Description', with: updated_desc) fill_in('Description', with: updated_desc)
start_date = updated_start_date click_button('Save changes')
due_date = updated_due_date
click_button('Update iteration')
aggregate_failures do
expect(page).to have_content(updated_title)
expect(page).to have_content(updated_desc) expect(page).to have_content(updated_desc)
expect(page).to have_content(start_date)
expect(page).to have_content(due_date)
expect(page).to have_current_path(iteration_page)
end
end end
end end
...@@ -122,19 +132,29 @@ RSpec.describe 'User edits iteration' do ...@@ -122,19 +132,29 @@ RSpec.describe 'User edits iteration' do
visit iteration_page visit iteration_page
end end
it 'prefills fields and updates URL' do it 'prefills description and updates URL' do
find(dropdown_selector).click find(dropdown_selector).click
click_link_or_button('Edit') click_link_or_button('Edit')
aggregate_failures do expect(find_field("Description").value).to eq(iteration.description)
expect(title_input.value).to eq(iteration.title)
expect(description_input.value).to eq(iteration.description)
expect(start_date_input.value).to have_content(iteration.start_date)
expect(due_date_input.value).to have_content(iteration.due_date)
expect(page).to have_current_path(edit_iteration_page) expect(page).to have_current_path(edit_iteration_page)
end end
end end
end end
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/354878
# Remove with :iteration_cadences FF rollout
context 'using iteration without cadences' do
let(:iteration_page) { group_iteration_path(iteration.group, iteration.id) }
let(:edit_iteration_page) { edit_group_iteration_path(iteration.group, iteration.id) }
let(:new_start_date) { now - 1.day }
let(:new_due_date) { now }
let(:start_date_field) { page.first('[data-testid="gl-datepicker-input"]') }
let(:due_date_field) { all('[data-testid="gl-datepicker-input"]').last }
let(:start_date_input) { start_date_field.set(new_start_date) }
let(:due_date_input) { due_date_field.set(new_due_date) }
it_behaves_like 'manually managed iteration'
end end
end end
...@@ -174,13 +194,5 @@ RSpec.describe 'User edits iteration' do ...@@ -174,13 +194,5 @@ RSpec.describe 'User edits iteration' do
end end
end end
end end
def title_input
page.find('#iteration-title')
end
def description_input
page.find('#iteration-description')
end
end end
end end
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { GlFormInput } from '@gitlab/ui';
import IterationForm from 'ee/iterations/components/iteration_form.vue'; import IterationForm from 'ee/iterations/components/iteration_form.vue';
import readIteration from 'ee/iterations/queries/iteration.query.graphql'; import readIteration from 'ee/iterations/queries/iteration.query.graphql';
import createIteration from 'ee/iterations/queries/iteration_create.mutation.graphql'; import createIteration from 'ee/iterations/queries/iteration_create.mutation.graphql';
...@@ -15,14 +16,13 @@ import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; ...@@ -15,14 +16,13 @@ import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { dayAfter, formatDate } from '~/lib/utils/datetime_utility'; import { dayAfter, formatDate } from '~/lib/utils/datetime_utility';
import { import {
manualIterationCadence as cadence, manualIterationCadence as cadence,
mockGroupIterations, mockManualIterationNode as iteration,
mockIterationNode as iteration,
createMutationSuccess, createMutationSuccess,
createMutationFailure, createMutationFailure,
updateMutationSuccess, updateMutationSuccess,
emptyGroupIterationsSuccess, emptyGroupIterationsSuccess,
nonEmptyGroupIterationsSuccess, nonEmptyGroupIterationsSuccess,
readCadenceSuccess, readManualCadenceSuccess,
} from '../mock_data'; } from '../mock_data';
const baseUrl = '/cadences/'; const baseUrl = '/cadences/';
...@@ -35,6 +35,28 @@ function createMockApolloProvider(requestHandlers) { ...@@ -35,6 +35,28 @@ function createMockApolloProvider(requestHandlers) {
return createMockApollo(requestHandlers); return createMockApollo(requestHandlers);
} }
const mockGroupIterationsFactory = (nodes = [iteration]) => {
return {
data: {
group: {
id: 'gid://gitlab/Group/114',
iterations: {
nodes,
pageInfo: {
hasNextPage: true,
hasPreviousPage: true,
startCursor: 'first-item',
endCursor: 'last-item',
__typename: 'PageInfo',
},
__typename: 'IterationConnection',
},
__typename: 'Group',
},
},
};
};
describe('Iteration Form', () => { describe('Iteration Form', () => {
let wrapper; let wrapper;
let router; let router;
...@@ -44,7 +66,7 @@ describe('Iteration Form', () => { ...@@ -44,7 +66,7 @@ describe('Iteration Form', () => {
mutationQuery = createIteration, mutationQuery = createIteration,
mutationResult = createMutationSuccess, mutationResult = createMutationSuccess,
query = readIteration, query = readIteration,
result = mockGroupIterations, result = mockGroupIterationsFactory(),
resolverMock = jest.fn().mockResolvedValue(mutationResult), resolverMock = jest.fn().mockResolvedValue(mutationResult),
groupIterationsSuccess = emptyGroupIterationsSuccess, groupIterationsSuccess = emptyGroupIterationsSuccess,
} = {}) { } = {}) {
...@@ -52,7 +74,7 @@ describe('Iteration Form', () => { ...@@ -52,7 +74,7 @@ describe('Iteration Form', () => {
[query, jest.fn().mockResolvedValue(result)], [query, jest.fn().mockResolvedValue(result)],
[mutationQuery, resolverMock], [mutationQuery, resolverMock],
[groupIterationsInCadenceQuery, jest.fn().mockResolvedValue(groupIterationsSuccess)], [groupIterationsInCadenceQuery, jest.fn().mockResolvedValue(groupIterationsSuccess)],
[readCadence, jest.fn().mockResolvedValue(readCadenceSuccess)], [readCadence, jest.fn().mockResolvedValue(readManualCadenceSuccess)],
]); ]);
wrapper = extendedWrapper( wrapper = extendedWrapper(
mount(IterationForm, { mount(IterationForm, {
...@@ -78,10 +100,12 @@ describe('Iteration Form', () => { ...@@ -78,10 +100,12 @@ describe('Iteration Form', () => {
}); });
const findPageTitle = () => wrapper.findComponent({ ref: 'pageTitle' }); const findPageTitle = () => wrapper.findComponent({ ref: 'pageTitle' });
const findTitle = () => wrapper.find('#iteration-title'); const findTitle = () => wrapper.findByLabelText('Title');
const findDescription = () => wrapper.find('#iteration-description'); const findDescription = () => wrapper.findByLabelText('Description');
const findStartDate = () => wrapper.find('#iteration-start-date'); const findStartDate = () => wrapper.findByTestId('start-date');
const findDueDate = () => wrapper.find('#iteration-due-date'); const findStartDateInputText = () => findStartDate().find(GlFormInput).element.value;
const findDueDate = () => wrapper.findByTestId('due-date');
const findDueDateInputText = () => findDueDate().find(GlFormInput).element.value;
const findSaveButton = () => wrapper.findByTestId('save-iteration'); const findSaveButton = () => wrapper.findByTestId('save-iteration');
const findCancelButton = () => wrapper.findByTestId('cancel-iteration'); const findCancelButton = () => wrapper.findByTestId('cancel-iteration');
const clickSave = () => findSaveButton().trigger('click'); const clickSave = () => findSaveButton().trigger('click');
...@@ -112,11 +136,10 @@ describe('Iteration Form', () => { ...@@ -112,11 +136,10 @@ describe('Iteration Form', () => {
const startDate = '2020-05-05'; const startDate = '2020-05-05';
const dueDate = '2020-05-25'; const dueDate = '2020-05-25';
findTitle().vm.$emit('input', title); findTitle().setValue(title);
findDescription().setValue(description); findDescription().setValue(description);
findStartDate().vm.$emit('input', startDate); findStartDate().vm.$emit('input', new Date(startDate));
findDueDate().vm.$emit('input', dueDate); findDueDate().vm.$emit('input', new Date(dueDate));
await clickSave(); await clickSave();
expect(resolverMock).toHaveBeenCalledWith({ expect(resolverMock).toHaveBeenCalledWith({
...@@ -172,7 +195,7 @@ describe('Iteration Form', () => { ...@@ -172,7 +195,7 @@ describe('Iteration Form', () => {
'yyyy-mm-dd', 'yyyy-mm-dd',
); );
expect(findStartDate().element.value).toBe(expectedDate); expect(findStartDateInputText()).toBe(expectedDate);
}); });
}); });
...@@ -188,17 +211,17 @@ describe('Iteration Form', () => { ...@@ -188,17 +211,17 @@ describe('Iteration Form', () => {
it('uses cadence start date', () => { it('uses cadence start date', () => {
const expectedDate = cadence.startDate; const expectedDate = cadence.startDate;
expect(findStartDate().element.value).toBe(expectedDate); expect(findStartDateInputText()).toBe(expectedDate);
}); });
}); });
}); });
}); });
describe('Edit iteration', () => { describe('Edit iteration for manual cadence', () => {
beforeEach(() => { beforeEach(() => {
router.replace({ router.replace({
name: 'editIteration', name: 'editIteration',
params: { cadenceId: cadence.id, iterationId: iteration.id }, params: { cadenceId, iterationId },
}); });
}); });
...@@ -219,29 +242,31 @@ describe('Iteration Form', () => { ...@@ -219,29 +242,31 @@ describe('Iteration Form', () => {
expect(findTitle().element.value).toBe(iteration.title); expect(findTitle().element.value).toBe(iteration.title);
expect(findDescription().element.value).toBe(iteration.description); expect(findDescription().element.value).toBe(iteration.description);
expect(findStartDate().element.value).toBe(iteration.startDate); expect(findStartDateInputText()).toBe(iteration.startDate);
expect(findDueDate().element.value).toBe(iteration.dueDate); expect(findDueDateInputText()).toBe(iteration.dueDate);
}); });
it('shows update text on submit button', () => { it('shows update text on submit button', () => {
createComponent(); createComponent();
expect(findSaveButton().text()).toBe('Update iteration'); expect(findSaveButton().text()).toBe('Save changes');
}); });
it('triggers mutation with form data', async () => { it('triggers mutation with form data', async () => {
const resolverMock = jest.fn().mockResolvedValue(updateMutationSuccess); const resolverMock = jest.fn().mockResolvedValue(updateMutationSuccess);
createComponent({ mutationQuery: updateIteration, resolverMock }); createComponent({ mutationQuery: updateIteration, resolverMock });
await waitForPromises();
const title = 'Updated title'; const title = 'Updated title';
const description = 'Updated description'; const description = 'Updated description';
const startDate = '2020-05-06'; const startDate = '2020-05-06';
const dueDate = '2020-05-26'; const dueDate = '2020-05-26';
findTitle().vm.$emit('input', title); findTitle().setValue(title);
findDescription().setValue(description); findDescription().setValue(description);
findStartDate().vm.$emit('input', startDate); findStartDate().vm.$emit('input', new Date(startDate));
findDueDate().vm.$emit('input', dueDate); findDueDate().vm.$emit('input', new Date(dueDate));
clickSave(); clickSave();
await waitForPromises(); await waitForPromises();
...@@ -249,7 +274,7 @@ describe('Iteration Form', () => { ...@@ -249,7 +274,7 @@ describe('Iteration Form', () => {
expect(resolverMock).toHaveBeenCalledWith({ expect(resolverMock).toHaveBeenCalledWith({
input: { input: {
groupPath, groupPath,
id: iteration.id, id: iterationId,
title, title,
description, description,
startDate, startDate,
...@@ -264,6 +289,7 @@ describe('Iteration Form', () => { ...@@ -264,6 +289,7 @@ describe('Iteration Form', () => {
mutationQuery: updateIteration, mutationQuery: updateIteration,
resolverMock, resolverMock,
}); });
await waitForPromises();
clickSave(); clickSave();
await nextTick(); await nextTick();
...@@ -274,11 +300,11 @@ describe('Iteration Form', () => { ...@@ -274,11 +300,11 @@ describe('Iteration Form', () => {
expect(resolverMock).toHaveBeenCalledWith({ expect(resolverMock).toHaveBeenCalledWith({
input: { input: {
groupPath, groupPath,
id: iteration.id, id: iterationId,
startDate: '', startDate: iteration.startDate,
dueDate: '', dueDate: iteration.dueDate,
title: '', title: iteration.title,
description: '', description: iteration.description,
}, },
}); });
}); });
......
...@@ -181,7 +181,7 @@ describe('Iteration Form', () => { ...@@ -181,7 +181,7 @@ describe('Iteration Form', () => {
props: propsWithIteration, props: propsWithIteration,
}); });
expect(findSaveButton().text()).toBe('Update iteration'); expect(findSaveButton().text()).toBe('Save changes');
}); });
it('triggers mutation with form data', () => { it('triggers mutation with form data', () => {
......
...@@ -17,7 +17,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper'; ...@@ -17,7 +17,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { import {
mockIterationNode, mockIterationNode,
mockPastIterationNode, mockManualIterationNode,
createMockGroupIterations, createMockGroupIterations,
mockIterationNodeWithoutTitle, mockIterationNodeWithoutTitle,
mockProjectIterations, mockProjectIterations,
...@@ -155,8 +155,16 @@ describe('Iterations report', () => { ...@@ -155,8 +155,16 @@ describe('Iterations report', () => {
}); });
describe('delete iteration', () => { describe('delete iteration', () => {
it('does not show delete option for past iterations', async () => { it('does not show delete option when iteration belongs to automatic cadence', async () => {
mountComponent({ mockQueryResponse: createMockGroupIterations(mockPastIterationNode) }); mountComponent({ mockQueryResponse: createMockGroupIterations(mockIterationNode) });
await waitForPromises();
expect(findDeleteButton().exists()).toBe(false);
});
it('shows delete option when iteration belongs to automatic cadence', async () => {
mountComponent({ mockQueryResponse: createMockGroupIterations(mockManualIterationNode) });
await waitForPromises(); await waitForPromises();
......
...@@ -11,12 +11,21 @@ export const mockIterationNode = { ...@@ -11,12 +11,21 @@ export const mockIterationNode = {
title: 'top-level-iteration', title: 'top-level-iteration',
webPath: '/groups/top-level-group/-/iterations/4', webPath: '/groups/top-level-group/-/iterations/4',
scopedPath: '/groups/top-level-group/-/iterations/4', scopedPath: '/groups/top-level-group/-/iterations/4',
iterationCadence: {
__typename: 'IterationCadence',
id: 'gid://gitlab/Iterations::Cadence/72',
automatic: true,
},
__typename: 'Iteration', __typename: 'Iteration',
}; };
export const mockPastIterationNode = { export const mockManualIterationNode = {
...mockIterationNode, ...mockIterationNode,
state: iterationStates.closed, iterationCadence: {
__typename: 'IterationCadence',
id: 'gid://gitlab/Iterations::Cadence/72',
automatic: false,
},
}; };
export const mockIterationNodeWithoutTitle = { export const mockIterationNodeWithoutTitle = {
...@@ -171,7 +180,18 @@ export const nonEmptyGroupIterationsSuccess = { ...@@ -171,7 +180,18 @@ export const nonEmptyGroupIterationsSuccess = {
}, },
}; };
export const readCadenceSuccess = { export const readAutomaticCadenceSuccess = {
data: {
group: {
id: 'gid://gitlab/Group/114',
iterationCadences: {
nodes: [automaticIterationCadence],
},
},
},
};
export const readManualCadenceSuccess = {
data: { data: {
group: { group: {
id: 'gid://gitlab/Group/114', id: 'gid://gitlab/Group/114',
......
...@@ -21125,12 +21125,18 @@ msgstr "" ...@@ -21125,12 +21125,18 @@ msgstr ""
msgid "Iterations|Cadence name" msgid "Iterations|Cadence name"
msgstr "" msgstr ""
msgid "Iterations|Cancel"
msgstr ""
msgid "Iterations|Couldn't find iteration cadence" msgid "Iterations|Couldn't find iteration cadence"
msgstr "" msgstr ""
msgid "Iterations|Create cadence" msgid "Iterations|Create cadence"
msgstr "" msgstr ""
msgid "Iterations|Create iteration"
msgstr ""
msgid "Iterations|Delete cadence" msgid "Iterations|Delete cadence"
msgstr "" msgstr ""
...@@ -21140,9 +21146,15 @@ msgstr "" ...@@ -21140,9 +21146,15 @@ msgstr ""
msgid "Iterations|Delete iteration?" msgid "Iterations|Delete iteration?"
msgstr "" msgstr ""
msgid "Iterations|Description"
msgstr ""
msgid "Iterations|Done" msgid "Iterations|Done"
msgstr "" msgstr ""
msgid "Iterations|Due date"
msgstr ""
msgid "Iterations|Duration" msgid "Iterations|Duration"
msgstr "" msgstr ""
...@@ -40277,9 +40289,6 @@ msgstr "" ...@@ -40277,9 +40289,6 @@ msgstr ""
msgid "Update it" msgid "Update it"
msgstr "" msgstr ""
msgid "Update iteration"
msgstr ""
msgid "Update milestone" msgid "Update milestone"
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