Commit 965ce095 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'nfriend-prevent-submission-of-invalid-form-on-new-release-page' into 'master'

Modify submission behavior of the New/Edit Release page

See merge request gitlab-org/gitlab!39145
parents 482c83f6 7b2be900
...@@ -2,3 +2,4 @@ ...@@ -2,3 +2,4 @@
export const ESC_KEY = 'Escape'; export const ESC_KEY = 'Escape';
export const ESC_KEY_IE11 = 'Esc'; // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key export const ESC_KEY_IE11 = 'Esc'; // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
export const ENTER_KEY = 'Enter';
...@@ -85,7 +85,7 @@ export default { ...@@ -85,7 +85,7 @@ export default {
saveButtonLabel() { saveButtonLabel() {
return this.isExistingRelease ? __('Save changes') : __('Create release'); return this.isExistingRelease ? __('Save changes') : __('Create release');
}, },
isSaveChangesDisabled() { isFormSubmissionDisabled() {
return this.isUpdatingRelease || !this.isValid; return this.isUpdatingRelease || !this.isValid;
}, },
milestoneComboboxExtraLinks() { milestoneComboboxExtraLinks() {
...@@ -116,13 +116,18 @@ export default { ...@@ -116,13 +116,18 @@ export default {
'updateReleaseNotes', 'updateReleaseNotes',
'updateReleaseMilestones', 'updateReleaseMilestones',
]), ]),
submitForm() {
if (!this.isFormSubmissionDisabled) {
this.saveRelease();
}
},
}, },
}; };
</script> </script>
<template> <template>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<p class="pt-3 js-subtitle-text" v-html="subtitleText"></p> <p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
<form v-if="showForm" @submit.prevent="saveRelease()"> <form v-if="showForm" class="js-quick-submit" @submit.prevent="submitForm">
<tag-field /> <tag-field />
<gl-form-group> <gl-form-group>
<label for="release-title">{{ __('Release title') }}</label> <label for="release-title">{{ __('Release title') }}</label>
...@@ -134,7 +139,7 @@ export default { ...@@ -134,7 +139,7 @@ export default {
class="form-control" class="form-control"
/> />
</gl-form-group> </gl-form-group>
<gl-form-group class="w-50"> <gl-form-group class="w-50" @keydown.enter.prevent.capture>
<label>{{ __('Milestones') }}</label> <label>{{ __('Milestones') }}</label>
<div class="d-flex flex-column col-md-6 col-sm-10 pl-0"> <div class="d-flex flex-column col-md-6 col-sm-10 pl-0">
<milestone-combobox <milestone-combobox
...@@ -163,8 +168,6 @@ export default { ...@@ -163,8 +168,6 @@ export default {
data-supports-quick-actions="false" data-supports-quick-actions="false"
:aria-label="__('Release notes')" :aria-label="__('Release notes')"
:placeholder="__('Write your release notes or drag your files here…')" :placeholder="__('Write your release notes or drag your files here…')"
@keydown.meta.enter="saveRelease()"
@keydown.ctrl.enter="saveRelease()"
></textarea> ></textarea>
</template> </template>
</markdown-field> </markdown-field>
...@@ -179,7 +182,7 @@ export default { ...@@ -179,7 +182,7 @@ export default {
category="primary" category="primary"
variant="success" variant="success"
type="submit" type="submit"
:disabled="isSaveChangesDisabled" :disabled="isFormSubmissionDisabled"
data-testid="submit-button" data-testid="submit-button"
> >
{{ saveButtonLabel }} {{ saveButtonLabel }}
......
...@@ -49,6 +49,12 @@ export default { ...@@ -49,6 +49,12 @@ export default {
this.removeAssetLink(linkId); this.removeAssetLink(linkId);
this.ensureAtLeastOneLink(); this.ensureAtLeastOneLink();
}, },
updateUrl(link, newUrl) {
this.updateAssetLinkUrl({ linkIdToUpdate: link.id, newUrl });
},
updateName(link, newName) {
this.updateAssetLinkName({ linkIdToUpdate: link.id, newName });
},
hasDuplicateUrl(link) { hasDuplicateUrl(link) {
return Boolean(this.getLinkErrors(link).isDuplicate); return Boolean(this.getLinkErrors(link).isDuplicate);
}, },
...@@ -138,7 +144,9 @@ export default { ...@@ -138,7 +144,9 @@ export default {
type="text" type="text"
class="form-control" class="form-control"
:state="isUrlValid(link)" :state="isUrlValid(link)"
@change="updateAssetLinkUrl({ linkIdToUpdate: link.id, newUrl: $event })" @change="updateUrl(link, $event)"
@keydown.ctrl.enter="updateUrl(link, $event.target.value)"
@keydown.meta.enter="updateUrl(link, $event.target.value)"
/> />
<template #invalid-feedback> <template #invalid-feedback>
<span v-if="hasEmptyUrl(link)" class="invalid-feedback d-inline"> <span v-if="hasEmptyUrl(link)" class="invalid-feedback d-inline">
...@@ -175,7 +183,9 @@ export default { ...@@ -175,7 +183,9 @@ export default {
type="text" type="text"
class="form-control" class="form-control"
:state="isNameValid(link)" :state="isNameValid(link)"
@change="updateAssetLinkName({ linkIdToUpdate: link.id, newName: $event })" @change="updateName(link, $event)"
@keydown.ctrl.enter="updateName(link, $event.target.value)"
@keydown.meta.enter="updateName(link, $event.target.value)"
/> />
<template #invalid-feedback> <template #invalid-feedback>
<span v-if="hasEmptyName(link)" class="invalid-feedback d-inline"> <span v-if="hasEmptyName(link)" class="invalid-feedback d-inline">
......
---
title: Improve submission behavior of the New/Edit Release page
merge_request: 39145
author:
type: added
...@@ -83,11 +83,10 @@ describe('Release edit/new component', () => { ...@@ -83,11 +83,10 @@ describe('Release edit/new component', () => {
}); });
const findSubmitButton = () => wrapper.find('button[type=submit]'); const findSubmitButton = () => wrapper.find('button[type=submit]');
const findForm = () => wrapper.find('form');
describe(`basic functionality tests: all tests unrelated to the "${BACK_URL_PARAM}" parameter`, () => { describe(`basic functionality tests: all tests unrelated to the "${BACK_URL_PARAM}" parameter`, () => {
beforeEach(() => { beforeEach(factory);
factory();
});
it('calls initializeRelease when the component is created', () => { it('calls initializeRelease when the component is created', () => {
expect(actions.initializeRelease).toHaveBeenCalledTimes(1); expect(actions.initializeRelease).toHaveBeenCalledTimes(1);
...@@ -122,15 +121,14 @@ describe('Release edit/new component', () => { ...@@ -122,15 +121,14 @@ describe('Release edit/new component', () => {
}); });
it('calls saveRelease when the form is submitted', () => { it('calls saveRelease when the form is submitted', () => {
wrapper.find('form').trigger('submit'); findForm().trigger('submit');
expect(actions.saveRelease).toHaveBeenCalledTimes(1); expect(actions.saveRelease).toHaveBeenCalledTimes(1);
}); });
}); });
describe(`when the URL does not contain a "${BACK_URL_PARAM}" parameter`, () => { describe(`when the URL does not contain a "${BACK_URL_PARAM}" parameter`, () => {
beforeEach(() => { beforeEach(factory);
factory();
});
it(`renders a "Cancel" button with an href pointing to "${BACK_URL_PARAM}"`, () => { it(`renders a "Cancel" button with an href pointing to "${BACK_URL_PARAM}"`, () => {
const cancelButton = wrapper.find('.js-cancel-button'); const cancelButton = wrapper.find('.js-cancel-button');
...@@ -246,6 +244,12 @@ describe('Release edit/new component', () => { ...@@ -246,6 +244,12 @@ describe('Release edit/new component', () => {
it('renders the submit button as disabled', () => { it('renders the submit button as disabled', () => {
expect(findSubmitButton().attributes('disabled')).toBe('disabled'); expect(findSubmitButton().attributes('disabled')).toBe('disabled');
}); });
it('does not allow the form to be submitted', () => {
findForm().trigger('submit');
expect(actions.saveRelease).not.toHaveBeenCalled();
});
}); });
}); });
}); });
...@@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils'; ...@@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import AssetLinksForm from '~/releases/components/asset_links_form.vue'; import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import { release as originalRelease } from '../mock_data'; import { release as originalRelease } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import { ENTER_KEY } from '~/lib/utils/keys';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants'; import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -91,14 +92,28 @@ describe('Release edit component', () => { ...@@ -91,14 +92,28 @@ describe('Release edit component', () => {
expect(actions.removeAssetLink).toHaveBeenCalledTimes(1); expect(actions.removeAssetLink).toHaveBeenCalledTimes(1);
}); });
it('calls the "updateAssetLinkUrl" store method when text is entered into the "URL" input field', () => { describe('URL input field', () => {
const linkIdToUpdate = release.assets.links[0].id; let input;
const newUrl = 'updated url'; let linkIdToUpdate;
let newUrl;
beforeEach(() => {
input = wrapper.find({ ref: 'urlInput' }).element;
linkIdToUpdate = release.assets.links[0].id;
newUrl = 'updated url';
});
const expectStoreMethodNotToBeCalled = () => {
expect(actions.updateAssetLinkUrl).not.toHaveBeenCalled(); expect(actions.updateAssetLinkUrl).not.toHaveBeenCalled();
};
wrapper.find({ ref: 'urlInput' }).vm.$emit('change', newUrl); const dispatchKeydowEvent = eventParams => {
const event = new KeyboardEvent('keydown', eventParams);
input.dispatchEvent(event);
};
const expectStoreMethodToBeCalled = () => {
expect(actions.updateAssetLinkUrl).toHaveBeenCalledTimes(1); expect(actions.updateAssetLinkUrl).toHaveBeenCalledTimes(1);
expect(actions.updateAssetLinkUrl).toHaveBeenCalledWith( expect(actions.updateAssetLinkUrl).toHaveBeenCalledWith(
expect.anything(), expect.anything(),
...@@ -108,16 +123,59 @@ describe('Release edit component', () => { ...@@ -108,16 +123,59 @@ describe('Release edit component', () => {
}, },
undefined, undefined,
); );
};
it('calls the "updateAssetLinkUrl" store method when text is entered into the "URL" input field', () => {
expectStoreMethodNotToBeCalled();
wrapper.find({ ref: 'urlInput' }).vm.$emit('change', newUrl);
expectStoreMethodToBeCalled();
}); });
it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => { it('calls the "updateAssetLinkUrl" store method when Ctrl+Enter is pressed inside the "URL" input field', () => {
const linkIdToUpdate = release.assets.links[0].id; expectStoreMethodNotToBeCalled();
const newName = 'updated name';
expect(actions.updateAssetLinkName).not.toHaveBeenCalled(); input.value = newUrl;
wrapper.find({ ref: 'nameInput' }).vm.$emit('change', newName); dispatchKeydowEvent({ key: ENTER_KEY, ctrlKey: true });
expectStoreMethodToBeCalled();
});
it('calls the "updateAssetLinkUrl" store method when Cmd+Enter is pressed inside the "URL" input field', () => {
expectStoreMethodNotToBeCalled();
input.value = newUrl;
dispatchKeydowEvent({ key: ENTER_KEY, metaKey: true });
expectStoreMethodToBeCalled();
});
});
describe('Link title field', () => {
let input;
let linkIdToUpdate;
let newName;
beforeEach(() => {
input = wrapper.find({ ref: 'nameInput' }).element;
linkIdToUpdate = release.assets.links[0].id;
newName = 'updated name';
});
const expectStoreMethodNotToBeCalled = () => {
expect(actions.updateAssetLinkUrl).not.toHaveBeenCalled();
};
const dispatchKeydowEvent = eventParams => {
const event = new KeyboardEvent('keydown', eventParams);
input.dispatchEvent(event);
};
const expectStoreMethodToBeCalled = () => {
expect(actions.updateAssetLinkName).toHaveBeenCalledTimes(1); expect(actions.updateAssetLinkName).toHaveBeenCalledTimes(1);
expect(actions.updateAssetLinkName).toHaveBeenCalledWith( expect(actions.updateAssetLinkName).toHaveBeenCalledWith(
expect.anything(), expect.anything(),
...@@ -127,6 +185,35 @@ describe('Release edit component', () => { ...@@ -127,6 +185,35 @@ describe('Release edit component', () => {
}, },
undefined, undefined,
); );
};
it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => {
expectStoreMethodNotToBeCalled();
wrapper.find({ ref: 'nameInput' }).vm.$emit('change', newName);
expectStoreMethodToBeCalled();
});
it('calls the "updateAssetLinkName" store method when Ctrl+Enter is pressed inside the "Link title" input field', () => {
expectStoreMethodNotToBeCalled();
input.value = newName;
dispatchKeydowEvent({ key: ENTER_KEY, ctrlKey: true });
expectStoreMethodToBeCalled();
});
it('calls the "updateAssetLinkName" store method when Cmd+Enter is pressed inside the "Link title" input field', () => {
expectStoreMethodNotToBeCalled();
input.value = newName;
dispatchKeydowEvent({ key: ENTER_KEY, metaKey: true });
expectStoreMethodToBeCalled();
});
}); });
it('calls the "updateAssetLinkType" store method when an option is selected from the "Type" dropdown', () => { it('calls the "updateAssetLinkType" store method when an option is selected from the "Type" dropdown', () => {
......
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