Commit 2b2f2b34 authored by Savas Vedova's avatar Savas Vedova

Merge branch '348304-blob-conntrols-component' into 'master'

Blob refactor: Show correct control buttons when viewing a blob

See merge request gitlab-org/gitlab!77110
parents 39c9ceef 0ceeb713
<script>
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import createFlash from '~/flash';
import getRefMixin from '~/repository/mixins/get_ref';
import { updateElementsVisibility } from '../utils/dom';
import blobControlsQuery from '../queries/blob_controls.query.graphql';
export default {
i18n: {
findFile: __('Find file'),
blame: __('Blame'),
history: __('History'),
permalink: __('Permalink'),
errorMessage: __('An error occurred while loading the blob controls.'),
},
buttonClassList: 'gl-sm-w-auto gl-w-full gl-sm-mt-0 gl-mt-3',
components: {
GlButton,
},
mixins: [getRefMixin],
apollo: {
project: {
query: blobControlsQuery,
variables() {
return {
projectPath: this.projectPath,
filePath: this.filePath,
ref: this.ref,
};
},
skip() {
return !this.filePath;
},
error() {
createFlash({ message: this.$options.i18n.errorMessage });
},
},
},
props: {
projectPath: {
type: String,
required: true,
},
},
data() {
return {
project: {
repository: {
blobs: {
nodes: [
{
findFilePath: null,
blamePath: null,
historyPath: null,
permalinkPath: null,
},
],
},
},
},
};
},
computed: {
filePath() {
const { path } = this.$route.params;
updateElementsVisibility('.tree-controls', !path);
return path;
},
blobInfo() {
return this.project?.repository?.blobs?.nodes[0] || {};
},
},
};
</script>
<template>
<div v-if="filePath">
<gl-button data-testid="find" :href="blobInfo.findFilePath" :class="$options.buttonClassList">
{{ $options.i18n.findFile }}
</gl-button>
<gl-button data-testid="blame" :href="blobInfo.blamePath" :class="$options.buttonClassList">
{{ $options.i18n.blame }}
</gl-button>
<gl-button data-testid="history" :href="blobInfo.historyPath" :class="$options.buttonClassList">
{{ $options.i18n.history }}
</gl-button>
<gl-button
data-testid="permalink"
:href="blobInfo.permalinkPath"
:class="$options.buttonClassList"
>
{{ $options.i18n.permalink }}
</gl-button>
</div>
</template>
......@@ -9,6 +9,7 @@ import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
import LastCommit from './components/last_commit.vue';
import BlobControls from './components/blob_controls.vue';
import apolloProvider from './graphql';
import commitsQuery from './queries/commits.query.graphql';
import projectPathQuery from './queries/project_path.query.graphql';
......@@ -71,8 +72,26 @@ export default function setupVueRepositoryList() {
},
});
const initBlobControlsApp = () =>
new Vue({
el: document.getElementById('js-blob-controls'),
router,
apolloProvider,
render(h) {
return h(BlobControls, {
props: {
projectPath,
},
});
},
});
initLastCommitApp();
if (gon.features.refactorBlobViewer) {
initBlobControlsApp();
}
router.afterEach(({ params: { path } }) => {
setTitle(path, ref, fullName);
});
......
query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!) {
project(fullPath: $projectPath) {
id
repository {
blobs(paths: [$filePath], ref: $ref) {
nodes {
id
findFilePath
blamePath
historyPath
permalinkPath
}
}
}
}
}
......@@ -74,6 +74,19 @@ module Types
field :pipeline_editor_path, GraphQL::Types::String, null: true,
description: 'Web path to edit .gitlab-ci.yml file.'
field :find_file_path, GraphQL::Types::String, null: true,
description: 'Web path to find file.'
field :blame_path, GraphQL::Types::String, null: true,
description: 'Web path to blob blame page.'
field :history_path, GraphQL::Types::String, null: true,
description: 'Web path to blob history page.'
field :permalink_path, GraphQL::Types::String, null: true,
description: 'Web path to blob permalink.',
calls_gitaly: true
field :code_owners, [Types::UserType], null: true,
description: 'List of code owners for the blob.',
calls_gitaly: true
......
......@@ -63,6 +63,22 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
project_ci_pipeline_editor_path(project, branch_name: blob.commit_id) if can_collaborate_with_project?(project) && blob.path == project.ci_config_path_or_default
end
def find_file_path
url_helpers.project_find_file_path(project, ref_qualified_path)
end
def blame_path
url_helpers.project_blame_path(project, ref_qualified_path)
end
def history_path
url_helpers.project_commits_path(project, ref_qualified_path)
end
def permalink_path
url_helpers.project_blob_path(project, File.join(project.repository.commit.sha, blob.path))
end
# Will be overridden in EE
def code_owners
[]
......
......@@ -4,6 +4,7 @@
#js-repo-breadcrumb{ data: breadcrumb_data_attributes }
#js-blob-controls
.tree-controls
.d-block.d-sm-flex.flex-wrap.align-items-start.gl-children-ml-sm-3<
= render_if_exists 'projects/tree/lock_link'
......
......@@ -14348,6 +14348,7 @@ Returns [`Tree`](#tree).
| Name | Type | Description |
| ---- | ---- | ----------- |
| <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="repositoryblobcodeowners"></a>`codeOwners` | [`[UserCore!]`](#usercore) | List of code owners for the blob. |
......@@ -14355,7 +14356,9 @@ Returns [`Tree`](#tree).
| <a id="repositoryblobexternalstorage"></a>`externalStorage` | [`String`](#string) | External storage being used, if enabled (for instance, 'LFS'). |
| <a id="repositoryblobexternalstorageurl"></a>`externalStorageUrl` | [`String`](#string) | Web path to download the raw blob via external storage, if enabled. |
| <a id="repositoryblobfiletype"></a>`fileType` | [`String`](#string) | Expected format of the blob based on the extension. |
| <a id="repositoryblobfindfilepath"></a>`findFilePath` | [`String`](#string) | Web path to find file. |
| <a id="repositoryblobforkandeditpath"></a>`forkAndEditPath` | [`String`](#string) | Web path to edit this blob using a forked project. |
| <a id="repositoryblobhistorypath"></a>`historyPath` | [`String`](#string) | Web path to blob history page. |
| <a id="repositoryblobid"></a>`id` | [`ID!`](#id) | ID of the blob. |
| <a id="repositoryblobideeditpath"></a>`ideEditPath` | [`String`](#string) | Web path to edit this blob in the Web IDE. |
| <a id="repositoryblobideforkandeditpath"></a>`ideForkAndEditPath` | [`String`](#string) | Web path to edit this blob in the Web IDE using a forked project. |
......@@ -14364,6 +14367,7 @@ Returns [`Tree`](#tree).
| <a id="repositoryblobname"></a>`name` | [`String`](#string) | Blob name. |
| <a id="repositorybloboid"></a>`oid` | [`String!`](#string) | OID of the blob. |
| <a id="repositoryblobpath"></a>`path` | [`String!`](#string) | Path of the blob. |
| <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="repositoryblobrawblob"></a>`rawBlob` | [`String`](#string) | Raw content of the blob. |
......@@ -3875,6 +3875,9 @@ msgstr ""
msgid "An error occurred while loading the access tokens form, please try again."
msgstr ""
msgid "An error occurred while loading the blob controls."
msgstr ""
msgid "An error occurred while loading the data. Please try again."
msgstr ""
......@@ -25663,6 +25666,9 @@ msgstr ""
msgid "Period in seconds"
msgstr ""
msgid "Permalink"
msgstr ""
msgid "Permanently delete project"
msgstr ""
......
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import BlobControls from '~/repository/components/blob_controls.vue';
import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createRouter from '~/repository/router';
import { blobControlsDataMock, refMock } from '../mock_data';
let router;
let wrapper;
let mockResolver;
const localVue = createLocalVue();
const createComponent = async () => {
localVue.use(VueApollo);
const project = { ...blobControlsDataMock };
const projectPath = 'some/project';
router = createRouter(projectPath, refMock);
router.replace({ name: 'blobPath', params: { path: '/some/file.js' } });
mockResolver = jest.fn().mockResolvedValue({ data: { project } });
wrapper = shallowMountExtended(BlobControls, {
localVue,
router,
apolloProvider: createMockApollo([[blobControlsQuery, mockResolver]]),
propsData: { projectPath },
mixins: [{ data: () => ({ ref: refMock }) }],
});
await waitForPromises();
};
describe('Blob controls component', () => {
const findFindButton = () => wrapper.findByTestId('find');
const findBlameButton = () => wrapper.findByTestId('blame');
const findHistoryButton = () => wrapper.findByTestId('history');
const findPermalinkButton = () => wrapper.findByTestId('permalink');
beforeEach(() => createComponent());
afterEach(() => wrapper.destroy());
it('renders a find button with the correct href', () => {
expect(findFindButton().attributes('href')).toBe('find/file.js');
});
it('renders a blame button with the correct href', () => {
expect(findBlameButton().attributes('href')).toBe('blame/file.js');
});
it('renders a history button with the correct href', () => {
expect(findHistoryButton().attributes('href')).toBe('history/file.js');
});
it('renders a permalink button with the correct href', () => {
expect(findPermalinkButton().attributes('href')).toBe('permalink/file.js');
});
it('does not render any buttons if no filePath is provided', async () => {
router.replace({ name: 'blobPath', params: { path: null } });
await nextTick();
expect(findFindButton().exists()).toBe(false);
expect(findBlameButton().exists()).toBe(false);
expect(findHistoryButton().exists()).toBe(false);
expect(findPermalinkButton().exists()).toBe(false);
});
});
......@@ -64,3 +64,20 @@ export const projectMock = {
export const propsMock = { path: 'some_file.js', projectPath: 'some/path' };
export const refMock = 'default-ref';
export const blobControlsDataMock = {
id: '1234',
repository: {
blobs: {
nodes: [
{
id: '5678',
findFilePath: 'find/file.js',
blamePath: 'blame/file.js',
historyPath: 'history/file.js',
permalinkPath: 'permalink/file.js',
},
],
},
},
};
......@@ -25,6 +25,10 @@ RSpec.describe Types::Repository::BlobType do
:raw_path,
:replace_path,
:pipeline_editor_path,
:find_file_path,
:blame_path,
:history_path,
:permalink_path,
:code_owners,
:simple_viewer,
:rich_viewer,
......
......@@ -67,6 +67,22 @@ RSpec.describe BlobPresenter do
end
end
describe '#find_file_path' do
it { expect(presenter.find_file_path).to eq("/#{project.full_path}/-/find_file/HEAD/files/ruby/regex.rb") }
end
describe '#blame_path' do
it { expect(presenter.blame_path).to eq("/#{project.full_path}/-/blame/HEAD/files/ruby/regex.rb") }
end
describe '#history_path' do
it { expect(presenter.history_path).to eq("/#{project.full_path}/-/commits/HEAD/files/ruby/regex.rb") }
end
describe '#permalink_path' do
it { expect(presenter.permalink_path).to eq("/#{project.full_path}/-/blob/#{project.repository.commit.sha}/files/ruby/regex.rb") }
end
describe '#code_owners' do
it { expect(presenter.code_owners).to match_array([]) }
end
......
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