Commit ba4c9478 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '343553-adjust-license-upload-style' into 'master'

Change upload license page file input to dropzone

See merge request gitlab-org/gitlab!77363
parents 9ee6926f ce1c426d
...@@ -41,6 +41,16 @@ export default { ...@@ -41,6 +41,16 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
inputFieldName: {
type: String,
required: false,
default: 'upload_file',
},
shouldUpdateInputOnFileDrop: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -84,6 +94,30 @@ export default { ...@@ -84,6 +94,30 @@ export default {
return; return;
} }
// NOTE: This is a temporary solution to integrate dropzone into a Rails
// form. On file drop if `shouldUpdateInputOnFileDrop` is true, the file
// input value is updated. So that when the form is submitted — the file
// value would be send together with the form data. This solution should
// be removed when License file upload page is fully migrated:
// https://gitlab.com/gitlab-org/gitlab/-/issues/352501
// NOTE: as per https://caniuse.com/mdn-api_htmlinputelement_files, IE11
// is not able to set input.files property, thought the user would still
// be able to use the file picker dialogue option, by clicking the
// "openFileUpload" button
if (this.shouldUpdateInputOnFileDrop) {
// Since FileList cannot be easily manipulated, to match requirement of
// singleFileSelection, we're throwing an error if multiple files were
// dropped on the dropzone
// NOTE: we can drop this logic together with
// `shouldUpdateInputOnFileDrop` flag
if (this.singleFileSelection && files.length > 1) {
this.$emit('error');
return;
}
this.$refs.fileUpload.files = files;
}
this.$emit('change', this.singleFileSelection ? files[0] : files); this.$emit('change', this.singleFileSelection ? files[0] : files);
}, },
ondragenter(e) { ondragenter(e) {
...@@ -116,6 +150,7 @@ export default { ...@@ -116,6 +150,7 @@ export default {
<slot> <slot>
<button <button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
type="button"
@click="openFileUpload" @click="openFileUpload"
> >
<div <div
...@@ -147,7 +182,7 @@ export default { ...@@ -147,7 +182,7 @@ export default {
<input <input
ref="fileUpload" ref="fileUpload"
type="file" type="file"
name="upload_file" :name="inputFieldName"
:accept="validFileMimetypes" :accept="validFileMimetypes"
class="hide" class="hide"
:multiple="!singleFileSelection" :multiple="!singleFileSelection"
......
...@@ -59,8 +59,10 @@ Otherwise, to upload your license: ...@@ -59,8 +59,10 @@ Otherwise, to upload your license:
1. On the left sidebar, select **Settings**. 1. On the left sidebar, select **Settings**.
1. In the **License file** area, select **Upload a license**. 1. In the **License file** area, select **Upload a license**.
1. Upload a license: 1. Upload a license:
- For a file, select **Upload `.gitlab-license` file**, **Choose file**, and - For a file, either:
select the license file from your local machine. - Select **Upload `.gitlab-license` file**, then **Choose File** and
select the license file from your local machine.
- Drag and drop the license file to the **Drag your license file here** area.
- For plain text, select **Enter license key** and paste the contents in - For plain text, select **Enter license key** and paste the contents in
**License key**. **License key**.
1. Select the **Terms of Service** checkbox. 1. Select the **Terms of Service** checkbox.
......
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import createFlash from '~/flash';
import {
DROPZONE_DESCRIPTION_TEXT,
FILE_UPLOAD_ERROR_MESSAGE,
FILE_DROP_ERROR_MESSAGE,
DROP_TO_START_MESSAGE,
} from '../constants';
const VALID_LICENSE_FILE_MIMETYPES = ['.gitlab_license', '.gitlab-license', '.txt'];
const FILE_EXTENSION_REGEX = /\.(gitlab[-_]license|txt)$/;
const isValidLicenseFile = ({ name }) => {
return FILE_EXTENSION_REGEX.test(name);
};
export default {
name: 'LicenseNewApp',
components: {
UploadDropzone,
GlLink,
GlSprintf,
},
VALID_LICENSE_FILE_MIMETYPES,
isValidLicenseFile,
i18n: {
DROPZONE_DESCRIPTION_TEXT,
FILE_UPLOAD_ERROR_MESSAGE,
FILE_DROP_ERROR_MESSAGE,
DROP_TO_START_MESSAGE,
},
data() {
return { fileName: null };
},
computed: {
dropzoneDescription() {
return this.fileName ?? this.$options.i18n.DROPZONE_DESCRIPTION_TEXT;
},
},
methods: {
onChange(file) {
this.fileName = file?.name;
},
onError() {
createFlash({ message: this.$options.i18n.FILE_UPLOAD_ERROR_MESSAGE });
},
},
};
</script>
<template>
<upload-dropzone
input-field-name="license[data_file]"
:is-file-valid="$options.isValidLicenseFile"
:valid-file-mimetypes="$options.VALID_LICENSE_FILE_MIMETYPES"
:should-update-input-on-file-drop="true"
:single-file-selection="true"
:enable-drag-behavior="false"
:drop-to-start-message="$options.i18n.DROP_TO_START_MESSAGE"
@change="onChange"
@error="onError"
>
<template #upload-text="{ openFileUpload }">
<gl-sprintf :message="dropzoneDescription">
<template #link="{ content }">
<gl-link @click.stop="openFileUpload">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
<template #invalid-drag-data-slot>
{{ $options.i18n.FILE_DROP_ERROR_MESSAGE }}
</template>
</upload-dropzone>
</template>
import { s__ } from '~/locale';
export const DROPZONE_DESCRIPTION_TEXT = s__(
'Licenses|Drag your license file here or %{linkStart}click to upload%{linkEnd}.',
);
export const FILE_UPLOAD_ERROR_MESSAGE = s__('Licenses|The file could not be uploaded.');
export const FILE_DROP_ERROR_MESSAGE = s__(
'Licenses|Error: You are trying to upload something other than a file',
);
export const DROP_TO_START_MESSAGE = s__('Licenses|Drop your license file to start the upload.');
import Vue from 'vue';
import LicenseNewApp from 'ee/admin/licenses/new/components/license_new_app.vue';
const licenseFile = document.querySelector('.license-file'); const licenseFile = document.querySelector('.license-file');
const licenseKey = document.querySelector('.license-key'); const licenseKey = document.querySelector('.license-key');
const acceptEULACheckBox = document.querySelector('#accept_eula'); const acceptEULACheckBox = document.querySelector('#accept_eula');
...@@ -15,6 +18,21 @@ const toggleUploadLicenseButton = () => { ...@@ -15,6 +18,21 @@ const toggleUploadLicenseButton = () => {
uploadLicenseBtn.toggleAttribute('disabled', !acceptEULACheckBox.checked); uploadLicenseBtn.toggleAttribute('disabled', !acceptEULACheckBox.checked);
}; };
const initLicenseUploadDropzone = () => {
const el = document.getElementById('js-license-new-app');
return new Vue({
el,
components: {
LicenseNewApp,
},
render(createElement) {
return createElement(LicenseNewApp);
},
});
};
licenseType.forEach((el) => el.addEventListener('change', showLicenseType)); licenseType.forEach((el) => el.addEventListener('change', showLicenseType));
acceptEULACheckBox.addEventListener('change', toggleUploadLicenseButton); acceptEULACheckBox.addEventListener('change', toggleUploadLicenseButton);
showLicenseType(); showLicenseType();
initLicenseUploadDropzone();
...@@ -31,9 +31,10 @@ ...@@ -31,9 +31,10 @@
= label_tag :license_type_file, class: 'form-check-label' do = label_tag :license_type_file, class: 'form-check-label' do
.option-title .option-title
= _('Upload %{file_name} file').html_safe % { file_name: '<code>.gitlab-license</code>'.html_safe } = _('Upload %{file_name} file').html_safe % { file_name: '<code>.gitlab-license</code>'.html_safe }
.form-group.license-file.gl-mt-4 .form-group.license-file.gl-mt-4
= f.label :data_file, _('License file'), class: 'gl-sr-only' #js-license-new-app
= f.file_field :data_file, accept: ".gitlab-license,.gitlab_license,.txt", class: "form-control"
.form-check.gl-my-4 .form-check.gl-my-4
= radio_button_tag :license_type, :key, @license.data.present?, class: 'form-check-input', data: { qa_selector: 'license_type_key_radio' } = radio_button_tag :license_type, :key, @license.data.present?, class: 'form-check-input', data: { qa_selector: 'license_type_key_radio' }
= label_tag :license_type_key, class: 'form-check-label' do = label_tag :license_type_key, class: 'form-check-label' do
......
...@@ -113,7 +113,7 @@ RSpec.describe "Admin uploads license", :js do ...@@ -113,7 +113,7 @@ RSpec.describe "Admin uploads license", :js do
private private
def attach_and_upload(path) def attach_and_upload(path)
attach_file("license_data_file", path) attach_file("license[data_file]", path, make_visible: true)
check("accept_eula") check("accept_eula")
click_button("Upload License") click_button("Upload License")
end end
......
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import LicenseNewApp from 'ee/admin/licenses/new/components/license_new_app.vue';
import { FILE_UPLOAD_ERROR_MESSAGE } from 'ee/admin/licenses/new/constants';
import createFlash from '~/flash';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
jest.mock('~/flash');
describe('Upload dropzone component', () => {
let wrapper;
const findUploadDropzone = () => wrapper.find(UploadDropzone);
function createComponent() {
wrapper = shallowMount(LicenseNewApp, {
stubs: {
GlSprintf,
},
});
}
beforeEach(() => {
createFlash.mockClear();
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('displays an error when upload-dropzone emits an error', async () => {
findUploadDropzone().vm.$emit('error');
await nextTick();
expect(createFlash).toHaveBeenCalledWith({ message: FILE_UPLOAD_ERROR_MESSAGE });
});
it('displays filename when the file is set in upload-dropzone', async () => {
const uploadDropzone = findUploadDropzone();
uploadDropzone.vm.$emit('change', { name: 'test-license.txt' });
await nextTick();
expect(wrapper.text()).toEqual(expect.stringContaining('test-license.txt'));
});
it('properly resets filename when the file was unset by the upload-dropzone', async () => {
const uploadDropzone = findUploadDropzone();
uploadDropzone.vm.$emit('change', { name: 'test-license.txt' });
await nextTick();
uploadDropzone.vm.$emit('change', null);
await nextTick();
expect(wrapper.text()).not.toEqual(expect.stringContaining('test-license.txt'));
});
describe('allows only license file types for the dropzone', () => {
const properLicenseFileExtensions = ['.gitlab_license', '.gitlab-license', '.txt'];
let isFileValid;
let validFileMimetypes;
beforeEach(() => {
createComponent();
const uploadDropzone = findUploadDropzone();
isFileValid = uploadDropzone.props('isFileValid');
validFileMimetypes = uploadDropzone.props('validFileMimetypes');
});
it('should pass proper extension list for file picker dialogue', () => {
expect(validFileMimetypes).toEqual(properLicenseFileExtensions);
});
it.each(properLicenseFileExtensions)('allows %s file extension', (extension) => {
expect(isFileValid({ name: `license${extension}` })).toBe(true);
});
it.each(['.pdf', '.jpg', '.html'])('rejects %s file extension', (extension) => {
expect(isFileValid({ name: `license${extension}` })).toBe(false);
});
});
});
...@@ -21660,9 +21660,18 @@ msgstr "" ...@@ -21660,9 +21660,18 @@ msgstr ""
msgid "Licenses|Displays licenses detected in the project, based on the %{linkStart}latest successful%{linkEnd} scan" msgid "Licenses|Displays licenses detected in the project, based on the %{linkStart}latest successful%{linkEnd} scan"
msgstr "" msgstr ""
msgid "Licenses|Drag your license file here or %{linkStart}click to upload%{linkEnd}."
msgstr ""
msgid "Licenses|Drop your license file to start the upload."
msgstr ""
msgid "Licenses|Error fetching the license list. Please check your network connection and try again." msgid "Licenses|Error fetching the license list. Please check your network connection and try again."
msgstr "" msgstr ""
msgid "Licenses|Error: You are trying to upload something other than a file"
msgstr ""
msgid "Licenses|License Compliance" msgid "Licenses|License Compliance"
msgstr "" msgstr ""
...@@ -21681,6 +21690,9 @@ msgstr "" ...@@ -21681,6 +21690,9 @@ msgstr ""
msgid "Licenses|Specified policies in this project" msgid "Licenses|Specified policies in this project"
msgstr "" msgstr ""
msgid "Licenses|The file could not be uploaded."
msgstr ""
msgid "Licenses|The license list details information about the licenses used within your project." msgid "Licenses|The license list details information about the licenses used within your project."
msgstr "" msgstr ""
......
...@@ -6,6 +6,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess ...@@ -6,6 +6,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess
> >
<button <button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
type="button"
> >
<div <div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
...@@ -86,6 +87,7 @@ exports[`Upload dropzone component when dragging renders correct template when d ...@@ -86,6 +87,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
> >
<button <button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
type="button"
> >
<div <div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
...@@ -170,6 +172,7 @@ exports[`Upload dropzone component when dragging renders correct template when d ...@@ -170,6 +172,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
> >
<button <button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
type="button"
> >
<div <div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
...@@ -254,6 +257,7 @@ exports[`Upload dropzone component when dragging renders correct template when d ...@@ -254,6 +257,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
> >
<button <button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
type="button"
> >
<div <div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
...@@ -339,6 +343,7 @@ exports[`Upload dropzone component when dragging renders correct template when d ...@@ -339,6 +343,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
> >
<button <button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
type="button"
> >
<div <div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
...@@ -424,6 +429,7 @@ exports[`Upload dropzone component when dragging renders correct template when d ...@@ -424,6 +429,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
> >
<button <button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
type="button"
> >
<div <div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
...@@ -509,6 +515,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon ...@@ -509,6 +515,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
> >
<button <button
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
type="button"
> >
<div <div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
......
...@@ -16,6 +16,7 @@ describe('Upload dropzone component', () => { ...@@ -16,6 +16,7 @@ describe('Upload dropzone component', () => {
const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]'); const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]');
const findIcon = () => wrapper.find(GlIcon); const findIcon = () => wrapper.find(GlIcon);
const findUploadText = () => wrapper.find('[data-testid="upload-text"]').text(); const findUploadText = () => wrapper.find('[data-testid="upload-text"]').text();
const findFileInput = () => wrapper.find('input[type="file"]');
function createComponent({ slots = {}, data = {}, props = {} } = {}) { function createComponent({ slots = {}, data = {}, props = {} } = {}) {
wrapper = shallowMount(UploadDropzone, { wrapper = shallowMount(UploadDropzone, {
...@@ -197,4 +198,60 @@ describe('Upload dropzone component', () => { ...@@ -197,4 +198,60 @@ describe('Upload dropzone component', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
describe('file input form name', () => {
it('applies inputFieldName as file input name', () => {
createComponent({ props: { inputFieldName: 'test_field_name' } });
expect(findFileInput().attributes('name')).toBe('test_field_name');
});
it('uses default file input name if no inputFieldName provided', () => {
createComponent();
expect(findFileInput().attributes('name')).toBe('upload_file');
});
});
describe('updates file input files value', () => {
// NOTE: the component assigns dropped files from the drop event to the
// input.files property. There's a restriction that nothing but a FileList
// can be assigned to this property. While FileList can't be created
// manually: it has no constructor. And currently there's no good workaround
// for jsdom. So we have to stub the file input in vm.$refs to ensure that
// the files property is updated. This enforces following tests to know a
// bit too much about the SUT internals See this thread for more details on
// FileList in jsdom: https://github.com/jsdom/jsdom/issues/1272
function stubFileInputOnWrapper() {
const fakeFileInput = { files: [] };
wrapper.vm.$refs.fileUpload = fakeFileInput;
}
it('assigns dragged files to the input files property', async () => {
const mockFile = { name: 'test', type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
createComponent({ props: { shouldUpdateInputOnFileDrop: true } });
stubFileInputOnWrapper();
wrapper.trigger('dragenter', mockEvent);
await nextTick();
wrapper.trigger('drop', mockEvent);
await nextTick();
expect(wrapper.vm.$refs.fileUpload.files).toEqual([mockFile]);
});
it('throws an error when multiple files are dropped on a single file input dropzone', async () => {
const mockFile = { name: 'test', type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile, mockFile] });
createComponent({ props: { shouldUpdateInputOnFileDrop: true, singleFileSelection: true } });
stubFileInputOnWrapper();
wrapper.trigger('dragenter', mockEvent);
await nextTick();
wrapper.trigger('drop', mockEvent);
await nextTick();
expect(wrapper.vm.$refs.fileUpload.files).toEqual([]);
expect(wrapper.emitted('error')).toHaveLength(1);
});
});
}); });
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