From 58c6a35ec89664361e08ad788ef04b4b5e7ea04e Mon Sep 17 00:00:00 2001 From: Denys Mishunov Date: Mon, 29 Jun 2020 23:50:47 +0200 Subject: [PATCH 1/4] Process blobs array In the multi-file scenario for snippets we switch to 'blobs' array instead of direct 'blob' object --- .../javascripts/snippets/fragments/snippetBase.fragment.graphql | 2 +- app/assets/javascripts/snippets/mixins/snippets.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql index e7765dfd8ba5e7..2cca71708cad95 100644 --- a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql +++ b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql @@ -11,7 +11,7 @@ fragment SnippetBase on Snippet { webUrl httpUrlToRepo sshUrlToRepo - blob { + blobs { binary name path diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js index 837c41cdf6b7a6..87a00cb8e90e07 100644 --- a/app/assets/javascripts/snippets/mixins/snippets.js +++ b/app/assets/javascripts/snippets/mixins/snippets.js @@ -11,6 +11,7 @@ export const getSnippetMixin = { }, update: data => data.snippets.edges[0]?.node, result(res) { + this.blobs = res.data.snippets.edges[0].node.blobs; if (this.onSnippetFetch) { this.onSnippetFetch(res); } @@ -27,6 +28,7 @@ export const getSnippetMixin = { return { snippet: {}, newSnippet: false, + blobs: [], }; }, computed: { -- GitLab From 567aef3cd84fc939f4e063480176309e3a48db1d Mon Sep 17 00:00:00 2001 From: Denys Mishunov Date: Mon, 29 Jun 2020 23:53:05 +0200 Subject: [PATCH 2/4] Moved blob-embeddable component out of blob --- app/assets/javascripts/snippets/components/show.vue | 1 + app/assets/javascripts/snippets/components/snippet_blob_view.vue | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue index bc0034d397efd0..a1dec92593aae5 100644 --- a/app/assets/javascripts/snippets/components/show.vue +++ b/app/assets/javascripts/snippets/components/show.vue @@ -27,6 +27,7 @@ export default { diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue index 7472aff33182b6..41f83e2d258f70 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue @@ -93,7 +93,6 @@ export default { diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue index 41f83e2d258f70..be294d3a628a59 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue @@ -1,6 +1,4 @@ diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue index 2a06296cb15d3a..4bd1453830ca51 100644 --- a/app/assets/javascripts/snippets/components/snippet_header.vue +++ b/app/assets/javascripts/snippets/components/snippet_header.vue @@ -65,14 +65,17 @@ export default { }; }, computed: { + snippetHasBinary() { + return Boolean(this.snippet.blobs.find(blob => blob.binary)); + }, personalSnippetActions() { return [ { condition: this.snippet.userPermissions.updateSnippet, text: __('Edit'), href: this.editLink, - disabled: this.snippet.blob.binary, - title: this.snippet.blob.binary + disabled: this.snippetHasBinary, + title: this.snippetHasBinary ? __('Snippets with non-text files can only be edited via Git.') : undefined, }, diff --git a/changelogs/unreleased/217786-snippet-blobs.yml b/changelogs/unreleased/217786-snippet-blobs.yml new file mode 100644 index 00000000000000..3c812eb57045bc --- /dev/null +++ b/changelogs/unreleased/217786-snippet-blobs.yml @@ -0,0 +1,5 @@ +--- +title: Accept multiple blobs in snippets +merge_request: 35605 +author: +type: changed diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js index 0f7193846ff154..58aa1dc6dc9c6a 100644 --- a/spec/frontend/blob/components/mock_data.js +++ b/spec/frontend/blob/components/mock_data.js @@ -32,6 +32,20 @@ export const Blob = { }, }; +export const BinaryBlob = { + binary: true, + name: 'dummy.png', + path: 'foo/bar/dummy.png', + rawPath: '/flightjs/flight/snippets/51/raw', + size: 75, + simpleViewer: { + ...SimpleViewerMock, + }, + richViewer: { + ...RichViewerMock, + }, +}; + export const RichBlobContentMock = { richData: '

Rich

', }; diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js index 33608df8cf20d4..b5446e70028e3e 100644 --- a/spec/frontend/snippets/components/show_spec.js +++ b/spec/frontend/snippets/components/show_spec.js @@ -1,10 +1,13 @@ import SnippetApp from '~/snippets/components/show.vue'; +import BlobEmbeddable from '~/blob/components/blob_embeddable.vue'; import SnippetHeader from '~/snippets/components/snippet_header.vue'; import SnippetTitle from '~/snippets/components/snippet_title.vue'; import SnippetBlob from '~/snippets/components/snippet_blob_view.vue'; import { GlLoadingIcon } from '@gitlab/ui'; +import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; import { shallowMount } from '@vue/test-utils'; +import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants'; describe('Snippet view app', () => { let wrapper; @@ -12,7 +15,7 @@ describe('Snippet view app', () => { snippetGid: 'gid://gitlab/PersonalSnippet/42', }; - function createComponent({ props = defaultProps, loading = false } = {}) { + function createComponent({ props = defaultProps, data = {}, loading = false } = {}) { const $apollo = { queries: { snippet: { @@ -26,6 +29,9 @@ describe('Snippet view app', () => { propsData: { ...props, }, + data() { + return data; + }, }); } afterEach(() => { @@ -37,10 +43,33 @@ describe('Snippet view app', () => { expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); }); - it('renders all components after the query is finished', () => { + it('renders all simple components after the query is finished', () => { createComponent(); expect(wrapper.find(SnippetHeader).exists()).toBe(true); expect(wrapper.find(SnippetTitle).exists()).toBe(true); - expect(wrapper.find(SnippetBlob).exists()).toBe(true); + }); + + it('renders embeddable component if visibility allows', () => { + createComponent({ + data: { + snippet: { + visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, + webUrl: 'http://foo.bar', + }, + }, + }); + expect(wrapper.contains(BlobEmbeddable)).toBe(true); + }); + + it('renders correct snippet-blob components', () => { + createComponent({ + data: { + blobs: [Blob, BinaryBlob], + }, + }); + const blobs = wrapper.findAll(SnippetBlob); + expect(blobs.length).toBe(2); + expect(blobs.at(0).props('blob')).toEqual(Blob); + expect(blobs.at(1).props('blob')).toEqual(BinaryBlob); }); }); diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index e4d8ee9b7df0dc..c8f1c8fc8a9e87 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -23,13 +23,17 @@ describe('Blob Embeddable', () => { id: 'gid://foo.bar/snippet', webUrl: 'https://foo.bar', visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, - blob: BlobMock, }; const dataMock = { activeViewerType: SimpleViewerMock.type, }; - function createComponent(props = {}, data = dataMock, contentLoading = false) { + function createComponent({ + snippetProps = {}, + data = dataMock, + blob = BlobMock, + contentLoading = false, + } = {}) { const $apollo = { queries: { blobContent: { @@ -44,8 +48,9 @@ describe('Blob Embeddable', () => { propsData: { snippet: { ...snippet, - ...props, + ...snippetProps, }, + blob, }, data() { return { @@ -63,7 +68,6 @@ describe('Blob Embeddable', () => { describe('rendering', () => { it('renders correct components', () => { createComponent(); - expect(wrapper.find(BlobEmbeddable).exists()).toBe(true); expect(wrapper.find(BlobHeader).exists()).toBe(true); expect(wrapper.find(BlobContent).exists()).toBe(true); }); @@ -72,19 +76,14 @@ describe('Blob Embeddable', () => { 'does not render blob-embeddable by default', visibilityLevel => { createComponent({ - visibilityLevel, + snippetProps: { + visibilityLevel, + }, }); expect(wrapper.find(BlobEmbeddable).exists()).toBe(false); }, ); - it('does render blob-embeddable for public snippet', () => { - createComponent({ - visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, - }); - expect(wrapper.find(BlobEmbeddable).exists()).toBe(true); - }); - it('sets simple viewer correctly', () => { createComponent(); expect(wrapper.find(SimpleViewer).exists()).toBe(true); @@ -92,7 +91,9 @@ describe('Blob Embeddable', () => { it('sets rich viewer correctly', () => { const data = { ...dataMock, activeViewerType: RichViewerMock.type }; - createComponent({}, data); + createComponent({ + data, + }); expect(wrapper.find(RichViewer).exists()).toBe(true); }); @@ -137,7 +138,9 @@ describe('Blob Embeddable', () => { }); it('renders simple viewer by default if URL contains hash', () => { - createComponent({}, {}); + createComponent({ + data: {}, + }); expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type); expect(wrapper.find(SimpleViewer).exists()).toBe(true); @@ -183,12 +186,11 @@ describe('Blob Embeddable', () => { }); it(`sets '${SimpleViewerMock.type}' as active on ${BLOB_RENDER_EVENT_SHOW_SOURCE} event`, () => { - createComponent( - {}, - { + createComponent({ + data: { activeViewerType: RichViewerMock.type, }, - ); + }); findContentEl().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE); expect(wrapper.vm.activeViewerType).toEqual(SimpleViewerMock.type); diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js index 5230910b6f5d47..0825da92118be6 100644 --- a/spec/frontend/snippets/components/snippet_header_spec.js +++ b/spec/frontend/snippets/components/snippet_header_spec.js @@ -3,6 +3,7 @@ import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.g import { ApolloMutation } from 'vue-apollo'; import { GlButton, GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; describe('Snippet header component', () => { let wrapper; @@ -20,9 +21,7 @@ describe('Snippet header component', () => { author: { name: 'Thor Odinson', }, - blob: { - binary: false, - }, + blobs: [Blob], }; const mutationVariables = { mutation: DeleteSnippetMutation, @@ -49,7 +48,6 @@ describe('Snippet header component', () => { mutationRes = mutationTypes.RESOLVE, snippetProps = {}, } = {}) { - // const defaultProps = Object.assign({}, snippet, snippetProps); const defaultProps = Object.assign(snippet, snippetProps); if (permissions) { Object.assign(defaultProps.userPermissions, { @@ -131,15 +129,18 @@ describe('Snippet header component', () => { expect(wrapper.find(GlModal).exists()).toBe(true); }); - it('renders Edit button as disabled for binary snippets', () => { + it.each` + blobs | isDisabled | condition + ${[Blob]} | ${false} | ${'no binary'} + ${[Blob, BinaryBlob]} | ${true} | ${'several blobs. incl. a binary'} + ${[BinaryBlob]} | ${true} | ${'binary'} + `('renders Edit button when snippet contains $condition file', ({ blobs, isDisabled }) => { createComponent({ snippetProps: { - blob: { - binary: true, - }, + blobs, }, }); - expect(wrapper.find('[href*="edit"]').props('disabled')).toBe(true); + expect(wrapper.find('[href*="edit"]').props('disabled')).toBe(isDisabled); }); describe('Delete mutation', () => { -- GitLab From b0d64e5f2a3344ef65ae40ee24e292d0acc091d1 Mon Sep 17 00:00:00 2001 From: Denys Mishunov Date: Tue, 30 Jun 2020 21:56:35 +0200 Subject: [PATCH 4/4] Replacing bootstrap utilities with gl-* As per maintainer's review --- app/assets/javascripts/snippets/components/show.vue | 2 +- .../javascripts/snippets/components/snippet_blob_view.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue index 8ce092eaa0580a..0779e87e6b6437 100644 --- a/app/assets/javascripts/snippets/components/show.vue +++ b/app/assets/javascripts/snippets/components/show.vue @@ -35,7 +35,7 @@ export default {