diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 70918dd55e4b631ae8ef1ba470378cd0f5383dd5..8ea5fce92fa7c74db496a1325070c079abd5f9f8 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -12,6 +12,7 @@ import { escapeRegExp } from 'lodash'; import { escapeFileUrl } from '~/lib/utils/url_utility'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import getRefMixin from '../../mixins/get_ref'; import commitQuery from '../../queries/commit.query.graphql'; @@ -41,7 +42,7 @@ export default { }, }, }, - mixins: [getRefMixin], + mixins: [getRefMixin, glFeatureFlagMixin()], props: { id: { type: String, @@ -103,10 +104,21 @@ export default { }; }, computed: { + refactorBlobViewerEnabled() { + return this.glFeatures.refactorBlobViewer; + }, routerLinkTo() { - return this.isFolder - ? { path: `/-/tree/${this.escapedRef}/${escapeFileUrl(this.path)}` } - : null; + const blobRouteConfig = { path: `/-/blob/${this.escapedRef}/${escapeFileUrl(this.path)}` }; + const treeRouteConfig = { path: `/-/tree/${this.escapedRef}/${escapeFileUrl(this.path)}` }; + + if (this.refactorBlobViewerEnabled && this.isBlob) { + return blobRouteConfig; + } + + return this.isFolder ? treeRouteConfig : null; + }, + isBlob() { + return this.type === 'blob'; }, isFolder() { return this.type === 'tree'; @@ -115,7 +127,7 @@ export default { return this.type === 'commit'; }, linkComponent() { - return this.isFolder ? 'router-link' : 'a'; + return this.isFolder || (this.refactorBlobViewerEnabled && this.isBlob) ? 'router-link' : 'a'; }, fullPath() { return this.path.replace(new RegExp(`^${escapeRegExp(this.currentPath)}/`), ''); diff --git a/app/assets/javascripts/repository/pages/blob.vue b/app/assets/javascripts/repository/pages/blob.vue new file mode 100644 index 0000000000000000000000000000000000000000..c492f9663644aa98177e5d475335d7e685ea46d1 --- /dev/null +++ b/app/assets/javascripts/repository/pages/blob.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js index ad6e32d7055c7b86a1a7d1c6c78b201ace6f4918..c7f7451fb55f726119638ed6f700b0f46b0a22f0 100644 --- a/app/assets/javascripts/repository/router.js +++ b/app/assets/javascripts/repository/router.js @@ -2,6 +2,7 @@ import { escapeRegExp } from 'lodash'; import Vue from 'vue'; import VueRouter from 'vue-router'; import { joinPaths } from '../lib/utils/url_utility'; +import BlobPage from './pages/blob.vue'; import IndexPage from './pages/index.vue'; import TreePage from './pages/tree.vue'; @@ -15,6 +16,13 @@ export default function createRouter(base, baseRef) { }), }; + const blobPathRoute = { + component: BlobPage, + props: (route) => ({ + path: route.params.path, + }), + }; + return new VueRouter({ mode: 'history', base: joinPaths(gon.relative_url_root || '', base), @@ -31,6 +39,18 @@ export default function createRouter(base, baseRef) { path: `(/-)?/tree/${escapeRegExp(baseRef)}/:path*`, ...treePathRoute, }, + { + name: 'blobPathDecoded', + // Sometimes the ref needs decoding depending on how the backend sends it to us + path: `(/-)?/blob/${decodeURI(baseRef)}/:path*`, + ...blobPathRoute, + }, + { + name: 'blobPath', + // Support without decoding as well just in case the ref doesn't need to be decoded + path: `(/-)?/blob/${escapeRegExp(baseRef)}/:path*`, + ...blobPathRoute, + }, { path: '/', name: 'projectRoot', diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 00390d90e8db974c10aeffa20c08f03fa6fc3f21..4f28207564aec1a457fefd931804e87e165efaaf 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -35,6 +35,10 @@ class ProjectsController < Projects::ApplicationController push_frontend_feature_flag(:allow_editing_commit_messages, @project) end + before_action do + push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml) + end + layout :determine_layout feature_category :projects, [ diff --git a/config/feature_flags/development/refactor_blob_viewer.yml b/config/feature_flags/development/refactor_blob_viewer.yml new file mode 100644 index 0000000000000000000000000000000000000000..231e26840234cde7fa6f8b23612546ab4c627786 --- /dev/null +++ b/config/feature_flags/development/refactor_blob_viewer.yml @@ -0,0 +1,8 @@ +--- +name: refactor_blob_viewer +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57326 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324351 +milestone: '13.11' +type: development +group: group::source code +default_enabled: false diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 4730679feb86d425e48036f3572271721816fed3..34601cab24fac8d9088aa1c5b83fab3464d24d63 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -290,6 +290,7 @@ let(:project) { create(:forked_project_with_submodules) } before do + stub_feature_flags(refactor_blob_viewer: false) project.add_maintainer(user) sign_in user visit project_path(project) diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index 69cb69de5df5f959aac7e27c990a35ad153db3a0..3ebffbedcdba8daae5de94caf730499d8026d4be 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -19,6 +19,9 @@ function factory(propsData = {}) { projectPath: 'gitlab-org/gitlab-ce', url: `https://test.com`, }, + provide: { + glFeatures: { refactorBlobViewer: true }, + }, mocks: { $router, }, @@ -81,7 +84,7 @@ describe('Repository table row component', () => { it.each` type | component | componentName ${'tree'} | ${RouterLinkStub} | ${'RouterLink'} - ${'file'} | ${'a'} | ${'hyperlink'} + ${'blob'} | ${RouterLinkStub} | ${'RouterLink'} ${'commit'} | ${'a'} | ${'hyperlink'} `('renders a $componentName for type $type', ({ type, component }) => { factory({ diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js index 3c7dda05ca3d8f828efa510847e47ab42fbe87d2..3354b2315fca9cd71e0a29d2506d24fe14ac2e17 100644 --- a/spec/frontend/repository/router_spec.js +++ b/spec/frontend/repository/router_spec.js @@ -1,3 +1,4 @@ +import BlobPage from '~/repository/pages/blob.vue'; import IndexPage from '~/repository/pages/index.vue'; import TreePage from '~/repository/pages/tree.vue'; import createRouter from '~/repository/router'; @@ -11,6 +12,7 @@ describe('Repository router spec', () => { ${'/-/tree/master'} | ${'master'} | ${TreePage} | ${'TreePage'} ${'/-/tree/master/app/assets'} | ${'master'} | ${TreePage} | ${'TreePage'} ${'/-/tree/123/app/assets'} | ${'master'} | ${null} | ${'null'} + ${'/-/blob/master/file.md'} | ${'master'} | ${BlobPage} | ${'BlobPage'} `('sets component as $componentName for path "$path"', ({ path, component, branch }) => { const router = createRouter('', branch);