diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue index 2ff79e8f03ecd70273df9fb7a6771b928277a5d4..0c8a971e1b6b51c35453e22c3f4033cde86dbc5f 100644 --- a/app/assets/javascripts/snippets/components/show.vue +++ b/app/assets/javascripts/snippets/components/show.vue @@ -7,9 +7,6 @@ import { SNIPPET_MEASURE_BLOBS_CONTENT, } from '~/performance/constants'; import { performanceMarkAndMeasure } from '~/performance/utils'; -import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants'; -import SnippetCodeDropdown from '~/vue_shared/components/code_dropdown/snippet_code_dropdown.vue'; - import { getSnippetMixin } from '../mixins/snippets'; import { markBlobPerformance } from '../utils/blob'; import SnippetBlob from './snippet_blob_view.vue'; @@ -25,23 +22,9 @@ export default { GlAlert, GlLoadingIcon, SnippetBlob, - SnippetCodeDropdown, }, mixins: [getSnippetMixin], computed: { - embeddable() { - return ( - this.snippet.visibilityLevel === VISIBILITY_LEVEL_PUBLIC_STRING && !this.isInPrivateProject - ); - }, - isInPrivateProject() { - const projectVisibility = this.snippet?.project?.visibility; - const isLimitedVisibilityProject = projectVisibility !== VISIBILITY_LEVEL_PUBLIC_STRING; - return projectVisibility ? isLimitedVisibilityProject : false; - }, - canBeCloned() { - return Boolean(this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo); - }, hasUnretrievableBlobs() { return this.snippet.hasUnretrievableBlobs; }, @@ -62,17 +45,6 @@ export default { <template v-else> <snippet-header :snippet="snippet" /> <snippet-description :snippet="snippet" /> - <div class="gl-display-flex gl-justify-content-end gl-mb-5"> - <snippet-code-dropdown - v-if="canBeCloned" - class="gl-ml-3" - :ssh-link="snippet.sshUrlToRepo" - :http-link="snippet.httpUrlToRepo" - :url="snippet.webUrl" - :embeddable="embeddable" - data-testid="code-button" - /> - </div> <gl-alert v-if="hasUnretrievableBlobs" variant="danger" class="gl-mb-3" :dismissible="false"> {{ __( @@ -85,7 +57,7 @@ export default { :key="blob.path" :snippet="snippet" :blob="blob" - class="project-highlight-puc" + class="project-highlight-puc gl-mt-5" /> </template> </div> diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue index 05c3163cf2517703b57e1660743faf50684e8e4d..f639b64aeba845a962da7b55b9befafc577bb42d 100644 --- a/app/assets/javascripts/snippets/components/snippet_header.vue +++ b/app/assets/javascripts/snippets/components/snippet_header.vue @@ -17,9 +17,10 @@ import { fetchPolicies } from '~/lib/graphql'; import axios from '~/lib/utils/axios_utils'; import { joinPaths } from '~/lib/utils/url_utility'; import { __, s__, sprintf } from '~/locale'; +import SnippetCodeDropdown from '~/vue_shared/components/code_dropdown/snippet_code_dropdown.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/alert'; - +import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants'; import DeleteSnippetMutation from '../mutations/delete_snippet.mutation.graphql'; export const i18n = { @@ -35,6 +36,7 @@ export const i18n = { export default { components: { + SnippetCodeDropdown, GlIcon, GlSprintf, GlModal, @@ -136,7 +138,9 @@ export default { this.snippet.userPermissions.updateSnippet || this.canCreateSnippet || this.snippet.userPermissions.adminSnippet || - this.canReportSpaCheck + this.canReportSpaCheck || + this.embedDropdown || + this.canBeCloned ); }, editLink() { @@ -170,6 +174,20 @@ export default { showDropdownTooltip() { return !this.isDropdownShown ? this.$options.i18n.snippetAction : ''; }, + isInPrivateProject() { + const projectVisibility = this.snippet?.project?.visibility; + const isLimitedVisibilityProject = projectVisibility !== VISIBILITY_LEVEL_PUBLIC_STRING; + return projectVisibility ? isLimitedVisibilityProject : false; + }, + embeddable() { + return this.visibility === VISIBILITY_LEVEL_PUBLIC_STRING && !this.isInPrivateProject; + }, + canBeCloned() { + return Boolean(this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo); + }, + canBeClonedOrEmbedded() { + return this.canBeCloned || this.embeddable; + }, }, methods: { redirectToSnippets() { @@ -256,7 +274,7 @@ export default { <div v-if="hasPersonalSnippetActions" - class="detail-page-header-actions gl-display-flex gl-align-self-center gl-gap-3 gl-relative gl-w-full gl-sm-w-auto" + class="gl-display-flex gl-align-self-center gl-gap-3 gl-w-full gl-sm-w-auto gl-flex-direction-column gl-sm-flex-direction-row" > <gl-button v-if="snippet.userPermissions.updateSnippet" @@ -270,15 +288,26 @@ export default { {{ editItem.text }} </gl-button> + <snippet-code-dropdown + v-if="canBeClonedOrEmbedded" + :ssh-link="snippet.sshUrlToRepo" + :http-link="snippet.httpUrlToRepo" + :url="snippet.webUrl" + :embeddable="embeddable" + data-testid="code-button" + /> + <gl-disclosure-dropdown data-testid="snippets-more-actions-dropdown" + placement="right" + block @shown="onShowDropdown" @hidden="onHideDropdown" > <template #toggle> <div class="gl-w-full gl-min-h-7"> <gl-button - class="gl-sm-display-none! gl-new-dropdown-toggle gl-absolute gl-top-0 gl-left-0 gl-w-full" + class="gl-sm-display-none! gl-new-dropdown-toggle gl-w-full" button-text-classes="gl-display-flex gl-justify-content-space-between gl-w-full" category="secondary" tabindex="0" @@ -314,11 +343,11 @@ export default { </div> <div - class="detail-page-header gl-flex-direction-column gl-md-flex-direction-row gl-p-0 gl-mt-2 gl-mb-6" + class="detail-page-header gl-flex-direction-column gl-md-flex-direction-row gl-p-0 gl-mb-5" > - <div class="detail-page-header-body gl-align-items-baseline"> + <div class="gl-display-flex gl-align-items-baseline"> <div - class="snippet-box has-tooltip gl-display-flex gl-align-self-baseline gl-mt-3 gl-mr-2" + class="has-tooltip gl-display-flex gl-align-self-baseline gl-mt-3 gl-mr-2" data-testid="snippet-container" :title="snippetVisibilityLevelDescription" data-container="body" diff --git a/app/assets/javascripts/vue_shared/components/code_dropdown/snippet_code_dropdown.vue b/app/assets/javascripts/vue_shared/components/code_dropdown/snippet_code_dropdown.vue index 25d5881bc36191e3d42ebb284a473cc285b93a90..c846704dec72b73c28cd7a83bfd9c8c96c38de7e 100644 --- a/app/assets/javascripts/vue_shared/components/code_dropdown/snippet_code_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/code_dropdown/snippet_code_dropdown.vue @@ -65,10 +65,11 @@ export default { </script> <template> <gl-disclosure-dropdown - :toggle-text="$options.labels.defaultLabel" category="primary" variant="confirm" placement="right" + block + :toggle-text="$options.labels.defaultLabel" > <code-dropdown-item v-for="{ label, link, testId } in sections" diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index cc23bb7221fc48ce6a494bbba7058ea4d6a4a885..7f821c6c1ac2fb15151e3ccaa043de6546bd09da 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -9,9 +9,3 @@ margin-bottom: 0; } } - -.snippet-title { - color: $gl-text-color; - font-size: 2em; - font-weight: $gl-font-weight-bold; -} diff --git a/qa/qa/page/component/snippet.rb b/qa/qa/page/component/snippet.rb index 2920a5e1e5dfd743e0ac7ede54e4e5522eb43551..0ce89c18a60f8e026b076ec2e8d93bc5fe96b44d 100644 --- a/qa/qa/page/component/snippet.rb +++ b/qa/qa/page/component/snippet.rb @@ -22,6 +22,7 @@ def self.included(base) element 'snippet-container' element 'snippet-action-button' element 'delete-snippet-button' + element 'code-button' end base.view 'app/assets/javascripts/blob/components/blob_header_filepath.vue' do @@ -32,10 +33,6 @@ def self.included(base) element 'blob-viewer-file-content' end - base.view 'app/assets/javascripts/snippets/components/show.vue' do - element 'code-button' - end - base.view 'app/assets/javascripts/vue_shared/components/code_dropdown/snippet_code_dropdown.vue' do element 'copy-http-url' element 'copy-ssh-url' diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js index 1ec3d52134d70d852e149d0ba2be2f25ad3fd447..2c2e8e74d7f65428b79518dd57f1dff2769222ee 100644 --- a/spec/frontend/snippets/components/show_spec.js +++ b/spec/frontend/snippets/components/show_spec.js @@ -5,12 +5,6 @@ import SnippetApp from '~/snippets/components/show.vue'; import SnippetBlob from '~/snippets/components/snippet_blob_view.vue'; import SnippetHeader from '~/snippets/components/snippet_header.vue'; import SnippetDescription from '~/snippets/components/snippet_description.vue'; -import { - VISIBILITY_LEVEL_INTERNAL_STRING, - VISIBILITY_LEVEL_PRIVATE_STRING, - VISIBILITY_LEVEL_PUBLIC_STRING, -} from '~/visibility_level/constants'; -import SnippetCodeDropdown from '~/vue_shared/components/code_dropdown/snippet_code_dropdown.vue'; import { stubPerformanceWebAPI } from 'helpers/performance'; describe('Snippet view app', () => { @@ -18,9 +12,6 @@ describe('Snippet view app', () => { const defaultProps = { snippetGid: 'gid://gitlab/PersonalSnippet/42', }; - const webUrl = 'http://foo.bar'; - const dummyHTTPUrl = webUrl; - const dummySSHUrl = 'ssh://foo.bar'; function createComponent({ props = defaultProps, data = {}, loading = false } = {}) { const $apollo = { @@ -63,7 +54,6 @@ describe('Snippet view app', () => { createComponent({ data: { snippet: { - webUrl, blobs: [Blob, BinaryBlob], }, }, @@ -83,7 +73,6 @@ describe('Snippet view app', () => { createComponent({ data: { snippet: { - webUrl, hasUnretrievableBlobs, }, }, @@ -91,58 +80,4 @@ describe('Snippet view app', () => { expect(wrapper.findComponent(GlAlert).exists()).toBe(isRendered); }); }); - - describe('Code button rendering', () => { - it.each` - httpUrlToRepo | sshUrlToRepo | shouldRender | isRendered - ${null} | ${null} | ${'Should not'} | ${false} - ${null} | ${dummySSHUrl} | ${'Should'} | ${true} - ${dummyHTTPUrl} | ${null} | ${'Should'} | ${true} - ${dummyHTTPUrl} | ${dummySSHUrl} | ${'Should'} | ${true} - `( - '$shouldRender render "Clone" button when `httpUrlToRepo` is $httpUrlToRepo and `sshUrlToRepo` is $sshUrlToRepo', - ({ httpUrlToRepo, sshUrlToRepo, isRendered }) => { - createComponent({ - data: { - snippet: { - sshUrlToRepo, - httpUrlToRepo, - webUrl, - }, - }, - }); - expect(wrapper.findComponent(SnippetCodeDropdown).exists()).toBe(isRendered); - }, - ); - - it.each` - snippetVisibility | projectVisibility | condition | embeddable - ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${'not'} | ${false} - ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${'not'} | ${false} - ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${undefined} | ${''} | ${true} - ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${''} | ${true} - ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${'not'} | ${false} - ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${undefined} | ${'not'} | ${false} - ${'foo'} | ${undefined} | ${'not'} | ${false} - ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${'not'} | ${false} - `( - 'is $condition embeddable if snippetVisibility is $snippetVisibility and projectVisibility is $projectVisibility', - ({ snippetVisibility, projectVisibility, embeddable }) => { - createComponent({ - data: { - snippet: { - sshUrlToRepo: dummySSHUrl, - httpUrlToRepo: dummyHTTPUrl, - visibilityLevel: snippetVisibility, - webUrl, - project: { - visibility: projectVisibility, - }, - }, - }, - }); - expect(wrapper.findComponent(SnippetCodeDropdown).props('embeddable')).toBe(embeddable); - }, - ); - }); }); diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js index 6e35517442cf450d63f38be0f4ac9cc4b26e75ba..7f121a61f699531a84cd382dea44010dce03af7a 100644 --- a/spec/frontend/snippets/components/snippet_header_spec.js +++ b/spec/frontend/snippets/components/snippet_header_spec.js @@ -14,9 +14,15 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component'; +import { + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, +} from '~/visibility_level/constants'; import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; import { differenceInMilliseconds } from '~/lib/utils/datetime_utility'; import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue'; +import SnippetCodeDropdown from '~/vue_shared/components/code_dropdown/snippet_code_dropdown.vue'; import DeleteSnippetMutation from '~/snippets/mutations/delete_snippet.mutation.graphql'; import axios from '~/lib/utils/axios_utils'; import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/alert'; @@ -79,6 +85,7 @@ describe('Snippet header component', () => { }, }, stubs: { + SnippetCodeDropdown, GlButton, GlDisclosureDropdown, GlDisclosureDropdownGroup, @@ -105,7 +112,11 @@ describe('Snippet header component', () => { const findIcon = () => wrapper.findComponent(GlIcon); const findTooltip = () => getBinding(findIcon().element, 'gl-tooltip'); const findSpamIcon = () => wrapper.findByTestId('snippets-spam-icon'); + const findCodeDropdown = () => wrapper.findComponent(SnippetCodeDropdown); + const webUrl = 'http://foo.bar'; + const dummyHTTPUrl = webUrl; + const dummySSHUrl = 'ssh://foo.bar'; const title = 'The property of Thor'; beforeEach(() => { @@ -404,4 +415,54 @@ describe('Snippet header component', () => { expect(findTooltip()).toBeDefined(); }); }); + + describe('Code button rendering', () => { + it.each` + httpUrlToRepo | sshUrlToRepo | shouldRender | isRendered + ${null} | ${null} | ${'Should not'} | ${false} + ${null} | ${dummySSHUrl} | ${'Should'} | ${true} + ${dummyHTTPUrl} | ${null} | ${'Should'} | ${true} + ${dummyHTTPUrl} | ${dummySSHUrl} | ${'Should'} | ${true} + `( + '$shouldRender render "Code" button when `httpUrlToRepo` is $httpUrlToRepo and `sshUrlToRepo` is $sshUrlToRepo', + ({ httpUrlToRepo, sshUrlToRepo, isRendered }) => { + createComponent({ + snippetProps: { + sshUrlToRepo, + httpUrlToRepo, + webUrl, + }, + }); + expect(findCodeDropdown().exists()).toBe(isRendered); + }, + ); + + it.each` + snippetVisibility | projectVisibility | condition | embeddable + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${'not'} | ${false} + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${'not'} | ${false} + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${undefined} | ${''} | ${true} + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${''} | ${true} + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${'not'} | ${false} + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${undefined} | ${'not'} | ${false} + ${'foo'} | ${undefined} | ${'not'} | ${false} + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${'not'} | ${false} + `( + 'is $condition embeddable if snippetVisibility is $snippetVisibility and projectVisibility is $projectVisibility', + ({ snippetVisibility, projectVisibility, embeddable }) => { + createComponent({ + snippetProps: { + sshUrlToRepo: dummySSHUrl, + httpUrlToRepo: dummyHTTPUrl, + visibilityLevel: snippetVisibility, + webUrl, + project: { + visibility: projectVisibility, + }, + }, + }); + expect(findCodeDropdown().props('embeddable')).toBe(embeddable); + }, + ); + }); }); diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb index 9bfbeb56e689a20b9b711d7573e1283c05cd3947..157a0a64291bc8c6add3d95897779465db09adb6 100644 --- a/spec/support/shared_examples/features/snippets_shared_examples.rb +++ b/spec/support/shared_examples/features/snippets_shared_examples.rb @@ -54,7 +54,6 @@ RSpec.shared_examples 'does not show New Snippet button' do specify do expect(page).to have_link(text: "$#{snippet.id}") - expect(page).not_to have_selector('[data-testid="snippets-more-actions-dropdown"]') expect(page).not_to have_link('New snippet') end end