Commit 2596f952 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch 'update-iterations-form-pajamas' into 'master'

Update new iterations form to match pajamas specs

See merge request gitlab-org/gitlab!82210
parents 1f55fa41 a2113aff
<script>
import { GlButton, GlForm, GlFormInput } from '@gitlab/ui';
import initDatePicker from '~/behaviors/date_picker';
import { GlButton, GlForm, GlFormGroup, GlFormInput, GlDatepicker } from '@gitlab/ui';
import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import createIteration from '../queries/create_iteration.mutation.graphql';
import updateIteration from '../queries/update_iteration.mutation.graphql';
......@@ -12,8 +12,10 @@ export default {
components: {
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
MarkdownField,
GlDatepicker,
},
props: {
groupPath: {
......@@ -47,8 +49,9 @@ export default {
loading: false,
title: this.iteration.title,
description: this.iteration.description ?? '',
startDate: this.iteration.startDate,
dueDate: this.iteration.dueDate,
startDate: this.iteration.startDate ? new Date(this.iteration.startDate) : null,
dueDate: this.iteration.dueDate ? new Date(this.iteration.dueDate) : null,
showValidation: false,
};
},
computed: {
......@@ -58,18 +61,35 @@ export default {
groupPath: this.groupPath,
title: this.title,
description: this.description,
startDate: this.startDate,
dueDate: this.dueDate,
startDate: this.formattedDate(this.startDate),
dueDate: this.formattedDate(this.dueDate),
},
};
},
},
mounted() {
// TODO: utilize GlDatepicker instead of relying on this jQuery behavior
initDatePicker();
invalidFeedback() {
return __('This field is required.');
},
isValid() {
return this.titleState && this.startDateState;
},
titleState() {
return !this.showValidation || Boolean(this.title);
},
startDateState() {
return !this.showValidation || Boolean(this.startDate);
},
},
methods: {
formattedDate(date) {
return date ? formatDate(date, 'yyyy-mm-dd') : null;
},
save() {
this.showValidation = true;
if (!this.isValid) {
return {};
}
this.loading = true;
return this.isEditing ? this.updateIteration() : this.createIteration();
},
......@@ -154,93 +174,89 @@ export default {
</h3>
</div>
<hr class="gl-mt-0" />
<gl-form class="row common-note-form">
<gl-form class="row common-note-form" novalidate>
<div class="col-md-6">
<div class="form-group row">
<div class="col-form-label col-sm-2">
<label for="iteration-title">{{ __('Title') }}</label>
</div>
<div class="col-sm-10">
<gl-form-input
id="iteration-title"
v-model="title"
autocomplete="off"
data-qa-selector="iteration_title_field"
/>
</div>
</div>
<gl-form-group
:label="__('Title')"
class="gl-flex-grow-1"
label-for="iteration-title"
:state="titleState"
:invalid-feedback="invalidFeedback"
>
<gl-form-input
id="iteration-title"
v-model="title"
autocomplete="off"
data-qa-selector="iteration_title_field"
:state="titleState"
required
/>
</gl-form-group>
<div class="form-group row">
<div class="col-form-label col-sm-2">
<label for="iteration-description">{{ __('Description') }}</label>
</div>
<div class="col-sm-10">
<markdown-field
:markdown-preview-path="previewMarkdownPath"
:can-attach-file="false"
:enable-autocomplete="true"
label="Description"
:textarea-value="description"
markdown-docs-path="/help/user/markdown"
:add-spacing-classes="false"
class="md-area"
>
<template #textarea>
<textarea
id="iteration-description"
v-model="description"
class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
data-supports-quick-actions="false"
:aria-label="__('Description')"
data-qa-selector="iteration_description_field"
>
</textarea>
</template>
</markdown-field>
</div>
</div>
<gl-form-group :label="__('Description')" label-for="iteration-description">
<markdown-field
:markdown-preview-path="previewMarkdownPath"
:can-attach-file="false"
:enable-autocomplete="true"
label="__('Description')"
:textarea-value="description"
markdown-docs-path="/help/user/markdown"
:add-spacing-classes="false"
class="md-area"
>
<template #textarea>
<textarea
id="iteration-description"
v-model="description"
class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
data-supports-quick-actions="false"
:aria-label="__('Description')"
data-qa-selector="iteration_description_field"
>
</textarea>
</template>
</markdown-field>
</gl-form-group>
</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
<gl-form-group
:label="__('Start date')"
:state="startDateState"
:invalid-feedback="invalidFeedback"
>
<div class="gl-display-inline-block gl-mr-2">
<gl-datepicker
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"
:state="startDateState"
required
/>
<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>
<gl-button
v-show="startDate"
variant="link"
class="gl-white-space-nowrap"
@click="updateStartDate(null)"
>
{{ __('Clear start date') }}
</gl-button>
</gl-form-group>
<gl-form-group :label="__('Due date')">
<div class="gl-display-inline-block gl-mr-2">
<gl-datepicker id="iteration-due-date" v-model="dueDate" />
</div>
</div>
<gl-button
v-show="dueDate"
variant="link"
class="gl-white-space-nowrap"
@click="updateDueDate(null)"
>
{{ __('Clear due date') }}
</gl-button>
</gl-form-group>
</div>
</gl-form>
......
......@@ -9,6 +9,8 @@ RSpec.describe 'User edits iteration' do
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(:new_start_date) { now + 4.days }
let_it_be(:new_due_date) { now + 5.days }
dropdown_selector = '[data-testid="actions-dropdown"]'
......@@ -24,12 +26,62 @@ RSpec.describe 'User edits iteration' do
sign_in(user)
end
where(using_cadences: [true, false])
let(:start_date_with_cadences_input) do
page.find('#iteration-start-date')
end
with_them do
let(:iteration_page) { using_cadences ? group_iteration_cadence_iteration_path(group, iteration_cadence_id: cadence.id, id: iteration.id) : group_iteration_path(iteration.group, iteration.id) }
let(:edit_iteration_page) { using_cadences ? edit_group_iteration_cadence_iteration_path(group, iteration_cadence_id: cadence.id, id: iteration.id) : edit_group_iteration_path(iteration.group, iteration.id) }
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
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
......@@ -47,20 +99,19 @@ RSpec.describe 'User edits iteration' do
updated_title = 'Updated iteration title'
updated_desc = 'Updated iteration desc'
updated_start_date = now + 4.days
updated_due_date = now + 5.days
fill_in('Title', with: updated_title)
fill_in('Description', with: updated_desc)
fill_in('Start date', with: updated_start_date.strftime('%Y-%m-%d'))
fill_in('Due date', with: updated_due_date.strftime('%Y-%m-%d'))
start_date = updated_start_date
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_start_date.strftime('%b %-d, %Y'))
expect(page).to have_content(updated_due_date.strftime('%b %-d, %Y'))
expect(page).to have_content(start_date)
expect(page).to have_content(due_date)
expect(page).to have_current_path(iteration_page)
end
end
......@@ -131,13 +182,5 @@ RSpec.describe 'User edits iteration' do
def description_input
page.find('#iteration-description')
end
def start_date_input
page.find('#iteration-start-date')
end
def due_date_input
page.find('#iteration-due-date')
end
end
end
......@@ -19,10 +19,15 @@ describe('Iteration Form', () => {
id: `gid://gitlab/Iteration/${id}`,
title: 'An iteration',
description: 'The words',
startDate: '2020-06-28',
dueDate: '2020-07-05',
startDate: new Date('2020-06-28'),
dueDate: new Date('2020-07-05'),
};
const title = 'Updated title';
const description = 'Updated description';
const startDate = '2020-05-06';
const dueDate = '2020-05-26';
const createMutationSuccess = { data: { createIteration: { iteration, errors: [] } } };
const createMutationFailure = {
data: { createIteration: { iteration, errors: ['alas, your data is unchanged'] } },
......@@ -63,6 +68,16 @@ describe('Iteration Form', () => {
const clickSave = () => findSaveButton().vm.$emit('click');
const clickCancel = () => findCancelButton().vm.$emit('click');
const inputFormData = () => {
findTitle().vm.$emit('input', title);
findDescription().setValue(description);
findStartDate().vm.$emit('input', startDate ? new Date(startDate) : null);
findDueDate().vm.$emit('input', dueDate ? new Date(dueDate) : null);
findTitle().trigger('change');
findStartDate().trigger('change');
};
it('renders a form', () => {
createComponent();
expect(wrapper.findComponent(GlForm).exists()).toBe(true);
......@@ -81,16 +96,7 @@ describe('Iteration Form', () => {
describe('save', () => {
it('triggers mutation with form data', () => {
const title = 'Iteration 5';
const description = 'The fifth iteration';
const startDate = '2020-05-05';
const dueDate = '2020-05-25';
findTitle().vm.$emit('input', title);
findDescription().setValue(description);
findStartDate().vm.$emit('input', startDate);
findDueDate().vm.$emit('input', dueDate);
inputFormData();
clickSave();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
......@@ -109,7 +115,7 @@ describe('Iteration Form', () => {
it('redirects to Iteration page on success', async () => {
createComponent();
inputFormData();
clickSave();
await nextTick();
......@@ -117,6 +123,20 @@ describe('Iteration Form', () => {
expect(visitUrl).toHaveBeenCalled();
});
it('validates required fields and sets isValid state to false', async () => {
createComponent();
clickSave();
await nextTick();
expect(findSaveButton().props('loading')).toBe(false);
expect(wrapper.vm.isValid).toBe(false);
expect(wrapper.vm.titleState).toBe(false);
expect(wrapper.vm.startDateState).toBe(false);
expect(visitUrl).not.toHaveBeenCalled();
});
it('loading=false on error', () => {
createComponent({ mutationResult: createMutationFailure });
......@@ -151,8 +171,9 @@ describe('Iteration Form', () => {
expect(findTitle().attributes('value')).toBe(iteration.title);
expect(findDescription().element.value).toBe(iteration.description);
expect(findStartDate().attributes('value')).toBe(iteration.startDate);
expect(findDueDate().attributes('value')).toBe(iteration.dueDate);
expect(new Date(findStartDate().attributes('value'))).toEqual(iteration.startDate);
expect(new Date(findDueDate().attributes('value'))).toEqual(iteration.dueDate);
});
it('shows update text on submit button', () => {
......@@ -168,16 +189,7 @@ describe('Iteration Form', () => {
props: propsWithIteration,
});
const title = 'Updated title';
const description = 'Updated description';
const startDate = '2020-05-06';
const dueDate = '2020-05-26';
findTitle().vm.$emit('input', title);
findDescription().setValue(description);
findStartDate().vm.$emit('input', startDate);
findDueDate().vm.$emit('input', dueDate);
inputFormData();
clickSave();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
......@@ -220,6 +232,28 @@ describe('Iteration Form', () => {
expect(wrapper.emitted('updated')).toBeUndefined();
});
it('validates required fields and sets isValid state to false', async () => {
createComponent({
props: propsWithIteration,
});
// remove input from edit page
findTitle().vm.$emit('input', '');
findStartDate().vm.$emit('input', null);
findTitle().trigger('change');
findStartDate().trigger('change');
clickSave();
await nextTick();
expect(findSaveButton().props('loading')).toBe(false);
expect(wrapper.vm.isValid).toBe(false);
expect(wrapper.vm.titleState).toBe(false);
expect(wrapper.vm.startDateState).toBe(false);
expect(visitUrl).not.toHaveBeenCalled();
});
it('emits cancel when cancel clicked', async () => {
createComponent({
props: propsWithIteration,
......
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