Commit af72f143 authored by Brandon Labuschagne's avatar Brandon Labuschagne

Migrate bootstrap modal to vue

This is being done for the blob dropdown
zone functionality within the project
breadcrumbs
parent 60444b92
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlDropdownItem, GlDropdownItem,
GlIcon, GlIcon,
GlModalDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import permissionsQuery from 'shared_queries/repository/permissions.query.graphql'; import permissionsQuery from 'shared_queries/repository/permissions.query.graphql';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
...@@ -12,12 +13,15 @@ import { __ } from '../../locale'; ...@@ -12,12 +13,15 @@ import { __ } from '../../locale';
import getRefMixin from '../mixins/get_ref'; import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql'; import projectPathQuery from '../queries/project_path.query.graphql';
import projectShortPathQuery from '../queries/project_short_path.query.graphql'; import projectShortPathQuery from '../queries/project_short_path.query.graphql';
import UploadBlobModal from './upload_blob_modal.vue';
const ROW_TYPES = { const ROW_TYPES = {
header: 'header', header: 'header',
divider: 'divider', divider: 'divider',
}; };
const UPLOAD_BLOB_MODAL_ID = 'modal-upload-blob';
export default { export default {
components: { components: {
GlDropdown, GlDropdown,
...@@ -25,6 +29,7 @@ export default { ...@@ -25,6 +29,7 @@ export default {
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlDropdownItem, GlDropdownItem,
GlIcon, GlIcon,
UploadBlobModal,
}, },
apollo: { apollo: {
projectShortPath: { projectShortPath: {
...@@ -46,6 +51,9 @@ export default { ...@@ -46,6 +51,9 @@ export default {
}, },
}, },
}, },
directives: {
GlModal: GlModalDirective,
},
mixins: [getRefMixin], mixins: [getRefMixin],
props: { props: {
currentPath: { currentPath: {
...@@ -63,6 +71,21 @@ export default { ...@@ -63,6 +71,21 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
canPushCode: {
type: Boolean,
required: false,
default: false,
},
selectedBranch: {
type: String,
required: false,
default: '',
},
originalBranch: {
type: String,
required: false,
default: '',
},
newBranchPath: { newBranchPath: {
type: String, type: String,
required: false, required: false,
...@@ -93,7 +116,13 @@ export default { ...@@ -93,7 +116,13 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
uploadPath: {
type: String,
required: false,
default: '',
},
}, },
uploadBlobModalId: UPLOAD_BLOB_MODAL_ID,
data() { data() {
return { return {
projectShortPath: '', projectShortPath: '',
...@@ -126,7 +155,10 @@ export default { ...@@ -126,7 +155,10 @@ export default {
); );
}, },
canCreateMrFromFork() { canCreateMrFromFork() {
return this.userPermissions.forkProject && this.userPermissions.createMergeRequestIn; return this.userPermissions?.forkProject && this.userPermissions?.createMergeRequestIn;
},
showUploadModal() {
return this.canEditTree && !this.$apollo.queries.userPermissions.loading;
}, },
dropdownItems() { dropdownItems() {
const items = []; const items = [];
...@@ -149,10 +181,9 @@ export default { ...@@ -149,10 +181,9 @@ export default {
{ {
attrs: { attrs: {
href: '#modal-upload-blob', href: '#modal-upload-blob',
'data-target': '#modal-upload-blob',
'data-toggle': 'modal',
}, },
text: __('Upload file'), text: __('Upload file'),
modalId: UPLOAD_BLOB_MODAL_ID,
}, },
{ {
attrs: { attrs: {
...@@ -253,12 +284,26 @@ export default { ...@@ -253,12 +284,26 @@ export default {
<gl-icon name="chevron-down" :size="16" class="float-left" /> <gl-icon name="chevron-down" :size="16" class="float-left" />
</template> </template>
<template v-for="(item, i) in dropdownItems"> <template v-for="(item, i) in dropdownItems">
<component :is="getComponent(item.type)" :key="i" v-bind="item.attrs"> <component
:is="getComponent(item.type)"
:key="i"
v-bind="item.attrs"
v-gl-modal="item.modalId || null"
>
{{ item.text }} {{ item.text }}
</component> </component>
</template> </template>
</gl-dropdown> </gl-dropdown>
</li> </li>
</ol> </ol>
<upload-blob-modal
v-if="showUploadModal"
:modal-id="$options.uploadBlobModalId"
:commit-message="__('Upload New File')"
:target-branch="selectedBranch"
:original-branch="originalBranch"
:can-push-code="canPushCode"
:path="uploadPath"
/>
</nav> </nav>
</template> </template>
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import Vue from 'vue'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link'; import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import { parseBoolean } from '../lib/utils/common_utils';
import { escapeFileUrl } from '../lib/utils/url_utility';
import { __ } from '../locale';
import App from './components/app.vue'; import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue'; import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue'; import DirectoryDownloadLinks from './components/directory_download_links.vue';
...@@ -55,6 +55,8 @@ export default function setupVueRepositoryList() { ...@@ -55,6 +55,8 @@ export default function setupVueRepositoryList() {
const { const {
canCollaborate, canCollaborate,
canEditTree, canEditTree,
canPushCode,
selectedBranch,
newBranchPath, newBranchPath,
newTagPath, newTagPath,
newBlobPath, newBlobPath,
...@@ -65,8 +67,7 @@ export default function setupVueRepositoryList() { ...@@ -65,8 +67,7 @@ export default function setupVueRepositoryList() {
newDirPath, newDirPath,
} = breadcrumbEl.dataset; } = breadcrumbEl.dataset;
router.afterEach(({ params: { path = '/' } }) => { router.afterEach(({ params: { path } }) => {
updateFormAction('.js-upload-blob-form', uploadPath, path);
updateFormAction('.js-create-dir-form', newDirPath, path); updateFormAction('.js-create-dir-form', newDirPath, path);
}); });
...@@ -81,12 +82,16 @@ export default function setupVueRepositoryList() { ...@@ -81,12 +82,16 @@ export default function setupVueRepositoryList() {
currentPath: this.$route.params.path, currentPath: this.$route.params.path,
canCollaborate: parseBoolean(canCollaborate), canCollaborate: parseBoolean(canCollaborate),
canEditTree: parseBoolean(canEditTree), canEditTree: parseBoolean(canEditTree),
canPushCode: parseBoolean(canPushCode),
originalBranch: ref,
selectedBranch,
newBranchPath, newBranchPath,
newTagPath, newTagPath,
newBlobPath, newBlobPath,
forkNewBlobPath, forkNewBlobPath,
forkNewDirectoryPath, forkNewDirectoryPath,
forkUploadBlobPath, forkUploadBlobPath,
uploadPath,
}, },
}); });
}, },
......
...@@ -131,6 +131,8 @@ module TreeHelper ...@@ -131,6 +131,8 @@ module TreeHelper
def breadcrumb_data_attributes def breadcrumb_data_attributes
attrs = { attrs = {
selected_branch: selected_branch,
can_push_code: can?(current_user, :push_code, @project).to_s,
can_collaborate: can_collaborate_with_project?(@project).to_s, can_collaborate: can_collaborate_with_project?(@project).to_s,
new_blob_path: project_new_blob_path(@project, @ref), new_blob_path: project_new_blob_path(@project, @ref),
upload_path: project_create_blob_path(@project, @ref), upload_path: project_create_blob_path(@project, @ref),
......
...@@ -21,5 +21,4 @@ ...@@ -21,5 +21,4 @@
#js-tree-list{ data: vue_file_list_data(project, ref) } #js-tree-list{ data: vue_file_list_data(project, ref) }
- if can_edit_tree? - if can_edit_tree?
= render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
= render 'projects/blob/new_dir' = render 'projects/blob/new_dir'
---
title: Migrate bootstrap modal to GlModal for repo single file uploads
merge_request: 55587
author:
type: changed
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Projects > Files > User uploads files' do RSpec.describe 'Projects > Files > User uploads files' do
include DropzoneHelper
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository, name: 'Shop', creator: user) } let(:project) { create(:project, :repository, name: 'Shop', creator: user) }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
...@@ -17,36 +15,15 @@ RSpec.describe 'Projects > Files > User uploads files' do ...@@ -17,36 +15,15 @@ RSpec.describe 'Projects > Files > User uploads files' do
context 'when a user has write access' do context 'when a user has write access' do
before do before do
visit(project_tree_path(project)) visit(project_tree_path(project))
wait_for_requests
end end
include_examples 'it uploads and commit a new text file' include_examples 'it uploads and commit a new text file'
include_examples 'it uploads and commit a new image file' include_examples 'it uploads and commit a new image file'
it 'uploads a file to a sub-directory', :js do include_examples 'it uploads a file to a sub-directory'
click_link 'files'
page.within('.repo-breadcrumb') do
expect(page).to have_content('files')
end
find('.add-to-tree').click
click_link('Upload file')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
end
click_button('Upload file')
expect(page).to have_content('New commit message')
page.within('.repo-breadcrumb') do
expect(page).to have_content('files')
expect(page).to have_content('doc_sample.txt')
end
end
end end
context 'when a user does not have write access' do context 'when a user does not have write access' do
......
...@@ -17,11 +17,15 @@ RSpec.describe 'Projects > Show > User uploads files' do ...@@ -17,11 +17,15 @@ RSpec.describe 'Projects > Show > User uploads files' do
context 'when a user has write access' do context 'when a user has write access' do
before do before do
visit(project_path(project)) visit(project_path(project))
wait_for_requests
end end
include_examples 'it uploads and commit a new text file' include_examples 'it uploads and commit a new text file'
include_examples 'it uploads and commit a new image file' include_examples 'it uploads and commit a new image file'
include_examples 'it uploads a file to a sub-directory'
end end
context 'when a user does not have write access' do context 'when a user does not have write access' do
......
import { GlDropdown } from '@gitlab/ui'; import { GlDropdown } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils'; import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue'; import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
let vm;
function factory(currentPath, extraProps = {}) {
vm = shallowMount(Breadcrumbs, {
propsData: {
currentPath,
...extraProps,
},
stubs: {
RouterLink: RouterLinkStub,
},
});
}
describe('Repository breadcrumbs component', () => { describe('Repository breadcrumbs component', () => {
let wrapper;
const factory = (currentPath, extraProps = {}) => {
const $apollo = {
queries: {
userPermissions: {
loading: true,
},
},
};
wrapper = shallowMount(Breadcrumbs, {
propsData: {
currentPath,
...extraProps,
},
stubs: {
RouterLink: RouterLinkStub,
},
mocks: { $apollo },
});
};
const findUploadBlobModal = () => wrapper.find(UploadBlobModal);
afterEach(() => { afterEach(() => {
vm.destroy(); wrapper.destroy();
}); });
it.each` it.each`
...@@ -30,13 +42,13 @@ describe('Repository breadcrumbs component', () => { ...@@ -30,13 +42,13 @@ describe('Repository breadcrumbs component', () => {
`('renders $linkCount links for path $path', ({ path, linkCount }) => { `('renders $linkCount links for path $path', ({ path, linkCount }) => {
factory(path); factory(path);
expect(vm.findAll(RouterLinkStub).length).toEqual(linkCount); expect(wrapper.findAll(RouterLinkStub).length).toEqual(linkCount);
}); });
it('escapes hash in directory path', () => { it('escapes hash in directory path', () => {
factory('app/assets/javascripts#'); factory('app/assets/javascripts#');
expect(vm.findAll(RouterLinkStub).at(3).props('to')).toEqual( expect(wrapper.findAll(RouterLinkStub).at(3).props('to')).toEqual(
'/-/tree/app/assets/javascripts%23', '/-/tree/app/assets/javascripts%23',
); );
}); });
...@@ -44,26 +56,44 @@ describe('Repository breadcrumbs component', () => { ...@@ -44,26 +56,44 @@ describe('Repository breadcrumbs component', () => {
it('renders last link as active', () => { it('renders last link as active', () => {
factory('app/assets'); factory('app/assets');
expect(vm.findAll(RouterLinkStub).at(2).attributes('aria-current')).toEqual('page'); expect(wrapper.findAll(RouterLinkStub).at(2).attributes('aria-current')).toEqual('page');
}); });
it('does not render add to tree dropdown when permissions are false', () => { it('does not render add to tree dropdown when permissions are false', async () => {
factory('/', { canCollaborate: false }); factory('/', { canCollaborate: false });
vm.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } }); wrapper.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } });
return vm.vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(vm.find(GlDropdown).exists()).toBe(false);
}); expect(wrapper.find(GlDropdown).exists()).toBe(false);
}); });
it('renders add to tree dropdown when permissions are true', () => { it('renders add to tree dropdown when permissions are true', async () => {
factory('/', { canCollaborate: true }); factory('/', { canCollaborate: true });
vm.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } }); wrapper.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } });
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDropdown).exists()).toBe(true);
});
describe('renders the upload blob modal', () => {
beforeEach(() => {
factory('/', { canEditTree: true });
});
it('does not render the modal while loading', () => {
expect(findUploadBlobModal().exists()).toBe(false);
});
it('renders the modal once loaded', async () => {
wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
await wrapper.vm.$nextTick();
return vm.vm.$nextTick(() => { expect(findUploadBlobModal().exists()).toBe(true);
expect(vm.find(GlDropdown).exists()).toBe(true);
}); });
}); });
}); });
...@@ -10,7 +10,7 @@ RSpec.shared_examples 'it uploads and commit a new text file' do ...@@ -10,7 +10,7 @@ RSpec.shared_examples 'it uploads and commit a new text file' do
wait_for_requests wait_for_requests
end end
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true)
page.within('#modal-upload-blob') do page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message') fill_in(:commit_message, with: 'New commit message')
...@@ -42,7 +42,7 @@ RSpec.shared_examples 'it uploads and commit a new image file' do ...@@ -42,7 +42,7 @@ RSpec.shared_examples 'it uploads and commit a new image file' do
wait_for_requests wait_for_requests
end end
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg')) attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg'), make_visible: true)
page.within('#modal-upload-blob') do page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message') fill_in(:commit_message, with: 'New commit message')
...@@ -70,9 +70,11 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do ...@@ -70,9 +70,11 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do
expect(page).to have_content(fork_message) expect(page).to have_content(fork_message)
wait_for_all_requests
find('.add-to-tree').click find('.add-to-tree').click
click_link('Upload file') click_link('Upload file')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true)
page.within('#modal-upload-blob') do page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message') fill_in(:commit_message, with: 'New commit message')
...@@ -95,6 +97,33 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do ...@@ -95,6 +97,33 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do
end end
end end
RSpec.shared_examples 'it uploads a file to a sub-directory' do
it 'uploads a file to a sub-directory', :js do
click_link 'files'
page.within('.repo-breadcrumb') do
expect(page).to have_content('files')
end
find('.add-to-tree').click
click_link('Upload file')
attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true)
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
end
click_button('Upload file')
expect(page).to have_content('New commit message')
page.within('.repo-breadcrumb') do
expect(page).to have_content('files')
expect(page).to have_content('doc_sample.txt')
end
end
end
RSpec.shared_examples 'uploads and commits a new text file via "upload file" button' do RSpec.shared_examples 'uploads and commits a new text file via "upload file" button' do
it 'uploads and commits a new text file via "upload file" button', :js do it 'uploads and commits a new text file via "upload file" button', :js do
find('[data-testid="upload-file-button"]').click find('[data-testid="upload-file-button"]').click
......
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