Commit 3f3f5e28 authored by Brandon Labuschagne's avatar Brandon Labuschagne

Add upload blob component

The standalone component is being added here
and intergration will happen seperately.
parent 5cbdfdbd
<script>
import {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlToggle,
GlButton,
GlAlert,
} from '@gitlab/ui';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { ContentTypeMultipartFormData } from '~/lib/utils/headers';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
const PRIMARY_OPTIONS_TEXT = __('Upload file');
const SECONDARY_OPTIONS_TEXT = __('Cancel');
const MODAL_TITLE = __('Upload New File');
const COMMIT_LABEL = __('Commit message');
const TARGET_BRANCH_LABEL = __('Target branch');
const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes');
const REMOVE_FILE_TEXT = __('Remove file');
const NEW_BRANCH_IN_FORK = __(
'A new branch will be created in your fork and a new merge request will be started.',
);
const ERROR_MESSAGE = __('Error uploading file. Please try again.');
export default {
components: {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlToggle,
GlButton,
UploadDropzone,
GlAlert,
},
i18n: {
MODAL_TITLE,
COMMIT_LABEL,
TARGET_BRANCH_LABEL,
TOGGLE_CREATE_MR_LABEL,
REMOVE_FILE_TEXT,
NEW_BRANCH_IN_FORK,
},
props: {
modalId: {
type: String,
required: true,
},
commitMessage: {
type: String,
required: true,
},
targetBranch: {
type: String,
required: true,
},
origionalBranch: {
type: String,
required: true,
},
canPushCode: {
type: Boolean,
required: true,
},
path: {
type: String,
required: true,
},
},
data() {
return {
commit: this.commitMessage,
target: this.targetBranch,
createNewMr: true,
file: null,
filePreviewURL: null,
fileBinary: null,
loading: false,
};
},
computed: {
primaryOptions() {
return {
text: PRIMARY_OPTIONS_TEXT,
attributes: [
{
variant: 'success',
loading: this.loading,
disabled: !this.formCompleted || this.loading,
},
],
};
},
cancelOptions() {
return {
text: SECONDARY_OPTIONS_TEXT,
attributes: [
{
disabled: this.loading,
},
],
};
},
formattedFileSize() {
return numberToHumanSize(this.file.size);
},
showCreateNewMrToggle() {
return this.canPushCode && this.target !== this.origionalBranch;
},
formCompleted() {
return this.file && this.commit && this.target;
},
},
methods: {
setFile(file) {
this.file = file;
const fileUurlReader = new FileReader();
fileUurlReader.readAsDataURL(this.file);
fileUurlReader.onload = (e) => {
this.filePreviewURL = e.target?.result;
};
},
removeFile() {
this.file = null;
this.filePreviewURL = null;
},
uploadFile() {
this.loading = true;
const {
$route: {
params: { path },
},
} = this;
const uploadPath = joinPaths(this.path, path);
const formData = new FormData();
formData.append('branch_name', this.target);
formData.append('create_merge_request', this.createNewMr);
formData.append('commit_message', this.commit);
formData.append('file', this.file);
return axios
.post(uploadPath, formData, {
headers: {
...ContentTypeMultipartFormData,
},
})
.then((response) => {
visitUrl(response.data.filePath);
})
.catch(() => {
this.loading = false;
createFlash(ERROR_MESSAGE);
});
},
},
};
</script>
<template>
<gl-form>
<gl-modal
:modal-id="modalId"
:title="$options.i18n.MODAL_TITLE"
:action-primary="primaryOptions"
:action-cancel="cancelOptions"
@primary.prevent="uploadFile"
>
<upload-dropzone class="gl-h-200! gl-mb-4" single-file-selection @change="setFile">
<div
v-if="file"
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"
>
<img v-if="filePreviewURL" :src="filePreviewURL" class="gl-h-11" />
<div>{{ formattedFileSize }}</div>
<div>{{ file.name }}</div>
<gl-button
category="tertiary"
variant="confirm"
:disabled="loading"
@click="removeFile"
>{{ $options.i18n.REMOVE_FILE_TEXT }}</gl-button
>
</div>
</upload-dropzone>
<gl-form-group :label="$options.i18n.COMMIT_LABEL" label-for="commit_message">
<gl-form-textarea v-model="commit" name="commit_message" :disabled="loading" />
</gl-form-group>
<gl-form-group
v-if="canPushCode"
:label="$options.i18n.TARGET_BRANCH_LABEL"
label-for="branch_name"
>
<gl-form-input v-model="target" :disabled="loading" name="branch_name" />
</gl-form-group>
<gl-toggle
v-if="showCreateNewMrToggle"
v-model="createNewMr"
:disabled="loading"
:label="$options.i18n.TOGGLE_CREATE_MR_LABEL"
/>
<gl-alert v-if="!canPushCode" variant="info" :dismissible="false" class="gl-mt-3">
{{ $options.i18n.NEW_BRANCH_IN_FORK }}
</gl-alert>
</gl-modal>
</gl-form>
</template>
...@@ -12044,6 +12044,9 @@ msgstr "" ...@@ -12044,6 +12044,9 @@ msgstr ""
msgid "Error uploading file" msgid "Error uploading file"
msgstr "" msgstr ""
msgid "Error uploading file. Please try again."
msgstr ""
msgid "Error uploading file: %{stripped}" msgid "Error uploading file: %{stripped}"
msgstr "" msgstr ""
...@@ -25011,6 +25014,9 @@ msgstr "" ...@@ -25011,6 +25014,9 @@ msgstr ""
msgid "Remove due date" msgid "Remove due date"
msgstr "" msgstr ""
msgid "Remove file"
msgstr ""
msgid "Remove fork relationship" msgid "Remove fork relationship"
msgstr "" msgstr ""
...@@ -28374,6 +28380,9 @@ msgstr "" ...@@ -28374,6 +28380,9 @@ msgstr ""
msgid "Start a new merge request" msgid "Start a new merge request"
msgstr "" msgstr ""
msgid "Start a new merge request with these changes"
msgstr ""
msgid "Start a review" msgid "Start a review"
msgstr "" msgstr ""
......
import { GlModal, GlFormInput, GlFormTextarea, GlToggle, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
joinPaths: () => '/new_upload',
}));
const initialProps = {
modalId: 'upload-blob',
commitMessage: 'Upload New File',
targetBranch: 'master',
origionalBranch: 'master',
canPushCode: true,
path: 'new_upload',
};
describe('UploadBlobModal', () => {
let wrapper;
let mock;
const mockEvent = { preventDefault: jest.fn() };
const createComponent = (props) => {
wrapper = shallowMount(UploadBlobModal, {
propsData: {
...initialProps,
...props,
},
mocks: {
$route: {
params: {
path: '',
},
},
},
});
};
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
const findCommitMessage = () => wrapper.find(GlFormTextarea);
const findBranchName = () => wrapper.find(GlFormInput);
const findMrToggle = () => wrapper.find(GlToggle);
const findUploadDropzone = () => wrapper.find(UploadDropzone);
const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes[0].disabled;
const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes[0].disabled;
const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes[0].loading;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe.each`
canPushCode | displayBranchName | displayForkedBranchMessage
${true} | ${true} | ${false}
${false} | ${false} | ${true}
`(
'canPushCode = $canPushCode',
({ canPushCode, displayBranchName, displayForkedBranchMessage }) => {
beforeEach(() => {
createComponent({ canPushCode });
});
it('displays the modal', () => {
expect(findModal().exists()).toBe(true);
});
it('includes the upload dropzone', () => {
expect(findUploadDropzone().exists()).toBe(true);
});
it('includes the commit message', () => {
expect(findCommitMessage().exists()).toBe(true);
});
it('displays the disabled upload button', () => {
expect(actionButtonDisabledState()).toBe(true);
});
it('displays the enabled cancel button', () => {
expect(cancelButtonDisabledState()).toBe(false);
});
it('does not display the MR toggle', () => {
expect(findMrToggle().exists()).toBe(false);
});
it(`${
displayForkedBranchMessage ? 'displays' : 'does not display'
} the forked branch message`, () => {
expect(findAlert().exists()).toBe(displayForkedBranchMessage);
});
it(`${displayBranchName ? 'displays' : 'does not display'} the branch name`, () => {
expect(findBranchName().exists()).toBe(displayBranchName);
});
if (canPushCode) {
describe('when changing the branch name', () => {
it('displays the MR toggle', async () => {
wrapper.setData({ target: 'Not master' });
await wrapper.vm.$nextTick();
expect(findMrToggle().exists()).toBe(true);
});
});
}
describe('completed form', () => {
beforeEach(() => {
wrapper.setData({
file: { type: 'jpg' },
filePreviewURL: 'http://file.com?format=jpg',
});
});
it('enables the upload button when the form is completed', () => {
expect(actionButtonDisabledState()).toBe(false);
});
describe('form submission', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
findModal().vm.$emit('primary', mockEvent);
});
afterEach(() => {
mock.restore();
});
it('disables the upload button', () => {
expect(actionButtonDisabledState()).toBe(true);
});
it('sets the upload button to loading', () => {
expect(actionButtonLoadingState()).toBe(true);
});
});
describe('successful response', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
mock.onPost(initialProps.path).reply(httpStatusCodes.OK, { filePath: 'blah' });
findModal().vm.$emit('primary', mockEvent);
await waitForPromises();
});
it('redirects to the uploaded file', () => {
expect(visitUrl).toHaveBeenCalled();
});
afterEach(() => {
mock.restore();
});
});
describe('error response', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
mock.onPost(initialProps.path).timeout();
findModal().vm.$emit('primary', mockEvent);
await waitForPromises();
});
it('creates a flash error', () => {
expect(createFlash).toHaveBeenCalledWith('Error uploading file. Please try again.');
});
afterEach(() => {
mock.restore();
});
});
});
},
);
});
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