Commit 2b719bdf authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '340999-text-viewer-ff' into 'master'

Add text viewer feature flag

See merge request gitlab-org/gitlab!70936
parents 487bc0b5 e865188a
...@@ -41,6 +41,11 @@ export default { ...@@ -41,6 +41,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
hideLineNumbers: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
viewer() { viewer() {
...@@ -80,6 +85,7 @@ export default { ...@@ -80,6 +85,7 @@ export default {
:is-raw-content="isRawContent" :is-raw-content="isRawContent"
:file-name="blob.name" :file-name="blob.name"
:type="activeViewer.fileType" :type="activeViewer.fileType"
:hide-line-numbers="hideLineNumbers"
data-qa-selector="file_content" data-qa-selector="file_content"
/> />
</template> </template>
......
...@@ -42,9 +42,6 @@ export default { ...@@ -42,9 +42,6 @@ export default {
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,
); );
if (this.hasRichViewer && !this.blobViewer) {
this.loadLegacyViewer();
}
}, },
error() { error() {
this.displayError(); this.displayError();
...@@ -69,6 +66,7 @@ export default { ...@@ -69,6 +66,7 @@ export default {
data() { data() {
return { return {
legacyRichViewer: null, legacyRichViewer: null,
legacySimpleViewer: null,
isBinary: false, isBinary: false,
isLoadingLegacyViewer: false, isLoadingLegacyViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER, activeViewerType: SIMPLE_BLOB_VIEWER,
...@@ -115,7 +113,7 @@ export default { ...@@ -115,7 +113,7 @@ export default {
return isLoggedIn(); return isLoggedIn();
}, },
isLoading() { isLoading() {
return this.$apollo.queries.project.loading || this.isLoadingLegacyViewer; return this.$apollo.queries.project.loading;
}, },
isBinaryFileType() { isBinaryFileType() {
return this.isBinary || this.blobInfo.simpleViewer?.fileType !== 'text'; return this.isBinary || this.blobInfo.simpleViewer?.fileType !== 'text';
...@@ -153,22 +151,41 @@ export default { ...@@ -153,22 +151,41 @@ export default {
}, },
}, },
methods: { methods: {
loadLegacyViewer() { loadLegacyViewer(type) {
if (this.legacyViewerLoaded(type)) {
return;
}
this.isLoadingLegacyViewer = true; this.isLoadingLegacyViewer = true;
axios axios
.get(`${this.blobInfo.webPath}?format=json&viewer=rich`) .get(`${this.blobInfo.webPath}?format=json&viewer=${type}`)
.then(({ data: { html, binary } }) => { .then(({ data: { html, binary } }) => {
this.legacyRichViewer = html; if (type === 'simple') {
this.legacySimpleViewer = html;
} else {
this.legacyRichViewer = html;
}
this.isBinary = binary; this.isBinary = binary;
this.isLoadingLegacyViewer = false; this.isLoadingLegacyViewer = false;
}) })
.catch(() => this.displayError()); .catch(() => this.displayError());
}, },
legacyViewerLoaded(type) {
return (
(type === SIMPLE_BLOB_VIEWER && this.legacySimpleViewer) ||
(type === RICH_BLOB_VIEWER && this.legacyRichViewer)
);
},
displayError() { displayError() {
createFlash({ message: __('An error occurred while loading the file. Please try again.') }); 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;
if (!this.blobViewer) {
this.loadLegacyViewer(this.activeViewerType);
}
}, },
}, },
}; };
...@@ -210,10 +227,11 @@ export default { ...@@ -210,10 +227,11 @@ export default {
v-if="!blobViewer" v-if="!blobViewer"
:rich-viewer="legacyRichViewer" :rich-viewer="legacyRichViewer"
:blob="blobInfo" :blob="blobInfo"
:content="blobInfo.rawTextBlob" :content="legacySimpleViewer"
:is-raw-content="true" :is-raw-content="true"
:active-viewer="viewer" :active-viewer="viewer"
:loading="false" :hide-line-numbers="true"
:loading="isLoadingLegacyViewer"
/> />
<component :is="blobViewer" v-else v-bind="viewerProps" class="blob-viewer" /> <component :is="blobViewer" v-else v-bind="viewerProps" class="blob-viewer" />
</div> </div>
......
...@@ -3,7 +3,9 @@ export const loadViewer = (type) => { ...@@ -3,7 +3,9 @@ export const loadViewer = (type) => {
case 'empty': case 'empty':
return () => import(/* webpackChunkName: 'blob_empty_viewer' */ './empty_viewer.vue'); return () => import(/* webpackChunkName: 'blob_empty_viewer' */ './empty_viewer.vue');
case 'text': case 'text':
return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue'); return gon.features.refactorTextViewer
? () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue')
: null;
case 'download': case 'download':
return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue'); return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
case 'image': case 'image':
......
...@@ -27,6 +27,11 @@ export default { ...@@ -27,6 +27,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
hideLineNumbers: {
type: Boolean,
required: false,
default: false,
},
}, },
mounted() { mounted() {
eventHub.$emit(SNIPPET_MEASURE_BLOBS_CONTENT); eventHub.$emit(SNIPPET_MEASURE_BLOBS_CONTENT);
......
...@@ -8,8 +8,6 @@ export default { ...@@ -8,8 +8,6 @@ export default {
name: 'SimpleViewer', name: 'SimpleViewer',
components: { components: {
GlIcon, GlIcon,
SourceEditor: () =>
import(/* webpackChunkName: 'SourceEditor' */ '~/vue_shared/components/source_editor.vue'),
}, },
mixins: [ViewerMixin, glFeatureFlagsMixin()], mixins: [ViewerMixin, glFeatureFlagsMixin()],
inject: ['blobHash'], inject: ['blobHash'],
...@@ -22,9 +20,6 @@ export default { ...@@ -22,9 +20,6 @@ export default {
lineNumbers() { lineNumbers() {
return this.content.split('\n').length; return this.content.split('\n').length;
}, },
refactorBlobViewerEnabled() {
return this.glFeatures.refactorBlobViewer;
},
}, },
mounted() { mounted() {
const { hash } = window.location; const { hash } = window.location;
...@@ -52,14 +47,8 @@ export default { ...@@ -52,14 +47,8 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<source-editor <div class="file-content code js-syntax-highlight" :class="$options.userColorScheme">
v-if="isRawContent && refactorBlobViewerEnabled" <div v-if="!hideLineNumbers" class="line-numbers">
:value="content"
:file-name="fileName"
:editor-options="{ readOnly: true }"
/>
<div v-else class="file-content code js-syntax-highlight" :class="$options.userColorScheme">
<div class="line-numbers">
<a <a
v-for="line in lineNumbers" v-for="line in lineNumbers"
:id="`L${line}`" :id="`L${line}`"
......
...@@ -43,6 +43,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -43,6 +43,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml) push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:consolidated_edit_button, @project, default_enabled: :yaml) push_frontend_feature_flag(:consolidated_edit_button, @project, default_enabled: :yaml)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks) push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
end end
......
...@@ -33,6 +33,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -33,6 +33,7 @@ class ProjectsController < Projects::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml) push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml) push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml) push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml)
end end
......
---
name: refactor_text_viewer
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70909
rollout_issue_url:
milestone: '14.4'
type: development
group: 'group::source code'
default_enabled: false
...@@ -159,8 +159,13 @@ describe('Blob content viewer component', () => { ...@@ -159,8 +159,13 @@ describe('Blob content viewer component', () => {
const findBlobContent = () => wrapper.findComponent(BlobContent); const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup); const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
beforeEach(() => {
gon.features = { refactorTextViewer: true };
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mockAxios.reset();
}); });
it('renders a GlLoadingIcon component', () => { it('renders a GlLoadingIcon component', () => {
...@@ -183,7 +188,6 @@ describe('Blob content viewer component', () => { ...@@ -183,7 +188,6 @@ describe('Blob content viewer component', () => {
it('renders a BlobContent component', () => { it('renders a BlobContent component', () => {
expect(findBlobContent().props('loading')).toEqual(false); expect(findBlobContent().props('loading')).toEqual(false);
expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true); expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({ expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'text', fileType: 'text',
...@@ -192,6 +196,16 @@ describe('Blob content viewer component', () => { ...@@ -192,6 +196,16 @@ describe('Blob content viewer component', () => {
renderError: null, renderError: null,
}); });
}); });
describe('legacy viewers', () => {
it('loads a legacy viewer when a viewer component is not available', async () => {
createComponentWithApollo({ blobs: { ...simpleMockData, fileType: 'unknown' } });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
});
});
}); });
describe('rich viewer', () => { describe('rich viewer', () => {
...@@ -210,7 +224,6 @@ describe('Blob content viewer component', () => { ...@@ -210,7 +224,6 @@ describe('Blob content viewer component', () => {
it('renders a BlobContent component', () => { it('renders a BlobContent component', () => {
expect(findBlobContent().props('loading')).toEqual(false); expect(findBlobContent().props('loading')).toEqual(false);
expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true); expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({ expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'markup', fileType: 'markup',
...@@ -241,18 +254,12 @@ describe('Blob content viewer component', () => { ...@@ -241,18 +254,12 @@ describe('Blob content viewer component', () => {
}); });
describe('legacy viewers', () => { describe('legacy viewers', () => {
it('does not load a legacy viewer when a rich viewer is not available', async () => { it('loads a legacy viewer when a viewer component is not available', async () => {
createComponentWithApollo({ blobs: simpleMockData }); createComponentWithApollo({ blobs: { ...richMockData, fileType: 'unknown' } });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(0);
});
it('loads a legacy viewer when a rich viewer is available', async () => {
createComponentWithApollo({ blobs: richMockData });
await waitForPromises(); await waitForPromises();
expect(mockAxios.history.get).toHaveLength(1); expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=rich');
}); });
}); });
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants'; import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue'; import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
describe('Blob Simple Viewer component', () => { describe('Blob Simple Viewer component', () => {
let wrapper; let wrapper;
const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`; const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
const blobHash = 'foo-bar'; const blobHash = 'foo-bar';
function createComponent( function createComponent(content = contentMock, isRawContent = false) {
content = contentMock,
isRawContent = false,
isRefactorFlagEnabled = false,
) {
wrapper = shallowMount(SimpleViewer, { wrapper = shallowMount(SimpleViewer, {
provide: { provide: {
blobHash, blobHash,
glFeatures: {
refactorBlobViewer: isRefactorFlagEnabled,
},
}, },
propsData: { propsData: {
content, content,
...@@ -94,32 +85,4 @@ describe('Blob Simple Viewer component', () => { ...@@ -94,32 +85,4 @@ describe('Blob Simple Viewer component', () => {
}); });
}); });
}); });
describe('Vue refactoring to use Source Editor', () => {
const findSourceEditor = () => wrapper.find(SourceEditor);
it.each`
doesRender | condition | isRawContent | isRefactorFlagEnabled
${'Does not'} | ${'rawContent is not specified'} | ${false} | ${true}
${'Does not'} | ${'feature flag is disabled is not specified'} | ${true} | ${false}
${'Does not'} | ${'both, the FF and rawContent are not specified'} | ${false} | ${false}
${'Does'} | ${'both, the FF and rawContent are specified'} | ${true} | ${true}
`(
'$doesRender render Source Editor component in readonly mode when $condition',
async ({ isRawContent, isRefactorFlagEnabled } = {}) => {
createComponent('raw content', isRawContent, isRefactorFlagEnabled);
await waitForPromises();
if (isRawContent && isRefactorFlagEnabled) {
expect(findSourceEditor().exists()).toBe(true);
expect(findSourceEditor().props('value')).toBe('raw content');
expect(findSourceEditor().props('fileName')).toBe('test.js');
expect(findSourceEditor().props('editorOptions')).toEqual({ readOnly: true });
} else {
expect(findSourceEditor().exists()).toBe(false);
}
},
);
});
}); });
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