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