Commit a468d2ff authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '217786-snippet-blobs' into 'master'

Use snippet `blobs` field instead of `blob` for Snippet VIEW

See merge request gitlab-org/gitlab!35605
parents deb965bd b0d64e5f
<script> <script>
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import SnippetHeader from './snippet_header.vue'; import SnippetHeader from './snippet_header.vue';
import SnippetTitle from './snippet_title.vue'; import SnippetTitle from './snippet_title.vue';
import SnippetBlob from './snippet_blob_view.vue'; import SnippetBlob from './snippet_blob_view.vue';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { getSnippetMixin } from '../mixins/snippets'; import { getSnippetMixin } from '../mixins/snippets';
import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
export default { export default {
components: { components: {
BlobEmbeddable,
SnippetHeader, SnippetHeader,
SnippetTitle, SnippetTitle,
GlLoadingIcon, GlLoadingIcon,
SnippetBlob, SnippetBlob,
}, },
mixins: [getSnippetMixin], mixins: [getSnippetMixin],
computed: {
embeddable() {
return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
},
},
}; };
</script> </script>
<template> <template>
...@@ -27,7 +35,10 @@ export default { ...@@ -27,7 +35,10 @@ export default {
<template v-else> <template v-else>
<snippet-header :snippet="snippet" /> <snippet-header :snippet="snippet" />
<snippet-title :snippet="snippet" /> <snippet-title :snippet="snippet" />
<snippet-blob :snippet="snippet" /> <blob-embeddable v-if="embeddable" class="gl-mb-5" :url="snippet.webUrl" />
<div v-for="blob in blobs" :key="blob.path">
<snippet-blob :snippet="snippet" :blob="blob" />
</div>
</template> </template>
</div> </div>
</template> </template>
<script> <script>
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import { SNIPPET_VISIBILITY_PUBLIC } from '../constants';
import BlobHeader from '~/blob/components/blob_header.vue'; import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContent from '~/blob/components/blob_content.vue'; import BlobContent from '~/blob/components/blob_content.vue';
import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue'; import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
...@@ -16,7 +14,6 @@ import { ...@@ -16,7 +14,6 @@ import {
export default { export default {
components: { components: {
BlobEmbeddable,
BlobHeader, BlobHeader,
BlobContent, BlobContent,
CloneDropdownButton, CloneDropdownButton,
...@@ -49,21 +46,19 @@ export default { ...@@ -49,21 +46,19 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
blob: {
type: Object,
required: true,
},
}, },
data() { data() {
return { return {
blob: this.snippet.blob,
blobContent: '', blobContent: '',
activeViewerType: activeViewerType:
this.snippet.blob?.richViewer && !window.location.hash this.blob?.richViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
? RICH_BLOB_VIEWER
: SIMPLE_BLOB_VIEWER,
}; };
}, },
computed: { computed: {
embeddable() {
return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
},
isContentLoading() { isContentLoading() {
return this.$apollo.queries.blobContent.loading; return this.$apollo.queries.blobContent.loading;
}, },
...@@ -92,33 +87,30 @@ export default { ...@@ -92,33 +87,30 @@ export default {
}; };
</script> </script>
<template> <template>
<div> <article class="file-holder snippet-file-content">
<blob-embeddable v-if="embeddable" class="mb-3" :url="snippet.webUrl" /> <blob-header
<article class="file-holder snippet-file-content"> :blob="blob"
<blob-header :active-viewer-type="viewer.type"
:blob="blob" :has-render-error="hasRenderError"
:active-viewer-type="viewer.type" @viewer-changed="switchViewer"
:has-render-error="hasRenderError" >
@viewer-changed="switchViewer" <template #actions>
> <clone-dropdown-button
<template #actions> v-if="canBeCloned"
<clone-dropdown-button class="gl-mr-3"
v-if="canBeCloned" :ssh-link="snippet.sshUrlToRepo"
class="mr-2" :http-link="snippet.httpUrlToRepo"
:ssh-link="snippet.sshUrlToRepo" data-qa-selector="clone_button"
:http-link="snippet.httpUrlToRepo" />
data-qa-selector="clone_button" </template>
/> </blob-header>
</template> <blob-content
</blob-header> :loading="isContentLoading"
<blob-content :content="blobContent"
:loading="isContentLoading" :active-viewer="viewer"
:content="blobContent" :blob="blob"
:active-viewer="viewer" @[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery"
:blob="blob" @[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer"
@[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery" />
@[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer" </article>
/>
</article>
</div>
</template> </template>
...@@ -65,14 +65,17 @@ export default { ...@@ -65,14 +65,17 @@ export default {
}; };
}, },
computed: { computed: {
snippetHasBinary() {
return Boolean(this.snippet.blobs.find(blob => blob.binary));
},
personalSnippetActions() { personalSnippetActions() {
return [ return [
{ {
condition: this.snippet.userPermissions.updateSnippet, condition: this.snippet.userPermissions.updateSnippet,
text: __('Edit'), text: __('Edit'),
href: this.editLink, href: this.editLink,
disabled: this.snippet.blob.binary, disabled: this.snippetHasBinary,
title: this.snippet.blob.binary title: this.snippetHasBinary
? __('Snippets with non-text files can only be edited via Git.') ? __('Snippets with non-text files can only be edited via Git.')
: undefined, : undefined,
}, },
......
...@@ -11,7 +11,7 @@ fragment SnippetBase on Snippet { ...@@ -11,7 +11,7 @@ fragment SnippetBase on Snippet {
webUrl webUrl
httpUrlToRepo httpUrlToRepo
sshUrlToRepo sshUrlToRepo
blob { blobs {
binary binary
name name
path path
......
...@@ -11,6 +11,7 @@ export const getSnippetMixin = { ...@@ -11,6 +11,7 @@ export const getSnippetMixin = {
}, },
update: data => data.snippets.edges[0]?.node, update: data => data.snippets.edges[0]?.node,
result(res) { result(res) {
this.blobs = res.data.snippets.edges[0].node.blobs;
if (this.onSnippetFetch) { if (this.onSnippetFetch) {
this.onSnippetFetch(res); this.onSnippetFetch(res);
} }
...@@ -27,6 +28,7 @@ export const getSnippetMixin = { ...@@ -27,6 +28,7 @@ export const getSnippetMixin = {
return { return {
snippet: {}, snippet: {},
newSnippet: false, newSnippet: false,
blobs: [],
}; };
}, },
computed: { computed: {
......
---
title: Accept multiple blobs in snippets
merge_request: 35605
author:
type: changed
...@@ -32,6 +32,20 @@ export const Blob = { ...@@ -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 = { export const RichBlobContentMock = {
richData: '<h1>Rich</h1>', richData: '<h1>Rich</h1>',
}; };
......
import SnippetApp from '~/snippets/components/show.vue'; import SnippetApp from '~/snippets/components/show.vue';
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import SnippetHeader from '~/snippets/components/snippet_header.vue'; import SnippetHeader from '~/snippets/components/snippet_header.vue';
import SnippetTitle from '~/snippets/components/snippet_title.vue'; import SnippetTitle from '~/snippets/components/snippet_title.vue';
import SnippetBlob from '~/snippets/components/snippet_blob_view.vue'; import SnippetBlob from '~/snippets/components/snippet_blob_view.vue';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
describe('Snippet view app', () => { describe('Snippet view app', () => {
let wrapper; let wrapper;
...@@ -12,7 +15,7 @@ describe('Snippet view app', () => { ...@@ -12,7 +15,7 @@ describe('Snippet view app', () => {
snippetGid: 'gid://gitlab/PersonalSnippet/42', snippetGid: 'gid://gitlab/PersonalSnippet/42',
}; };
function createComponent({ props = defaultProps, loading = false } = {}) { function createComponent({ props = defaultProps, data = {}, loading = false } = {}) {
const $apollo = { const $apollo = {
queries: { queries: {
snippet: { snippet: {
...@@ -26,6 +29,9 @@ describe('Snippet view app', () => { ...@@ -26,6 +29,9 @@ describe('Snippet view app', () => {
propsData: { propsData: {
...props, ...props,
}, },
data() {
return data;
},
}); });
} }
afterEach(() => { afterEach(() => {
...@@ -37,10 +43,33 @@ describe('Snippet view app', () => { ...@@ -37,10 +43,33 @@ describe('Snippet view app', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); 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(); createComponent();
expect(wrapper.find(SnippetHeader).exists()).toBe(true); expect(wrapper.find(SnippetHeader).exists()).toBe(true);
expect(wrapper.find(SnippetTitle).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);
}); });
}); });
...@@ -23,13 +23,17 @@ describe('Blob Embeddable', () => { ...@@ -23,13 +23,17 @@ describe('Blob Embeddable', () => {
id: 'gid://foo.bar/snippet', id: 'gid://foo.bar/snippet',
webUrl: 'https://foo.bar', webUrl: 'https://foo.bar',
visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, visibilityLevel: SNIPPET_VISIBILITY_PUBLIC,
blob: BlobMock,
}; };
const dataMock = { const dataMock = {
activeViewerType: SimpleViewerMock.type, activeViewerType: SimpleViewerMock.type,
}; };
function createComponent(props = {}, data = dataMock, contentLoading = false) { function createComponent({
snippetProps = {},
data = dataMock,
blob = BlobMock,
contentLoading = false,
} = {}) {
const $apollo = { const $apollo = {
queries: { queries: {
blobContent: { blobContent: {
...@@ -44,8 +48,9 @@ describe('Blob Embeddable', () => { ...@@ -44,8 +48,9 @@ describe('Blob Embeddable', () => {
propsData: { propsData: {
snippet: { snippet: {
...snippet, ...snippet,
...props, ...snippetProps,
}, },
blob,
}, },
data() { data() {
return { return {
...@@ -63,7 +68,6 @@ describe('Blob Embeddable', () => { ...@@ -63,7 +68,6 @@ describe('Blob Embeddable', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders correct components', () => { it('renders correct components', () => {
createComponent(); createComponent();
expect(wrapper.find(BlobEmbeddable).exists()).toBe(true);
expect(wrapper.find(BlobHeader).exists()).toBe(true); expect(wrapper.find(BlobHeader).exists()).toBe(true);
expect(wrapper.find(BlobContent).exists()).toBe(true); expect(wrapper.find(BlobContent).exists()).toBe(true);
}); });
...@@ -72,19 +76,14 @@ describe('Blob Embeddable', () => { ...@@ -72,19 +76,14 @@ describe('Blob Embeddable', () => {
'does not render blob-embeddable by default', 'does not render blob-embeddable by default',
visibilityLevel => { visibilityLevel => {
createComponent({ createComponent({
visibilityLevel, snippetProps: {
visibilityLevel,
},
}); });
expect(wrapper.find(BlobEmbeddable).exists()).toBe(false); 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', () => { it('sets simple viewer correctly', () => {
createComponent(); createComponent();
expect(wrapper.find(SimpleViewer).exists()).toBe(true); expect(wrapper.find(SimpleViewer).exists()).toBe(true);
...@@ -92,7 +91,9 @@ describe('Blob Embeddable', () => { ...@@ -92,7 +91,9 @@ describe('Blob Embeddable', () => {
it('sets rich viewer correctly', () => { it('sets rich viewer correctly', () => {
const data = { ...dataMock, activeViewerType: RichViewerMock.type }; const data = { ...dataMock, activeViewerType: RichViewerMock.type };
createComponent({}, data); createComponent({
data,
});
expect(wrapper.find(RichViewer).exists()).toBe(true); expect(wrapper.find(RichViewer).exists()).toBe(true);
}); });
...@@ -137,7 +138,9 @@ describe('Blob Embeddable', () => { ...@@ -137,7 +138,9 @@ describe('Blob Embeddable', () => {
}); });
it('renders simple viewer by default if URL contains hash', () => { it('renders simple viewer by default if URL contains hash', () => {
createComponent({}, {}); createComponent({
data: {},
});
expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type); expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
expect(wrapper.find(SimpleViewer).exists()).toBe(true); expect(wrapper.find(SimpleViewer).exists()).toBe(true);
...@@ -183,12 +186,11 @@ describe('Blob Embeddable', () => { ...@@ -183,12 +186,11 @@ describe('Blob Embeddable', () => {
}); });
it(`sets '${SimpleViewerMock.type}' as active on ${BLOB_RENDER_EVENT_SHOW_SOURCE} event`, () => { it(`sets '${SimpleViewerMock.type}' as active on ${BLOB_RENDER_EVENT_SHOW_SOURCE} event`, () => {
createComponent( createComponent({
{}, data: {
{
activeViewerType: RichViewerMock.type, activeViewerType: RichViewerMock.type,
}, },
); });
findContentEl().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE); findContentEl().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE);
expect(wrapper.vm.activeViewerType).toEqual(SimpleViewerMock.type); expect(wrapper.vm.activeViewerType).toEqual(SimpleViewerMock.type);
......
...@@ -3,6 +3,7 @@ import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.g ...@@ -3,6 +3,7 @@ import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.g
import { ApolloMutation } from 'vue-apollo'; import { ApolloMutation } from 'vue-apollo';
import { GlButton, GlModal } from '@gitlab/ui'; import { GlButton, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
describe('Snippet header component', () => { describe('Snippet header component', () => {
let wrapper; let wrapper;
...@@ -20,9 +21,7 @@ describe('Snippet header component', () => { ...@@ -20,9 +21,7 @@ describe('Snippet header component', () => {
author: { author: {
name: 'Thor Odinson', name: 'Thor Odinson',
}, },
blob: { blobs: [Blob],
binary: false,
},
}; };
const mutationVariables = { const mutationVariables = {
mutation: DeleteSnippetMutation, mutation: DeleteSnippetMutation,
...@@ -49,7 +48,6 @@ describe('Snippet header component', () => { ...@@ -49,7 +48,6 @@ describe('Snippet header component', () => {
mutationRes = mutationTypes.RESOLVE, mutationRes = mutationTypes.RESOLVE,
snippetProps = {}, snippetProps = {},
} = {}) { } = {}) {
// const defaultProps = Object.assign({}, snippet, snippetProps);
const defaultProps = Object.assign(snippet, snippetProps); const defaultProps = Object.assign(snippet, snippetProps);
if (permissions) { if (permissions) {
Object.assign(defaultProps.userPermissions, { Object.assign(defaultProps.userPermissions, {
...@@ -131,15 +129,18 @@ describe('Snippet header component', () => { ...@@ -131,15 +129,18 @@ describe('Snippet header component', () => {
expect(wrapper.find(GlModal).exists()).toBe(true); 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({ createComponent({
snippetProps: { snippetProps: {
blob: { blobs,
binary: true,
},
}, },
}); });
expect(wrapper.find('[href*="edit"]').props('disabled')).toBe(true); expect(wrapper.find('[href*="edit"]').props('disabled')).toBe(isDisabled);
}); });
describe('Delete mutation', () => { describe('Delete mutation', () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment