From 4644a2daf5ec5e86e2b2989f04e99e4f081f6fef Mon Sep 17 00:00:00 2001
From: Phil Hughes <me@iamphill.com>
Date: Tue, 4 Jun 2019 14:38:18 +0100
Subject: [PATCH] Add web_url to tree entry in GraphQL API

---
 .../repository/components/table/index.vue     |  1 +
 .../repository/components/table/row.vue       |  7 ++++-
 .../repository/queries/getFiles.graphql       |  2 ++
 app/graphql/types/tree/blob_type.rb           |  4 +++
 app/graphql/types/tree/tree_entry_type.rb     |  4 +++
 app/graphql/types/tree/tree_type.rb           | 10 ++++--
 app/presenters/blob_presenter.rb              |  4 +++
 app/presenters/tree_entry_presenter.rb        |  9 ++++++
 .../graphql/representation/tree_entry.rb      | 31 +++++++++++++++++++
 .../repository/components/table/row_spec.js   | 12 +++++++
 spec/graphql/types/tree/blob_type_spec.rb     |  2 +-
 .../types/tree/tree_entry_type_spec.rb        |  2 +-
 .../graphql/representation/tree_entry_spec.rb | 20 ++++++++++++
 spec/presenters/blob_presenter_spec.rb        | 10 ++++++
 spec/presenters/tree_entry_presenter_spec.rb  | 16 ++++++++++
 15 files changed, 129 insertions(+), 5 deletions(-)
 create mode 100644 app/presenters/tree_entry_presenter.rb
 create mode 100644 lib/gitlab/graphql/representation/tree_entry.rb
 create mode 100644 spec/lib/gitlab/graphql/representation/tree_entry_spec.rb
 create mode 100644 spec/presenters/tree_entry_presenter_spec.rb

diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index cccde1bb2784b..d2198bcccfee9 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -134,6 +134,7 @@ export default {
               :current-path="path"
               :path="entry.flatPath"
               :type="entry.type"
+              :url="entry.webUrl"
             />
           </template>
         </tbody>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 9a264bef87e8c..764882a793615 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -21,6 +21,11 @@ export default {
       type: String,
       required: true,
     },
+    url: {
+      type: String,
+      required: false,
+      default: null,
+    },
   },
   computed: {
     routerLinkTo() {
@@ -59,7 +64,7 @@ export default {
   <tr v-once :class="`file_${id}`" class="tree-item" @click="openRow">
     <td class="tree-item-file-name">
       <i :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i>
-      <component :is="linkComponent" :to="routerLinkTo" class="str-truncated">
+      <component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated">
         {{ fullPath }}
       </component>
       <template v-if="isSubmodule">
diff --git a/app/assets/javascripts/repository/queries/getFiles.graphql b/app/assets/javascripts/repository/queries/getFiles.graphql
index a9b61d285607a..7d92bc4645573 100644
--- a/app/assets/javascripts/repository/queries/getFiles.graphql
+++ b/app/assets/javascripts/repository/queries/getFiles.graphql
@@ -23,6 +23,7 @@ query getFiles(
           edges {
             node {
               ...TreeEntry
+              webUrl
             }
           }
           pageInfo {
@@ -43,6 +44,7 @@ query getFiles(
           edges {
             node {
               ...TreeEntry
+              webUrl
             }
           }
           pageInfo {
diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb
index 230624201b06e..f2b7d5df2b23a 100644
--- a/app/graphql/types/tree/blob_type.rb
+++ b/app/graphql/types/tree/blob_type.rb
@@ -4,7 +4,11 @@ module Tree
     class BlobType < BaseObject
       implements Types::Tree::EntryType
 
+      present_using BlobPresenter
+
       graphql_name 'Blob'
+
+      field :web_url, GraphQL::STRING_TYPE, null: true
     end
   end
 end
diff --git a/app/graphql/types/tree/tree_entry_type.rb b/app/graphql/types/tree/tree_entry_type.rb
index d5cfb898aea6c..23ec2ef0ec2dc 100644
--- a/app/graphql/types/tree/tree_entry_type.rb
+++ b/app/graphql/types/tree/tree_entry_type.rb
@@ -4,8 +4,12 @@ module Tree
     class TreeEntryType < BaseObject
       implements Types::Tree::EntryType
 
+      present_using TreeEntryPresenter
+
       graphql_name 'TreeEntry'
       description 'Represents a directory'
+
+      field :web_url, GraphQL::STRING_TYPE, null: true
     end
   end
 end
diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb
index 1eb6c43972e64..1ee93ed95420a 100644
--- a/app/graphql/types/tree/tree_type.rb
+++ b/app/graphql/types/tree/tree_type.rb
@@ -4,9 +4,15 @@ module Tree
     class TreeType < BaseObject
       graphql_name 'Tree'
 
-      field :trees, Types::Tree::TreeEntryType.connection_type, null: false
+      field :trees, Types::Tree::TreeEntryType.connection_type, null: false, resolve: -> (obj, args, ctx) do
+        Gitlab::Graphql::Representation::TreeEntry.decorate(obj.trees, obj.repository)
+      end
+
       field :submodules, Types::Tree::SubmoduleType.connection_type, null: false
-      field :blobs, Types::Tree::BlobType.connection_type, null: false
+
+      field :blobs, Types::Tree::BlobType.connection_type, null: false, resolve: -> (obj, args, ctx) do
+        Gitlab::Graphql::Representation::TreeEntry.decorate(obj.blobs, obj.repository)
+      end
     end
   end
 end
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index 6323c1b338985..c5675ef3ea322 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -13,4 +13,8 @@ def highlight(plain: nil)
       plain: plain
     )
   end
+
+  def web_url
+    Gitlab::Routing.url_helpers.project_blob_url(blob.repository.project, File.join(blob.commit_id, blob.path))
+  end
 end
diff --git a/app/presenters/tree_entry_presenter.rb b/app/presenters/tree_entry_presenter.rb
new file mode 100644
index 0000000000000..7bb10cd1455ed
--- /dev/null
+++ b/app/presenters/tree_entry_presenter.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class TreeEntryPresenter < Gitlab::View::Presenter::Delegated
+  presents :tree
+
+  def web_url
+    Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, File.join(tree.commit_id, tree.path))
+  end
+end
diff --git a/lib/gitlab/graphql/representation/tree_entry.rb b/lib/gitlab/graphql/representation/tree_entry.rb
new file mode 100644
index 0000000000000..7ea83db5876b5
--- /dev/null
+++ b/lib/gitlab/graphql/representation/tree_entry.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Graphql
+    module Representation
+      class TreeEntry < SimpleDelegator
+        class << self
+          def decorate(entries, repository)
+            return if entries.nil?
+
+            entries.map do |entry|
+              if entry.is_a?(TreeEntry)
+                entry
+              else
+                self.new(entry, repository)
+              end
+            end
+          end
+        end
+
+        attr_accessor :repository
+
+        def initialize(raw_entry, repository)
+          @repository = repository
+
+          super(raw_entry)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 6b4508c418ea5..a70dc7bb8668a 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -86,4 +86,16 @@ describe('Repository table row component', () => {
 
     expect(vm.find('.commit-sha').text()).toContain('1');
   });
+
+  it('renders link with href', () => {
+    factory({
+      id: '1',
+      path: 'test',
+      type: 'blob',
+      url: 'https://test.com',
+      currentPath: '/',
+    });
+
+    expect(vm.find('a').attributes('href')).toEqual('https://test.com');
+  });
 });
diff --git a/spec/graphql/types/tree/blob_type_spec.rb b/spec/graphql/types/tree/blob_type_spec.rb
index fa29bb5fff79b..b12e214ca848a 100644
--- a/spec/graphql/types/tree/blob_type_spec.rb
+++ b/spec/graphql/types/tree/blob_type_spec.rb
@@ -5,5 +5,5 @@
 describe Types::Tree::BlobType do
   it { expect(described_class.graphql_name).to eq('Blob') }
 
-  it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path) }
+  it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url) }
 end
diff --git a/spec/graphql/types/tree/tree_entry_type_spec.rb b/spec/graphql/types/tree/tree_entry_type_spec.rb
index 397cabde8e545..ea1b6426034b5 100644
--- a/spec/graphql/types/tree/tree_entry_type_spec.rb
+++ b/spec/graphql/types/tree/tree_entry_type_spec.rb
@@ -5,5 +5,5 @@
 describe Types::Tree::TreeEntryType do
   it { expect(described_class.graphql_name).to eq('TreeEntry') }
 
-  it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path) }
+  it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url) }
 end
diff --git a/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb b/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb
new file mode 100644
index 0000000000000..d45e690160cf6
--- /dev/null
+++ b/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Representation::TreeEntry do
+  let(:project) { create(:project, :repository) }
+  let(:repository) { project.repository }
+
+  describe '.decorate' do
+    it 'returns NilClass when given nil' do
+      expect(described_class.decorate(nil, repository)).to be_nil
+    end
+
+    it 'returns array of TreeEntry' do
+      entries = described_class.decorate(repository.tree.blobs, repository)
+
+      expect(entries.first).to be_a(described_class)
+    end
+  end
+end
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index bb1db9a3d51e9..eacf383be7dfa 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -14,6 +14,16 @@
   end
   let(:blob) { Blob.new(git_blob) }
 
+  describe '.web_url' do
+    let(:project) { create(:project, :repository) }
+    let(:repository) { project.repository }
+    let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(repository.tree.blobs.first, repository) }
+
+    subject { described_class.new(blob) }
+
+    it { expect(subject.web_url).to eq("http://localhost/#{project.full_path}/blob/#{blob.commit_id}/#{blob.path}") }
+  end
+
   describe '#highlight' do
     subject { described_class.new(blob) }
 
diff --git a/spec/presenters/tree_entry_presenter_spec.rb b/spec/presenters/tree_entry_presenter_spec.rb
new file mode 100644
index 0000000000000..d74ee5dc28f4c
--- /dev/null
+++ b/spec/presenters/tree_entry_presenter_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe TreeEntryPresenter do
+  include Gitlab::Routing.url_helpers
+
+  let(:project) { create(:project, :repository) }
+  let(:repository) { project.repository }
+  let(:tree) { Gitlab::Graphql::Representation::TreeEntry.new(repository.tree.trees.first, repository) }
+  let(:presenter) { described_class.new(tree) }
+
+  describe '.web_url' do
+    it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/tree/#{tree.commit_id}/#{tree.path}") }
+  end
+end
-- 
GitLab