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 {

-
- - +
+ + + +
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 @@ + + + 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) }