From 17f29187ce6037235543961f3abf411f7372d2f1 Mon Sep 17 00:00:00 2001 From: Sascha Eggenberger <seggenberger@gitlab.com> Date: Tue, 23 Jan 2024 15:07:43 +0100 Subject: [PATCH] Code dropdown: Migrate to GlDisclosureDropdown Changelog: changed --- .../javascripts/pages/projects/show/index.js | 27 +++ app/assets/javascripts/repository/index.js | 58 +++-- .../clone_dropdown/clone_dropdown_item.vue | 31 ++- .../code_dropdown/code_dropdown.vue | 213 ++++++++++++++++++ app/views/projects/buttons/_code.html.haml | 63 +----- app/views/projects/empty.html.haml | 4 +- ee/app/views/projects/buttons/_code.html.haml | 13 ++ .../kerberos_clone_instructions_spec.rb | 4 +- locale/gitlab.pot | 6 +- qa/qa/page/component/clone_panel.rb | 42 ---- qa/qa/page/project/show.rb | 1 - ...admin_disables_git_access_protocol_spec.rb | 13 +- .../projects/show/clone_button_spec.rb | 14 +- .../show/user_sees_git_instructions_spec.rb | 12 +- .../code_dropdown/code_dropdown_spec.js | 143 ++++++++++++ 15 files changed, 497 insertions(+), 147 deletions(-) create mode 100644 app/assets/javascripts/vue_shared/components/code_dropdown/code_dropdown.vue create mode 100644 ee/app/views/projects/buttons/_code.html.haml delete mode 100644 qa/qa/page/component/clone_panel.rb create mode 100644 spec/frontend/vue_shared/components/code_dropdown/code_dropdown_spec.js diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 150c702f1fe9..adcbf1217e14 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -1,3 +1,4 @@ +import Vue from 'vue'; import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert'; @@ -10,6 +11,7 @@ import initReadMore from '~/read_more'; import initForksButton from '~/forks/init_forks_button'; import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal'; import InitMoreActionsDropdown from '~/groups_projects/init_more_actions_dropdown'; +import CodeDropdown from '~/vue_shared/components/code_dropdown/code_dropdown.vue'; // Project show page loads different overview content based on user preferences if (document.getElementById('js-tree-list')) { @@ -63,3 +65,28 @@ if (document.querySelector('.js-autodevops-banner')) { initForksButton(); InitMoreActionsDropdown(); leaveByUrl('project'); + +if (document.getElementById('empty-page')) { + const initCodeDropdown = () => { + const codeDropdownEl = document.getElementById('js-code-dropdown'); + + if (!codeDropdownEl) return false; + + const { sshUrl, httpUrl, kerberosUrl } = codeDropdownEl.dataset; + + return new Vue({ + el: codeDropdownEl, + render(createElement) { + return createElement(CodeDropdown, { + props: { + sshUrl, + httpUrl, + kerberosUrl, + }, + }); + }, + }); + }; + + initCodeDropdown(); +} diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index afe3f7b1983e..25141ec31a8f 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -10,9 +10,9 @@ import PerformancePlugin from '~/performance/vue_performance_plugin'; import createStore from '~/code_navigation/store'; import RefSelector from '~/ref/components/ref_selector.vue'; import HighlightWorker from '~/vue_shared/components/source_viewer/workers/highlight_worker?worker'; +import CodeDropdown from '~/vue_shared/components/code_dropdown/code_dropdown.vue'; import App from './components/app.vue'; import Breadcrumbs from './components/breadcrumbs.vue'; -import DirectoryDownloadLinks from './components/directory_download_links.vue'; import ForkInfo from './components/fork_info.vue'; import LastCommit from './components/last_commit.vue'; import BlobControls from './components/blob_controls.vue'; @@ -170,6 +170,38 @@ export default function setupVueRepositoryList() { }); }; + const initCodeDropdown = () => { + const codeDropdownEl = document.getElementById('js-code-dropdown'); + + if (!codeDropdownEl) return false; + + const { + sshUrl, + httpUrl, + kerberosUrl, + xcodeUrl, + directoryDownloadLinks, + } = codeDropdownEl.dataset; + + return new Vue({ + el: codeDropdownEl, + router, + render(createElement) { + return createElement(CodeDropdown, { + props: { + sshUrl, + httpUrl, + kerberosUrl, + xcodeUrl, + currentPath: this.$route.params.path, + directoryDownloadLinks: JSON.parse(directoryDownloadLinks), + }, + }); + }, + }); + }; + + initCodeDropdown(); initLastCommitApp(); initBlobControlsApp(); initRefSwitcher(); @@ -259,30 +291,6 @@ export default function setupVueRepositoryList() { initWebIdeLink({ el: document.getElementById('js-tree-web-ide-link'), router }); - const directoryDownloadLinks = document.querySelector('.js-directory-downloads'); - - if (directoryDownloadLinks) { - // eslint-disable-next-line no-new - new Vue({ - el: directoryDownloadLinks, - router, - render(h) { - const currentPath = this.$route.params.path || '/'; - - if (currentPath !== '/') { - return h(DirectoryDownloadLinks, { - props: { - currentPath: currentPath.replace(/^\//, ''), - links: JSON.parse(directoryDownloadLinks.dataset.links), - }, - }); - } - - return null; - }, - }); - } - // eslint-disable-next-line no-new new Vue({ el, diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue index 6980e19733aa..db5db7ebdfac 100644 --- a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue +++ b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue @@ -23,10 +23,25 @@ export default { type: String, required: true, }, + labelClass: { + type: String, + required: false, + default: '', + }, link: { type: String, required: true, }, + inputTestId: { + type: String, + required: false, + default: '', + }, + name: { + type: String, + required: false, + default: null, + }, testId: { type: String, required: true, @@ -37,8 +52,20 @@ export default { </script> <template> <gl-disclosure-dropdown-item> - <gl-form-group :label="label" class="gl-px-3 gl-mb-3"> - <gl-form-input-group :value="link" readonly select-on-click> + <gl-form-group + :label="label" + :label-class="labelClass" + :label-for="inputTestId" + class="gl-px-3 gl-mb-3 gl-text-left" + > + <gl-form-input-group + :id="inputTestId" + :value="link" + :name="name" + :data-testid="inputTestId" + readonly + select-on-click + > <template #append> <gl-button v-gl-tooltip.hover diff --git a/app/assets/javascripts/vue_shared/components/code_dropdown/code_dropdown.vue b/app/assets/javascripts/vue_shared/components/code_dropdown/code_dropdown.vue new file mode 100644 index 000000000000..e42c086b541d --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/code_dropdown/code_dropdown.vue @@ -0,0 +1,213 @@ +<script> +import { GlDisclosureDropdown, GlDisclosureDropdownGroup, GlTooltipDirective } from '@gitlab/ui'; +import { getHTTPProtocol } from '~/lib/utils/url_utility'; +import { __, sprintf } from '~/locale'; +import CloneDropdownItem from '~/vue_shared/components/clone_dropdown/clone_dropdown_item.vue'; + +export default { + components: { + GlDisclosureDropdown, + GlDisclosureDropdownGroup, + CloneDropdownItem, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + sshUrl: { + type: String, + required: false, + default: '', + }, + httpUrl: { + type: String, + required: false, + default: '', + }, + kerberosUrl: { + type: String, + required: false, + default: null, + }, + xcodeUrl: { + type: String, + required: false, + default: '', + }, + currentPath: { + type: String, + required: false, + default: null, + }, + directoryDownloadLinks: { + type: Array, + required: false, + default: null, + }, + }, + computed: { + httpLabel() { + const protocol = this.httpUrl ? getHTTPProtocol(this.httpUrl)?.toUpperCase() : ''; + return sprintf(__('Clone with %{protocol}'), { protocol }); + }, + sshUrlEncoded() { + return encodeURIComponent(this.sshUrl); + }, + httpUrlEncoded() { + return encodeURIComponent(this.httpUrl); + }, + unfilteredDropdownItems() { + return [ + { + item: { + text: __('Visual Studio Code (SSH)'), + href: `${this.vsCodeBaseUrl}${this.sshUrlEncoded}`, + }, + isIncluded: Boolean(this.sshUrl), + }, + { + item: { + text: __('Visual Studio Code (HTTPS)'), + href: `${this.vsCodeBaseUrl}${this.httpUrlEncoded}`, + }, + isIncluded: Boolean(this.httpUrl), + }, + { + item: { + text: __('IntelliJ IDEA (SSH)'), + href: `${this.jetBrainsBaseUrl}${this.sshUrlEncoded}`, + }, + isIncluded: Boolean(this.sshUrl), + }, + { + item: { + text: __('IntelliJ IDEA (HTTPS)'), + href: `${this.jetBrainsBaseUrl}${this.httpUrlEncoded}`, + }, + isIncluded: Boolean(this.httpUrl), + }, + { + item: { + text: __('Xcode'), + href: this.xcodeUrl, + }, + isIncluded: Boolean(this.xcodeUrl), + }, + ]; + }, + ideGroup() { + const items = []; + + this.unfilteredDropdownItems.forEach(({ item, isIncluded }) => { + if (isIncluded) { + items.push(item); + } + }); + + return { + name: this.$options.i18n.openInIDE, + items, + }; + }, + sourceCodeGroup() { + const items = this.directoryDownloadLinks.map((link) => ({ + text: link.text, + href: link.path, + extraAttrs: { + rel: 'nofollow', + download: '', + }, + })); + + return { + name: this.$options.i18n.downloadSourceCode, + items, + }; + }, + directoryDownloadLinksGroup() { + const items = this.directoryDownloadLinks.map((link) => ({ + text: link.text, + href: `${link.path}?path=${this.currentPath}`, + extraAttrs: { + rel: 'nofollow', + download: '', + }, + })); + + return { + name: this.$options.i18n.downloadDirectory, + items, + }; + }, + }, + vsCodeBaseUrl: 'vscode://vscode.git/clone?url=', + jetBrainsBaseUrl: + 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=', + i18n: { + defaultLabel: __('Code'), + cloneWithSsh: __('Clone with SSH'), + cloneWithKerberos: __('Clone with KRB5'), + openInIDE: __('Open in your IDE'), + downloadSourceCode: __('Download Source Code'), + downloadDirectory: __('Download this directory'), + }, +}; +</script> +<template> + <gl-disclosure-dropdown + :toggle-text="$options.i18n.defaultLabel" + category="primary" + variant="confirm" + placement="right" + class="code-dropdown gl-text-left" + fluid-width + data-testid="code-dropdown" + > + <gl-disclosure-dropdown-group v-if="sshUrl"> + <clone-dropdown-item + :label="$options.i18n.cloneWithSsh" + label-class="gl-font-sm! gl-pt-2!" + :link="sshUrl" + name="ssh_project_clone" + input-test-id="copy-ssh-url-input" + test-id="copy-ssh-url-button" + /> + </gl-disclosure-dropdown-group> + <gl-disclosure-dropdown-group v-if="httpUrl"> + <clone-dropdown-item + :label="httpLabel" + label-class="gl-font-sm! gl-pt-2!" + :link="httpUrl" + name="http_project_clone" + input-test-id="copy-http-url-input" + test-id="copy-http-url-button" + /> + </gl-disclosure-dropdown-group> + <gl-disclosure-dropdown-group v-if="kerberosUrl"> + <clone-dropdown-item + :label="$options.i18n.cloneWithKerberos" + label-class="gl-font-sm! gl-pt-2!" + :link="kerberosUrl" + name="kerberos_project_clone" + input-test-id="copy-http-url-input" + test-id="copy-http-url-button" + /> + </gl-disclosure-dropdown-group> + <gl-disclosure-dropdown-group :group="ideGroup" bordered /> + <gl-disclosure-dropdown-group v-if="directoryDownloadLinks" :group="sourceCodeGroup" bordered /> + <gl-disclosure-dropdown-group + v-if="currentPath && directoryDownloadLinks" + :group="directoryDownloadLinksGroup" + bordered + /> + </gl-disclosure-dropdown> +</template> +<style> +/* Temporary override until we have + * widths available in GlDisclosureDropdown + */ +.code-dropdown .gl-new-dropdown-panel { + width: 100%; + max-width: 348px; +} +</style> diff --git a/app/views/projects/buttons/_code.html.haml b/app/views/projects/buttons/_code.html.haml index 9cdbe1d5f6b5..6c894748bf04 100644 --- a/app/views/projects/buttons/_code.html.haml +++ b/app/views/projects/buttons/_code.html.haml @@ -1,59 +1,12 @@ - project = project || @project -- dropdown_class = local_assigns.fetch(:dropdown_class, '') -- ref = local_assigns.fetch(:ref) +- ref = local_assigns.fetch(:ref) || '' +- archive_prefix = ref ? "#{project.path}-#{ref.tr('/', '-')}" : '' - if can?(current_user, :download_code, @project) .git-clone-holder.js-git-clone-holder - = render Pajamas::ButtonComponent.new(variant: :confirm, button_options: { id: 'clone-dropdown', class: 'clone-dropdown-btn', data: { toggle: 'dropdown', testid: 'clone-dropdown' } }) do - %span.js-clone-dropdown-label - = _('Code') - = sprite_icon("chevron-down", css_class: "icon") - %ul.dropdown-menu.dropdown-menu-large.clone-options-dropdown{ role: 'menu', class: dropdown_class, data: { testid: 'clone-dropdown-content' } } - - if ssh_enabled? - %li.gl-dropdown-item.js-clone-links{ role: 'menuitem', class: 'gl-px-4!' } - %label.label-bold - = _('Clone with SSH') - .input-group.btn-group - = text_field_tag :ssh_project_clone, ssh_clone_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { testid: 'ssh-clone-url-content' } - .input-group-append - = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), category: :primary, size: :medium) - = render_if_exists 'projects/buttons/geo' - - if http_enabled? - %li.pt-2.gl-dropdown-item.js-clone-links{ role: 'menuitem', class: 'gl-px-4!' } - %label.label-bold - = _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase } - .input-group.btn-group - = text_field_tag :http_project_clone, http_clone_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { testid: 'http-clone-url-content' } - .input-group-append - = clipboard_button(target: '#http_project_clone', title: _("Copy URL"), category: :primary, size: :medium) - = render_if_exists 'projects/buttons/geo' - = render_if_exists 'projects/buttons/kerberos_clone_field' - %li.divider.mt-2{ role: 'presentation' } - %li.pt-2.gl-dropdown-item.js-clone-links{ role: 'menuitem' } - %label.label-bold{ class: 'gl-px-4!' } - = _('Open in your IDE') - - if ssh_enabled? - - escaped_ssh_url_to_repo = CGI.escape(ssh_clone_url_to_repo(project)) - %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_ssh_url_to_repo } - .gl-dropdown-item-text-wrapper - = _('Visual Studio Code (SSH)') - - if http_enabled? - - escaped_http_url_to_repo = CGI.escape(http_clone_url_to_repo(project)) - %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_http_url_to_repo } - .gl-dropdown-item-text-wrapper - = _('Visual Studio Code (HTTPS)') - - if ssh_enabled? - %a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_ssh_url_to_repo } - .gl-dropdown-item-text-wrapper - = _('IntelliJ IDEA (SSH)') - - if http_enabled? - %a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_http_url_to_repo } - .gl-dropdown-item-text-wrapper - = _('IntelliJ IDEA (HTTPS)') - - if show_xcode_link?(@project) - %a.dropdown-item.open-with-link{ href: xcode_uri_to_repo(@project) } - .gl-dropdown-item-text-wrapper - = _("Xcode") - - if !project.empty_repo? && can?(current_user, :download_code, project) - %li.divider.mt-2{ role: 'presentation' } - = render 'projects/buttons/download_menu_items', project: project, ref: ref + #js-code-dropdown{ data: { + ssh_url: ssh_enabled? ? ssh_clone_url_to_repo(@project) : '', + http_url: http_enabled? ? http_clone_url_to_repo(@project) : '', + xcode_url: show_xcode_link?(@project) ? xcode_uri_to_repo(@project) : '', + directory_download_links: directory_download_links(project, ref, archive_prefix).to_json, + } } diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index ac3b67d6157f..e2ad6dd83f8d 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -14,7 +14,7 @@ .project-page-indicator.js-show-on-project-root - .project-page-layout + #empty-page.project-page-layout .project-page-layout-content.gl-mt-5 .project-buttons.gl-mb-5{ data: { testid: 'quick-actions-container' } } .project-clone-holder.d-block.d-sm-none @@ -96,7 +96,7 @@ .project-clone-holder.d-block.d-sm-none.gl-mt-3.gl-mr-3 = render "shared/mobile_clone_panel" - .project-clone-holder.d-none.d-sm-inline-block.gl-mb-3.gl-mr-3.float-left + #empty-page.project-clone-holder.d-none.d-sm-inline-block.gl-mb-3.gl-mr-3.float-left = render "projects/buttons/code", ref: @ref = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons, project_buttons: true diff --git a/ee/app/views/projects/buttons/_code.html.haml b/ee/app/views/projects/buttons/_code.html.haml new file mode 100644 index 000000000000..8e1352a33a12 --- /dev/null +++ b/ee/app/views/projects/buttons/_code.html.haml @@ -0,0 +1,13 @@ +- project = project || @project +- ref = local_assigns.fetch(:ref) || '' +- archive_prefix = ref ? "#{project.path}-#{ref.tr('/', '-')}" : '' + +- if can?(current_user, :download_code, @project) + .git-clone-holder.js-git-clone-holder + #js-code-dropdown{ data: { + ssh_url: ssh_enabled? ? ssh_clone_url_to_repo(@project) : '', + http_url: http_enabled? ? http_clone_url_to_repo(@project) : '', + kerberos_url: alternative_kerberos_url? ? project.kerberos_url_to_repo : '', + xcode_url: show_xcode_link?(@project) ? xcode_uri_to_repo(@project) : '', + directory_download_links: directory_download_links(project, ref, archive_prefix).to_json, + } } diff --git a/ee/spec/features/projects/kerberos_clone_instructions_spec.rb b/ee/spec/features/projects/kerberos_clone_instructions_spec.rb index 618e1a85262c..b7c4b7acadb8 100644 --- a/ee/spec/features/projects/kerberos_clone_instructions_spec.rb +++ b/ee/spec/features/projects/kerberos_clone_instructions_spec.rb @@ -16,11 +16,11 @@ it 'shows Kerberos clone url' do visit_project - find('.clone-dropdown-btn').click + find_by_testid('code-dropdown').click expect(page).to have_content(project.kerberos_url_to_repo) - within('.git-clone-holder') do + within_testid('code-dropdown') do expect(page).to have_content('Clone with KRB5') end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b025513cce95..d732a25fa96a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10998,9 +10998,6 @@ msgstr "" msgid "Clone this issue" msgstr "" -msgid "Clone with %{http_label}" -msgstr "" - msgid "Clone with %{protocol}" msgstr "" @@ -18208,6 +18205,9 @@ msgstr "" msgid "Download PDF" msgstr "" +msgid "Download Source Code" +msgstr "" + msgid "Download artifacts" msgstr "" diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb deleted file mode 100644 index 4d239040faa3..000000000000 --- a/qa/qa/page/component/clone_panel.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module QA - module Page - module Component - module ClonePanel - extend QA::Page::PageConcern - - def self.included(base) - super - - base.view 'app/views/projects/buttons/_code.html.haml' do - element 'clone-dropdown' - element 'clone-dropdown-content' - element 'ssh-clone-url-content' - element 'http-clone-url-content' - end - end - - def repository_clone_http_location - repository_clone_location('http-clone-url-content') - end - - def repository_clone_ssh_location - repository_clone_location('ssh-clone-url-content') - end - - private - - def repository_clone_location(kind) - wait_until(reload: false) do - click_element 'clone-dropdown' - - within_element 'clone-dropdown-content' do - Git::Location.new(find_element(kind).value) - end - end - end - end - end - end -end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 36c14054188d..fac8762e5eb3 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -5,7 +5,6 @@ module Page module Project class Show < Page::Base include Layout::Flash - include Page::Component::ClonePanel include Page::Component::Breadcrumbs include Page::File::Shared::CommitMessage include Page::Component::Dropdown diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb index 039968025a92..6a694376d84c 100644 --- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb +++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb @@ -25,9 +25,9 @@ expect(page).to have_content("git clone #{project.ssh_url_to_repo}") - find('.clone-dropdown-btn').click + find('[data-testid="code-dropdown"] button').click - within('.git-clone-holder') do + within_testid('code-dropdown') do expect(page).to have_content('Clone with SSH') expect(page).not_to have_content('Clone with HTTP') end @@ -55,11 +55,12 @@ it 'shows only HTTP url' do visit_project - find('.clone-dropdown-btn').click + + find('[data-testid="code-dropdown"] button').click expect(page).to have_content("git clone #{project.http_url_to_repo}") - within('.git-clone-holder') do + within_testid('code-dropdown') do expect(page).to have_content('Clone with HTTP') expect(page).not_to have_content('Clone with SSH') end @@ -91,9 +92,9 @@ expect(page).to have_content("git clone #{project.ssh_url_to_repo}") - find('.clone-dropdown-btn').click + find('[data-testid="code-dropdown"] button').click - within('.git-clone-holder') do + within_testid('code-dropdown') do expect(page).to have_content('Clone with SSH') expect(page).to have_content('Clone with HTTP') end diff --git a/spec/features/projects/show/clone_button_spec.rb b/spec/features/projects/show/clone_button_spec.rb index 83e75101427e..0dea5887c013 100644 --- a/spec/features/projects/show/clone_button_spec.rb +++ b/spec/features/projects/show/clone_button_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Projects > Show > Clone button', feature_category: :groups_and_projects do +RSpec.describe 'Projects > Show > Code button', feature_category: :groups_and_projects do let_it_be(:admin) { create(:admin) } let_it_be(:guest) { create(:user) } let_it_be(:project) { create(:project, :private, :in_group, :repository) } @@ -19,10 +19,10 @@ expect(page).to have_content project.name end - it 'sees clone button', :js do - find_by_testid('clone-dropdown').click - expect(page).to have_content _('Clone') - expect(page).to be_axe_clean.within('.clone-options-dropdown') + it 'sees code button', :js do + find_by_testid('code-dropdown').click + expect(page).to have_content _('Code') + expect(page).to be_axe_clean.within('[data-testid="code-dropdown"]') # rubocop: disable Capybara/TestidFinders -- within_testid does not work here end end @@ -37,8 +37,8 @@ expect(page).to have_content project.name end - it 'does not see clone button' do - expect(page).not_to have_content _('Clone') + it 'does not see code button' do + expect(page).not_to have_content _('Code') end end end diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb index 40549beae9f8..8d2f93ba3b71 100644 --- a/spec/features/projects/show/user_sees_git_instructions_spec.rb +++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb @@ -34,13 +34,17 @@ shared_examples_for 'shows details of empty project' do let(:user_has_ssh_key) { false } - it 'shows details' do + it 'shows details', :js do expect(page).not_to have_content('Git global setup') page.all(:css, '.git-empty .clone').each do |element| expect(element.text).to include(project.http_url_to_repo) end + find_by_testid('code-dropdown').click + + wait_for_requests + expect(page).to have_field('http_project_clone', with: project.http_url_to_repo) unless user_has_ssh_key end end @@ -48,11 +52,15 @@ shared_examples_for 'shows details of non empty project' do let(:user_has_ssh_key) { false } - it 'shows details' do + it 'shows details', :js do page.within('.breadcrumbs .js-breadcrumb-item-text') do expect(page).to have_content(project.title) end + find_by_testid('code-dropdown').click + + wait_for_requests + expect(page).to have_field('http_project_clone', with: project.http_url_to_repo) unless user_has_ssh_key end end diff --git a/spec/frontend/vue_shared/components/code_dropdown/code_dropdown_spec.js b/spec/frontend/vue_shared/components/code_dropdown/code_dropdown_spec.js new file mode 100644 index 000000000000..294d0f091420 --- /dev/null +++ b/spec/frontend/vue_shared/components/code_dropdown/code_dropdown_spec.js @@ -0,0 +1,143 @@ +import { GlFormInputGroup, GlDisclosureDropdownGroup, GlDisclosureDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import CodeDropdown from '~/vue_shared/components/code_dropdown/code_dropdown.vue'; +import CloneDropdownItem from '~/vue_shared/components/clone_dropdown/clone_dropdown_item.vue'; + +describe('Clone Dropdown Button', () => { + let wrapper; + const sshUrl = 'ssh://foo.bar'; + const httpUrl = 'http://foo.bar'; + const httpsUrl = 'https://foo.bar'; + const xcodeUrl = 'xcode://foo.bar'; + const currentPath = null; + const directoryDownloadLinks = [ + { text: 'zip', path: httpUrl }, + { text: 'tar.gz', path: httpUrl }, + { text: 'tar.bz2', path: httpUrl }, + { text: 'tar', path: httpUrl }, + ]; + const defaultPropsData = { + sshUrl, + httpUrl, + xcodeUrl, + currentPath, + directoryDownloadLinks, + }; + const encodedSshUrl = encodeURIComponent(sshUrl); + const encodedHttpUrl = encodeURIComponent(httpUrl); + + const findCloneDropdownItems = () => wrapper.findAllComponents(CloneDropdownItem); + const findCloneDropdownItemAtIndex = (index) => findCloneDropdownItems().at(index); + const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem); + const findDropdownItemAtIndex = (index) => findDropdownItems().at(index); + + const createComponent = (propsData = defaultPropsData) => { + wrapper = shallowMount(CodeDropdown, { + propsData, + stubs: { + GlFormInputGroup, + GlDisclosureDropdownGroup, + }, + }); + }; + + describe('copyGroup', () => { + describe('rendering', () => { + it.each` + name | index | link + ${'SSH'} | ${0} | ${sshUrl} + ${'HTTP'} | ${1} | ${httpUrl} + `('renders correct link and a copy-button for $name', ({ index, link }) => { + createComponent(); + + const item = findCloneDropdownItemAtIndex(index); + expect(item.props('link')).toBe(link); + }); + + it.each` + name | value + ${'sshUrl'} | ${sshUrl} + ${'httpUrl'} | ${httpUrl} + `('does not fail if only $name is set', ({ name, value }) => { + createComponent({ [name]: value }); + + expect(findCloneDropdownItemAtIndex(0).props('link')).toBe(value); + }); + }); + + describe('functionality', () => { + it.each` + name | value + ${'sshUrl'} | ${null} + ${'httpUrl'} | ${null} + `('allows null values for the props', ({ name, value }) => { + createComponent({ ...defaultPropsData, [name]: value }); + + expect(findCloneDropdownItems().length).toBe(1); + }); + + it('correctly calculates httpLabel for HTTPS protocol', () => { + createComponent({ httpUrl: httpsUrl }); + + expect(findCloneDropdownItemAtIndex(0).attributes('label')).toContain('HTTPS'); + }); + }); + }); + + describe('ideGroup', () => { + it.each` + name | index | href + ${'Visual Studio Code (SSH)'} | ${0} | ${encodedSshUrl} + ${'Visual Studio Code (HTTPS)'} | ${1} | ${encodedHttpUrl} + ${'IntelliJ IDEA (SSH)'} | ${2} | ${encodedSshUrl} + ${'IntelliJ IDEA (HTTPS)'} | ${3} | ${encodedHttpUrl} + ${'Xcode'} | ${4} | ${xcodeUrl} + `('renders correct values for $name', ({ name, index, href }) => { + createComponent(); + + const item = findDropdownItemAtIndex(index); + expect(item.props('item').text).toBe(name); + expect(item.props('item').href).toContain(href); + }); + }); + + describe('sourceCodeGroup', () => { + it.each` + name | index | href + ${'zip'} | ${5} | ${httpUrl} + ${'tar.gz'} | ${6} | ${httpUrl} + ${'tar.bz2'} | ${7} | ${httpUrl} + ${'tar'} | ${8} | ${httpUrl} + `('renders correct values for $name', ({ name, index, href }) => { + createComponent(); + + const item = findDropdownItemAtIndex(index); + expect(item.props('item').text).toBe(name); + expect(item.props('item').href).toBe(href); + }); + }); + + describe('directoryDownloadLinksGroup', () => { + it('renders directory download links if currentPath is set', () => { + createComponent({ ...defaultPropsData, currentPath: '/subdir' }); + + expect(findDropdownItems().length).toEqual(13); + }); + + it.each` + name | index | href + ${'zip'} | ${9} | ${httpUrl} + ${'tar.gz'} | ${10} | ${httpUrl} + ${'tar.bz2'} | ${11} | ${httpUrl} + ${'tar'} | ${12} | ${httpUrl} + `('renders correct values for $name directory link', ({ name, index, href }) => { + const subPath = '/subdir'; + + createComponent({ ...defaultPropsData, currentPath: subPath }); + + const item = findDropdownItemAtIndex(index); + expect(item.props('item').text).toBe(name); + expect(item.props('item').href).toBe(`${href}?path=${subPath}`); + }); + }); +}); -- GitLab