diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 7ad9fb56972fc480bb5c3d98732a0d767eadc35e..2cc5a8a79d23b86d9e901a93fc1176542e194a5c 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 8e0b5e21ca332f0f18dd2c00dfeb640e7ab8c33a..cf3892802fdecf5d7a216b43e2a70bf351de9a74 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 ef7f535212f5015e44c0a4fc23a3901618545e29..104171e6772d3fe9ca3f703f4605dcd5dd8b3cc3 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 e8261e6f8df6dd11f0ca7670ccb33112575bea0f..e8e6c884c5ea965e3d84f378f62f94e5acd392a3 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 6e387cdec4abe487836968fb324116a896fd67bb..9054d4cebbb74a776cf880aabdc1414263fdae6e 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 5c8c809b513f6c48ad197293bbbe007137b7ff1a..c8520ced8fc220b6c4dd164baca3c40033d92127 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 2cb8b0b679e73c8a8e50be8dd5a201147578d828..d40e97bf5a3567bbed43fbcece236cf571e5e79c 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 a3c60032c8c50470266087dec444bd4ce46bfafb..adf5991ac3c8ec8e42022de7f9572d10dbdbfc7d 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 beab4dcebc2f904da37de1ada4899826dd09784e..7f37237f355b2ee3cc25761b3b479ab561c4785e 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 f433074e7d48cafe6af315b3ab34388581e2cad6..03e2b35933c7b61fe7eaccf2c9b2250585a093f7 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