diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index bc0034d397efd071f0fe4e5dbb571ae22893d053..0779e87e6b643716f9a2e8de47ac8ef7448d1330 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -1,19 +1,27 @@
@@ -27,7 +35,10 @@ 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 7472aff33182b6e22b3b738924d29e46bd8336b9..afd038eef58a8d56b72ff306ebddf7dedf080f31 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 2a06296cb15d3af214992ece64a6362d39b54a23..4bd1453830ca51f4caeb42ca99ef90f79638a364 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/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
index e7765dfd8ba5e7840e0748a64aa64b54abbceb27..2cca71708cad95f49d61e2ce8576c844e1788343 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 837c41cdf6b7a6d4432f110c090cbf4bc539de0f..87a00cb8e90e07d6f85e45f844e1b6ed117d5b9a 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: {
diff --git a/changelogs/unreleased/217786-snippet-blobs.yml b/changelogs/unreleased/217786-snippet-blobs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3c812eb57045bcc988f061491cce5d606292ef4f
--- /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 0f7193846ff15495eea03da7c15859bf2694b003..58aa1dc6dc9c6a190d7755c9a6ec751fa80b60fd 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 33608df8cf20d4357e2d6476fc5384366f30fe49..b5446e70028e3e102dfaa78c4936188ec4806b59 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 e4d8ee9b7df0dc6b0b8a5acf977ae6d9f0ebd02b..c8f1c8fc8a9e87ed1433d09a7e2afdd89911e2c2 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 5230910b6f5d47156749997090a78d3acd0586ba..0825da92118be6822b4b63c3ba3b943ce48df9bd 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', () => {