Merge branch 'vue-repo-breadcrumbs' into 'master'
Added repository file listing breadcrumbs See merge request gitlab-org/gitlab-ce!28706
This commit is contained in:
commit
4ca791e697
14 changed files with 208 additions and 69 deletions
61
app/assets/javascripts/repository/components/breadcrumbs.vue
Normal file
61
app/assets/javascripts/repository/components/breadcrumbs.vue
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<script>
|
||||||
|
import getRefMixin from '../mixins/get_ref';
|
||||||
|
import getProjectShortPath from '../queries/getProjectShortPath.graphql';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
apollo: {
|
||||||
|
projectShortPath: {
|
||||||
|
query: getProjectShortPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mixins: [getRefMixin],
|
||||||
|
props: {
|
||||||
|
currentPath: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
projectShortPath: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pathLinks() {
|
||||||
|
return this.currentPath
|
||||||
|
.split('/')
|
||||||
|
.filter(p => p !== '')
|
||||||
|
.reduce(
|
||||||
|
(acc, name, i) => {
|
||||||
|
const path = `${i > 0 ? acc[i].path : ''}/${name}`;
|
||||||
|
|
||||||
|
return acc.concat({
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
to: `/tree/${this.ref}${path}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[{ name: this.projectShortPath, path: '/', to: `/tree/${this.ref}` }],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isLast(i) {
|
||||||
|
return i === this.pathLinks.length - 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav :aria-label="__('Files breadcrumb')">
|
||||||
|
<ol class="breadcrumb repo-breadcrumb">
|
||||||
|
<li v-for="(link, i) in pathLinks" :key="i" class="breadcrumb-item">
|
||||||
|
<router-link :to="link.to" :aria-current="isLast(i) ? 'page' : null">
|
||||||
|
{{ link.name }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</template>
|
|
@ -1,24 +1,27 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import createRouter from './router';
|
import createRouter from './router';
|
||||||
import App from './components/app.vue';
|
import App from './components/app.vue';
|
||||||
|
import Breadcrumbs from './components/breadcrumbs.vue';
|
||||||
import apolloProvider from './graphql';
|
import apolloProvider from './graphql';
|
||||||
import { setTitle } from './utils/title';
|
import { setTitle } from './utils/title';
|
||||||
|
|
||||||
export default function setupVueRepositoryList() {
|
export default function setupVueRepositoryList() {
|
||||||
const el = document.getElementById('js-tree-list');
|
const el = document.getElementById('js-tree-list');
|
||||||
const { projectPath, ref, fullName } = el.dataset;
|
const { projectPath, projectShortPath, ref, fullName } = el.dataset;
|
||||||
const router = createRouter(projectPath, ref);
|
const router = createRouter(projectPath, ref);
|
||||||
|
|
||||||
apolloProvider.clients.defaultClient.cache.writeData({
|
apolloProvider.clients.defaultClient.cache.writeData({
|
||||||
data: {
|
data: {
|
||||||
projectPath,
|
projectPath,
|
||||||
|
projectShortPath,
|
||||||
ref,
|
ref,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
router.afterEach(({ params: { pathMatch } }) => setTitle(pathMatch, ref, fullName));
|
router.afterEach(({ params: { pathMatch } }) => {
|
||||||
router.afterEach(to => {
|
const isRoot = pathMatch === undefined || pathMatch === '/';
|
||||||
const isRoot = to.params.pathMatch === undefined || to.params.pathMatch === '/';
|
|
||||||
|
setTitle(pathMatch, ref, fullName);
|
||||||
|
|
||||||
if (!isRoot) {
|
if (!isRoot) {
|
||||||
document
|
document
|
||||||
|
@ -31,6 +34,20 @@ export default function setupVueRepositoryList() {
|
||||||
.forEach(elem => elem.classList.toggle('hidden', !isRoot));
|
.forEach(elem => elem.classList.toggle('hidden', !isRoot));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Vue({
|
||||||
|
el: document.getElementById('js-repo-breadcrumb'),
|
||||||
|
router,
|
||||||
|
apolloProvider,
|
||||||
|
render(h) {
|
||||||
|
return h(Breadcrumbs, {
|
||||||
|
props: {
|
||||||
|
currentPath: this.$route.params.pathMatch,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el,
|
el,
|
||||||
router,
|
router,
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
query getProjectShortPath {
|
||||||
|
projectShortPath @client
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export const setTitle = (pathMatch, ref, project) => {
|
export const setTitle = (pathMatch, ref, project) => {
|
||||||
|
if (!pathMatch) return;
|
||||||
|
|
||||||
const path = pathMatch.replace(/^\//, '');
|
const path = pathMatch.replace(/^\//, '');
|
||||||
const isEmpty = path === '';
|
const isEmpty = path === '';
|
||||||
|
|
||||||
|
|
|
@ -655,4 +655,8 @@ module ProjectsHelper
|
||||||
project.builds_enabled? &&
|
project.builds_enabled? &&
|
||||||
!project.repository.gitlab_ci_yml
|
!project.repository.gitlab_ci_yml
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def vue_file_list_enabled?
|
||||||
|
Gitlab::Graphql.enabled? && Feature.enabled?(:vue_file_list, @project)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
- project = local_assigns.fetch(:project) { @project }
|
- project = local_assigns.fetch(:project) { @project }
|
||||||
- content_url = local_assigns.fetch(:content_url) { @tree.readme ? project_blob_path(@project, tree_join(@ref, @tree.readme.path)) : project_tree_path(@project, @ref) }
|
- content_url = local_assigns.fetch(:content_url) { @tree.readme ? project_blob_path(@project, tree_join(@ref, @tree.readme.path)) : project_tree_path(@project, @ref) }
|
||||||
- show_auto_devops_callout = show_auto_devops_callout?(@project)
|
- show_auto_devops_callout = show_auto_devops_callout?(@project)
|
||||||
- vue_file_list = Feature.enabled?(:vue_file_list, @project)
|
|
||||||
|
|
||||||
#tree-holder.tree-holder.clearfix
|
#tree-holder.tree-holder.clearfix
|
||||||
.nav-block
|
.nav-block
|
||||||
|
@ -14,11 +13,11 @@
|
||||||
= render 'shared/commit_well', commit: commit, ref: ref, project: project
|
= render 'shared/commit_well', commit: commit, ref: ref, project: project
|
||||||
|
|
||||||
- if is_project_overview
|
- if is_project_overview
|
||||||
.project-buttons.append-bottom-default{ class: ("js-keep-hidden-on-navigation" if vue_file_list) }
|
.project-buttons.append-bottom-default{ class: ("js-keep-hidden-on-navigation" if vue_file_list_enabled?) }
|
||||||
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
|
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
|
||||||
|
|
||||||
- if vue_file_list
|
- if vue_file_list_enabled?
|
||||||
#js-tree-list{ data: { project_path: @project.full_path, full_name: @project.name_with_namespace, ref: ref } }
|
#js-tree-list{ data: { project_path: @project.full_path, project_short_path: @project.path, ref: ref, full_name: @project.name_with_namespace } }
|
||||||
- if @tree.readme
|
- if @tree.readme
|
||||||
= render "projects/tree/readme", readme: @tree.readme
|
= render "projects/tree/readme", readme: @tree.readme
|
||||||
- else
|
- else
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- empty_repo = @project.empty_repo?
|
- empty_repo = @project.empty_repo?
|
||||||
- show_auto_devops_callout = show_auto_devops_callout?(@project)
|
- show_auto_devops_callout = show_auto_devops_callout?(@project)
|
||||||
- max_project_topic_length = 15
|
- max_project_topic_length = 15
|
||||||
.project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if Feature.enabled?(:vue_file_list, @project))] }
|
.project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if vue_file_list_enabled?)] }
|
||||||
.row.append-bottom-8
|
.row.append-bottom-8
|
||||||
.home-panel-title-row.col-md-12.col-lg-6.d-flex
|
.home-panel-title-row.col-md-12.col-lg-6.d-flex
|
||||||
.avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none
|
.avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- if readme.rich_viewer
|
- if readme.rich_viewer
|
||||||
%article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-hide-on-navigation" if Feature.enabled?(:vue_file_list, @project))] }
|
%article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-hide-on-navigation" if vue_file_list_enabled?)] }
|
||||||
.js-file-title.file-title
|
.js-file-title.file-title
|
||||||
= blob_icon readme.mode, readme.name
|
= blob_icon readme.mode, readme.name
|
||||||
= link_to project_blob_path(@project, tree_join(@ref, readme.path)) do
|
= link_to project_blob_path(@project, tree_join(@ref, readme.path)) do
|
||||||
|
|
|
@ -6,71 +6,74 @@
|
||||||
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
|
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
|
||||||
|
|
||||||
- if on_top_of_branch?
|
- if on_top_of_branch?
|
||||||
- addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown', 'data-boundary': 'window' }
|
- addtotree_toggle_attributes = { 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown', 'data-boundary': 'window' }
|
||||||
- else
|
- else
|
||||||
- addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
|
- addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
|
||||||
|
|
||||||
%ul.breadcrumb.repo-breadcrumb
|
- if vue_file_list_enabled?
|
||||||
%li.breadcrumb-item
|
#js-repo-breadcrumb
|
||||||
= link_to project_tree_path(@project, @ref) do
|
- else
|
||||||
= @project.path
|
%ul.breadcrumb.repo-breadcrumb
|
||||||
- path_breadcrumbs do |title, path|
|
|
||||||
%li.breadcrumb-item
|
%li.breadcrumb-item
|
||||||
= link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
|
= link_to project_tree_path(@project, @ref) do
|
||||||
|
= @project.path
|
||||||
|
- path_breadcrumbs do |title, path|
|
||||||
|
%li.breadcrumb-item
|
||||||
|
= link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
|
||||||
|
|
||||||
- if can_collaborate || can_create_mr_from_fork
|
- if can_collaborate || can_create_mr_from_fork
|
||||||
%li.breadcrumb-item
|
%li.breadcrumb-item
|
||||||
%a.btn.add-to-tree.qa-add-to-tree{ addtotree_toggle_attributes }
|
%button.btn.add-to-tree.qa-add-to-tree{ addtotree_toggle_attributes, type: 'button' }
|
||||||
= sprite_icon('plus', size: 16, css_class: 'float-left')
|
= sprite_icon('plus', size: 16, css_class: 'float-left')
|
||||||
= sprite_icon('arrow-down', size: 16, css_class: 'float-left')
|
= sprite_icon('arrow-down', size: 16, css_class: 'float-left')
|
||||||
- if on_top_of_branch?
|
- if on_top_of_branch?
|
||||||
.add-to-tree-dropdown
|
.add-to-tree-dropdown
|
||||||
%ul.dropdown-menu
|
%ul.dropdown-menu
|
||||||
- if can_edit_tree?
|
- if can_edit_tree?
|
||||||
%li.dropdown-header
|
%li.dropdown-header
|
||||||
#{ _('This directory') }
|
#{ _('This directory') }
|
||||||
%li
|
%li
|
||||||
= link_to project_new_blob_path(@project, @id), class: 'qa-new-file-option' do
|
= link_to project_new_blob_path(@project, @id), class: 'qa-new-file-option' do
|
||||||
#{ _('New file') }
|
#{ _('New file') }
|
||||||
%li
|
%li
|
||||||
= link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
|
= link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
|
||||||
#{ _('Upload file') }
|
#{ _('Upload file') }
|
||||||
%li
|
%li
|
||||||
= link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
|
= link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
|
||||||
#{ _('New directory') }
|
#{ _('New directory') }
|
||||||
- elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
|
- elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
|
||||||
%li
|
%li
|
||||||
- continue_params = { to: project_new_blob_path(@project, @id),
|
- continue_params = { to: project_new_blob_path(@project, @id),
|
||||||
notice: edit_in_new_fork_notice,
|
notice: edit_in_new_fork_notice,
|
||||||
notice_now: edit_in_new_fork_notice_now }
|
notice_now: edit_in_new_fork_notice_now }
|
||||||
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
|
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
|
||||||
= link_to fork_path, method: :post do
|
= link_to fork_path, method: :post do
|
||||||
#{ _('New file') }
|
#{ _('New file') }
|
||||||
%li
|
%li
|
||||||
- continue_params = { to: request.fullpath,
|
- continue_params = { to: request.fullpath,
|
||||||
notice: edit_in_new_fork_notice + " Try to upload a file again.",
|
notice: edit_in_new_fork_notice + " Try to upload a file again.",
|
||||||
notice_now: edit_in_new_fork_notice_now }
|
notice_now: edit_in_new_fork_notice_now }
|
||||||
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
|
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
|
||||||
= link_to fork_path, method: :post do
|
= link_to fork_path, method: :post do
|
||||||
#{ _('Upload file') }
|
#{ _('Upload file') }
|
||||||
%li
|
%li
|
||||||
- continue_params = { to: request.fullpath,
|
- continue_params = { to: request.fullpath,
|
||||||
notice: edit_in_new_fork_notice + " Try to create a new directory again.",
|
notice: edit_in_new_fork_notice + " Try to create a new directory again.",
|
||||||
notice_now: edit_in_new_fork_notice_now }
|
notice_now: edit_in_new_fork_notice_now }
|
||||||
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
|
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
|
||||||
= link_to fork_path, method: :post do
|
= link_to fork_path, method: :post do
|
||||||
#{ _('New directory') }
|
#{ _('New directory') }
|
||||||
|
|
||||||
- if can?(current_user, :push_code, @project)
|
- if can?(current_user, :push_code, @project)
|
||||||
%li.divider
|
%li.divider
|
||||||
%li.dropdown-header
|
%li.dropdown-header
|
||||||
#{ _('This repository') }
|
#{ _('This repository') }
|
||||||
%li
|
%li
|
||||||
= link_to new_project_branch_path(@project) do
|
= link_to new_project_branch_path(@project) do
|
||||||
#{ _('New branch') }
|
#{ _('New branch') }
|
||||||
%li
|
%li
|
||||||
= link_to new_project_tag_path(@project) do
|
= link_to new_project_tag_path(@project) do
|
||||||
#{ _('New tag') }
|
#{ _('New tag') }
|
||||||
|
|
||||||
.tree-controls
|
.tree-controls
|
||||||
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
|
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
|
||||||
|
|
|
@ -4386,6 +4386,9 @@ msgstr ""
|
||||||
msgid "Files"
|
msgid "Files"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Files breadcrumb"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Files, directories, and submodules in the path %{path} for commit reference %{ref}"
|
msgid "Files, directories, and submodules in the path %{path} for commit reference %{ref}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do
|
||||||
let(:project_maintainer) { project.owner }
|
let(:project_maintainer) { project.owner }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
stub_feature_flags(vue_file_list: false)
|
||||||
project.repository.delete_file(project_maintainer, 'LICENSE',
|
project.repository.delete_file(project_maintainer, 'LICENSE',
|
||||||
message: 'Remove LICENSE', branch_name: 'master')
|
message: 'Remove LICENSE', branch_name: 'master')
|
||||||
sign_in(project_maintainer)
|
sign_in(project_maintainer)
|
||||||
|
|
|
@ -12,6 +12,7 @@ describe 'Projects > Files > User creates files' do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
stub_feature_flags(vue_file_list: false)
|
||||||
stub_feature_flags(web_ide_default: false)
|
stub_feature_flags(web_ide_default: false)
|
||||||
|
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
|
|
|
@ -5,6 +5,7 @@ describe 'Projects > Show > Collaboration links' do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
stub_feature_flags(vue_file_list: false)
|
||||||
project.add_developer(user)
|
project.add_developer(user)
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
end
|
end
|
||||||
|
|
44
spec/frontend/repository/components/breadcrumbs_spec.js
Normal file
44
spec/frontend/repository/components/breadcrumbs_spec.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
|
||||||
|
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
|
||||||
|
|
||||||
|
let vm;
|
||||||
|
|
||||||
|
function factory(currentPath) {
|
||||||
|
vm = shallowMount(Breadcrumbs, {
|
||||||
|
propsData: {
|
||||||
|
currentPath,
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
RouterLink: RouterLinkStub,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Repository breadcrumbs component', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vm.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
path | linkCount
|
||||||
|
${'/'} | ${1}
|
||||||
|
${'app'} | ${2}
|
||||||
|
${'app/assets'} | ${3}
|
||||||
|
${'app/assets/javascripts'} | ${4}
|
||||||
|
`('renders $linkCount links for path $path', ({ path, linkCount }) => {
|
||||||
|
factory(path);
|
||||||
|
|
||||||
|
expect(vm.findAll(RouterLinkStub).length).toEqual(linkCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders last link as active', () => {
|
||||||
|
factory('app/assets');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
vm
|
||||||
|
.findAll(RouterLinkStub)
|
||||||
|
.at(2)
|
||||||
|
.attributes('aria-current'),
|
||||||
|
).toEqual('page');
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue