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>
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import SnippetHeader from './snippet_header.vue';
import SnippetTitle from './snippet_title.vue';
import SnippetBlob from './snippet_blob_view.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { getSnippetMixin } from '../mixins/snippets';
import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
export default {
components: {
BlobEmbeddable,
SnippetHeader,
SnippetTitle,
GlLoadingIcon,
SnippetBlob,
},
mixins: [getSnippetMixin],
computed: {
embeddable() {
return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
},
},
};
</script>
<template>
......@@ -27,7 +35,10 @@ export default {
<template v-else>
<snippet-header :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>
</div>
</template>
<script>
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import { SNIPPET_VISIBILITY_PUBLIC } from '../constants';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContent from '~/blob/components/blob_content.vue';
import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
......@@ -16,7 +14,6 @@ import {
export default {
components: {
BlobEmbeddable,
BlobHeader,
BlobContent,
CloneDropdownButton,
......@@ -49,21 +46,19 @@ export default {
type: Object,
required: true,
},
blob: {
type: Object,
required: true,
},
},
data() {
return {
blob: this.snippet.blob,
blobContent: '',
activeViewerType:
this.snippet.blob?.richViewer && !window.location.hash
? RICH_BLOB_VIEWER
: SIMPLE_BLOB_VIEWER,
this.blob?.richViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
};
},
computed: {
embeddable() {
return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
},
isContentLoading() {
return this.$apollo.queries.blobContent.loading;
},
......@@ -92,33 +87,30 @@ export default {
};
</script>
<template>
<div>
<blob-embeddable v-if="embeddable" class="mb-3" :url="snippet.webUrl" />
<article class="file-holder snippet-file-content">
<blob-header
:blob="blob"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
@viewer-changed="switchViewer"
>
<template #actions>
<clone-dropdown-button
v-if="canBeCloned"
class="mr-2"
:ssh-link="snippet.sshUrlToRepo"
:http-link="snippet.httpUrlToRepo"
data-qa-selector="clone_button"
/>
</template>
</blob-header>
<blob-content
:loading="isContentLoading"
:content="blobContent"
:active-viewer="viewer"
:blob="blob"
@[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery"
@[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer"
/>
</article>
</div>
<article class="file-holder snippet-file-content">
<blob-header
:blob="blob"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
@viewer-changed="switchViewer"
>
<template #actions>
<clone-dropdown-button
v-if="canBeCloned"
class="gl-mr-3"
:ssh-link="snippet.sshUrlToRepo"
:http-link="snippet.httpUrlToRepo"
data-qa-selector="clone_button"
/>
</template>
</blob-header>
<blob-content
:loading="isContentLoading"
:content="blobContent"
:active-viewer="viewer"
:blob="blob"
@[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery"
@[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer"
/>
</article>
</template>
......@@ -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,
},
......
......@@ -11,7 +11,7 @@ fragment SnippetBase on Snippet {
webUrl
httpUrlToRepo
sshUrlToRepo
blob {
blobs {
binary
name
path
......
......@@ -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: {
......
---
title: Accept multiple blobs in snippets
merge_request: 35605
author:
type: changed
......@@ -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: '<h1>Rich</h1>',
};
......
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);
});
});
......@@ -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);
......
......@@ -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', () => {
......
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