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
......@@ -268,7 +268,7 @@ export default {
data-qa-selector="save_iteration_button"
@click="save"
>
{{ isEditing ? __('Update iteration') : __('Create iteration') }}
{{ isEditing ? __('Save changes') : __('Create iteration') }}
</gl-button>
<gl-button class="ml-auto" data-testid="cancel-iteration" @click="cancel">
{{ __('Cancel') }}
......
......@@ -14,7 +14,7 @@ import { TYPE_ITERATION } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import { s__ } from '~/locale';
import { Namespace, iterationStates } from '../constants';
import { Namespace } from '../constants';
import deleteIteration from '../queries/destroy_iteration.mutation.graphql';
import query from '../queries/iteration.query.graphql';
import { getIterationPeriod } from '../utils';
......@@ -95,7 +95,8 @@ export default {
return getIterationPeriod(this.iteration);
},
showDelete() {
return this.iteration.state !== iterationStates.closed;
// We only support deleting iterations for manual cadences.
return !this.iteration.iterationCadence.automatic;
},
},
methods: {
......
......@@ -8,4 +8,8 @@ fragment IterationReport on Iteration {
state
title
webPath
iterationCadence {
id
automatic
}
}
......@@ -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(:guest_user) { create(:group_member, :guest, user: create(:user), group: group ).user }
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_due_date) { now + 5.days }
......@@ -26,62 +26,10 @@ RSpec.describe 'User edits iteration' do
sign_in(user)
end
let(:start_date_with_cadences_input) do
page.find('#iteration-start-date')
end
let(:due_date_with_cadences_input) do
page.find('#iteration-due-date')
end
let(:start_date_without_cadences_input) do
input = page.first('[data-testid="gl-datepicker-input"]')
input.set(now - 1.day)
input
end
let(:due_date_without_cadences_input) do
input = all('[data-testid="gl-datepicker-input"]').last
input.set(now)
input
end
let(:updated_start_date_with_cadences) do
fill_in('Start date', with: new_start_date.strftime('%Y-%m-%d'))
new_start_date.strftime('%b %-d, %Y')
end
let(:updated_due_date_with_cadences) do
fill_in('Due date', with: new_due_date.strftime('%Y-%m-%d'))
new_due_date.strftime('%b %-d, %Y')
end
let(:updated_start_date_without_cadences) do
start_date_without_cadences_input.set(new_start_date)
new_start_date.strftime('%b %-d, %Y')
end
let(:updated_due_date_without_cadences) do
# TODO: Reported issue with Capybara
# Use fill_in instead, update datepicker to have labels
due_date_without_cadences_input.set('')
due_date_without_cadences_input.set(new_due_date)
new_due_date.strftime('%b %-d, %Y')
end
shared_examples 'manually managed iteration' do
let_it_be(:manual_cadence) { build(:iterations_cadence, group: group, automatic: false).tap { |cadence| cadence.save!(validate: false) } }
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(:iteration_with_cadences_page) { group_iteration_cadence_iteration_path(group, iteration_cadence_id: cadence.id, id: iteration.id) }
let(:iteration_without_cadences_page) { group_iteration_path(iteration.group, iteration.id) }
let(:edit_iteration_with_cadences_page) { edit_group_iteration_cadence_iteration_path(group, iteration_cadence_id: cadence.id, id: iteration.id) }
let(:edit_iteration_without_cadences_page) { edit_group_iteration_path(iteration.group, iteration.id) }
where(:using_cadences, :start_date_input, :due_date_input, :updated_start_date, :updated_due_date, :iteration_page, :edit_iteration_page) do
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
with_them do
context 'load edit page directly', :js do
before do
visit edit_iteration_page
......@@ -91,10 +39,10 @@ RSpec.describe 'User edits iteration' do
it 'prefills fields and allows updating all values' do
aggregate_failures do
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(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)
end
updated_title = 'Updated iteration title'
......@@ -102,16 +50,16 @@ RSpec.describe 'User edits iteration' do
fill_in('Title', with: updated_title)
fill_in('Description', with: updated_desc)
start_date = updated_start_date
due_date = updated_due_date
start_date_input
due_date_input
click_button('Update iteration')
click_button('Save changes')
aggregate_failures do
expect(page).to have_content(updated_title)
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_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
......@@ -127,15 +75,87 @@ RSpec.describe 'User edits iteration' do
click_link_or_button('Edit')
aggregate_failures do
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(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
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(:start_date_input) do
fill_in 'Start date', with: new_start_date
start_date_field.native.send_keys :enter
end
let(:due_date_input) do
fill_in 'Due date', with: new_due_date
due_date_field.native.send_keys :enter
end
it_behaves_like 'manually managed iteration'
end
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
before do
visit edit_iteration_page
wait_for_requests
end
it 'prefills and allows updating description', :aggregate_failures do
expect(find_field("Description").value).to eq(iteration.description)
updated_desc = 'Updated iteration desc'
fill_in('Description', with: updated_desc)
click_button('Save changes')
expect(page).to have_content(updated_desc)
end
end
context 'load edit page from report', :js do
before do
visit iteration_page
end
it 'prefills description and updates URL' do
find(dropdown_selector).click
click_link_or_button('Edit')
expect(find_field("Description").value).to eq(iteration.description)
expect(page).to have_current_path(edit_iteration_page)
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
context 'as guest user' do
......@@ -174,13 +194,5 @@ RSpec.describe 'User edits iteration' do
end
end
end
def title_input
page.find('#iteration-title')
end
def description_input
page.find('#iteration-description')
end
end
end
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlFormInput } from '@gitlab/ui';
import IterationForm from 'ee/iterations/components/iteration_form.vue';
import readIteration from 'ee/iterations/queries/iteration.query.graphql';
import createIteration from 'ee/iterations/queries/iteration_create.mutation.graphql';
......@@ -15,14 +16,13 @@ import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { dayAfter, formatDate } from '~/lib/utils/datetime_utility';
import {
manualIterationCadence as cadence,
mockGroupIterations,
mockIterationNode as iteration,
mockManualIterationNode as iteration,
createMutationSuccess,
createMutationFailure,
updateMutationSuccess,
emptyGroupIterationsSuccess,
nonEmptyGroupIterationsSuccess,
readCadenceSuccess,
readManualCadenceSuccess,
} from '../mock_data';
const baseUrl = '/cadences/';
......@@ -35,6 +35,28 @@ function createMockApolloProvider(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', () => {
let wrapper;
let router;
......@@ -44,7 +66,7 @@ describe('Iteration Form', () => {
mutationQuery = createIteration,
mutationResult = createMutationSuccess,
query = readIteration,
result = mockGroupIterations,
result = mockGroupIterationsFactory(),
resolverMock = jest.fn().mockResolvedValue(mutationResult),
groupIterationsSuccess = emptyGroupIterationsSuccess,
} = {}) {
......@@ -52,7 +74,7 @@ describe('Iteration Form', () => {
[query, jest.fn().mockResolvedValue(result)],
[mutationQuery, resolverMock],
[groupIterationsInCadenceQuery, jest.fn().mockResolvedValue(groupIterationsSuccess)],
[readCadence, jest.fn().mockResolvedValue(readCadenceSuccess)],
[readCadence, jest.fn().mockResolvedValue(readManualCadenceSuccess)],
]);
wrapper = extendedWrapper(
mount(IterationForm, {
......@@ -78,10 +100,12 @@ describe('Iteration Form', () => {
});
const findPageTitle = () => wrapper.findComponent({ ref: 'pageTitle' });
const findTitle = () => wrapper.find('#iteration-title');
const findDescription = () => wrapper.find('#iteration-description');
const findStartDate = () => wrapper.find('#iteration-start-date');
const findDueDate = () => wrapper.find('#iteration-due-date');
const findTitle = () => wrapper.findByLabelText('Title');
const findDescription = () => wrapper.findByLabelText('Description');
const findStartDate = () => wrapper.findByTestId('start-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 findCancelButton = () => wrapper.findByTestId('cancel-iteration');
const clickSave = () => findSaveButton().trigger('click');
......@@ -112,11 +136,10 @@ describe('Iteration Form', () => {
const startDate = '2020-05-05';
const dueDate = '2020-05-25';
findTitle().vm.$emit('input', title);
findTitle().setValue(title);
findDescription().setValue(description);
findStartDate().vm.$emit('input', startDate);
findDueDate().vm.$emit('input', dueDate);
findStartDate().vm.$emit('input', new Date(startDate));
findDueDate().vm.$emit('input', new Date(dueDate));
await clickSave();
expect(resolverMock).toHaveBeenCalledWith({
......@@ -172,7 +195,7 @@ describe('Iteration Form', () => {
'yyyy-mm-dd',
);
expect(findStartDate().element.value).toBe(expectedDate);
expect(findStartDateInputText()).toBe(expectedDate);
});
});
......@@ -188,17 +211,17 @@ describe('Iteration Form', () => {
it('uses cadence start date', () => {
const expectedDate = cadence.startDate;
expect(findStartDate().element.value).toBe(expectedDate);
expect(findStartDateInputText()).toBe(expectedDate);
});
});
});
});
describe('Edit iteration', () => {
describe('Edit iteration for manual cadence', () => {
beforeEach(() => {
router.replace({
name: 'editIteration',
params: { cadenceId: cadence.id, iterationId: iteration.id },
params: { cadenceId, iterationId },
});
});
......@@ -219,29 +242,31 @@ describe('Iteration Form', () => {
expect(findTitle().element.value).toBe(iteration.title);
expect(findDescription().element.value).toBe(iteration.description);
expect(findStartDate().element.value).toBe(iteration.startDate);
expect(findDueDate().element.value).toBe(iteration.dueDate);
expect(findStartDateInputText()).toBe(iteration.startDate);
expect(findDueDateInputText()).toBe(iteration.dueDate);
});
it('shows update text on submit button', () => {
createComponent();
expect(findSaveButton().text()).toBe('Update iteration');
expect(findSaveButton().text()).toBe('Save changes');
});
it('triggers mutation with form data', async () => {
const resolverMock = jest.fn().mockResolvedValue(updateMutationSuccess);
createComponent({ mutationQuery: updateIteration, resolverMock });
await waitForPromises();
const title = 'Updated title';
const description = 'Updated description';
const startDate = '2020-05-06';
const dueDate = '2020-05-26';
findTitle().vm.$emit('input', title);
findTitle().setValue(title);
findDescription().setValue(description);
findStartDate().vm.$emit('input', startDate);
findDueDate().vm.$emit('input', dueDate);
findStartDate().vm.$emit('input', new Date(startDate));
findDueDate().vm.$emit('input', new Date(dueDate));
clickSave();
await waitForPromises();
......@@ -249,7 +274,7 @@ describe('Iteration Form', () => {
expect(resolverMock).toHaveBeenCalledWith({
input: {
groupPath,
id: iteration.id,
id: iterationId,
title,
description,
startDate,
......@@ -264,6 +289,7 @@ describe('Iteration Form', () => {
mutationQuery: updateIteration,
resolverMock,
});
await waitForPromises();
clickSave();
await nextTick();
......@@ -274,11 +300,11 @@ describe('Iteration Form', () => {
expect(resolverMock).toHaveBeenCalledWith({
input: {
groupPath,
id: iteration.id,
startDate: '',
dueDate: '',
title: '',
description: '',
id: iterationId,
startDate: iteration.startDate,
dueDate: iteration.dueDate,
title: iteration.title,
description: iteration.description,
},
});
});
......
......@@ -181,7 +181,7 @@ describe('Iteration Form', () => {
props: propsWithIteration,
});
expect(findSaveButton().text()).toBe('Update iteration');
expect(findSaveButton().text()).toBe('Save changes');
});
it('triggers mutation with form data', () => {
......
......@@ -17,7 +17,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import {
mockIterationNode,
mockPastIterationNode,
mockManualIterationNode,
createMockGroupIterations,
mockIterationNodeWithoutTitle,
mockProjectIterations,
......@@ -155,8 +155,16 @@ describe('Iterations report', () => {
});
describe('delete iteration', () => {
it('does not show delete option for past iterations', async () => {
mountComponent({ mockQueryResponse: createMockGroupIterations(mockPastIterationNode) });
it('does not show delete option when iteration belongs to automatic cadence', async () => {
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();
......
......@@ -11,12 +11,21 @@ export const mockIterationNode = {
title: 'top-level-iteration',
webPath: '/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',
};
export const mockPastIterationNode = {
export const mockManualIterationNode = {
...mockIterationNode,
state: iterationStates.closed,
iterationCadence: {
__typename: 'IterationCadence',
id: 'gid://gitlab/Iterations::Cadence/72',
automatic: false,
},
};
export const mockIterationNodeWithoutTitle = {
......@@ -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: {
group: {
id: 'gid://gitlab/Group/114',
......
......@@ -21125,12 +21125,18 @@ msgstr ""
msgid "Iterations|Cadence name"
msgstr ""
msgid "Iterations|Cancel"
msgstr ""
msgid "Iterations|Couldn't find iteration cadence"
msgstr ""
msgid "Iterations|Create cadence"
msgstr ""
msgid "Iterations|Create iteration"
msgstr ""
msgid "Iterations|Delete cadence"
msgstr ""
......@@ -21140,9 +21146,15 @@ msgstr ""
msgid "Iterations|Delete iteration?"
msgstr ""
msgid "Iterations|Description"
msgstr ""
msgid "Iterations|Done"
msgstr ""
msgid "Iterations|Due date"
msgstr ""
msgid "Iterations|Duration"
msgstr ""
......@@ -40277,9 +40289,6 @@ msgstr ""
msgid "Update it"
msgstr ""
msgid "Update iteration"
msgstr ""
msgid "Update milestone"
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