diff --git a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js index 43ff617dabe2f57d712b284dd22748061a912ec6..ce36ff6a2305dc764b959f42e97f2a8dd6b59400 100644 --- a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js +++ b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js @@ -1,7 +1,15 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { joinPaths, webIDEUrl } from '~/lib/utils/url_utility'; import WebIdeButton from '~/vue_shared/components/web_ide_link.vue'; +import createDefaultClient from '~/lib/graphql'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); export default ({ el, router }) => { if (!el) return; @@ -15,6 +23,10 @@ export default ({ el, router }) => { new Vue({ el, router, + apolloProvider, + provide: { + projectPath, + }, render(h) { return h(WebIdeButton, { props: { diff --git a/app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue deleted file mode 100644 index 64e3b5d0baea7b6c14dffa2f72f11dd72a3e5eb2..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue +++ /dev/null @@ -1,68 +0,0 @@ - - diff --git a/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue b/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue new file mode 100644 index 0000000000000000000000000000000000000000..b4afb27c497a816872576edc5b8a95e9ae74a9fa --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue @@ -0,0 +1,114 @@ + + diff --git a/app/assets/javascripts/vue_shared/components/web_ide/get_writable_forks.query.graphql b/app/assets/javascripts/vue_shared/components/web_ide/get_writable_forks.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..044b79e64f3657720bca212865584c5a778fead2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/web_ide/get_writable_forks.query.graphql @@ -0,0 +1,12 @@ +query getWritableForks($projectPath: ID!) { + project(fullPath: $projectPath) { + id + visibleForks(minimumAccessLevel: DEVELOPER) { + nodes { + id + fullPath + webUrl + } + } + } +} diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue index 96944877f618248522e0e490c6b5c0f7c3ee3ae6..82f4edcbd5f80cbbdc876db5aec62f4c4bca5dae 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -3,7 +3,7 @@ import { GlModal, GlSprintf, GlLink } from '@gitlab/ui'; import { s__, __ } from '~/locale'; import { visitUrl } from '~/lib/utils/url_utility'; import ActionsButton from '~/vue_shared/components/actions_button.vue'; -import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue'; +import ConfirmForkModal from '~/vue_shared/components/web_ide/confirm_fork_modal.vue'; import { KEY_EDIT, KEY_WEB_IDE, KEY_GITPOD, KEY_PIPELINE_EDITOR } from './constants'; export const i18n = { @@ -152,9 +152,7 @@ export default { return this.actions.length > 0; }, editAction() { - if (!this.showEditButton) { - return null; - } + if (!this.showEditButton) return null; const handleOptions = this.needsToFork ? { @@ -194,9 +192,7 @@ export default { return __('Web IDE'); }, webIdeAction() { - if (!this.showWebIdeButton) { - return null; - } + if (!this.showWebIdeButton) return null; const handleOptions = this.needsToFork ? { @@ -298,6 +294,12 @@ export default { }, }; }, + mountForkModal() { + const { disableForkModal, showWebIdeButton, showEditButton } = this; + if (disableForkModal) return false; + + return showWebIdeButton || showEditButton; + }, }, methods: { showModal(dataKey) { @@ -330,7 +332,7 @@ export default { { + Vue.use(VueApollo); + let wrapper = null; const forkPath = '/fake/fork/path'; @@ -13,13 +22,18 @@ describe('vue_shared/components/confirm_fork_modal', () => { const findModalProp = (prop) => findModal().props(prop); const findModalActionProps = () => findModalProp('actionPrimary'); - const createComponent = (props = {}) => - shallowMountExtended(ConfirmForkModal, { + const createComponent = (props = {}, getWritableForksResponse = getNoWritableForksResponse) => { + const fakeApollo = createMockApollo([ + [getWritableForksQuery, jest.fn().mockResolvedValue(getWritableForksResponse)], + ]); + return shallowMountExtended(ConfirmForkModal, { propsData: { ...defaultProps, ...props, }, + apolloProvider: fakeApollo, }); + }; describe('visible = false', () => { beforeEach(() => { @@ -73,4 +87,45 @@ describe('vue_shared/components/confirm_fork_modal', () => { expect(wrapper.emitted('change')).toEqual([[false]]); }); }); + + describe('writable forks', () => { + describe('when loading', () => { + it('shows loading spinner', () => { + wrapper = createComponent(); + + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + }); + }); + + describe('with no writable forks', () => { + it('contains `newForkMessage`', async () => { + wrapper = createComponent(); + + await waitForPromises(); + + expect(wrapper.text()).toContain(i18n.newForkMessage); + }); + }); + + describe('with writable forks', () => { + it('contains `existingForksMessage`', async () => { + wrapper = createComponent(null, getSomeWritableForksResponse); + + await waitForPromises(); + + expect(wrapper.text()).toContain(i18n.existingForksMessage); + }); + + it('renders links to the forks', async () => { + wrapper = createComponent(null, getSomeWritableForksResponse); + + await waitForPromises(); + + const forks = getSomeWritableForksResponse.data.project.visibleForks.nodes; + + expect(wrapper.findByText(forks[0].fullPath).attributes('href')).toBe(forks[0].webUrl); + expect(wrapper.findByText(forks[1].fullPath).attributes('href')).toBe(forks[1].webUrl); + }); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index 26557c63a77ecd2b28cd62ac3b408772ef93d478..e54de25dc0d91c7816f275d1a709fdcc788d1419 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -1,14 +1,18 @@ import { GlModal } from '@gitlab/ui'; -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import getWritableForksResponse from 'test_fixtures/graphql/vue_shared/components/web_ide/get_writable_forks.query.graphql_none.json'; import ActionsButton from '~/vue_shared/components/actions_button.vue'; import WebIdeLink, { i18n } from '~/vue_shared/components/web_ide_link.vue'; -import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue'; +import ConfirmForkModal from '~/vue_shared/components/web_ide/confirm_fork_modal.vue'; import { stubComponent } from 'helpers/stub_component'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { visitUrl } from '~/lib/utils/url_utility'; +import getWritableForksQuery from '~/vue_shared/components/web_ide/get_writable_forks.query.graphql'; jest.mock('~/lib/utils/url_utility'); @@ -77,9 +81,14 @@ const ACTION_PIPELINE_EDITOR = { }; describe('vue_shared/components/web_ide_link', () => { + Vue.use(VueApollo); + let wrapper; function createComponent(props, { mountFn = shallowMountExtended, glFeatures = {} } = {}) { + const fakeApollo = createMockApollo([ + [getWritableForksQuery, jest.fn().mockResolvedValue(getWritableForksResponse)], + ]); wrapper = mountFn(WebIdeLink, { propsData: { editUrl: TEST_EDIT_URL, @@ -102,6 +111,7 @@ describe('vue_shared/components/web_ide_link', () => { `, }), }, + apolloProvider: fakeApollo, }); } diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index 01dacf5fcadcb8f3c76e1dae699d7b11c1747ccd..e13b83feefd1f16886b0ead8ec4e466b12e993f9 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -271,4 +271,42 @@ end end end + + describe '.fork_modal_options' do + let_it_be(:blob) { project.repository.blob_at('refs/heads/master', @path) } + + before do + allow(helper).to receive(:current_user).and_return(user) + end + + subject { helper.fork_modal_options(project, blob) } + + it 'returns correct fork path' do + expect(subject).to match a_hash_including(fork_path: '/namespace1/project-1/-/forks/new', fork_modal_id: nil) + end + + context 'when show_edit_button true' do + before do + allow(helper).to receive(:show_edit_button?).and_return(true) + end + + it 'returns correct fork path and modal id' do + expect(subject).to match a_hash_including( + fork_path: '/namespace1/project-1/-/forks/new', + fork_modal_id: 'modal-confirm-fork-edit') + end + end + + context 'when show_web_ide_button true' do + before do + allow(helper).to receive(:show_web_ide_button?).and_return(true) + end + + it 'returns correct fork path and modal id' do + expect(subject).to match a_hash_including( + fork_path: '/namespace1/project-1/-/forks/new', + fork_modal_id: 'modal-confirm-fork-webide') + end + end + end end