Commit fa917599 authored by Denys Mishunov's avatar Denys Mishunov

Work with blobs array instead of object

Even if we only support just one blob per
snippet, we're peparing ourselves for the
multi-file scenario in Snippets
parent 567aef3c
<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>
...@@ -28,7 +36,9 @@ export default { ...@@ -28,7 +36,9 @@ export default {
<snippet-header :snippet="snippet" /> <snippet-header :snippet="snippet" />
<snippet-title :snippet="snippet" /> <snippet-title :snippet="snippet" />
<blob-embeddable v-if="embeddable" class="mb-3" :url="snippet.webUrl" /> <blob-embeddable v-if="embeddable" class="mb-3" :url="snippet.webUrl" />
<snippet-blob :snippet="snippet" /> <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,32 +87,30 @@ export default { ...@@ -92,32 +87,30 @@ export default {
}; };
</script> </script>
<template> <template>
<div> <article class="file-holder snippet-file-content">
<article class="file-holder snippet-file-content"> <blob-header
<blob-header :blob="blob"
:blob="blob" :active-viewer-type="viewer.type"
:active-viewer-type="viewer.type" :has-render-error="hasRenderError"
:has-render-error="hasRenderError" @viewer-changed="switchViewer"
@viewer-changed="switchViewer" >
> <template #actions>
<template #actions> <clone-dropdown-button
<clone-dropdown-button v-if="canBeCloned"
v-if="canBeCloned" class="mr-2"
class="mr-2" :ssh-link="snippet.sshUrlToRepo"
:ssh-link="snippet.sshUrlToRepo" :http-link="snippet.httpUrlToRepo"
:http-link="snippet.httpUrlToRepo" data-qa-selector="clone_button"
data-qa-selector="clone_button" />
/> </template>
</template> </blob-header>
</blob-header> <blob-content
<blob-content :loading="isContentLoading"
:loading="isContentLoading" :content="blobContent"
:content="blobContent" :active-viewer="viewer"
:active-viewer="viewer" :blob="blob"
:blob="blob" @[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery"
@[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery" @[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer"
@[$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,
}, },
......
---
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