Commit 0ceeb713 authored by Jacques Erasmus's avatar Jacques Erasmus Committed by Savas Vedova

Blob refactor: Show correct control buttons when viewing a blob

parent ccf33f48
<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'; ...@@ -9,6 +9,7 @@ import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue'; import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue'; import DirectoryDownloadLinks from './components/directory_download_links.vue';
import LastCommit from './components/last_commit.vue'; import LastCommit from './components/last_commit.vue';
import BlobControls from './components/blob_controls.vue';
import apolloProvider from './graphql'; import apolloProvider from './graphql';
import commitsQuery from './queries/commits.query.graphql'; import commitsQuery from './queries/commits.query.graphql';
import projectPathQuery from './queries/project_path.query.graphql'; import projectPathQuery from './queries/project_path.query.graphql';
...@@ -71,8 +72,26 @@ export default function setupVueRepositoryList() { ...@@ -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(); initLastCommitApp();
if (gon.features.refactorBlobViewer) {
initBlobControlsApp();
}
router.afterEach(({ params: { path } }) => { router.afterEach(({ params: { path } }) => {
setTitle(path, ref, fullName); 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 ...@@ -74,6 +74,19 @@ module Types
field :pipeline_editor_path, GraphQL::Types::String, null: true, field :pipeline_editor_path, GraphQL::Types::String, null: true,
description: 'Web path to edit .gitlab-ci.yml file.' 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, field :code_owners, [Types::UserType], null: true,
description: 'List of code owners for the blob.', description: 'List of code owners for the blob.',
calls_gitaly: true calls_gitaly: true
......
...@@ -63,6 +63,22 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated ...@@ -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 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 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 # Will be overridden in EE
def code_owners def code_owners
[] []
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#js-repo-breadcrumb{ data: breadcrumb_data_attributes } #js-repo-breadcrumb{ data: breadcrumb_data_attributes }
#js-blob-controls
.tree-controls .tree-controls
.d-block.d-sm-flex.flex-wrap.align-items-start.gl-children-ml-sm-3< .d-block.d-sm-flex.flex-wrap.align-items-start.gl-children-ml-sm-3<
= render_if_exists 'projects/tree/lock_link' = render_if_exists 'projects/tree/lock_link'
......
...@@ -14348,6 +14348,7 @@ Returns [`Tree`](#tree). ...@@ -14348,6 +14348,7 @@ Returns [`Tree`](#tree).
| Name | Type | Description | | 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="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="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. | | <a id="repositoryblobcodeowners"></a>`codeOwners` | [`[UserCore!]`](#usercore) | List of code owners for the blob. |
...@@ -14355,7 +14356,9 @@ Returns [`Tree`](#tree). ...@@ -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="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="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="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="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="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="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. | | <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). ...@@ -14364,6 +14367,7 @@ Returns [`Tree`](#tree).
| <a id="repositoryblobname"></a>`name` | [`String`](#string) | Blob name. | | <a id="repositoryblobname"></a>`name` | [`String`](#string) | Blob name. |
| <a id="repositorybloboid"></a>`oid` | [`String!`](#string) | OID of the blob. | | <a id="repositorybloboid"></a>`oid` | [`String!`](#string) | OID of the blob. |
| <a id="repositoryblobpath"></a>`path` | [`String!`](#string) | Path 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="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="repositoryblobplaindata"></a>`plainData` | [`String`](#string) | Blob plain highlighted data. |
| <a id="repositoryblobrawblob"></a>`rawBlob` | [`String`](#string) | Raw content of the blob. | | <a id="repositoryblobrawblob"></a>`rawBlob` | [`String`](#string) | Raw content of the blob. |
...@@ -3875,6 +3875,9 @@ msgstr "" ...@@ -3875,6 +3875,9 @@ msgstr ""
msgid "An error occurred while loading the access tokens form, please try again." msgid "An error occurred while loading the access tokens form, please try again."
msgstr "" msgstr ""
msgid "An error occurred while loading the blob controls."
msgstr ""
msgid "An error occurred while loading the data. Please try again." msgid "An error occurred while loading the data. Please try again."
msgstr "" msgstr ""
...@@ -25663,6 +25666,9 @@ msgstr "" ...@@ -25663,6 +25666,9 @@ msgstr ""
msgid "Period in seconds" msgid "Period in seconds"
msgstr "" msgstr ""
msgid "Permalink"
msgstr ""
msgid "Permanently delete project" msgid "Permanently delete project"
msgstr "" 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 = { ...@@ -64,3 +64,20 @@ export const projectMock = {
export const propsMock = { path: 'some_file.js', projectPath: 'some/path' }; export const propsMock = { path: 'some_file.js', projectPath: 'some/path' };
export const refMock = 'default-ref'; 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 ...@@ -25,6 +25,10 @@ RSpec.describe Types::Repository::BlobType do
:raw_path, :raw_path,
:replace_path, :replace_path,
:pipeline_editor_path, :pipeline_editor_path,
:find_file_path,
:blame_path,
:history_path,
:permalink_path,
:code_owners, :code_owners,
:simple_viewer, :simple_viewer,
:rich_viewer, :rich_viewer,
......
...@@ -67,6 +67,22 @@ RSpec.describe BlobPresenter do ...@@ -67,6 +67,22 @@ RSpec.describe BlobPresenter do
end end
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 describe '#code_owners' do
it { expect(presenter.code_owners).to match_array([]) } it { expect(presenter.code_owners).to match_array([]) }
end 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