Commit d8458c4d authored by Himanshu Kapoor's avatar Himanshu Kapoor Committed by Ash McKenzie

Consolidate the Edit and Web IDE buttons

parent 49ed45ff
......@@ -3,6 +3,7 @@ import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_sta
import BlobViewer from '~/blob/viewer/index';
import initBlob from '~/pages/projects/init_blob';
import GpgBadges from '~/gpg_badges';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import '~/sourcegraph/load';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
......@@ -55,6 +56,8 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
initWebIdeLink({ el: document.getElementById('js-blob-web-ide-link') });
GpgBadges.fetch();
const codeNavEl = document.getElementById('js-code-navigation');
......
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { joinPaths, webIDEUrl } from '~/lib/utils/url_utility';
import WebIdeButton from '~/vue_shared/components/web_ide_link.vue';
export default ({ el, router }) => {
if (!el) return;
const { projectPath, ref, isBlob, webIdeUrl, ...options } = convertObjectPropsToCamelCase(
JSON.parse(el.dataset.options),
);
// eslint-disable-next-line no-new
new Vue({
el,
router,
render(h) {
return h(WebIdeButton, {
props: {
isBlob,
webIdeUrl: isBlob
? webIdeUrl
: webIDEUrl(
joinPaths('/', projectPath, 'edit', ref, '-', this.$route?.params.path || '', '/'),
),
...options,
},
});
},
});
};
import Vue from 'vue';
import PathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { escapeFileUrl, joinPaths, webIDEUrl } from '../lib/utils/url_utility';
import { escapeFileUrl } from '../lib/utils/url_utility';
import createRouter from './router';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import LastCommit from './components/last_commit.vue';
import TreeActionLink from './components/tree_action_link.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
import apolloProvider from './graphql';
import { setTitle } from './utils/title';
import { updateFormAction } from './utils/dom';
import { convertObjectPropsToCamelCase, parseBoolean } from '../lib/utils/common_utils';
import { parseBoolean } from '../lib/utils/common_utils';
import { __ } from '../locale';
export default function setupVueRepositoryList() {
......@@ -138,31 +138,7 @@ export default function setupVueRepositoryList() {
},
});
const webIdeLinkEl = document.getElementById('js-tree-web-ide-link');
if (webIdeLinkEl) {
const {
webIdeUrlData: { path: ideBasePath, isFork: webIdeIsFork },
...options
} = convertObjectPropsToCamelCase(JSON.parse(webIdeLinkEl.dataset.options), { deep: true });
// eslint-disable-next-line no-new
new Vue({
el: webIdeLinkEl,
router,
render(h) {
return h(WebIdeLink, {
props: {
webIdeUrl: webIDEUrl(
joinPaths('/', ideBasePath, 'edit', ref, '-', this.$route.params.path || '', '/'),
),
webIdeIsFork,
...options,
},
});
},
});
}
initWebIdeLink({ el: document.getElementById('js-tree-web-ide-link'), router });
const directoryDownloadLinks = document.getElementById('js-directory-downloads');
......
......@@ -3,7 +3,7 @@ import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlLink,
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
......@@ -12,7 +12,7 @@ export default {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlLink,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -27,6 +27,16 @@ export default {
required: false,
default: '',
},
category: {
type: String,
required: false,
default: 'secondary',
},
variant: {
type: String,
required: false,
default: 'default',
},
},
computed: {
hasMultipleActions() {
......@@ -54,6 +64,8 @@ export default {
class="gl-button-deprecated-adapter"
:text="selectedAction.text"
:split-href="selectedAction.href"
:variant="variant"
:category="category"
split
@click="handleClick(selectedAction, $event)"
>
......@@ -77,14 +89,15 @@ export default {
<gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" />
</template>
</gl-dropdown>
<gl-link
<gl-button
v-else-if="selectedAction"
v-gl-tooltip="selectedAction.tooltip"
v-bind="selectedAction.attrs"
class="btn"
:variant="variant"
:category="category"
:href="selectedAction.href"
@click="handleClick(selectedAction, $event)"
>
{{ selectedAction.text }}
</gl-link>
</gl-button>
</template>
......@@ -4,6 +4,7 @@ import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
const KEY_EDIT = 'edit';
const KEY_WEB_IDE = 'webide';
const KEY_GITPOD = 'gitpod';
......@@ -13,21 +14,31 @@ export default {
LocalStorageSync,
},
props: {
webIdeUrl: {
type: String,
isFork: {
type: Boolean,
required: false,
default: '',
default: false,
},
webIdeIsFork: {
needsToFork: {
type: Boolean,
required: false,
default: false,
},
needsToFork: {
gitpodEnabled: {
type: Boolean,
required: false,
default: false,
},
isBlob: {
type: Boolean,
required: false,
default: false,
},
showEditButton: {
type: Boolean,
required: false,
default: true,
},
showWebIdeButton: {
type: Boolean,
required: false,
......@@ -38,15 +49,20 @@ export default {
required: false,
default: false,
},
gitpodUrl: {
editUrl: {
type: String,
required: false,
default: '',
},
gitpodEnabled: {
type: Boolean,
webIdeUrl: {
type: String,
required: false,
default: false,
default: '',
},
gitpodUrl: {
type: String,
required: false,
default: '',
},
},
data() {
......@@ -56,7 +72,33 @@ export default {
},
computed: {
actions() {
return [this.webIdeAction, this.gitpodAction].filter(x => x);
return [this.webIdeAction, this.editAction, this.gitpodAction].filter(action => action);
},
editAction() {
if (!this.showEditButton) {
return null;
}
const handleOptions = this.needsToFork
? {
href: '#modal-confirm-fork-edit',
handle: () => this.showModal('#modal-confirm-fork-edit'),
}
: { href: this.editUrl };
return {
key: KEY_EDIT,
text: __('Edit'),
secondaryText: __('Edit this file only.'),
tooltip: '',
attrs: {
'data-qa-selector': 'edit_button',
'data-track-event': 'click_edit',
// eslint-disable-next-line @gitlab/require-i18n-strings
'data-track-label': 'Edit',
},
...handleOptions,
};
},
webIdeAction() {
if (!this.showWebIdeButton) {
......@@ -64,10 +106,19 @@ export default {
}
const handleOptions = this.needsToFork
? { href: '#modal-confirm-fork', handle: () => this.showModal('#modal-confirm-fork') }
? {
href: '#modal-confirm-fork-webide',
handle: () => this.showModal('#modal-confirm-fork-webide'),
}
: { href: this.webIdeUrl };
const text = this.webIdeIsFork ? __('Edit fork in Web IDE') : __('Web IDE');
let text = __('Web IDE');
if (this.isBlob) {
text = __('Edit in Web IDE');
} else if (this.isFork) {
text = __('Edit fork in Web IDE');
}
return {
key: KEY_WEB_IDE,
......@@ -76,6 +127,9 @@ export default {
tooltip: '',
attrs: {
'data-qa-selector': 'web_ide_button',
'data-track-event': 'click_edit_ide',
// eslint-disable-next-line @gitlab/require-i18n-strings
'data-track-label': 'Web IDE',
},
...handleOptions,
};
......@@ -115,8 +169,14 @@ export default {
</script>
<template>
<div>
<actions-button :actions="actions" :selected-key="selection" @select="select" />
<div class="d-inline-block gl-ml-3">
<actions-button
:actions="actions"
:selected-key="selection"
:variant="isBlob ? 'info' : 'default'"
:category="isBlob ? 'primary' : 'secondary'"
@select="select"
/>
<local-storage-sync
storage-key="gl-web-ide-button-selected"
:value="selection"
......
......@@ -27,11 +27,19 @@ module BlobHelper
end
def ide_fork_and_edit_path(project = @project, ref = @ref, path = @path, options = {})
if current_user
project_forks_path(project,
namespace_key: current_user&.namespace&.id,
continue: edit_blob_fork_params(ide_edit_path(project, ref, path)))
end
fork_path_for_current_user(project, ide_edit_path(project, ref, path))
end
def fork_and_edit_path(project = @project, ref = @ref, path = @path, options = {})
fork_path_for_current_user(project, edit_blob_path(project, ref, path, options))
end
def fork_path_for_current_user(project, path)
return unless current_user
project_forks_path(project,
namespace_key: current_user.namespace&.id,
continue: edit_blob_fork_params(path))
end
def encode_ide_path(path)
......
# frozen_string_literal: true
module TreeHelper
include BlobHelper
include WebIdeButtonHelper
FILE_LIMIT = 1_000
# Sorts a repository's tree so that folders are before files and renders
......@@ -199,38 +202,26 @@ module TreeHelper
}
end
def web_ide_url_data(project)
can_push_code = current_user&.can?(:push_code, project)
fork_path = current_user&.fork_of(project)&.full_path
def web_ide_button_data(options = {})
{
project_path: project_to_use.full_path,
ref: ActionDispatch::Journey::Router::Utils.escape_path(@ref),
if fork_path && !can_push_code
{ path: fork_path, is_fork: true }
else
{ path: project.full_path, is_fork: false }
end
end
is_fork: fork?,
needs_to_fork: needs_to_fork?,
gitpod_enabled: !current_user.nil? && current_user.gitpod_enabled,
is_blob: !options[:blob].nil?,
def vue_ide_link_data(project, ref)
can_collaborate = can_collaborate_with_project?(project)
can_create_mr_from_fork = can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
show_web_ide_button = (can_collaborate || current_user&.already_forked?(project) || can_create_mr_from_fork)
show_edit_button: show_edit_button?,
show_web_ide_button: show_web_ide_button?,
show_gitpod_button: show_gitpod_button?,
{
web_ide_url_data: web_ide_url_data(project),
needs_to_fork: !can_collaborate && !current_user&.already_forked?(project),
show_web_ide_button: show_web_ide_button,
show_gitpod_button: show_web_ide_button && Gitlab::Gitpod.feature_and_settings_enabled?(project),
gitpod_url: full_gitpod_url(project, ref),
gitpod_enabled: current_user&.gitpod_enabled
web_ide_url: web_ide_url,
edit_url: edit_url,
gitpod_url: gitpod_url
}
end
def full_gitpod_url(project, ref)
return "" unless Gitlab::Gitpod.feature_and_settings_enabled?(project)
"#{Gitlab::CurrentSettings.gitpod_url}##{project_tree_url(project, tree_join(ref, @path || ''))}"
end
def directory_download_links(project, ref, archive_prefix)
Gitlab::Workhorse::ARCHIVE_FORMATS.map do |fmt|
{
......
# frozen_string_literal: true
module WebIdeButtonHelper
def project_fork
current_user&.fork_of(@project)
end
def project_to_use
fork? ? project_fork : @project
end
def can_collaborate?
can_collaborate_with_project?(@project)
end
def can_create_mr_from_fork?
can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
end
def show_web_ide_button?
can_collaborate? || can_create_mr_from_fork?
end
def show_edit_button?
readable_blob? && show_web_ide_button?
end
def show_gitpod_button?
show_web_ide_button? && Gitlab::Gitpod.feature_and_settings_enabled?(@project)
end
def can_push_code?
current_user&.can?(:push_code, @project)
end
def fork?
!project_fork.nil? && !can_push_code?
end
def readable_blob?
!readable_blob({}, @path, @project, @ref).nil?
end
def needs_to_fork?
!can_collaborate? && !current_user&.already_forked?(@project)
end
def web_ide_url
ide_edit_path(project_to_use, @ref, @path || '')
end
def edit_url
readable_blob? ? edit_blob_path(@project, @ref, @path || '') : ''
end
def gitpod_url
return "" unless Gitlab::Gitpod.feature_and_settings_enabled?(@project)
"#{Gitlab::CurrentSettings.gitpod_url}##{project_tree_url(@project, tree_join(@ref, @path || ''))}"
end
end
......@@ -4,8 +4,11 @@
.file-actions<
= render 'projects/blob/viewer_switcher', blob: blob unless blame
= edit_blob_button(@project, @ref, @path, blob: blob)
= ide_edit_button(@project, @ref, @path, blob: blob)
- if Feature.enabled?(:consolidated_edit_button)
= render 'shared/web_ide_button', blob: blob
- else
= edit_blob_button(@project, @ref, @path, blob: blob)
= ide_edit_button(@project, @ref, @path, blob: blob)
.btn-group.ml-2{ role: "group" }>
= render_if_exists 'projects/blob/header_file_locks_link'
- if current_user
......
- can_collaborate = can_collaborate_with_project?(@project)
- can_create_mr_from_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
- can_visit_ide = can_collaborate || current_user&.already_forked?(@project)
.tree-ref-container
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
......@@ -14,13 +10,7 @@
#js-tree-history-link.d-inline-block{ data: { history_link: project_commits_path(@project, @ref) } }
= render 'projects/find_file_link'
- if can_visit_ide || can_create_mr_from_fork
#js-tree-web-ide-link.d-inline-block{ data: { options: vue_ide_link_data(@project, @ref).to_json } }
- if !can_visit_ide
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
- unless current_user&.gitpod_enabled
= render 'shared/gitpod/enable_gitpod_modal'
= render 'shared/web_ide_button', blob: nil
- if show_xcode_link?(@project)
.project-action-button.project-xcode.inline<
......
#modal-confirm-fork.modal{ data: { qa_selector: 'confirm_fork_modal' } }
.modal{ data: { qa_selector: 'confirm_fork_modal'}, id: "modal-confirm-fork-#{type}" }
.modal-dialog
.modal-content
.modal-header
......
- type = blob ? 'blob' : 'tree'
.d-inline-block{ data: { options: web_ide_button_data(blob: blob).to_json }, id: "js-#{type}-web-ide-link" }
- if show_edit_button?
= render 'shared/confirm_fork_modal', fork_path: fork_and_edit_path(@project, @ref, @path), type: 'edit'
- if show_web_ide_button?
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path), type: 'webide'
- if show_gitpod_button?
= render 'shared/gitpod/enable_gitpod_modal'
---
name: consolidated_edit_button
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44311
rollout_issue_url:
group: group::editor
type: development
default_enabled: false
......@@ -9545,6 +9545,9 @@ msgstr ""
msgid "Edit stage"
msgstr ""
msgid "Edit this file only."
msgstr ""
msgid "Edit this release"
msgstr ""
......
......@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Editing file blob', :js do
include TreeHelper
include BlobSpecHelpers
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') }
......@@ -21,13 +22,17 @@ RSpec.describe 'Editing file blob', :js do
end
def edit_and_commit(commit_changes: true, is_diff: false)
set_default_button('edit')
refresh
wait_for_requests
if is_diff
first('.js-diff-more-actions').click
click_link('Edit in single-file editor')
else
click_link('Edit')
end
first('.js-edit-blob').click
fill_editor(content: 'class NextFeature\\nend\\n')
if commit_changes
......
......@@ -66,10 +66,30 @@ RSpec.describe 'Projects > Files > User browses LFS files' do
expect(page).to have_content('History')
expect(page).to have_content('Permalink')
expect(page).to have_content('Replace')
expect(page).to have_link('Download')
expect(page).not_to have_content('Annotate')
expect(page).not_to have_content('Blame')
expect(page).not_to have_content('Edit')
expect(page).to have_link('Download')
expect(page).not_to have_selector(:link_or_button, text: /^Edit$/)
expect(page).to have_selector(:link_or_button, 'Edit in Web IDE')
end
end
context 'when feature flag :consolidated_edit_button is off' do
before do
stub_feature_flags(consolidated_edit_button: false)
click_link('files')
click_link('lfs')
click_link('lfs_object.iso')
end
it 'does not show single file edit link' do
page.within('.content') do
expect(page).to have_selector(:link_or_button, 'Web IDE')
expect(page).not_to have_selector(:link_or_button, 'Edit')
end
end
end
end
......
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User creates files', :js do
include BlobSpecHelpers
let(:fork_message) do
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
......@@ -103,6 +105,8 @@ RSpec.describe 'Projects > Files > User creates files', :js do
end
it 'creates and commit a new file with new lines at the end of file' do
set_default_button('edit')
find('#editor')
execute_script('monaco.editor.getModels()[0].setValue("Sample\n\n\n")')
fill_in(:file_name, with: 'not_a_file.md')
......@@ -113,7 +117,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
expect(current_path).to eq(new_file_path)
find('.js-edit-blob').click
click_link('Edit')
find('#editor')
expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq("Sample\n\n\n")
......
......@@ -4,6 +4,8 @@ require 'spec_helper'
RSpec.describe 'Projects > Files > User edits files', :js do
include ProjectForksHelper
include BlobSpecHelpers
let(:project) { create(:project, :repository, name: 'Shop') }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) }
......@@ -14,6 +16,10 @@ RSpec.describe 'Projects > Files > User edits files', :js do
sign_in(user)
end
after do
unset_default_button
end
shared_examples 'unavailable for an archived project' do
it 'does not show the edit link for an archived project', :js do
project.update!(archived: true)
......@@ -39,14 +45,15 @@ RSpec.describe 'Projects > Files > User edits files', :js do
end
it 'inserts a content of a file' do
set_default_button('edit')
click_link('.gitignore')
find('.js-edit-blob').click
click_link_or_button('Edit')
find('.file-editor', match: :first)
find('#editor')
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
set_editor_value('*.rbca')
expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq('*.rbca')
expect(editor_value).to eq('*.rbca')
end
it 'does not show the edit link if a file is binary' do
......@@ -60,12 +67,13 @@ RSpec.describe 'Projects > Files > User edits files', :js do
end
it 'commits an edited file' do
set_default_button('edit')
click_link('.gitignore')
find('.js-edit-blob').click
click_link_or_button('Edit')
find('.file-editor', match: :first)
find('#editor')
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
set_editor_value('*.rbca')
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
......@@ -77,13 +85,14 @@ RSpec.describe 'Projects > Files > User edits files', :js do
end
it 'commits an edited file to a new branch' do
set_default_button('edit')
click_link('.gitignore')
find('.js-edit-blob').click
click_link_or_button('Edit')
find('.file-editor', match: :first)
find('#editor')
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
set_editor_value('*.rbca')
fill_in(:commit_message, with: 'New commit message', visible: true)
fill_in(:branch_name, with: 'new_branch_name', visible: true)
click_button('Commit changes')
......@@ -96,12 +105,13 @@ RSpec.describe 'Projects > Files > User edits files', :js do
end
it 'shows the diff of an edited file' do
set_default_button('edit')
click_link('.gitignore')
find('.js-edit-blob').click
click_link_or_button('Edit')
find('.file-editor', match: :first)
find('#editor')
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
set_editor_value('*.rbca')
click_link('Preview changes')
expect(page).to have_css('.line_holder.new')
......@@ -118,8 +128,8 @@ RSpec.describe 'Projects > Files > User edits files', :js do
end
def expect_fork_prompt
expect(page).to have_link('Fork')
expect(page).to have_button('Cancel')
expect(page).to have_selector(:link_or_button, 'Fork')
expect(page).to have_selector(:link_or_button, 'Cancel')
expect(page).to have_content(
"You're not allowed to edit files in this project directly. "\
"Please fork this project, make your changes there, and submit a merge request."
......@@ -134,30 +144,32 @@ RSpec.describe 'Projects > Files > User edits files', :js do
end
it 'inserts a content of a file in a forked project', :sidekiq_might_not_need_inline do
set_default_button('edit')
click_link('.gitignore')
click_button('Edit')
click_link_or_button('Edit')
expect_fork_prompt
click_link('Fork')
click_link_or_button('Fork project')
expect_fork_status
find('.file-editor', match: :first)
find('#editor')
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
set_editor_value('*.rbca')
expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq('*.rbca')
expect(editor_value).to eq('*.rbca')
end
it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do
set_default_button('webide')
click_link('.gitignore')
click_button('Web IDE')
click_link_or_button('Web IDE')
expect_fork_prompt
click_link('Fork')
click_link_or_button('Fork project')
expect_fork_status
......@@ -166,17 +178,17 @@ RSpec.describe 'Projects > Files > User edits files', :js do
end
it 'commits an edited file in a forked project', :sidekiq_might_not_need_inline do
set_default_button('edit')
click_link('.gitignore')
find('.js-edit-blob').click
click_link_or_button('Edit')
expect_fork_prompt
click_link('Fork')
click_link_or_button('Fork project')
find('.file-editor', match: :first)
find('#editor')
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
set_editor_value('*.rbca')
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
......@@ -198,14 +210,14 @@ RSpec.describe 'Projects > Files > User edits files', :js do
end
it 'links to the forked project for editing', :sidekiq_might_not_need_inline do
set_default_button('edit')
click_link('.gitignore')
find('.js-edit-blob').click
click_link_or_button('Edit')
expect(page).not_to have_link('Fork')
expect(page).not_to have_button('Cancel')
expect(page).not_to have_link('Fork project')
find('#editor')
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
set_editor_value('*.rbca')
fill_in(:commit_message, with: 'Another commit', visible: true)
click_button('Commit changes')
......@@ -224,5 +236,116 @@ RSpec.describe 'Projects > Files > User edits files', :js do
let(:project) { project2 }
end
end
context 'when feature flag :consolidated_edit_button is off' do
before do
stub_feature_flags(consolidated_edit_button: false)
end
context 'when an user does not have write access', :js do
before do
project2.add_reporter(user)
visit(project2_tree_path_root_ref)
wait_for_requests
end
it 'inserts a content of a file in a forked project', :sidekiq_might_not_need_inline do
set_default_button('edit')
click_link('.gitignore')
click_link_or_button('Edit')
expect_fork_prompt
click_link_or_button('Fork')
expect_fork_status
find('.file-editor', match: :first)
find('#editor')
set_editor_value('*.rbca')
expect(editor_value).to eq('*.rbca')
end
it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do
set_default_button('webide')
click_link('.gitignore')
click_link_or_button('Web IDE')
expect_fork_prompt
click_link_or_button('Fork')
expect_fork_status
expect(page).to have_css('.ide-sidebar-project-title', text: "#{project2.name} #{user.namespace.full_path}/#{project2.path}")
expect(page).to have_css('.ide .multi-file-tab', text: '.gitignore')
end
it 'commits an edited file in a forked project', :sidekiq_might_not_need_inline do
set_default_button('edit')
click_link('.gitignore')
click_link_or_button('Edit')
expect_fork_prompt
click_link_or_button('Fork')
expect_fork_status
find('.file-editor', match: :first)
find('#editor')
set_editor_value('*.rbca')
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
wait_for_requests
expect(page).to have_content('New commit message')
end
context 'when the user already had a fork of the project', :js do
let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) }
before do
visit(project2_tree_path_root_ref)
wait_for_requests
end
it 'links to the forked project for editing', :sidekiq_might_not_need_inline do
set_default_button('edit')
click_link('.gitignore')
click_link_or_button('Edit')
expect(page).not_to have_link('Fork')
find('#editor')
set_editor_value('*.rbca')
fill_in(:commit_message, with: 'Another commit', visible: true)
click_button('Commit changes')
fork = user.fork_of(project2)
expect(current_path).to eq(project_new_merge_request_path(fork))
wait_for_requests
expect(page).to have_content('Another commit')
expect(page).to have_content("From #{forked_project.full_path}")
expect(page).to have_content("into #{project2.full_path}")
end
it_behaves_like 'unavailable for an archived project' do
let(:project) { project2 }
end
end
end
end
end
end
import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlLink } from '@gitlab/ui';
import { GlDropdown, GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
......@@ -9,7 +9,12 @@ const TEST_ACTION = {
secondaryText: 'Lorem ipsum.',
tooltip: '',
href: '/sample',
attrs: { 'data-test': '123' },
attrs: {
'data-test': '123',
category: 'secondary',
href: '/sample',
variant: 'default',
},
};
const TEST_ACTION_2 = {
key: 'action2',
......@@ -40,8 +45,8 @@ describe('Actions button component', () => {
return directiveBinding.value;
};
const findLink = () => wrapper.find(GlLink);
const findLinkTooltip = () => getTooltip(findLink());
const findButton = () => wrapper.find(GlButton);
const findButtonTooltip = () => getTooltip(findButton());
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownTooltip = () => getTooltip(findDropdown());
const parseDropdownItems = () =>
......@@ -63,7 +68,7 @@ describe('Actions button component', () => {
};
});
const clickOn = (child, evt = new Event('click')) => child.vm.$emit('click', evt);
const clickLink = (...args) => clickOn(findLink(), ...args);
const clickLink = (...args) => clickOn(findButton(), ...args);
const clickDropdown = (...args) => clickOn(findDropdown(), ...args);
describe('with 1 action', () => {
......@@ -76,22 +81,19 @@ describe('Actions button component', () => {
});
it('should render single button', () => {
const link = findLink();
expect(link.attributes()).toEqual({
class: expect.any(String),
expect(findButton().attributes()).toMatchObject({
href: TEST_ACTION.href,
...TEST_ACTION.attrs,
});
expect(link.text()).toBe(TEST_ACTION.text);
expect(findButton().text()).toBe(TEST_ACTION.text);
});
it('should have tooltip', () => {
expect(findLinkTooltip()).toBe(TEST_ACTION.tooltip);
expect(findButtonTooltip()).toBe(TEST_ACTION.tooltip);
});
it('should have attrs', () => {
expect(findLink().attributes()).toMatchObject(TEST_ACTION.attrs);
expect(findButton().attributes()).toMatchObject(TEST_ACTION.attrs);
});
it('can click', () => {
......@@ -103,7 +105,7 @@ describe('Actions button component', () => {
it('should have tooltip', () => {
createComponent({ actions: [{ ...TEST_ACTION, tooltip: TEST_TOOLTIP }] });
expect(findLinkTooltip()).toBe(TEST_TOOLTIP);
expect(findButtonTooltip()).toBe(TEST_TOOLTIP);
});
});
......
......@@ -3,9 +3,27 @@ import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
const TEST_EDIT_URL = '/gitlab-test/test/-/edit/master/';
const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/master/-/';
const TEST_GITPOD_URL = 'https://gitpod.test/';
const ACTION_EDIT = {
href: TEST_EDIT_URL,
key: 'edit',
text: 'Edit',
secondaryText: 'Edit this file only.',
tooltip: '',
attrs: {
'data-qa-selector': 'edit_button',
'data-track-event': 'click_edit',
'data-track-label': 'Edit',
},
};
const ACTION_EDIT_CONFIRM_FORK = {
...ACTION_EDIT,
href: '#modal-confirm-fork-edit',
handle: expect.any(Function),
};
const ACTION_WEB_IDE = {
href: TEST_WEB_IDE_URL,
key: 'webide',
......@@ -14,13 +32,16 @@ const ACTION_WEB_IDE = {
text: 'Web IDE',
attrs: {
'data-qa-selector': 'web_ide_button',
'data-track-event': 'click_edit_ide',
'data-track-label': 'Web IDE',
},
};
const ACTION_WEB_IDE_FORK = {
const ACTION_WEB_IDE_CONFIRM_FORK = {
...ACTION_WEB_IDE,
href: '#modal-confirm-fork',
href: '#modal-confirm-fork-webide',
handle: expect.any(Function),
};
const ACTION_WEB_IDE_EDIT_FORK = { ...ACTION_WEB_IDE, text: 'Edit fork in Web IDE' };
const ACTION_GITPOD = {
href: TEST_GITPOD_URL,
key: 'gitpod',
......@@ -43,6 +64,7 @@ describe('Web IDE link component', () => {
function createComponent(props) {
wrapper = shallowMount(WebIdeLink, {
propsData: {
editUrl: TEST_EDIT_URL,
webIdeUrl: TEST_WEB_IDE_URL,
gitpodUrl: TEST_GITPOD_URL,
...props,
......@@ -57,15 +79,36 @@ describe('Web IDE link component', () => {
const findActionsButton = () => wrapper.find(ActionsButton);
const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
it.each`
props | expectedActions
${{}} | ${[ACTION_WEB_IDE]}
${{ webIdeIsFork: true }} | ${[{ ...ACTION_WEB_IDE, text: 'Edit fork in Web IDE' }]}
${{ needsToFork: true }} | ${[ACTION_WEB_IDE_FORK]}
${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: true }} | ${[ACTION_GITPOD]}
${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_GITPOD_ENABLE]}
${{ showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_WEB_IDE, ACTION_GITPOD_ENABLE]}
`('renders actions with props=$props', ({ props, expectedActions }) => {
it.each([
{
props: {},
expectedActions: [ACTION_WEB_IDE, ACTION_EDIT],
},
{
props: { isFork: true },
expectedActions: [ACTION_WEB_IDE_EDIT_FORK, ACTION_EDIT],
},
{
props: { needsToFork: true },
expectedActions: [ACTION_WEB_IDE_CONFIRM_FORK, ACTION_EDIT_CONFIRM_FORK],
},
{
props: { showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: true },
expectedActions: [ACTION_EDIT, ACTION_GITPOD],
},
{
props: { showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: false },
expectedActions: [ACTION_EDIT, ACTION_GITPOD_ENABLE],
},
{
props: { showGitpodButton: true, gitpodEnabled: false },
expectedActions: [ACTION_WEB_IDE, ACTION_EDIT, ACTION_GITPOD_ENABLE],
},
{
props: { showEditButton: false },
expectedActions: [ACTION_WEB_IDE],
},
])('renders actions with appropriately for given props', ({ props, expectedActions }) => {
createComponent(props);
expect(findActionsButton().props('actions')).toEqual(expectedActions);
......@@ -73,7 +116,12 @@ describe('Web IDE link component', () => {
describe('with multiple actions', () => {
beforeEach(() => {
createComponent({ showWebIdeButton: true, showGitpodButton: true, gitpodEnabled: true });
createComponent({
showEditButton: false,
showWebIdeButton: true,
showGitpodButton: true,
gitpodEnabled: true,
});
});
it('selected Web IDE by default', () => {
......
......@@ -445,13 +445,14 @@ RSpec.describe BlobHelper do
end
describe '#ide_fork_and_edit_path' do
let(:project) { create(:project) }
let(:current_user) { create(:user) }
let(:can_push_code) { true }
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:current_user) { user }
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).and_return(can_push_code)
allow(helper).to receive(:can?).and_return(true)
end
it 'returns path to fork the repo with a redirect param to the full IDE path' do
......@@ -472,6 +473,35 @@ RSpec.describe BlobHelper do
end
end
describe '#fork_and_edit_path' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:current_user) { user }
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).and_return(true)
end
it 'returns path to fork the repo with a redirect param to the full edit path' do
uri = URI(helper.fork_and_edit_path(project, "master", ""))
params = CGI.unescape(uri.query)
expect(uri.path).to eq("/#{project.namespace.path}/#{project.path}/-/forks")
expect(params).to include("continue[to]=/#{project.namespace.path}/#{project.path}/-/edit/master/")
expect(params).to include("namespace_key=#{current_user.namespace.id}")
end
context 'when user is not logged in' do
let(:current_user) { nil }
it 'returns nil' do
expect(helper.ide_fork_and_edit_path(project, "master", "")).to be_nil
end
end
end
describe '#editing_ci_config?' do
let(:project) { build(:project) }
......
......@@ -167,31 +167,60 @@ RSpec.describe TreeHelper do
end
end
describe '#vue_ide_link_data' do
describe '#web_ide_button_data' do
let(:blob) { project.repository.blob_at('refs/heads/master', @path) }
before do
@path = ''
@project = project
@ref = sha
allow(helper).to receive(:current_user).and_return(nil)
allow(helper).to receive(:can_collaborate_with_project?).and_return(true)
allow(helper).to receive(:can?).and_return(true)
end
subject { helper.vue_ide_link_data(project, sha) }
subject { helper.web_ide_button_data(blob: blob) }
it 'returns a list of attributes related to the project' do
expect(subject).to include(
web_ide_url_data: { path: project.full_path, is_fork: false },
project_path: project.full_path,
ref: sha,
is_fork: false,
needs_to_fork: false,
gitpod_enabled: false,
is_blob: false,
show_edit_button: false,
show_web_ide_button: true,
show_gitpod_button: false,
gitpod_url: "",
gitpod_enabled: nil
edit_url: '',
web_ide_url: "/-/ide/project/#{project.full_path}/edit/#{sha}",
gitpod_url: ''
)
end
context 'a blob is passed' do
before do
@path = 'README.md'
end
it 'returns edit url and webide url for the blob' do
expect(subject).to include(
show_edit_button: true,
edit_url: "/#{project.full_path}/-/edit/#{sha}/#{@path}",
web_ide_url: "/-/ide/project/#{project.full_path}/edit/#{sha}/-/#{@path}"
)
end
end
context 'user does not have write access but a personal fork exists' do
include ProjectForksHelper
let_it_be(:user) { create(:user) }
let!(:forked_project) { create(:project, :repository, namespace: user.namespace) }
let(:forked_project) { create(:project, :repository, namespace: user.namespace) }
before do
project.add_guest(user)
......@@ -200,9 +229,49 @@ RSpec.describe TreeHelper do
allow(helper).to receive(:current_user).and_return(user)
end
it 'includes web_ide_url_data: forked_project.full_path' do
it 'includes forked project path as project_path' do
expect(subject).to include(
project_path: forked_project.full_path,
is_fork: true,
needs_to_fork: false,
show_edit_button: false,
web_ide_url: "/-/ide/project/#{forked_project.full_path}/edit/#{sha}"
)
end
context 'a blob is passed' do
before do
@path = 'README.md'
end
it 'returns edit url and web ide for the blob in the fork' do
expect(subject).to include(
is_blob: true,
show_edit_button: true,
# edit urls are automatically redirected to the fork
edit_url: "/#{project.full_path}/-/edit/#{sha}/#{@path}",
web_ide_url: "/-/ide/project/#{forked_project.full_path}/edit/#{sha}/-/#{@path}"
)
end
end
end
context 'for archived project' do
before do
allow(helper).to receive(:can_collaborate_with_project?).and_return(false)
allow(helper).to receive(:can?).and_return(false)
project.update!(archived: true)
@path = 'README.md'
end
it 'does not show any buttons' do
expect(subject).to include(
web_ide_url_data: { path: forked_project.full_path, is_fork: true }
is_blob: true,
show_edit_button: false,
show_web_ide_button: false,
show_gitpod_button: false
)
end
end
......@@ -216,11 +285,32 @@ RSpec.describe TreeHelper do
allow(helper).to receive(:current_user).and_return(user)
end
it 'includes web_ide_url_data: project.full_path' do
it 'includes original project path as project_path' do
expect(subject).to include(
web_ide_url_data: { path: project.full_path, is_fork: false }
project_path: project.full_path,
is_fork: false,
needs_to_fork: false,
show_edit_button: false,
web_ide_url: "/-/ide/project/#{project.full_path}/edit/#{sha}"
)
end
context 'a blob is passed' do
before do
@path = 'README.md'
end
it 'returns edit url and web ide for the blob in the fork' do
expect(subject).to include(
is_blob: true,
show_edit_button: true,
edit_url: "/#{project.full_path}/-/edit/#{sha}/#{@path}",
web_ide_url: "/-/ide/project/#{project.full_path}/edit/#{sha}/-/#{@path}"
)
end
end
end
context 'gitpod feature is enabled' do
......
# frozen_string_literal: true
# These helpers help you interact within the blobs page and blobs edit page (Single file editor).
module BlobSpecHelpers
include ActionView::Helpers::JavaScriptHelper
def set_default_button(type)
evaluate_script("localStorage.setItem('gl-web-ide-button-selected', '#{type}')")
end
def unset_default_button
set_default_button('')
end
def editor_value
evaluate_script('monaco.editor.getModels()[0].getValue()')
end
def set_editor_value(value)
execute_script("monaco.editor.getModels()[0].setValue('#{value}')")
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment