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 {
type: Object,
required: true,
},
hideLineNumbers: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
viewer() {
......@@ -80,6 +85,7 @@ export default {
:is-raw-content="isRawContent"
:file-name="blob.name"
:type="activeViewer.fileType"
:hide-line-numbers="hideLineNumbers"
data-qa-selector="file_content"
/>
</template>
......
......@@ -42,9 +42,6 @@ export default {
this.switchViewer(
this.hasRichViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
);
if (this.hasRichViewer && !this.blobViewer) {
this.loadLegacyViewer();
}
},
error() {
this.displayError();
......@@ -69,6 +66,7 @@ export default {
data() {
return {
legacyRichViewer: null,
legacySimpleViewer: null,
isBinary: false,
isLoadingLegacyViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER,
......@@ -115,7 +113,7 @@ export default {
return isLoggedIn();
},
isLoading() {
return this.$apollo.queries.project.loading || this.isLoadingLegacyViewer;
return this.$apollo.queries.project.loading;
},
isBinaryFileType() {
return this.isBinary || this.blobInfo.simpleViewer?.fileType !== 'text';
......@@ -153,22 +151,41 @@ export default {
},
},
methods: {
loadLegacyViewer() {
loadLegacyViewer(type) {
if (this.legacyViewerLoaded(type)) {
return;
}
this.isLoadingLegacyViewer = true;
axios
.get(`${this.blobInfo.webPath}?format=json&viewer=rich`)
.get(`${this.blobInfo.webPath}?format=json&viewer=${type}`)
.then(({ data: { html, binary } }) => {
this.legacyRichViewer = html;
if (type === 'simple') {
this.legacySimpleViewer = html;
} else {
this.legacyRichViewer = html;
}
this.isBinary = binary;
this.isLoadingLegacyViewer = false;
})
.catch(() => this.displayError());
},
legacyViewerLoaded(type) {
return (
(type === SIMPLE_BLOB_VIEWER && this.legacySimpleViewer) ||
(type === RICH_BLOB_VIEWER && this.legacyRichViewer)
);
},
displayError() {
createFlash({ message: __('An error occurred while loading the file. Please try again.') });
},
switchViewer(newViewer) {
this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER;
if (!this.blobViewer) {
this.loadLegacyViewer(this.activeViewerType);
}
},
},
};
......@@ -210,10 +227,11 @@ export default {
v-if="!blobViewer"
:rich-viewer="legacyRichViewer"
:blob="blobInfo"
:content="blobInfo.rawTextBlob"
:content="legacySimpleViewer"
:is-raw-content="true"
:active-viewer="viewer"
:loading="false"
:hide-line-numbers="true"
:loading="isLoadingLegacyViewer"
/>
<component :is="blobViewer" v-else v-bind="viewerProps" class="blob-viewer" />
</div>
......
......@@ -3,7 +3,9 @@ export const loadViewer = (type) => {
case 'empty':
return () => import(/* webpackChunkName: 'blob_empty_viewer' */ './empty_viewer.vue');
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':
return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
case 'image':
......
......@@ -27,6 +27,11 @@ export default {
required: false,
default: '',
},
hideLineNumbers: {
type: Boolean,
required: false,
default: false,
},
},
mounted() {
eventHub.$emit(SNIPPET_MEASURE_BLOBS_CONTENT);
......
......@@ -8,8 +8,6 @@ export default {
name: 'SimpleViewer',
components: {
GlIcon,
SourceEditor: () =>
import(/* webpackChunkName: 'SourceEditor' */ '~/vue_shared/components/source_editor.vue'),
},
mixins: [ViewerMixin, glFeatureFlagsMixin()],
inject: ['blobHash'],
......@@ -22,9 +20,6 @@ export default {
lineNumbers() {
return this.content.split('\n').length;
},
refactorBlobViewerEnabled() {
return this.glFeatures.refactorBlobViewer;
},
},
mounted() {
const { hash } = window.location;
......@@ -52,14 +47,8 @@ export default {
</script>
<template>
<div>
<source-editor
v-if="isRawContent && refactorBlobViewerEnabled"
: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">
<div class="file-content code js-syntax-highlight" :class="$options.userColorScheme">
<div v-if="!hideLineNumbers" class="line-numbers">
<a
v-for="line in lineNumbers"
:id="`L${line}`"
......
......@@ -43,6 +43,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action do
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_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
end
......
......@@ -33,6 +33,7 @@ class ProjectsController < Projects::ApplicationController
before_action do
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(:paginated_tree_graphql_query, @project, default_enabled: :yaml)
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', () => {
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
beforeEach(() => {
gon.features = { refactorTextViewer: true };
});
afterEach(() => {
wrapper.destroy();
mockAxios.reset();
});
it('renders a GlLoadingIcon component', () => {
......@@ -183,7 +188,6 @@ describe('Blob content viewer component', () => {
it('renders a BlobContent component', () => {
expect(findBlobContent().props('loading')).toEqual(false);
expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'text',
......@@ -192,6 +196,16 @@ describe('Blob content viewer component', () => {
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', () => {
......@@ -210,7 +224,6 @@ describe('Blob content viewer component', () => {
it('renders a BlobContent component', () => {
expect(findBlobContent().props('loading')).toEqual(false);
expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'markup',
......@@ -241,18 +254,12 @@ describe('Blob content viewer component', () => {
});
describe('legacy viewers', () => {
it('does not load a legacy viewer when a rich viewer is not available', async () => {
createComponentWithApollo({ blobs: simpleMockData });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(0);
});
it('loads a legacy viewer when a rich viewer is available', async () => {
createComponentWithApollo({ blobs: richMockData });
it('loads a legacy viewer when a viewer component is not available', async () => {
createComponentWithApollo({ blobs: { ...richMockData, fileType: 'unknown' } });
await waitForPromises();
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 waitForPromises from 'helpers/wait_for_promises';
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
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', () => {
let wrapper;
const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
const blobHash = 'foo-bar';
function createComponent(
content = contentMock,
isRawContent = false,
isRefactorFlagEnabled = false,
) {
function createComponent(content = contentMock, isRawContent = false) {
wrapper = shallowMount(SimpleViewer, {
provide: {
blobHash,
glFeatures: {
refactorBlobViewer: isRefactorFlagEnabled,
},
},
propsData: {
content,
......@@ -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