Commit 0219bbdd authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'psi-cadence-edit' into 'master'

Add cadence edit page

See merge request gitlab-org/gitlab!62587
parents 8ae2080e d02d5775
......@@ -9,8 +9,11 @@ import {
GlFormInput,
GlFormSelect,
} from '@gitlab/ui';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale';
import createCadence from '../queries/create_cadence.mutation.graphql';
import createCadence from '../queries/cadence_create.mutation.graphql';
import updateCadence from '../queries/cadence_update.mutation.graphql';
import readCadence from '../queries/iteration_cadence.query.graphql';
const i18n = Object.freeze({
title: {
......@@ -36,16 +39,22 @@ const i18n = Object.freeze({
description: s__('Iterations|Number of future iterations you would like to have scheduled'),
placeholder: s__('Iterations|Select number'),
},
pageTitle: s__('Iterations|New iteration cadence'),
create: s__('Iterations|Create cadence'),
edit: {
title: s__('Iterations|Edit iteration cadence'),
save: s__('Iterations|Save cadence'),
},
new: {
title: s__('Iterations|New iteration cadence'),
save: s__('Iterations|Create cadence'),
},
cancel: __('Cancel'),
requiredField: __('This field is required.'),
});
export default {
availableDurations: [{ value: null, text: i18n.duration.placeholder }, 1, 2, 3, 4, 5, 6],
availableDurations: [{ value: 0, text: i18n.duration.placeholder }, 1, 2, 3, 4, 5, 6],
availableFutureIterations: [
{ value: null, text: i18n.futureIterations.placeholder },
{ value: 0, text: i18n.futureIterations.placeholder },
2,
4,
6,
......@@ -67,15 +76,19 @@ export default {
inject: ['groupPath', 'cadencesListPath'],
data() {
return {
cadences: [],
group: {
loading: false,
iterationCadences: {
nodes: [],
},
},
loading: false,
errorMessage: '',
title: '',
automatic: true,
startDate: null,
durationInWeeks: null,
rollOverIssues: false,
iterationsInAdvance: null,
durationInWeeks: 0,
iterationsInAdvance: 0,
validationState: {
title: null,
startDate: null,
......@@ -86,29 +99,82 @@ export default {
};
},
computed: {
loadingCadence() {
return this.$apollo.queries.group.loading;
},
cadenceId() {
return this.$router.currentRoute.params.cadenceId;
},
isEdit() {
return Boolean(this.cadenceId);
},
page() {
return this.isEdit ? 'edit' : 'new';
},
mutation() {
return this.isEdit ? updateCadence : createCadence;
},
valid() {
return !Object.values(this.validationState).includes(false);
},
variables() {
const id = this.isEdit
? convertToGraphQLId('Iterations::Cadence', this.cadenceId)
: undefined;
const groupPath = this.isEdit ? undefined : this.groupPath;
const vars = {
input: {
groupPath: this.groupPath,
groupPath,
id,
title: this.title,
automatic: this.automatic,
startDate: this.startDate,
durationInWeeks: this.durationInWeeks,
active: true,
iterationsInAdvance: this.iterationsInAdvance,
},
};
if (this.automatic) {
vars.input = {
...vars.input,
iterationsInAdvance: this.iterationsInAdvance,
};
}
return vars;
},
},
apollo: {
group: {
skip() {
return !this.isEdit;
},
query: readCadence,
variables() {
return {
fullPath: this.groupPath,
id: this.cadenceId,
};
},
result({ data: { group, errors } }) {
if (errors?.length) {
[this.errorMessage] = errors;
return;
}
const cadence = group?.iterationCadences?.nodes?.[0];
if (!cadence) {
this.errorMessage = s__("Iterations|Couldn't find iteration cadence");
return;
}
this.title = cadence.title;
this.automatic = cadence.automatic;
this.startDate = cadence.startDate;
this.durationInWeeks = cadence.durationInWeeks;
this.iterationsInAdvance = cadence.iterationsInAdvance;
},
error(error) {
this.errorMessage = error;
},
},
},
methods: {
validate(field) {
this.validationState[field] = Boolean(this[field]);
......@@ -131,6 +197,12 @@ export default {
this.validationState.durationInWeeks = null;
this.validationState.iterationsInAdvance = null;
},
updateAutomatic(value) {
this.clearValidation();
if (!value) {
this.iterationsInAdvance = 0;
}
},
save() {
this.validateAllFields();
......@@ -147,18 +219,18 @@ export default {
createCadence() {
return this.$apollo
.mutate({
mutation: createCadence,
mutation: this.mutation,
variables: this.variables,
})
.then(({ data, errors: topLevelErrors = [] }) => {
.then(({ data, errors: topLevelErrors = [] } = {}) => {
if (topLevelErrors.length > 0) {
this.errorMessage = topLevelErrors[0].message;
return;
}
const { errors } = data.iterationCadenceCreate;
const { errors } = data?.result || {};
if (errors.length > 0) {
if (errors?.length > 0) {
[this.errorMessage] = errors;
return;
}
......@@ -181,7 +253,7 @@ export default {
<article>
<div class="gl-display-flex">
<h3 ref="pageTitle" class="page-title">
{{ i18n.pageTitle }}
{{ i18n[page].title }}
</h3>
</div>
<gl-form>
......@@ -205,6 +277,7 @@ export default {
:placeholder="i18n.title.placeholder"
size="xl"
:state="validationState.title"
:disabled="loadingCadence"
@blur="validate('title')"
/>
</gl-form-group>
......@@ -218,7 +291,8 @@ export default {
<gl-form-checkbox
id="cadence-automated-scheduling"
v-model="automatic"
@change="clearValidation"
:disabled="loadingCadence"
@change="updateAutomatic"
>
<span class="gl-font-weight-bold">{{ i18n.automatedScheduling.label }}</span>
</gl-form-checkbox>
......@@ -243,6 +317,7 @@ export default {
inputmode="none"
required
:state="validationState.startDate"
:disabled="loadingCadence"
data-qa-selector="cadence_start_date"
@blur="validate('startDate')"
/>
......@@ -265,6 +340,7 @@ export default {
class="gl-form-input-md"
required
data-qa-selector="iteration_cadence_name_field"
:disabled="loadingCadence"
@change="validate('durationInWeeks')"
/>
</gl-form-group>
......@@ -282,7 +358,7 @@ export default {
<gl-form-select
id="cadence-schedule-future-iterations"
v-model.number="iterationsInAdvance"
:disabled="!automatic"
:disabled="!automatic || loadingCadence"
:options="$options.availableFutureIterations"
:required="automatic"
class="gl-form-input-md"
......@@ -299,7 +375,7 @@ export default {
data-qa-selector="save_cadence_button"
@click="save"
>
{{ i18n.create }}
{{ i18n[page].save }}
</gl-button>
<gl-button class="ml-auto" data-testid="cancel-create-cadence" @click="cancel">
{{ i18n.cancel }}
......
......@@ -3,6 +3,8 @@ import {
GlAlert,
GlButton,
GlCollapse,
GlDropdown,
GlDropdownItem,
GlIcon,
GlInfiniteScroll,
GlSkeletonLoader,
......@@ -24,6 +26,8 @@ export default {
GlAlert,
GlButton,
GlCollapse,
GlDropdown,
GlDropdownItem,
GlIcon,
GlInfiniteScroll,
GlSkeletonLoader,
......@@ -42,7 +46,7 @@ export default {
},
},
},
inject: ['groupPath'],
inject: ['groupPath', 'canEditCadence'],
props: {
title: {
type: String,
......@@ -159,7 +163,7 @@ export default {
<div class="gl-display-flex gl-align-items-center">
<gl-button
variant="link"
class="gl-font-weight-bold gl-text-body! gl-py-5! gl-px-3! gl-mr-auto"
class="gl-font-weight-bold gl-text-body! gl-py-5! gl-px-3! gl-mr-auto gl-min-w-0"
:aria-expanded="expanded"
@click="expanded = !expanded"
>
......@@ -171,10 +175,23 @@ export default {
{{ title }}
</gl-button>
<span v-if="durationInWeeks" class="gl-mr-5">
<span v-if="durationInWeeks" class="gl-mr-5 gl-display-none gl-sm-display-inline-block">
<gl-icon name="clock" class="gl-mr-3" />
{{ n__('Every week', 'Every %d weeks', durationInWeeks) }}</span
>
<gl-dropdown
v-if="canEditCadence"
icon="ellipsis_v"
category="tertiary"
right
lazy
text-sr-only
no-caret
>
<gl-dropdown-item :to="editCadence">
{{ s__('Iterations|Edit cadence') }}
</gl-dropdown-item>
</gl-dropdown>
</div>
<gl-alert v-if="error" variant="danger" :dismissible="true" @dismiss="error = ''">
......
......@@ -32,10 +32,12 @@ export default {
data() {
return {
group: {
iterationCadences: [],
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
iterationCadences: {
nodes: [],
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
},
},
},
pagination: {},
......@@ -104,11 +106,13 @@ export default {
<template #title>
{{ tab }}
</template>
<gl-loading-icon v-if="loading" class="gl-my-5" size="lg" />
<gl-alert v-else-if="error" variant="danger" @dismiss="error = ''">
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">
{{ error }}
</gl-alert>
<gl-loading-icon v-if="loading" class="gl-my-5" size="lg" />
<template v-else>
<ul v-if="cadences.length" class="content-list">
<iteration-cadence-list-item
......
......@@ -114,6 +114,7 @@ export function initCadenceApp() {
router,
apolloProvider,
provide: {
fullPath: groupPath,
groupPath,
cadencesListPath,
canCreateCadence: parseBoolean(canCreateCadence),
......
#import "./iteration_cadence.fragment.graphql"
mutation createIterationCadence($input: IterationCadenceCreateInput!) {
iterationCadenceCreate(input: $input) {
result: iterationCadenceCreate(input: $input) {
iterationCadence {
id
title
...IterationCadence
}
errors
}
......
#import "./iteration_cadence.fragment.graphql"
mutation updateIterationCadence($input: IterationCadenceUpdateInput!) {
result: iterationCadenceUpdate(input: $input) {
iterationCadence {
...IterationCadence
}
errors
}
}
fragment IterationCadence on IterationCadence {
id
title
automatic
startDate
durationInWeeks
iterationsInAdvance
}
#import "./iteration_cadence.fragment.graphql"
# todo: should this use IterationsCadenceID! ?
query IterationCadences($fullPath: ID!, $id: ID!) {
group(fullPath: $fullPath) {
iterationCadences(id: $id) {
nodes {
...IterationCadence
}
}
}
}
......@@ -12,6 +12,11 @@ const routes = [
path: '/new',
component: IterationCadenceForm,
},
{
name: 'edit',
path: '/:cadenceId/edit',
component: IterationCadenceForm,
},
{
name: 'index',
path: '/',
......
import { GlFormCheckbox, GlFormGroup } from '@gitlab/ui';
import { GlAlert, GlFormCheckbox, GlFormGroup } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import IterationCadenceForm from 'ee/iterations/components/iteration_cadence_form.vue';
import createCadence from 'ee/iterations/queries/create_cadence.mutation.graphql';
import createCadence from 'ee/iterations/queries/cadence_create.mutation.graphql';
import readCadence from 'ee/iterations/queries/iteration_cadence.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
......@@ -11,6 +13,9 @@ import waitForPromises from 'helpers/wait_for_promises';
const push = jest.fn();
const $router = {
currentRoute: {
params: {},
},
push,
};
......@@ -25,24 +30,34 @@ describe('Iteration cadence form', () => {
const groupPath = 'gitlab-org';
const id = 72;
const iterationCadence = {
id: `gid://gitlab/Iteration/${id}`,
id: `gid://gitlab/Iterations::Cadence/${id}`,
title: 'An iteration',
description: 'The words',
automatic: true,
startDate: '2020-06-28',
dueDate: '2020-07-05',
durationInWeeks: '3',
iterationsInAdvance: '2',
};
const createMutationSuccess = {
data: { iterationCadenceCreate: { iterationCadence, errors: [] } },
data: { result: { iterationCadence, errors: [] } },
};
const createMutationFailure = {
data: {
iterationCadenceCreate: { iterationCadence, errors: ['alas, your data is unchanged'] },
result: { iterationCadence, errors: ['alas, your data is unchanged'] },
},
};
const getCadenceSuccess = {
data: {
group: {
iterationCadences: {
nodes: [iterationCadence],
},
},
},
};
function createComponent({ resolverMock } = {}) {
const apolloProvider = createMockApolloProvider([[createCadence, resolverMock]]);
function createComponent({ query = createCadence, resolverMock } = {}) {
const apolloProvider = createMockApolloProvider([[query, resolverMock]]);
wrapper = extendedWrapper(
mount(IterationCadenceForm, {
apolloProvider,
......@@ -67,11 +82,30 @@ describe('Iteration cadence form', () => {
const findDurationGroup = () => wrapper.findAllComponents(GlFormGroup).at(3);
const findFutureIterationsGroup = () => wrapper.findAllComponents(GlFormGroup).at(4);
const findError = () => wrapper.findComponent(GlAlert);
const findTitle = () => wrapper.find('#cadence-title');
const findStartDate = () => wrapper.find('#cadence-start-date');
const findFutureIterations = () => wrapper.find('#cadence-schedule-future-iterations');
const findDuration = () => wrapper.find('#cadence-duration');
const setTitle = (value) => findTitle().vm.$emit('input', value);
const setStartDate = (value) => findStartDate().vm.$emit('input', value);
const setFutureIterations = (value) => findFutureIterations().vm.$emit('input', value);
const setDuration = (value) => findDuration().vm.$emit('input', value);
const setAutomaticValue = (value) => {
const checkbox = findAutomatedSchedulingGroup().find(GlFormCheckbox).vm;
checkbox.$emit('input', value);
checkbox.$emit('change', value);
};
const findAllFields = () => [
findTitle(),
findStartDate(),
findFutureIterations(),
findDuration(),
];
const findSaveButton = () => wrapper.findByTestId('save-cadence');
const findCancelButton = () => wrapper.findByTestId('cancel-create-cadence');
const clickSave = () => findSaveButton().vm.$emit('click');
......@@ -92,19 +126,20 @@ describe('Iteration cadence form', () => {
});
describe('save', () => {
it('triggers mutation with form data', () => {
const title = 'Iteration 5';
const startDate = '2020-05-05';
const durationInWeeks = 2;
const iterationsInAdvance = 6;
const title = 'Iteration 5';
const startDate = '2020-05-05';
const durationInWeeks = 2;
const iterationsInAdvance = 6;
findTitle().vm.$emit('input', title);
findStartDate().vm.$emit('input', startDate);
findDuration().vm.$emit('input', durationInWeeks);
findFutureIterations().vm.$emit('input', iterationsInAdvance);
it('triggers mutation with form data', () => {
setTitle(title);
setStartDate(startDate);
setDuration(durationInWeeks);
setFutureIterations(iterationsInAdvance);
clickSave();
expect(findError().exists()).toBe(false);
expect(resolverMock).toHaveBeenCalledWith({
input: {
groupPath,
......@@ -119,15 +154,10 @@ describe('Iteration cadence form', () => {
});
it('redirects to Iteration page on success', async () => {
const title = 'Iteration 5';
const startDate = '2020-05-05';
const durationInWeeks = 2;
const iterationsInAdvance = 6;
findTitle().vm.$emit('input', title);
findStartDate().vm.$emit('input', startDate);
findDuration().vm.$emit('input', durationInWeeks);
findFutureIterations().vm.$emit('input', iterationsInAdvance);
setTitle(title);
setStartDate(startDate);
setDuration(durationInWeeks);
setFutureIterations(iterationsInAdvance);
clickSave();
......@@ -159,22 +189,28 @@ describe('Iteration cadence form', () => {
});
describe('automated scheduling disabled', () => {
beforeEach(() => {
findAutomatedSchedulingGroup().find(GlFormCheckbox).vm.$emit('input', false);
});
it('disables future iterations', async () => {
setAutomaticValue(false);
await nextTick();
it('disables future iterations', () => {
expect(findFutureIterations().attributes('disabled')).toBe('disabled');
});
it('does not require future iterations ', () => {
it('sets future iterations to 0', async () => {
const title = 'Iteration 5';
const startDate = '2020-05-05';
const durationInWeeks = 2;
findTitle().vm.$emit('input', title);
findStartDate().vm.$emit('input', startDate);
findDuration().vm.$emit('input', durationInWeeks);
setFutureIterations(10);
setAutomaticValue(false);
await nextTick();
setTitle(title);
setStartDate(startDate);
setDuration(durationInWeeks);
clickSave();
......@@ -185,10 +221,58 @@ describe('Iteration cadence form', () => {
automatic: false,
startDate,
durationInWeeks,
iterationsInAdvance: 0,
active: true,
},
});
});
});
});
describe('Edit cadence', () => {
const query = readCadence;
const resolverMock = jest.fn().mockResolvedValue(getCadenceSuccess);
beforeEach(() => {
$router.currentRoute.params.cadenceId = id;
});
afterEach(() => {
delete $router.currentRoute.params.cadenceId;
});
it('shows correct title and button text', () => {
createComponent({ query, resolverMock });
expect(wrapper.text()).toContain(wrapper.vm.i18n.edit.title);
expect(wrapper.text()).toContain(wrapper.vm.i18n.edit.save);
});
it('disables fields while loading', async () => {
createComponent({ query, resolverMock });
findAllFields().forEach(({ element }) => {
expect(element).toBeDisabled();
});
await waitForPromises();
findAllFields().forEach(({ element }) => {
expect(element).not.toBeDisabled();
});
});
it('fills fields with existing cadence info after loading', async () => {
createComponent({ query, resolverMock });
await waitForPromises();
await nextTick();
expect(findTitle().element.value).toBe(iterationCadence.title);
expect(findStartDate().element.value).toBe(iterationCadence.startDate);
expect(findFutureIterations().element.value).toBe(iterationCadence.iterationsInAdvance);
expect(findDuration().element.value).toBe(iterationCadence.durationInWeeks);
});
});
});
import { GlInfiniteScroll, GlSkeletonLoader } from '@gitlab/ui';
import { GlDropdown, GlInfiniteScroll, GlSkeletonLoader } from '@gitlab/ui';
import { createLocalVue, RouterLinkStub } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
......@@ -82,6 +82,7 @@ describe('Iteration cadence list item', () => {
function createComponent({
props = {},
canCreateCadence,
canEditCadence,
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse),
} = {}) {
apolloProvider = createMockApolloProvider([[iterationsInCadenceQuery, resolverMock]]);
......@@ -98,6 +99,7 @@ describe('Iteration cadence list item', () => {
provide: {
groupPath,
canCreateCadence,
canEditCadence,
},
propsData: {
title: cadence.title,
......@@ -185,4 +187,16 @@ describe('Iteration cadence list item', () => {
}),
);
});
it('hides dropdown when canEditCadence is false', async () => {
await createComponent({ canEditCadence: false });
expect(wrapper.find(GlDropdown).exists()).toBe(false);
});
it('shows dropdown when canEditCadence is true', async () => {
await createComponent({ canEditCadence: true });
expect(wrapper.find(GlDropdown).exists()).toBe(true);
});
});
......@@ -82,6 +82,7 @@ describe('Iteration cadences list', () => {
function createComponent({
canCreateCadence,
canEditCadence,
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse),
} = {}) {
apolloProvider = createMockApolloProvider([[cadencesListQuery, resolverMock]]);
......@@ -96,6 +97,7 @@ describe('Iteration cadences list', () => {
groupPath,
cadencesListPath,
canCreateCadence,
canEditCadence,
},
});
......
......@@ -18556,12 +18556,21 @@ msgstr ""
msgid "Iterations|Cadence name"
msgstr ""
msgid "Iterations|Couldn't find iteration cadence"
msgstr ""
msgid "Iterations|Create cadence"
msgstr ""
msgid "Iterations|Duration"
msgstr ""
msgid "Iterations|Edit cadence"
msgstr ""
msgid "Iterations|Edit iteration cadence"
msgstr ""
msgid "Iterations|Error loading iteration cadences."
msgstr ""
......@@ -18583,6 +18592,9 @@ msgstr ""
msgid "Iterations|Number of future iterations you would like to have scheduled"
msgstr ""
msgid "Iterations|Save cadence"
msgstr ""
msgid "Iterations|Select duration"
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