Commit e2e53c15 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'fix-code-inteligence' into 'master'

Init CodeIntelligence in the refactored blob viewer

See merge request gitlab-org/gitlab!81800
parents c9d6d99d 66c32b7f
......@@ -7,6 +7,23 @@ export default {
components: {
Popover,
},
props: {
codeNavigationPath: {
type: String,
required: false,
default: null,
},
blobPath: {
type: String,
required: false,
default: null,
},
pathPrefix: {
type: String,
required: false,
default: null,
},
},
computed: {
...mapState([
'currentDefinition',
......@@ -16,6 +33,14 @@ export default {
]),
},
mounted() {
if (this.codeNavigationPath && this.blobPath && this.pathPrefix) {
const initialData = {
blobs: [{ path: this.blobPath, codeNavigationPath: this.codeNavigationPath }],
definitionPathPrefix: this.pathPrefix,
};
this.setInitialData(initialData);
}
this.body = document.body;
eventHub.$on('showBlobInteractionZones', this.showBlobInteractionZones);
......@@ -28,7 +53,7 @@ export default {
this.removeGlobalEventListeners();
},
methods: {
...mapActions(['fetchData', 'showDefinition', 'showBlobInteractionZones']),
...mapActions(['fetchData', 'showDefinition', 'showBlobInteractionZones', 'setInitialData']),
addGlobalEventListeners() {
if (this.body) {
this.body.addEventListener('click', this.showDefinition);
......
import Vue from 'vue';
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import TableOfContents from '~/blob/components/table_contents.vue';
......@@ -11,7 +12,9 @@ import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import '~/sourcegraph/load';
import createStore from '~/code_navigation/store';
Vue.use(Vuex);
Vue.use(VueApollo);
Vue.use(VueRouter);
......@@ -29,6 +32,7 @@ if (viewBlobEl) {
// eslint-disable-next-line no-new
new Vue({
el: viewBlobEl,
store: createStore(),
router,
apolloProvider,
provide: {
......@@ -78,7 +82,7 @@ GpgBadges.fetch();
const codeNavEl = document.getElementById('js-code-navigation');
if (codeNavEl) {
if (codeNavEl && !viewBlobEl) {
const { codeNavigationPath, blobPath, definitionPathPrefix } = codeNavEl.dataset;
// eslint-disable-next-line promise/catch-or-return
......
......@@ -11,6 +11,7 @@ import { __ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import CodeIntelligence from '~/code_navigation/components/app.vue';
import getRefMixin from '../mixins/get_ref';
import blobInfoQuery from '../queries/blob_info.query.graphql';
import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE } from '../constants';
......@@ -30,6 +31,7 @@ export default {
GlButton,
ForkSuggestion,
WebIdeLink,
CodeIntelligence,
},
mixins: [getRefMixin, glFeatureFlagMixin()],
inject: {
......@@ -274,6 +276,12 @@ export default {
:loading="isLoadingLegacyViewer"
/>
<component :is="blobViewer" v-else :blob="blobInfo" class="blob-viewer" />
<code-intelligence
v-if="blobViewer || legacyViewerLoaded"
:code-navigation-path="blobInfo.codeNavigationPath"
:blob-path="blobInfo.path"
:path-prefix="blobInfo.projectBlobPathRoot"
/>
</div>
</div>
</template>
......@@ -52,6 +52,8 @@ export const DEFAULT_BLOB_INFO = {
ideEditPath: '',
forkAndEditPath: '',
ideForkAndEditPath: '',
codeNavigationPath: '',
projectBlobPathRoot: '',
forkAndViewPath: '',
storedExternally: false,
externalStorage: '',
......
import { GlButton } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import PerformancePlugin from '~/performance/vue_performance_plugin';
import createStore from '~/code_navigation/store';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
......@@ -19,6 +21,7 @@ import createRouter from './router';
import { updateFormAction } from './utils/dom';
import { setTitle } from './utils/title';
Vue.use(Vuex);
Vue.use(PerformancePlugin, {
components: ['SimpleViewer', 'BlobContent'],
});
......@@ -200,6 +203,7 @@ export default function setupVueRepositoryList() {
// eslint-disable-next-line no-new
new Vue({
el,
store: createStore(),
router,
apolloProvider,
render(h) {
......
......@@ -31,6 +31,8 @@ query getBlobInfo(
ideEditPath
forkAndEditPath
ideForkAndEditPath
codeNavigationPath
projectBlobPathRoot
forkAndViewPath
environmentFormattedExternalUrl
environmentExternalUrlForRouteMap
......
......@@ -134,6 +134,12 @@ module Types
null: true,
calls_gitaly: true
field :code_navigation_path, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Web path for code navigation.'
field :project_blob_path_root, GraphQL::Types::String, null: true,
description: 'Web path for the root of the blob.'
def raw_text_blob
object.data unless object.binary?
end
......
......@@ -131,6 +131,14 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
external_storage_url_or_path(url_helpers.project_raw_url(project, ref_qualified_path), project)
end
def code_navigation_path
Gitlab::CodeNavigationPath.new(project, blob.commit_id).full_json_path_for(blob.path)
end
def project_blob_path_root
project_blob_path(project, blob.commit_id)
end
private
def url_helpers
......
......@@ -15034,6 +15034,7 @@ Returns [`Tree`](#tree).
| <a id="repositoryblobblamepath"></a>`blamePath` | [`String`](#string) | Web path to blob blame page. |
| <a id="repositoryblobcancurrentuserpushtobranch"></a>`canCurrentUserPushToBranch` | [`Boolean`](#boolean) | Whether the current user can push to the branch. |
| <a id="repositoryblobcanmodifyblob"></a>`canModifyBlob` | [`Boolean`](#boolean) | Whether the current user can modify the blob. |
| <a id="repositoryblobcodenavigationpath"></a>`codeNavigationPath` | [`String`](#string) | Web path for code navigation. |
| <a id="repositoryblobcodeowners"></a>`codeOwners` | [`[UserCore!]`](#usercore) | List of code owners for the blob. |
| <a id="repositoryblobeditblobpath"></a>`editBlobPath` | [`String`](#string) | Web path to edit the blob in the old-style editor. |
| <a id="repositoryblobenvironmentexternalurlforroutemap"></a>`environmentExternalUrlForRouteMap` | [`String`](#string) | Web path to blob on an environment. |
......@@ -15057,6 +15058,7 @@ Returns [`Tree`](#tree).
| <a id="repositoryblobpermalinkpath"></a>`permalinkPath` | [`String`](#string) | Web path to blob permalink. |
| <a id="repositoryblobpipelineeditorpath"></a>`pipelineEditorPath` | [`String`](#string) | Web path to edit .gitlab-ci.yml file. |
| <a id="repositoryblobplaindata"></a>`plainData` | [`String`](#string) | Blob plain highlighted data. |
| <a id="repositoryblobprojectblobpathroot"></a>`projectBlobPathRoot` | [`String`](#string) | Web path for the root of the blob. |
| <a id="repositoryblobrawblob"></a>`rawBlob` | [`String`](#string) | Raw content of the blob. |
| <a id="repositoryblobrawpath"></a>`rawPath` | [`String`](#string) | Web path to download the raw blob. |
| <a id="repositoryblobrawsize"></a>`rawSize` | [`Int`](#int) | Size (in bytes) of the blob, or the blob target if stored externally. |
import Vue from 'vue';
import Vuex from 'vuex';
import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
import axios from 'axios';
......@@ -28,6 +29,9 @@ let mockResolver;
Vue.use(VueApollo);
const createMockStore = () =>
new Vuex.Store({ actions: { fetchData: jest.fn, setInitialData: jest.fn() } });
const createComponent = async (mockData = {}) => {
const {
blob = simpleViewerMock,
......@@ -58,6 +62,7 @@ const createComponent = async (mockData = {}) => {
const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]);
wrapper = mountExtended(BlobContentViewer, {
store: createMockStore(),
router,
apolloProvider: fakeApollo,
propsData: {
......
......@@ -5,13 +5,14 @@ import App from '~/code_navigation/components/app.vue';
import Popover from '~/code_navigation/components/popover.vue';
import createState from '~/code_navigation/store/state';
const setInitialData = jest.fn();
const fetchData = jest.fn();
const showDefinition = jest.fn();
let wrapper;
Vue.use(Vuex);
function factory(initialState = {}) {
function factory(initialState = {}, props = {}) {
const store = new Vuex.Store({
state: {
...createState(),
......@@ -19,12 +20,13 @@ function factory(initialState = {}) {
definitionPathPrefix: 'https://test.com/blob/main',
},
actions: {
setInitialData,
fetchData,
showDefinition,
},
});
wrapper = shallowMount(App, { store });
wrapper = shallowMount(App, { store, propsData: { ...props } });
}
describe('Code navigation app component', () => {
......@@ -32,6 +34,19 @@ describe('Code navigation app component', () => {
wrapper.destroy();
});
it('sets initial data on mount if the correct props are passed', () => {
const codeNavigationPath = 'code/nav/path.js';
const path = 'blob/path.js';
const definitionPathPrefix = 'path/prefix';
factory({}, { codeNavigationPath, blobPath: path, pathPrefix: definitionPathPrefix });
expect(setInitialData).toHaveBeenCalledWith(expect.anything(), {
blobs: [{ codeNavigationPath, path }],
definitionPathPrefix,
});
});
it('fetches data on mount', () => {
factory();
......
import { GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import Vue, { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
......@@ -17,9 +18,11 @@ import DownloadViewer from '~/repository/components/blob_viewers/download_viewer
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
import CodeIntelligence from '~/code_navigation/components/app.vue';
import { redirectTo } from '~/lib/utils/url_utility';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import {
simpleViewerMock,
richViewerMock,
......@@ -38,6 +41,9 @@ let mockResolver;
const mockAxios = new MockAdapter(axios);
const createMockStore = () =>
new Vuex.Store({ actions: { fetchData: jest.fn, setInitialData: jest.fn() } });
const createComponent = async (mockData = {}, mountFn = shallowMount) => {
Vue.use(VueApollo);
......@@ -75,6 +81,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
mountFn(BlobContentViewer, {
store: createMockStore(),
apolloProvider: fakeApollo,
propsData: propsMock,
mixins: [{ data: () => ({ ref: refMock }) }],
......@@ -104,6 +111,7 @@ describe('Blob content viewer component', () => {
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
const findCodeIntelligence = () => wrapper.findComponent(CodeIntelligence);
beforeEach(() => {
isLoggedIn.mockReturnValue(true);
......@@ -219,6 +227,26 @@ describe('Blob content viewer component', () => {
loadViewer.mockRestore();
});
it('renders a CodeIntelligence component with the correct props', async () => {
loadViewer.mockReturnValue(SourceViewer);
await createComponent();
expect(findCodeIntelligence().props()).toMatchObject({
codeNavigationPath: simpleViewerMock.codeNavigationPath,
blobPath: simpleViewerMock.path,
pathPrefix: simpleViewerMock.projectBlobPathRoot,
});
});
it('does not load a CodeIntelligence component when no viewers are loaded', async () => {
const url = 'some_file.js?format=json&viewer=rich';
mockAxios.onGet(url).replyOnce(httpStatusCodes.INTERNAL_SERVER_ERROR);
await createComponent({ blob: { ...richViewerMock, fileType: 'unknown' } });
expect(findCodeIntelligence().exists()).toBe(false);
});
it('does not render a BlobContent component if a Blob viewer is available', async () => {
loadViewer.mockReturnValue(() => true);
await createComponent({ blob: richViewerMock });
......
......@@ -13,6 +13,8 @@ export const simpleViewerMock = {
forkAndEditPath: 'some_file.js/fork/edit',
ideForkAndEditPath: 'some_file.js/fork/ide',
forkAndViewPath: 'some_file.js/fork/view',
codeNavigationPath: '',
projectBlobPathRoot: '',
environmentFormattedExternalUrl: '',
environmentExternalUrlForRouteMap: '',
canModifyBlob: true,
......
......@@ -31,6 +31,8 @@ RSpec.describe Types::Repository::BlobType do
:permalink_path,
:environment_formatted_external_url,
:environment_external_url_for_route_map,
:code_navigation_path,
:project_blob_path_root,
:code_owners,
:simple_viewer,
:rich_viewer,
......
......@@ -154,6 +154,16 @@ RSpec.describe BlobPresenter do
end
end
describe '#code_navigation_path' do
let(:code_navigation_path) { Gitlab::CodeNavigationPath.new(project, blob.commit_id).full_json_path_for(blob.path) }
it { expect(presenter.code_navigation_path).to eq(code_navigation_path) }
end
describe '#project_blob_path_root' do
it { expect(presenter.project_blob_path_root).to eq("/#{project.full_path}/-/blob/HEAD") }
end
context 'given a Gitlab::Graphql::Representation::TreeEntry' do
let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(super(), repository) }
......
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