Commit 9181f41d authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '216156-ability-to-create-an-iteration' into 'master'

Add new iteration page behind feature flag

See merge request gitlab-org/gitlab!31689
parents a3b58596 641f52f6
<script>
import { GlButton, GlForm, GlFormInput } 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 createIteration from '../queries/create_iteration.mutation.graphql';
import DueDateSelectors from '~/due_date_select';
export default {
components: {
GlButton,
GlForm,
GlFormInput,
MarkdownField,
},
props: {
groupPath: {
type: String,
required: true,
},
previewMarkdownPath: {
type: String,
required: false,
default: '',
},
iterationsListPath: {
type: String,
required: true,
},
},
data() {
return {
iterations: [],
loading: false,
title: '',
description: '',
startDate: '',
dueDate: '',
};
},
mounted() {
// eslint-disable-next-line no-new
new DueDateSelectors();
},
methods: {
save() {
this.loading = true;
return this.$apollo
.mutate({
mutation: createIteration,
variables: {
input: {
groupPath: this.groupPath,
title: this.title,
description: this.description,
startDate: this.startDate,
dueDate: this.dueDate,
},
},
})
.then(({ data }) => {
const { errors, iteration } = data.createIteration;
if (errors?.length > 0) {
this.loading = false;
createFlash(errors[0]);
return;
}
visitUrl(iteration.webUrl);
})
.catch(() => {
this.loading = false;
createFlash(__('Unable to save iteration. Please try again'));
});
},
updateDueDate(val) {
this.dueDate = val;
},
updateStartDate(val) {
this.startDate = val;
},
},
};
</script>
<template>
<div>
<div class="gl-display-flex">
<h3 class="page-title">{{ __('New Iteration') }}</h3>
</div>
<hr />
<gl-form class="row common-note-form">
<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" />
</div>
</div>
<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')"
>
</textarea>
</template>
</markdown-field>
</div>
</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"
@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"
@change="updateDueDate"
/>
<a class="inline float-right gl-mt-2 js-clear-due-date" href="#">{{
__('Clear due date')
}}</a>
</div>
</div>
</div>
</gl-form>
<div class="form-actions d-flex">
<gl-button :loading="loading" data-testid="save-iteration" variant="success" @click="save">{{
__('Create iteration')
}}</gl-button>
<gl-button class="ml-auto" data-testid="cancel-iteration" :href="iterationsListPath">{{
__('Cancel')
}}</gl-button>
</div>
</div>
</template>
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import IterationForm from './components/iteration_form.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export function initIterationForm() {
const el = document.querySelector('.js-iteration-new');
if (!el) {
return null;
}
return new Vue({
el,
apolloProvider,
render(createElement) {
return createElement(IterationForm, {
props: {
groupPath: el.dataset.groupFullPath,
previewMarkdownPath: el.dataset.previewMarkdownPath,
iterationsListPath: el.dataset.iterationsListPath,
},
});
},
});
}
export default {};
mutation createIteration($input: CreateIterationInput!) {
createIteration(input: $input) {
iteration {
title,
description,
startDate,
dueDate,
webUrl
}
errors
}
}
query GroupIterations($fullPath: ID!, $state: IterationState) {
group(fullPath: $fullPath) {
sprints(state: $state, first: 20) {
nodes {
title
state
id
webPath
startDate
dueDate
}
}
}
}
import { initIterationForm } from 'ee/iterations';
document.addEventListener('DOMContentLoaded', () => {
initIterationForm();
});
......@@ -2,7 +2,4 @@
- breadcrumb_title _("New")
- page_title _("Iterations")
%h3.page-title
= _("New Iteration")
.js-iteration-new{ data: { group_full_path: @group.full_path } }
.js-iteration-new{ data: { group_full_path: @group.full_path, preview_markdown_path: preview_markdown_path(@group), iterations_list_path: group_iterations_path(@group) } }
# frozen_string_literal: true
require 'spec_helper'
describe 'Group iterations' do
let_it_be(:title_selector) { 'iteration-title' }
let_it_be(:description_selector) { '#iteration-description' }
let_it_be(:start_date_selector) { '#iteration-start-date' }
let_it_be(:due_date_selector) { '#iteration-due-date' }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project_empty_repo, group: group) }
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
around do |example|
Timecop.freeze { example.run }
end
before do
sign_in(user)
end
context 'create an iteration', :js do
before do
visit new_group_iteration_path(group)
end
it 'renders description preview' do
description = find(description_selector)
description.native.send_keys('')
click_button('Preview')
preview = find('.js-vue-md-preview')
expect(preview).to have_content('Nothing to preview.')
click_button('Write')
description.native.send_keys(':+1: Nice')
click_button('Preview')
expect(preview).to have_css('gl-emoji')
expect(find('#iteration-description', visible: false)).not_to be_visible
end
it 'description input does not support autocomplete' do
description = find(description_selector)
description.native.send_keys('!')
expect(page).not_to have_selector('.atwho-view')
end
end
end
import { shallowMount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
import { GlForm } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import IterationForm from 'ee/iterations/components/iteration_form.vue';
import createIteration from 'ee/iterations/queries/create_iteration.mutation.graphql';
import { TEST_HOST } from 'helpers/test_constants';
jest.mock('~/lib/utils/url_utility');
describe('Iteration Form', () => {
let wrapper;
const groupPath = 'gitlab-org';
const successfulMutation = { data: { createIteration: { iteration: {}, errors: [] } } };
const failedMutation = {
data: { createIteration: { iteration: {}, errors: ['alas, your data is unchanged'] } },
};
const props = { groupPath, iterationsListPath: TEST_HOST };
function createComponent({ mutationResult = successfulMutation } = {}) {
wrapper = shallowMount(IterationForm, {
propsData: props,
stubs: {
ApolloMutation,
MarkdownField: '<div><slot name="textarea"></slot></div>',
},
mocks: {
$apollo: {
mutate: jest.fn().mockResolvedValue(mutationResult),
},
},
});
}
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
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 findSaveButton = () => wrapper.find('[data-testid="save-iteration"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-iteration"]');
it('renders a form', () => {
createComponent();
expect(wrapper.find(GlForm).exists()).toBe(true);
});
it('cancel button links to list page', () => {
createComponent();
expect(findCancelButton().attributes('href')).toBe(TEST_HOST);
});
describe('save', () => {
it('trigges mutation with form data', () => {
createComponent();
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);
findSaveButton().vm.$emit('click');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: createIteration,
variables: {
input: {
groupPath,
title,
description,
startDate,
dueDate,
},
},
});
});
it('loading=true immediately', () => {
createComponent();
wrapper.vm.save();
expect(wrapper.vm.loading).toBeTruthy();
});
it('redirects to Iteration page on success', () => {
createComponent();
return wrapper.vm.save().then(() => {
expect(findSaveButton().props('loading')).toBeTruthy();
expect(visitUrl).toHaveBeenCalled();
});
});
it('loading=false on error', () => {
createComponent({ mutationResult: failedMutation });
return wrapper.vm.save().then(() => {
expect(findSaveButton().props('loading')).toBeFalsy();
});
});
});
});
......@@ -6415,6 +6415,9 @@ msgstr ""
msgid "Create issue"
msgstr ""
msgid "Create iteration"
msgstr ""
msgid "Create lists from labels. Issues with that label appear in that list."
msgstr ""
......@@ -23341,6 +23344,9 @@ msgstr ""
msgid "Unable to resolve"
msgstr ""
msgid "Unable to save iteration. Please try again"
msgstr ""
msgid "Unable to save your changes. Please try again."
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