From 9c1c8b999facbb1be0078f0309cdab77636ad68d Mon Sep 17 00:00:00 2001
From: Jacques <jerasmus@gitlab.com>
Date: Wed, 27 Oct 2021 17:01:04 +0200
Subject: [PATCH] Display pipeline editor button

Display a pipeline edito button when vewing ci file
---
 .../components/blob_content_viewer.vue        | 19 ++++++++++++-
 .../queries/blob_info.query.graphql           |  1 +
 app/graphql/types/repository/blob_type.rb     |  3 +++
 app/presenters/blob_presenter.rb              |  4 +++
 doc/api/graphql/reference/index.md            |  1 +
 locale/gitlab.pot                             |  3 +++
 .../components/blob_content_viewer_spec.js    | 27 ++++++++++++++-----
 spec/frontend/repository/mock_data.js         |  1 +
 .../types/repository/blob_type_spec.rb        |  1 +
 spec/presenters/blob_presenter_spec.rb        | 14 ++++++++++
 10 files changed, 66 insertions(+), 8 deletions(-)

diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 7ad9fb56972fc..2cc5a8a79d23b 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -1,5 +1,5 @@
 <script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton } from '@gitlab/ui';
 import { uniqueId } from 'lodash';
 import BlobContent from '~/blob/components/blob_content.vue';
 import BlobHeader from '~/blob/components/blob_header.vue';
@@ -17,12 +17,16 @@ import ForkSuggestion from './fork_suggestion.vue';
 import { loadViewer, viewerProps } from './blob_viewers';
 
 export default {
+  i18n: {
+    pipelineEditor: __('Pipeline Editor'),
+  },
   components: {
     BlobHeader,
     BlobEdit,
     BlobButtonGroup,
     BlobContent,
     GlLoadingIcon,
+    GlButton,
     ForkSuggestion,
   },
   mixins: [getRefMixin],
@@ -105,6 +109,7 @@ export default {
                 rawPath: '',
                 externalStorageUrl: '',
                 replacePath: '',
+                pipelineEditorPath: '',
                 deletePath: '',
                 simpleViewer: {},
                 richViewer: null,
@@ -242,6 +247,18 @@ export default {
             :needs-to-fork="showForkSuggestion"
             @edit="editBlob"
           />
+
+          <gl-button
+            v-if="blobInfo.pipelineEditorPath"
+            class="gl-mr-3"
+            category="secondary"
+            variant="confirm"
+            data-testid="pipeline-editor"
+            :href="blobInfo.pipelineEditorPath"
+          >
+            {{ $options.i18n.pipelineEditor }}
+          </gl-button>
+
           <blob-button-group
             v-if="isLoggedIn"
             :path="path"
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index 8e0b5e21ca332..cf3892802fdec 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -31,6 +31,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
           storedExternally
           rawPath
           replacePath
+          pipelineEditorPath
           simpleViewer {
             fileType
             tooLarge
diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb
index ef7f535212f50..104171e6772d3 100644
--- a/app/graphql/types/repository/blob_type.rb
+++ b/app/graphql/types/repository/blob_type.rb
@@ -68,6 +68,9 @@ class BlobType < BaseObject
       field :replace_path, GraphQL::Types::String, null: true,
             description: 'Web path to replace the blob content.'
 
+      field :pipeline_editor_path, GraphQL::Types::String, null: true,
+            description: 'Web path to edit .gitlab-ci.yml file.'
+
       field :file_type, GraphQL::Types::String, null: true,
             description: 'Expected format of the blob based on the extension.'
 
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index e8261e6f8df6d..e8e6c884c5ea9 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -62,6 +62,10 @@ def replace_path
     url_helpers.project_create_blob_path(project, ref_qualified_path)
   end
 
+  def pipeline_editor_path
+    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 fork_and_edit_path
     fork_path_for_current_user(project, edit_blob_path)
   end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 6e387cdec4abe..9054d4cebbb74 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -14085,6 +14085,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="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. |
 | <a id="repositoryblobrawpath"></a>`rawPath` | [`String`](#string) | Web path to download the raw blob. |
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5c8c809b513f6..c8520ced8fc22 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -25149,6 +25149,9 @@ msgstr ""
 msgid "Pipeline %{label} for \"%{dataTitle}\""
 msgstr ""
 
+msgid "Pipeline Editor"
+msgstr ""
+
 msgid "Pipeline ID"
 msgstr ""
 
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 2cb8b0b679e73..d40e97bf5a356 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -19,6 +19,7 @@ import TextViewer from '~/repository/components/blob_viewers/text_viewer.vue';
 import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
 import { redirectTo } from '~/lib/utils/url_utility';
 import { isLoggedIn } from '~/lib/utils/common_utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
 import {
   simpleViewerMock,
   richViewerMock,
@@ -72,13 +73,15 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
 
   const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]);
 
-  wrapper = mountFn(BlobContentViewer, {
-    localVue,
-    apolloProvider: fakeApollo,
-    propsData: propsMock,
-    mixins: [{ data: () => ({ ref: refMock }) }],
-    provide: { ...inject },
-  });
+  wrapper = extendedWrapper(
+    mountFn(BlobContentViewer, {
+      localVue,
+      apolloProvider: fakeApollo,
+      propsData: propsMock,
+      mixins: [{ data: () => ({ ref: refMock }) }],
+      provide: { ...inject },
+    }),
+  );
 
   wrapper.setData({ project, isBinary });
 
@@ -89,6 +92,7 @@ describe('Blob content viewer component', () => {
   const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
   const findBlobHeader = () => wrapper.findComponent(BlobHeader);
   const findBlobEdit = () => wrapper.findComponent(BlobEdit);
+  const findPipelineEditor = () => wrapper.findByTestId('pipeline-editor');
   const findBlobContent = () => wrapper.findComponent(BlobContent);
   const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
   const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
@@ -270,6 +274,15 @@ describe('Blob content viewer component', () => {
       });
     });
 
+    it('renders Pipeline Editor button for .gitlab-ci files', async () => {
+      const pipelineEditorPath = 'some/path/.gitlab-ce';
+      const blob = { ...simpleViewerMock, pipelineEditorPath };
+      await createComponent({ blob, inject: { BlobContent: true, BlobReplace: true } }, mount);
+
+      expect(findPipelineEditor().exists()).toBe(true);
+      expect(findPipelineEditor().attributes('href')).toBe(pipelineEditorPath);
+    });
+
     describe('blob header binary file', () => {
       it('passes the correct isBinary value when viewing a binary file', async () => {
         await createComponent({ blob: richViewerMock, isBinary: true });
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index a3c60032c8c50..adf5991ac3c8e 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -14,6 +14,7 @@ export const simpleViewerMock = {
   storedExternally: false,
   rawPath: 'some_file.js',
   replacePath: 'some_file.js/replace',
+  pipelineEditorPath: '',
   simpleViewer: {
     fileType: 'text',
     tooLarge: false,
diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb
index beab4dcebc2f9..7f37237f355b2 100644
--- a/spec/graphql/types/repository/blob_type_spec.rb
+++ b/spec/graphql/types/repository/blob_type_spec.rb
@@ -23,6 +23,7 @@
       :stored_externally,
       :raw_path,
       :replace_path,
+      :pipeline_editor_path,
       :simple_viewer,
       :rich_viewer,
       :plain_data,
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index f433074e7d48c..03e2b35933c7b 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -31,6 +31,20 @@
     it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/create/#{blob.commit_id}/#{blob.path}") }
   end
 
+  describe '#pipeline_editor_path' do
+    context 'when blob is .gitlab-ci.yml' do
+      before do
+        project.repository.create_file(user, '.gitlab-ci.yml', '',
+        message: 'Add a ci file',
+        branch_name: 'main')
+      end
+
+      let(:blob) { repository.blob_at('main', '.gitlab-ci.yml') }
+
+      it { expect(presenter.pipeline_editor_path).to eq("/#{project.full_path}/-/ci/editor?branch_name=#{blob.commit_id}") }
+    end
+  end
+
   describe '#ide_edit_path' do
     it { expect(presenter.ide_edit_path).to eq("/-/ide/project/#{project.full_path}/edit/HEAD/-/files/ruby/regex.rb") }
   end
-- 
GitLab