diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 730ea9fe80125da4869b2f3bca73bddc62485e93..7e097041beb874744105071045f974db2df605e9 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { createAlert } from '~/alert';
-import NewCommitForm from '../new_commit_form';
+import initBlobEditHeader from '~/blob_edit/blob_edit_header';
export default () => {
const editBlobForm = $('.js-edit-blob-form');
@@ -20,8 +20,7 @@ export default () => {
import('./edit_blob')
.then(({ default: EditBlob } = {}) => {
- // eslint-disable-next-line no-new
- new EditBlob({
+ const editor = new EditBlob({
assetsPath: `${urlRoot}${assetsPath}`,
filePath,
currentAction,
@@ -30,6 +29,8 @@ export default () => {
isMarkdown,
previewMarkdownPath,
});
+
+ initBlobEditHeader(editor);
})
.catch((e) =>
createAlert({
@@ -47,8 +48,6 @@ export default () => {
window.onbeforeunload = null;
});
- new NewCommitForm(editBlobForm); // eslint-disable-line no-new
-
// returning here blocks page navigation
window.onbeforeunload = () => '';
}
diff --git a/app/assets/javascripts/blob_edit/blob_edit_header.js b/app/assets/javascripts/blob_edit/blob_edit_header.js
new file mode 100644
index 0000000000000000000000000000000000000000..ea2891a191187f048e5542b6939d26c4439fd52c
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/blob_edit_header.js
@@ -0,0 +1,44 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import BlobEditHeader from '~/repository/pages/blob_edit_header.vue';
+
+export default function initBlobEditHeader(editor) {
+ const el = document.querySelector('.js-blob-edit-header');
+
+ if (!el) {
+ return null;
+ }
+
+ const {
+ updatePath,
+ cancelPath,
+ originalBranch,
+ targetBranch,
+ canPushCode,
+ canPushToBranch,
+ emptyRepo,
+ isUsingLfs,
+ blobName,
+ branchAllowsCollaboration,
+ lastCommitSha,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ editor,
+ updatePath,
+ cancelPath,
+ originalBranch,
+ targetBranch,
+ blobName,
+ lastCommitSha,
+ emptyRepo: parseBoolean(emptyRepo),
+ canPushCode: parseBoolean(canPushCode),
+ canPushToBranch: parseBoolean(canPushToBranch),
+ isUsingLfs: parseBoolean(isUsingLfs),
+ branchAllowsCollaboration: parseBoolean(branchAllowsCollaboration),
+ },
+ render: (createElement) => createElement(BlobEditHeader),
+ });
+}
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 123e59bdff0431b9c182166e36c811b86e7b424d..0a35ba64c075d06f8d2412e6a3d945d30e2e5f90 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -216,4 +216,8 @@ export default class EditBlob {
this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
this.editor.updateOptions({ wordWrap: this.isSoftWrapped ? 'on' : 'off' });
}
+
+ getFileContent() {
+ return this.editor?.getValue();
+ }
}
diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue
index 3aec35ee96000bcb34e8f8e4e6d6b60254c41450..c8095c5c3c4c69697a861b826374ce007b91cfb0 100644
--- a/app/assets/javascripts/repository/components/blob_button_group.vue
+++ b/app/assets/javascripts/repository/components/blob_button_group.vue
@@ -144,7 +144,7 @@ export default {
-
-
-
+
+
+
+
+
+
+
+
+
@@ -321,7 +355,6 @@ export default {
:disabled="loading"
name="branch_name"
required
- :placeholder="__('example-branch-name')"
class="gl-mt-2"
/>
@@ -333,17 +366,17 @@ export default {
-
+
+
@@ -353,6 +386,17 @@ export default {
+
+
+ {{ originalBranch }}
+
+
+ {{ $options.i18n.BRANCH_IN_FORK_MESSAGE }}
+
diff --git a/app/assets/javascripts/repository/pages/blob_edit_header.vue b/app/assets/javascripts/repository/pages/blob_edit_header.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b372e91a043b3655e61a0346d5925685715459fc
--- /dev/null
+++ b/app/assets/javascripts/repository/pages/blob_edit_header.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+ {{ $options.i18n.headerText }}
+
+
+ {{ $options.i18n.cancelButtonText }}
+
+ {{ $options.i18n.commitButtonText }}
+
+
+
+
+
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 91a7c8dbaf117ae384da4e61c878aefdbc63a060..aaf78bc93f9e690f1bf25c657fdecf161ab5e855 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -297,11 +297,27 @@ def vue_blob_app_data(project, blob, ref)
project_path: project.full_path,
resource_id: project.to_global_id,
user_id: current_user.present? ? current_user.to_global_id : '',
- target_branch: project.empty_repo? ? ref : @ref,
- original_branch: @ref,
+ target_branch: selected_branch,
+ original_branch: ref,
can_download_code: can?(current_user, :download_code, project).to_s
}
end
+
+ def edit_blob_app_data(project, id, blob, ref)
+ {
+ update_path: project_update_blob_path(project, id),
+ cancel_path: project_blob_path(project, id),
+ original_branch: ref,
+ target_branch: selected_branch,
+ can_push_code: can?(current_user, :push_code, project).to_s,
+ can_push_to_branch: project.present(current_user: current_user).can_current_user_push_to_branch?(ref).to_s,
+ empty_repo: project.empty_repo?.to_s,
+ is_using_lfs: blob.stored_externally?.to_s,
+ blob_name: blob.name,
+ branch_allows_collaboration: project.branch_allows_collaboration?(current_user, ref).to_s,
+ last_commit_sha: @last_commit_sha
+ }
+ end
end
BlobHelper.prepend_mod_with('BlobHelper')
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index d607a94ca5dbc1ca58dbe2af9d5b0600eb481f89..d6b825621e7ab91168e4d5a9620dc4e0e2d8857b 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -3,6 +3,7 @@
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco')
- add_page_specific_style 'page_bundles/editor'
+
- if @conflict
= render Pajamas::AlertComponent.new(alert_options: { class: 'gl-mb-5 gl-mt-5' },
variant: :danger,
@@ -18,8 +19,17 @@
- blob_url = project_blob_path(@project, @id)
= _('Someone edited the file the same time you did. Please check out %{link_start}the file %{icon}%{link_end} and make sure your changes will not unintentionally remove theirs.').html_safe % { link_start: blob_link_start % { url: blob_url }, link_end: link_end , icon: external_link_icon }
-%h1.page-title.gl-text-size-h-display.blob-edit-page-title
- Edit file
+
+.js-blob-edit-header{ data: edit_blob_app_data(@project, @id, @blob, @ref) }
+ .gl-mb-4.gl-mt-5.gl-items-center.gl-justify-between.md:gl-flex.lg:gl-my-5
+ %h1{ class: 'md:!gl-mb-0 gl-heading-1 gl-inline-block' }
+ = _('Edit file')
+ .gl-flex.gl-gap-3
+ = render Pajamas::ButtonComponent.new do
+ = _('Cancel')
+ = render Pajamas::ButtonComponent.new(variant: :confirm) do
+ = _('Commit changes')
+
.file-editor
= gl_tabs_nav({ class: 'js-edit-mode nav-links gl-border-0'}) do
= gl_tab_link_to _('Write'), '#editor', { tab_class: 'active' }
@@ -28,8 +38,3 @@
= form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-edit-blob-form', data: blob_editor_paths(@project, 'put')) do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
- = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
- = hidden_field_tag 'last_commit_sha', @last_commit_sha
- = hidden_field_tag 'content', '', id: "file-content"
- = hidden_field_tag 'from_merge_request_iid', params[:from_merge_request_iid]
- = render 'projects/commit_button', ref: @ref, cancel_path: project_blob_path(@project, @id)
diff --git a/ee/spec/helpers/ee/blob_helper_spec.rb b/ee/spec/helpers/ee/blob_helper_spec.rb
index a7fd1460a44f74e1a35f7b9e2fea424033f5ce8c..d9eeefb7d1d30cb155378fe96d9ebfad70666aa7 100644
--- a/ee/spec/helpers/ee/blob_helper_spec.rb
+++ b/ee/spec/helpers/ee/blob_helper_spec.rb
@@ -69,7 +69,7 @@
let(:ref) { 'main' }
it 'returns data related to blob app' do
- allow(helper).to receive(:current_user).and_return(nil)
+ allow(helper).to receive_messages(selected_branch: ref, current_user: nil)
expect(helper.vue_blob_app_data(project, blob, ref)).to include({
user_id: '',
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 42b14a7ab30c4e063d7f6e594273daa82dcd688d..48f8472eafe59e7a2ce131578ae7497d3e011c8b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -64398,6 +64398,9 @@ msgstr ""
msgid "Your browser doesn't support WebAuthn. Please use a supported browser, e.g. Chrome (67+) or Firefox (60+)."
msgstr ""
+msgid "Your changes can be committed to %{branchName} because a merge request is open."
+msgstr ""
+
msgid "Your changes can be committed to %{branch_name} because a merge request is open."
msgstr ""
@@ -65493,9 +65496,6 @@ msgstr ""
msgid "event"
msgstr ""
-msgid "example-branch-name"
-msgstr ""
-
msgid "example.com"
msgstr ""
diff --git a/qa/qa/page/file/shared/editor.rb b/qa/qa/page/file/shared/editor.rb
index 86d044bb4fa117adc5ab256544c5667ff4930340..93db7b6807fe15258e0e52e6d26110956c79649a 100644
--- a/qa/qa/page/file/shared/editor.rb
+++ b/qa/qa/page/file/shared/editor.rb
@@ -13,6 +13,18 @@ def self.included(base)
base.view 'app/views/projects/blob/_editor.html.haml' do
element 'source-editor-preview-container'
end
+
+ base.view 'app/assets/javascripts/repository/components/commit_changes_modal.vue' do
+ element 'commit-change-modal'
+ end
+
+ base.view 'app/assets/javascripts/repository/components/commit_changes_modal.vue' do
+ element 'commit-change-modal-commit-button'
+ end
+
+ base.view 'app/assets/javascripts/repository/pages/blob_edit_header.vue' do
+ element 'blob-edit-header-commit-button'
+ end
end
def add_content(content)
@@ -27,6 +39,20 @@ def remove_content
end
end
+ def click_commit_changes_in_header
+ click_element('blob-edit-header-commit-button')
+ end
+
+ def commit_changes_through_modal
+ within_element 'commit-change-modal' do
+ click_element('commit-change-modal-commit-button')
+ end
+ end
+
+ def has_modal_commit_button?
+ has_element?('commit-change-modal-commit-button')
+ end
+
private
def text_area
diff --git a/qa/qa/page/file/show.rb b/qa/qa/page/file/show.rb
index 4637cedad418a5e7dec6d53f56c596c833c963f2..6bb4b9b1b80448afd14691c92d144a092254feaa 100644
--- a/qa/qa/page/file/show.rb
+++ b/qa/qa/page/file/show.rb
@@ -7,6 +7,7 @@ class Show < Page::Base
include Shared::CommitMessage
include Layout::Flash
include Page::Component::BlobContent
+ include Shared::Editor
view 'app/assets/javascripts/repository/components/blob_button_group.vue' do
element 'lock-button'
@@ -34,10 +35,6 @@ def highlight_text
def explain_code
click_element('question-icon')
end
-
- def click_commit_changes
- click_on 'Commit changes'
- end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/file/delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/file/delete_file_via_web_spec.rb
index b00a34de3432005d04e0922e82be224a618cd55b..865c632e71725a8a59763b66e262e3df1855e08a 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/file/delete_file_via_web_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/file/delete_file_via_web_spec.rb
@@ -16,9 +16,10 @@ module QA
Page::File::Show.perform do |file|
file.click_delete
file.add_commit_message(commit_message_for_delete)
- file.click_commit_changes
end
+ Page::File::Edit.perform(&:commit_changes_through_modal)
+
Page::Project::Show.perform do |project|
aggregate_failures 'file details' do
expect(project).to have_notice('The file has been successfully deleted.')
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb
index e119b8e2e62849e52e1e3c1eb5504efdb6bf39ea..4c56ae4bd31c37c9a1bb557a37f0ffbb92a01cab 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb
@@ -19,8 +19,9 @@ module QA
Page::File::Form.perform do |file|
file.remove_content
file.add_content(updated_file_content)
+ file.click_commit_changes_in_header
file.add_commit_message(commit_message_for_update)
- file.commit_changes
+ file.commit_changes_through_modal
end
Page::File::Show.perform do |file|
diff --git a/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
index 9558d15ab14b86f56ca58b000d41ff2039502fc1..d04775b9d42a4f976da4ced7725cb180790d9eee 100644
--- a/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
@@ -29,7 +29,8 @@ module QA
file.add_content("# #{edited_readme_content}")
file.preview
expect(file.has_markdown_preview?('h1', edited_readme_content)).to be true
- file.commit_changes
+ file.click_commit_changes_in_header
+ file.commit_changes_through_modal
end
Page::File::Show.perform do |file|
diff --git a/qa/qa/specs/features/browser_ui/9_data_stores/user/user_inherited_access_spec.rb b/qa/qa/specs/features/browser_ui/9_data_stores/user/user_inherited_access_spec.rb
index 3d9a98048376a1a9c12b58c7563ad8243683cd53..eb588880982e2e26f45d109d68046b33a4090417 100644
--- a/qa/qa/specs/features/browser_ui/9_data_stores/user/user_inherited_access_spec.rb
+++ b/qa/qa/specs/features/browser_ui/9_data_stores/user/user_inherited_access_spec.rb
@@ -41,8 +41,9 @@ module QA
Page::File::Show.perform(&:click_edit)
- Page::File::Form.perform do |file_form|
- expect(file_form).to have_element('data-testid': 'commit-button')
+ Page::File::Edit.perform do |file|
+ file.click_commit_changes_in_header
+ expect(file).to have_modal_commit_button
end
end
end
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index 8618dca5873dbf69de90c128868c9e7bbf5516bd..f50af8dace9da4a3880786524140df6e0d6cd8af 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -40,6 +40,8 @@
end
it 'mentions commits will go to the source branch' do
+ click_button 'Commit changes'
+
expect(page).to have_content('Your changes can be committed to fix because a merge request is open.')
end
@@ -48,6 +50,11 @@
editor_set_value(content)
click_button 'Commit changes'
+
+ within_testid('commit-change-modal') do
+ click_button 'Commit changes'
+ end
+
wait_for_requests
expect(page).to have_content('Your changes have been committed successfully')
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index d8b1971007b8a5ff0368cb8fd96402e18a53f30e..fe79dd7d6d475c0c7b964aab4f41cd6ee65f38ba 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -35,7 +35,11 @@ def edit_and_commit(commit_changes: true, is_diff: false)
fill_editor(content: "class NextFeature#{object_id}\\nend\\n")
if commit_changes
- click_button 'Commit changes'
+ click_button('Commit changes')
+
+ within_testid('commit-change-modal') do
+ click_button('Commit changes')
+ end
end
end
@@ -206,18 +210,23 @@ def has_toolbar_buttons
it 'shows blob editor with same branch' do
expect(page).to have_current_path(project_edit_blob_path(project, tree_join(branch, file_path)))
- expect(find('.js-branch-name').value).to eq(branch)
+
+ click_button('Commit changes')
+
+ expect(page).to have_selector('code', text: branch)
end
end
context 'with protected branch' do
- it 'shows blob editor with patch branch' do
+ it 'shows blob editor with patch branch and option to create MR' do
freeze_time do
visit project_edit_blob_path(project, tree_join(protected_branch, file_path))
- epoch = Time.zone.now.strftime('%s%L').last(5)
+ click_button('Commit changes')
- expect(find('.js-branch-name').value).to eq "#{user.username}-protected-branch-patch-#{epoch}"
+ epoch = Time.zone.now.strftime('%s%L').last(5)
+ expect(page).to have_checked_field _('Create a merge request for this change')
+ expect(find_field(_('Commit to a new branch')).value).to eq "#{user.username}-protected-branch-patch-#{epoch}"
end
end
end
@@ -234,7 +243,10 @@ def has_toolbar_buttons
it 'shows blob editor with same branch' do
expect(page).to have_current_path(project_edit_blob_path(project, tree_join(branch, file_path)))
- expect(find('.js-branch-name').value).to eq(branch)
+
+ click_button('Commit changes')
+
+ expect(page).to have_selector('code', text: branch)
end
end
end
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
index 39e75f2cee6629ffcafaae255abf674fec5a4dd8..9f3636405e2e0898efccecc716286176ef3bf190 100644
--- a/spec/features/projects/files/editing_a_file_spec.rb
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User wants to edit a file', feature_category: :source_code_management do
+RSpec.describe 'Projects > Files > User wants to edit a file', :js, feature_category: :source_code_management do
include ProjectForksHelper
let(:project) { create(:project, :repository, :public) }
let(:user) { project.first_owner }
@@ -28,6 +28,10 @@
click_button 'Commit changes'
+ within_testid('commit-change-modal') do
+ click_button('Commit changes')
+ end
+
expect(page).to have_content 'Someone edited the file the same time you did.'
end
end
@@ -52,6 +56,10 @@
it 'renders an error message' do
click_button 'Commit changes'
+ within_testid('commit-change-modal') do
+ click_button('Commit changes')
+ end
+
expect(page).to have_content(
%(Error: Can't edit this file. The fork and upstream project have diverged. Edit the file on the fork)
)
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 81cebde16ca4a3984d69392ab4e57b1fe7d76dde..2b6e3a7ab753b0c616b8aa9ad0fb2007b5ed2db7 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -81,9 +81,13 @@
find('.file-editor', match: :first)
editor_set_value('*.rbca')
- fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
+ within_testid('commit-change-modal') do
+ fill_in(:commit_message, with: 'New commit message', visible: true)
+ click_button('Commit changes')
+ end
+
expect(page).to have_current_path(project_blob_path(project, 'master/.gitignore'), ignore_query: true)
wait_for_requests
@@ -97,9 +101,13 @@
find('.file-editor', match: :first)
editor_set_value('*.rbca')
- fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
+ within_testid('commit-change-modal') do
+ fill_in(:commit_message, with: 'New commit message', visible: true)
+ click_button('Commit changes')
+ end
+
expect(page).to have_current_path(project_blob_path(project, 'master/.gitignore'), ignore_query: true)
wait_for_requests
@@ -117,10 +125,14 @@
find('.file-editor', match: :first)
editor_set_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')
+ within_testid('commit-change-modal') do
+ fill_in(:commit_message, with: 'New commit message', visible: true)
+ choose(option: true)
+ fill_in(:branch_name, with: 'new_branch_name', visible: true)
+ click_button('Commit changes')
+ end
expect(page).to have_current_path(project_new_merge_request_path(project), ignore_query: true)
click_link('Changes')
@@ -206,9 +218,13 @@ def expect_fork_status
find('.file-editor', match: :first)
editor_set_value('*.rbca')
- fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
+ within_testid('commit-change-modal') do
+ fill_in(:commit_message, with: 'New commit message', visible: true)
+ click_button('Commit changes')
+ end
+
fork = user.fork_of(project2.reload)
expect(page).to have_current_path(project_new_merge_request_path(fork), ignore_query: true)
@@ -233,9 +249,13 @@ def expect_fork_status
expect(page).not_to have_link('Fork')
editor_set_value('*.rbca')
- fill_in(:commit_message, with: 'Another commit', visible: true)
click_button('Commit changes')
+ within_testid('commit-change-modal') do
+ fill_in(:commit_message, with: 'Another commit', visible: true)
+ click_button('Commit changes')
+ end
+
fork = user.fork_of(project2)
expect(page).to have_current_path(project_new_merge_request_path(fork), ignore_query: true)
diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js
index ca75cb00fbd367437fe03d18a7265cd8b8dbbbcf..b39e471272cc0377114f56e0459db488258f6e99 100644
--- a/spec/frontend/blob_edit/blob_bundle_spec.js
+++ b/spec/frontend/blob_edit/blob_bundle_spec.js
@@ -2,12 +2,14 @@ import $ from 'jquery';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import blobBundle from '~/blob_edit/blob_bundle';
+import initBlobEditHeader from '~/blob_edit/blob_edit_header';
import SourceEditor from '~/blob_edit/edit_blob';
import { createAlert } from '~/alert';
jest.mock('~/blob_edit/edit_blob');
jest.mock('~/alert');
+jest.mock('~/blob_edit/blob_edit_header');
describe('BlobBundle', () => {
beforeAll(() => {
@@ -27,7 +29,8 @@ describe('BlobBundle', () => {
blobBundle();
await waitForPromises();
expect(SourceEditor).toHaveBeenCalled();
-
+ expect(initBlobEditHeader).toHaveBeenCalledTimes(1);
+ expect(initBlobEditHeader).toHaveBeenCalledWith(expect.any(SourceEditor));
resetHTMLFixture();
});
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index 27e456437586c58a80250743c7ca93a36b046e6a..974f0d57c0ebad05d533b3eaa23f1cd380bafada 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -49,12 +49,14 @@ describe('Blob Editing', () => {
const filePath = 'path/to/file.js';
const useMock = jest.fn(() => markdownExtensions);
const unuseMock = jest.fn();
+ const valueMock = 'test value';
+ const getValueMock = jest.fn().mockReturnValue('test value');
const emitter = new Emitter();
const mockInstance = {
use: useMock,
unuse: unuseMock,
setValue: jest.fn(),
- getValue: jest.fn().mockReturnValue('test value'),
+ getValue: getValueMock,
focus: jest.fn(),
onDidChangeModelLanguage: emitter.event,
updateModelLanguage: jest.fn(),
@@ -114,6 +116,11 @@ describe('Blob Editing', () => {
}),
);
});
+
+ it('returns content from the editor', () => {
+ expect(blobInstance.getFileContent()).toBe(valueMock);
+ expect(getValueMock).toHaveBeenCalled();
+ });
});
it('loads SourceEditorExtension and FileTemplateExtension by default', async () => {
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
index 42aed016f40f1b2f5b0444ad832f95319ad43d12..2deed06dfd765b0da2d5e0193dad4376f9e83d90 100644
--- a/spec/frontend/repository/components/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -155,7 +155,7 @@ describe('BlobButtonGroup component', () => {
targetBranch,
originalBranch,
canPushCode,
- deletePath,
+ actionPath: deletePath,
emptyRepo,
isUsingLfs,
});
diff --git a/spec/frontend/repository/components/commit_changes_modal_spec.js b/spec/frontend/repository/components/commit_changes_modal_spec.js
index e945f2cc37f683929f63920cdb90e70ea9dea7e4..c9980415667181b691c5a55254b704c3fd027c1f 100644
--- a/spec/frontend/repository/components/commit_changes_modal_spec.js
+++ b/spec/frontend/repository/components/commit_changes_modal_spec.js
@@ -12,6 +12,7 @@ import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { RENDER_ALL_SLOTS_TEMPLATE, stubComponent } from 'helpers/stub_component';
+import setWindowLocation from 'helpers/set_window_location_helper';
import CommitChangesModal from '~/repository/components/commit_changes_modal.vue';
import { sprintf } from '~/locale';
@@ -19,7 +20,7 @@ jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
const initialProps = {
modalId: 'Delete-blob',
- deletePath: 'some/path',
+ actionPath: 'some/path',
commitMessage: 'Delete File',
targetBranch: 'some-target-branch',
originalBranch: 'main',
@@ -84,21 +85,33 @@ describe('CommitChangesModal', () => {
linkEnd: '',
});
- beforeEach(() => createComponent({ isUsingLfs: true }));
+ describe('when deleting a file', () => {
+ beforeEach(() => createComponent({ isUsingLfs: true }));
- it('renders a modal containing LFS text', () => {
- expect(findModal().props('title')).toBe(lfsTitleText);
- expect(findModal().text()).toContain(primaryLfsText);
- expect(findModal().text()).toContain(secondaryLfsText);
+ it('renders a modal containing LFS text', () => {
+ expect(findModal().props('title')).toBe(lfsTitleText);
+ expect(findModal().text()).toContain(primaryLfsText);
+ expect(findModal().text()).toContain(secondaryLfsText);
+ });
+
+ it('hides the LFS content when the continue button is clicked', async () => {
+ findModal().vm.$emit('primary', { preventDefault: jest.fn() });
+ await nextTick();
+
+ expect(findModal().props('title')).not.toBe(lfsTitleText);
+ expect(findModal().text()).not.toContain(primaryLfsText);
+ expect(findModal().text()).not.toContain(secondaryLfsText);
+ });
});
- it('hides the LFS content if the continue button is clicked', async () => {
- findModal().vm.$emit('primary', { preventDefault: jest.fn() });
- await nextTick();
+ describe('when editing a file', () => {
+ beforeEach(() => createComponent({ isUsingLfs: true, isEdit: true }));
- expect(findModal().props('title')).not.toBe(lfsTitleText);
- expect(findModal().text()).not.toContain(primaryLfsText);
- expect(findModal().text()).not.toContain(secondaryLfsText);
+ it('does not render LFS text', () => {
+ expect(findModal().props('title')).not.toBe(lfsTitleText);
+ expect(findModal().text()).not.toContain(primaryLfsText);
+ expect(findModal().text()).not.toContain(secondaryLfsText);
+ });
});
});
@@ -119,7 +132,7 @@ describe('CommitChangesModal', () => {
describe('form', () => {
it('gets passed the path for action attribute', () => {
createComponent();
- expect(findForm().attributes('action')).toBe(initialProps.deletePath);
+ expect(findForm().attributes('action')).toBe(initialProps.actionPath);
});
it('shows the correct form fields when commit to current branch', () => {
@@ -143,53 +156,65 @@ describe('CommitChangesModal', () => {
it('shows the correct form fields when `canPushToBranch` is `false`', () => {
createComponent({ canPushToBranch: false, canPushCode: true });
- expect(wrapper.vm.$data.form.fields.branch_name.value).toBe('');
+ expect(wrapper.vm.$data.form.fields.branch_name.value).toBe('some-target-branch');
expect(findCommitTextarea().exists()).toBe(true);
expect(findRadioGroup().exists()).toBe(false);
expect(findTargetInput().exists()).toBe(true);
expect(findCreateMrCheckbox().text()).toBe('Create a merge request for this change');
});
- it('clear branch name when new branch option is selected', async () => {
- createComponent();
- expect(wrapper.vm.$data.form.fields.branch_name).toEqual({
- feedback: null,
- required: true,
- state: true,
- value: 'main',
- });
-
- findFormRadioGroup().vm.$emit('input', true);
- await nextTick();
-
- expect(wrapper.vm.$data.form.fields.branch_name).toEqual({
- feedback: null,
- required: true,
- state: true,
- value: '',
- });
- });
-
it.each`
- input | value | emptyRepo | canPushCode | canPushToBranch | exist
- ${'authenticity_token'} | ${'mock-csrf-token'} | ${false} | ${true} | ${true} | ${true}
- ${'authenticity_token'} | ${'mock-csrf-token'} | ${true} | ${false} | ${true} | ${true}
- ${'_method'} | ${'delete'} | ${false} | ${true} | ${true} | ${true}
- ${'_method'} | ${'delete'} | ${true} | ${false} | ${true} | ${true}
- ${'original_branch'} | ${initialProps.originalBranch} | ${false} | ${true} | ${true} | ${true}
- ${'original_branch'} | ${undefined} | ${true} | ${true} | ${true} | ${false}
- ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true} | ${true}
- ${'create_merge_request'} | ${'1'} | ${false} | ${true} | ${true} | ${true}
- ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${false} | ${true}
- ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true} | ${true}
- ${'create_merge_request'} | ${undefined} | ${true} | ${false} | ${true} | ${false}
+ input | value | emptyRepo | canPushCode | canPushToBranch | method | fileContent | filePath | lastCommitSha | fromMergeRequestIid | isEdit | exist
+ ${'authenticity_token'} | ${'mock-csrf-token'} | ${false} | ${true} | ${true} | ${'delete'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${true}
+ ${'authenticity_token'} | ${'mock-csrf-token'} | ${true} | ${false} | ${true} | ${'delete'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${true}
+ ${'_method'} | ${'delete'} | ${false} | ${true} | ${true} | ${'delete'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${true}
+ ${'_method'} | ${'delete'} | ${true} | ${false} | ${true} | ${'delete'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${true}
+ ${'_method'} | ${'put'} | ${false} | ${true} | ${true} | ${'put'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${true}
+ ${'_method'} | ${'put'} | ${true} | ${false} | ${true} | ${'put'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${true}
+ ${'original_branch'} | ${initialProps.originalBranch} | ${false} | ${true} | ${true} | ${'delete'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${true}
+ ${'original_branch'} | ${undefined} | ${true} | ${true} | ${true} | ${'delete'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${false}
+ ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true} | ${'delete'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${true}
+ ${'create_merge_request'} | ${'1'} | ${false} | ${true} | ${true} | ${'delete'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${true}
+ ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${false} | ${'delete'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${true}
+ ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true} | ${'delete'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${true}
+ ${'create_merge_request'} | ${undefined} | ${true} | ${false} | ${true} | ${'delete'} | ${''} | ${''} | ${''} | ${''} | ${false} | ${false}
+ ${'content'} | ${'some new content'} | ${false} | ${false} | ${true} | ${'put'} | ${'some new content'} | ${'.gitignore'} | ${''} | ${''} | ${false} | ${false}
+ ${'file_path'} | ${'.gitignore'} | ${false} | ${false} | ${true} | ${'put'} | ${'some new content'} | ${'.gitignore'} | ${''} | ${''} | ${false} | ${false}
+ ${'file_path'} | ${'.gitignore'} | ${false} | ${false} | ${true} | ${'put'} | ${'some new content'} | ${'.gitignore'} | ${''} | ${''} | ${true} | ${true}
+ ${'last_commit_sha'} | ${'782426692977b2cedb4452ee6501a404410f9b00'} | ${false} | ${false} | ${true} | ${'put'} | ${''} | ${''} | ${'782426692977b2cedb4452ee6501a404410f9b00'} | ${''} | ${false} | ${false}
+ ${'last_commit_sha'} | ${'782426692977b2cedb4452ee6501a404410f9b00'} | ${false} | ${false} | ${true} | ${'put'} | ${''} | ${''} | ${'782426692977b2cedb4452ee6501a404410f9b00'} | ${''} | ${true} | ${true}
+ ${'from_merge_request_iid'} | ${'17'} | ${false} | ${false} | ${true} | ${'put'} | ${''} | ${''} | ${''} | ${'17'} | ${false} | ${false}
+ ${'from_merge_request_iid'} | ${'17'} | ${false} | ${false} | ${true} | ${'put'} | ${''} | ${''} | ${''} | ${'17'} | ${true} | ${true}
`(
'passes $input as a hidden input with the correct value',
- ({ input, value, emptyRepo, canPushCode, canPushToBranch, exist }) => {
+ ({
+ input,
+ value,
+ emptyRepo,
+ canPushCode,
+ canPushToBranch,
+ exist,
+ method,
+ fileContent,
+ lastCommitSha,
+ fromMergeRequestIid,
+ isEdit,
+ filePath,
+ }) => {
+ if (fromMergeRequestIid) {
+ setWindowLocation(
+ `https://gitlab.test/foo?from_merge_request_iid=${fromMergeRequestIid}`,
+ );
+ }
createComponent({
emptyRepo,
canPushCode,
canPushToBranch,
+ method,
+ fileContent,
+ lastCommitSha,
+ isEdit,
+ filePath,
});
const inputMethod = findForm().find(`input[name="${input}"]`);
diff --git a/spec/frontend/repository/pages/blob_edit_header_spec.js b/spec/frontend/repository/pages/blob_edit_header_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f167780ef57f8335278740ed8e0e82de8ac92c94
--- /dev/null
+++ b/spec/frontend/repository/pages/blob_edit_header_spec.js
@@ -0,0 +1,80 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CommitChangesModal from '~/repository/components/commit_changes_modal.vue';
+import BlobEditHeader from '~/repository/pages/blob_edit_header.vue';
+import { stubComponent } from 'helpers/stub_component';
+
+describe('BlobEditHeader', () => {
+ let wrapper;
+ const mockEditor = {
+ getFileContent: jest.fn().mockReturnValue('test content'),
+ filepathFormMediator: { $filenameInput: { val: jest.fn().mockReturnValue('.gitignore') } },
+ };
+
+ const createWrapper = () => {
+ return shallowMountExtended(BlobEditHeader, {
+ provide: {
+ editor: mockEditor,
+ updatePath: '/update',
+ cancelPath: '/cancel',
+ originalBranch: 'main',
+ targetBranch: 'feature',
+ blobName: 'test.js',
+ canPushCode: true,
+ canPushToBranch: true,
+ emptyRepo: false,
+ isUsingLfs: false,
+ branchAllowsCollaboration: false,
+ lastCommitSha: '782426692977b2cedb4452ee6501a404410f9b00',
+ },
+ stubs: {
+ CommitChangesModal: stubComponent(CommitChangesModal, {
+ methods: {
+ show: jest.fn(),
+ },
+ }),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ const findTitle = () => wrapper.find('h1');
+ const findButtons = () => wrapper.findAllComponents(GlButton);
+ const findCommitChangesModal = () => wrapper.findComponent(CommitChangesModal);
+ const findCommitChangesButton = () => wrapper.findByTestId('blob-edit-header-commit-button');
+
+ it('renders title with two buttons', () => {
+ expect(findTitle().text()).toBe('Edit file');
+ const buttons = findButtons();
+ expect(buttons).toHaveLength(2);
+ expect(buttons.at(0).text()).toBe('Cancel');
+ expect(buttons.at(1).text()).toBe('Commit changes');
+ });
+
+ it('opens commit changes modal with correct props', async () => {
+ findCommitChangesButton().vm.$emit('click');
+ await nextTick();
+ expect(mockEditor.getFileContent).toHaveBeenCalled();
+ expect(findCommitChangesModal().props()).toEqual({
+ actionPath: '/update',
+ canPushCode: true,
+ canPushToBranch: true,
+ commitMessage: 'Edit test.js',
+ emptyRepo: false,
+ fileContent: 'test content',
+ filePath: '.gitignore',
+ isUsingLfs: false,
+ method: 'put',
+ modalId: 'update-modal3',
+ originalBranch: 'main',
+ targetBranch: 'feature',
+ isEdit: true,
+ branchAllowsCollaboration: false,
+ lastCommitSha: '782426692977b2cedb4452ee6501a404410f9b00',
+ });
+ });
+});
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 0e2f3552387f57839688616c7e4f7024b5caf49b..6d155e32e04c8147fa30993e8f65a25f30d9b775 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -210,7 +210,7 @@
end
describe '#ide_edit_path' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
let(:current_user) { create(:user) }
let(:can_push_code) { true }
@@ -398,8 +398,12 @@
let(:user) { build_stubbed(:user) }
let(:ref) { 'main' }
- it 'returns data related to blob app' do
+ before do
+ allow(helper).to receive(:selected_branch).and_return(ref)
allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ it 'returns data related to blob app' do
assign(:ref, ref)
expect(helper.vue_blob_app_data(project, blob, ref)).to include({
@@ -417,7 +421,6 @@
let_it_be(:user) { build_stubbed(:user) }
before do
- allow(helper).to receive(:current_user).and_return(user)
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user, :download_code, project).and_return(true)
end
@@ -430,6 +433,101 @@
end
end
+ describe '#edit_blob_app_data' do
+ let(:project) { build_stubbed(:project) }
+ let(:user) { build_stubbed(:user) }
+ let(:blob) { fake_blob(path: 'test.rb', size: 100.bytes) }
+ let(:ref) { 'main' }
+ let(:id) { "#{ref}/#{blob.path}" }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:selected_branch).and_return(ref)
+
+ assign(:project, project)
+ assign(:id, id)
+ assign(:ref, ref)
+ assign(:blob, blob)
+ end
+
+ it 'returns data related to blob editing' do
+ project_presenter = instance_double(ProjectPresenter)
+
+ allow(helper).to receive(:can?).with(user, :push_code, project).and_return(true)
+ allow(project).to receive(:present).and_return(project_presenter)
+ allow(project_presenter).to receive(:can_current_user_push_to_branch?).with(ref).and_return(true)
+ allow(project).to receive(:empty_repo?).and_return(false)
+ allow(blob).to receive(:stored_externally?).and_return(false)
+ allow(project).to receive(:branch_allows_collaboration?).with(user, ref).and_return(false)
+
+ expect(helper.edit_blob_app_data(project, id, blob, ref)).to include({
+ update_path: project_update_blob_path(project, id),
+ cancel_path: project_blob_path(project, id),
+ original_branch: ref,
+ target_branch: ref,
+ can_push_code: 'true',
+ can_push_to_branch: 'true',
+ empty_repo: 'false',
+ is_using_lfs: 'false',
+ blob_name: blob.name,
+ branch_allows_collaboration: 'false'
+ })
+ end
+
+ context 'when user cannot push code' do
+ it 'returns false for push permissions' do
+ allow(helper).to receive(:can?).with(user, :push_code, project).and_return(false)
+
+ expect(helper.edit_blob_app_data(project, id, blob, ref)).to include(
+ can_push_code: 'false'
+ )
+ end
+ end
+
+ context 'when user cannot push to branch' do
+ it 'returns false for branch push permissions' do
+ project_presenter = instance_double(ProjectPresenter)
+
+ allow(project).to receive(:present).and_return(project_presenter)
+ allow(project_presenter).to receive(:can_current_user_push_to_branch?).with(ref).and_return(false)
+
+ expect(helper.edit_blob_app_data(project, id, blob, ref)).to include(
+ can_push_to_branch: 'false'
+ )
+ end
+ end
+
+ context 'when repository is empty' do
+ it 'returns true for empty_repo' do
+ allow(project).to receive(:empty_repo?).and_return(true)
+
+ expect(helper.edit_blob_app_data(project, id, blob, ref)).to include(
+ empty_repo: 'true'
+ )
+ end
+ end
+
+ context 'when blob is stored externally' do
+ it 'returns true for is_using_lfs' do
+ allow(blob).to receive(:stored_externally?).and_return(true)
+
+ expect(helper.edit_blob_app_data(project, id, blob, ref)).to include(
+ is_using_lfs: 'true'
+ )
+ end
+ end
+
+ context 'branch collaboration' do
+ it 'returns true when branch allows collaboration' do
+ allow(project).to receive(:branch_allows_collaboration?).with(user, ref).and_return(true)
+
+ expect(helper.edit_blob_app_data(project, id, blob, ref)).to include(
+ branch_allows_collaboration: 'true'
+ )
+ end
+ end
+ end
+
describe "#copy_blob_source_button" do
let(:project) { build_stubbed(:project) }