Commit 34c70444 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '333893-add-support-for-legacy-viewer-blob-refactor' into 'master'

Add support for legacy blob viewers

See merge request gitlab-org/gitlab!64839
parents e2905b3a 440dd4f4
...@@ -27,6 +27,11 @@ export default { ...@@ -27,6 +27,11 @@ export default {
default: false, default: false,
required: false, required: false,
}, },
richViewer: {
type: String,
default: '',
required: false,
},
loading: { loading: {
type: Boolean, type: Boolean,
default: true, default: true,
...@@ -71,6 +76,7 @@ export default { ...@@ -71,6 +76,7 @@ export default {
v-else v-else
ref="contentViewer" ref="contentViewer"
:content="content" :content="content"
:rich-viewer="richViewer"
:is-raw-content="isRawContent" :is-raw-content="isRawContent"
:file-name="blob.name" :file-name="blob.name"
:type="activeViewer.fileType" :type="activeViewer.fileType"
......
...@@ -5,6 +5,7 @@ import BlobContent from '~/blob/components/blob_content.vue'; ...@@ -5,6 +5,7 @@ import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue'; import BlobHeader from '~/blob/components/blob_header.vue';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants'; import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { isLoggedIn } from '~/lib/utils/common_utils'; import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import blobInfoQuery from '../queries/blob_info.query.graphql'; import blobInfoQuery from '../queries/blob_info.query.graphql';
...@@ -29,12 +30,15 @@ export default { ...@@ -29,12 +30,15 @@ export default {
}; };
}, },
result() { result() {
if (this.hasRichViewer) {
this.loadLegacyViewer();
}
this.switchViewer( this.switchViewer(
this.hasRichViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER, this.hasRichViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
); );
}, },
error() { error() {
createFlash({ message: __('An error occurred while loading the file. Please try again.') }); this.displayError();
}, },
}, },
}, },
...@@ -55,6 +59,9 @@ export default { ...@@ -55,6 +59,9 @@ export default {
}, },
data() { data() {
return { return {
legacyRichViewer: null,
isBinary: false,
isLoadingLegacyViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER, activeViewerType: SIMPLE_BLOB_VIEWER,
project: { project: {
repository: { repository: {
...@@ -94,7 +101,7 @@ export default { ...@@ -94,7 +101,7 @@ export default {
return isLoggedIn(); return isLoggedIn();
}, },
isLoading() { isLoading() {
return this.$apollo.queries.project.loading; return this.$apollo.queries.project.loading || this.isLoadingLegacyViewer;
}, },
blobInfo() { blobInfo() {
const nodes = this.project?.repository?.blobs?.nodes; const nodes = this.project?.repository?.blobs?.nodes;
...@@ -113,6 +120,20 @@ export default { ...@@ -113,6 +120,20 @@ export default {
}, },
}, },
methods: { methods: {
loadLegacyViewer() {
this.isLoadingLegacyViewer = true;
axios
.get(`${this.blobInfo.webPath}?format=json&viewer=rich`)
.then(({ data: { html, binary } }) => {
this.legacyRichViewer = html;
this.isBinary = binary;
this.isLoadingLegacyViewer = false;
})
.catch(() => this.displayError());
},
displayError() {
createFlash({ message: __('An error occurred while loading the file. Please try again.') });
},
switchViewer(newViewer) { switchViewer(newViewer) {
this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER; this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER;
}, },
...@@ -126,13 +147,17 @@ export default { ...@@ -126,13 +147,17 @@ export default {
<div v-if="blobInfo && !isLoading" class="file-holder"> <div v-if="blobInfo && !isLoading" class="file-holder">
<blob-header <blob-header
:blob="blobInfo" :blob="blobInfo"
:hide-viewer-switcher="!hasRichViewer" :hide-viewer-switcher="!hasRichViewer || isBinary"
: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>
<blob-edit :edit-path="blobInfo.editBlobPath" :web-ide-path="blobInfo.ideEditPath" /> <blob-edit
v-if="!isBinary"
:edit-path="blobInfo.editBlobPath"
:web-ide-path="blobInfo.ideEditPath"
/>
<blob-button-group <blob-button-group
v-if="isLoggedIn" v-if="isLoggedIn"
:path="path" :path="path"
...@@ -143,6 +168,7 @@ export default { ...@@ -143,6 +168,7 @@ export default {
</template> </template>
</blob-header> </blob-header>
<blob-content <blob-content
:rich-viewer="legacyRichViewer"
:blob="blobInfo" :blob="blobInfo"
:content="blobInfo.rawTextBlob" :content="blobInfo.rawTextBlob"
:is-raw-content="true" :is-raw-content="true"
......
...@@ -5,7 +5,13 @@ export default { ...@@ -5,7 +5,13 @@ export default {
props: { props: {
content: { content: {
type: String, type: String,
required: true, required: false,
default: null,
},
richViewer: {
type: String,
default: '',
required: false,
}, },
type: { type: {
type: String, type: String,
......
...@@ -18,5 +18,5 @@ export default { ...@@ -18,5 +18,5 @@ export default {
}; };
</script> </script>
<template> <template>
<markdown-field-view ref="content" v-safe-html="content" /> <markdown-field-view ref="content" v-safe-html="richViewer || content" />
</template> </template>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import BlobContent from '~/blob/components/blob_content.vue'; import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue'; import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue'; import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue'; import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobEdit from '~/repository/components/blob_edit.vue'; import BlobEdit from '~/repository/components/blob_edit.vue';
import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
let wrapper; let wrapper;
const simpleMockData = { const simpleMockData = {
...@@ -17,6 +23,7 @@ const simpleMockData = { ...@@ -17,6 +23,7 @@ const simpleMockData = {
fileType: 'text', fileType: 'text',
tooLarge: false, tooLarge: false,
path: 'some_file.js', path: 'some_file.js',
webPath: 'some_file.js',
editBlobPath: 'some_file.js/edit', editBlobPath: 'some_file.js/edit',
ideEditPath: 'some_file.js/ide/edit', ideEditPath: 'some_file.js/ide/edit',
storedExternally: false, storedExternally: false,
...@@ -47,6 +54,28 @@ const richMockData = { ...@@ -47,6 +54,28 @@ const richMockData = {
}, },
}; };
const localVue = createLocalVue();
const mockAxios = new MockAdapter(axios);
const createComponentWithApollo = (mockData) => {
localVue.use(VueApollo);
const mockResolver = jest
.fn()
.mockResolvedValue({ data: { project: { repository: { blobs: { nodes: [mockData] } } } } });
const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]);
wrapper = shallowMount(BlobContentViewer, {
localVue,
apolloProvider: fakeApollo,
propsData: {
path: 'some_file.js',
projectPath: 'some/path',
},
});
};
const createFactory = (mountFn) => ( const createFactory = (mountFn) => (
{ props = {}, mockData = {}, stubs = {} } = {}, { props = {}, mockData = {}, stubs = {} } = {},
loading = false, loading = false,
...@@ -163,6 +192,22 @@ describe('Blob content viewer component', () => { ...@@ -163,6 +192,22 @@ describe('Blob content viewer component', () => {
}); });
}); });
describe('legacy viewers', () => {
it('does not load a legacy viewer when a rich viewer is not available', async () => {
createComponentWithApollo(simpleMockData);
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(0);
});
it('loads a legacy viewer when a rich viewer is available', async () => {
createComponentWithApollo(richMockData);
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(1);
});
});
describe('BlobHeader action slot', () => { describe('BlobHeader action slot', () => {
const { ideEditPath, editBlobPath } = simpleMockData; const { ideEditPath, editBlobPath } = simpleMockData;
...@@ -200,6 +245,20 @@ describe('Blob content viewer component', () => { ...@@ -200,6 +245,20 @@ describe('Blob content viewer component', () => {
}); });
}); });
it('does not render BlobHeaderEdit button when viewing a binary file', async () => {
fullFactory({
mockData: { blobInfo: richMockData, isBinary: true },
stubs: {
BlobContent: true,
BlobReplace: true,
},
});
await nextTick();
expect(findBlobEdit().exists()).toBe(false);
});
describe('BlobButtonGroup', () => { describe('BlobButtonGroup', () => {
const { name, path } = simpleMockData; const { name, path } = simpleMockData;
......
...@@ -10,9 +10,10 @@ describe('Blob Rich Viewer component', () => { ...@@ -10,9 +10,10 @@ describe('Blob Rich Viewer component', () => {
const content = '<h1 id="markdown">Foo Bar</h1>'; const content = '<h1 id="markdown">Foo Bar</h1>';
const defaultType = 'markdown'; const defaultType = 'markdown';
function createComponent(type = defaultType) { function createComponent(type = defaultType, richViewer) {
wrapper = shallowMount(RichViewer, { wrapper = shallowMount(RichViewer, {
propsData: { propsData: {
richViewer,
content, content,
type, type,
}, },
...@@ -31,6 +32,12 @@ describe('Blob Rich Viewer component', () => { ...@@ -31,6 +32,12 @@ describe('Blob Rich Viewer component', () => {
expect(wrapper.html()).toContain(content); expect(wrapper.html()).toContain(content);
}); });
it('renders the richViewer if one is present', () => {
const richViewer = '<div class="js-pdf-viewer"></div>';
createComponent('pdf', richViewer);
expect(wrapper.html()).toContain(richViewer);
});
it('queries for advanced viewer', () => { it('queries for advanced viewer', () => {
expect(handleBlobRichViewer).toHaveBeenCalledWith(expect.anything(), defaultType); expect(handleBlobRichViewer).toHaveBeenCalledWith(expect.anything(), defaultType);
}); });
......
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