diff --git a/app/assets/javascripts/repository/components/header_area.vue b/app/assets/javascripts/repository/components/header_area.vue index 64428119b44419ff39ade2b64774686c83b75170..93c8481a3b75489458aad31731cd7358af13533e 100644 --- a/app/assets/javascripts/repository/components/header_area.vue +++ b/app/assets/javascripts/repository/components/header_area.vue @@ -1,6 +1,5 @@ + + + + + {{ + __( + "You're not allowed to make changes to this project directly. Create a fork to make changes and submit a merge request.", + ) + }} + + + diff --git a/app/assets/javascripts/repository/queries/blob_controls.query.graphql b/app/assets/javascripts/repository/queries/blob_controls.query.graphql index 9c50ba6236b9df9f62b71e6c69b93214d2da9ec1..2be31da1e0eeaa244bcf5e354bf8969a1fe8b85f 100644 --- a/app/assets/javascripts/repository/queries/blob_controls.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_controls.query.graphql @@ -26,6 +26,7 @@ query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!, $ref canCurrentUserPushToBranch canModifyBlob canModifyBlobWithWebIde + forkAndViewPath simpleViewer { __typename fileType diff --git a/app/assets/javascripts/repository/utils/fork_suggestion_utils.js b/app/assets/javascripts/repository/utils/fork_suggestion_utils.js index 2e5b22a1d9d39bdad7f115ca0c26b8ae0ce73fd8..24b477e5991accd39c8d07d5f2d757b9b3265a2d 100644 --- a/app/assets/javascripts/repository/utils/fork_suggestion_utils.js +++ b/app/assets/javascripts/repository/utils/fork_suggestion_utils.js @@ -1,31 +1,4 @@ import { isLoggedIn } from '~/lib/utils/common_utils'; -import { createAlert, VARIANT_INFO } from '~/alert'; -import { __ } from '~/locale'; - -export function showForkSuggestionAlert(forkAndViewPath) { - const i18n = { - forkSuggestion: __( - "You can't edit files directly in this project. Fork this project and submit a merge request with your changes.", - ), - fork: __('Fork'), - cancel: __('Cancel'), - }; - - const alert = createAlert({ - message: i18n.forkSuggestion, - variant: VARIANT_INFO, - primaryButton: { - text: i18n.fork, - link: forkAndViewPath, - }, - secondaryButton: { - text: i18n.cancel, - clickHandler: () => alert.dismiss(), - }, - }); - - return alert; -} /** * Checks if the user can fork the project diff --git a/ee/spec/frontend/repository/mock_data.js b/ee/spec/frontend/repository/mock_data.js index 52efd94c53237a8ae18ab1fc19dfef3e4261624b..8a5b1a9ac8a183a868dbcb7577d4772a2e75eccc 100644 --- a/ee/spec/frontend/repository/mock_data.js +++ b/ee/spec/frontend/repository/mock_data.js @@ -226,6 +226,7 @@ export const blobControlsDataMock = { canCurrentUserPushToBranch: true, canModifyBlob: true, canModifyBlobWithWebIde: true, + forkAndViewPath: 'fork/view/path', simpleViewer: { __typename: 'BlobViewer', collapsed: false, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f4483e682b30a39313606881868617c5e2c21b04..41df5dc3d0cd92d6dbd5cedccc4e8edd24a1c457 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -25551,6 +25551,9 @@ msgstr "" msgid "Fork project?" msgstr "" +msgid "Fork to make changes" +msgstr "" + msgid "ForkProject|A fork is a copy of a project." msgstr "" @@ -67393,9 +67396,6 @@ msgstr "" msgid "You can't approve because you added one or more commits to this merge request." msgstr "" -msgid "You can't edit files directly in this project. Fork this project and submit a merge request with your changes." -msgstr "" - msgid "You can't follow more than %{limit} users. To follow more users, unfollow some others." msgstr "" @@ -67843,6 +67843,9 @@ msgstr "" msgid "You're not allowed to make changes to this project directly. A fork of this project is being created that you can make changes in, so you can submit a merge request." msgstr "" +msgid "You're not allowed to make changes to this project directly. Create a fork to make changes and submit a merge request." +msgstr "" + msgid "You're receiving this email because of your account on %{host}." msgstr "" diff --git a/spec/frontend/repository/components/header_area/blob_button_group_spec.js b/spec/frontend/repository/components/header_area/blob_button_group_spec.js index d4485ca0816bf51d10d2d64f5b7a616aa6039173..1711d49bab61e3f86a684d3cdc2329c379fdb90c 100644 --- a/spec/frontend/repository/components/header_area/blob_button_group_spec.js +++ b/spec/frontend/repository/components/header_area/blob_button_group_spec.js @@ -1,8 +1,10 @@ +import { nextTick } from 'vue'; import { GlDisclosureDropdownItem } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import BlobButtonGroup from '~/repository/components/header_area/blob_button_group.vue'; +import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; import { blobControlsDataMock, refMock } from 'ee_else_ce_jest/repository/mock_data'; @@ -55,6 +57,7 @@ describe('BlobButtonGroup component', () => { const findReplaceItem = () => wrapper.findComponent(GlDisclosureDropdownItem); const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal); + const findForkSuggestionModal = () => wrapper.findComponent(ForkSuggestionModal); beforeEach(async () => { await createComponent(); @@ -100,13 +103,21 @@ describe('BlobButtonGroup component', () => { it('does not trigger the UploadBlobModal from the replace item', () => { findReplaceItem().vm.$emit('action'); - expect(findReplaceItem().props('item')).toMatchObject({ - extraAttrs: { disabled: true }, - }); - expect(showUploadBlobModalMock).not.toHaveBeenCalled(); - expect(wrapper.emitted().fork).toHaveLength(1); }); + + it('triggers ForkSuggestionModal from the replace item', async () => { + findReplaceItem().vm.$emit('action'); + await nextTick(); + + expect(findForkSuggestionModal().props('visible')).toBe(true); + }); + }); + }); + + it('renders ForkSuggestionModal', () => { + expect(findForkSuggestionModal().props()).toMatchObject({ + forkPath: 'fork/view/path', }); }); diff --git a/spec/frontend/repository/components/header_area/blob_controls_spec.js b/spec/frontend/repository/components/header_area/blob_controls_spec.js index f9df72abe2be306edd4b70ab09b8c3116c6666eb..dbf76ef42e55c17b154eb4a805c2f7812b79f0a5 100644 --- a/spec/frontend/repository/components/header_area/blob_controls_spec.js +++ b/spec/frontend/repository/components/header_area/blob_controls_spec.js @@ -202,9 +202,11 @@ describe('Blob controls component', () => { }); describe('BlobOverflow dropdown', () => { - it('renders BlobOverflow component with correct props', async () => { + beforeEach(async () => { await createComponent({ glFeatures: { blobOverflowMenu: true } }); + }); + it('renders BlobOverflow component with correct props', () => { expect(findOverflowMenu().exists()).toBe(true); expect(findOverflowMenu().props()).toEqual({ projectPath: 'some/project', @@ -234,9 +236,7 @@ describe('Blob controls component', () => { expect(findOverflowMenu().props('isBinary')).toBe(true); }); - it('copies to clipboard raw blob text, when receives copy event', async () => { - await createComponent({ glFeatures: { blobOverflowMenu: true } }); - + it('copies to clipboard raw blob text, when receives copy event', () => { jest.spyOn(navigator.clipboard, 'writeText'); findOverflowMenu().vm.$emit('copy'); diff --git a/spec/frontend/repository/components/header_area/blob_delete_file_group_spec.js b/spec/frontend/repository/components/header_area/blob_delete_file_group_spec.js index c718e0c1ce73a2e96b4bd8759816a41045f89941..f4e89f8d4971a3b988ceeb0ec70239e77efb2ee8 100644 --- a/spec/frontend/repository/components/header_area/blob_delete_file_group_spec.js +++ b/spec/frontend/repository/components/header_area/blob_delete_file_group_spec.js @@ -1,9 +1,11 @@ +import { nextTick } from 'vue'; import { GlDisclosureDropdownItem } from '@gitlab/ui'; import { blobControlsDataMock } from 'ee_else_ce_jest/repository/mock_data'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import BlobDeleteFileGroup from '~/repository/components/header_area/blob_delete_file_group.vue'; +import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue'; import CommitChangesModal from '~/repository/components/commit_changes_modal.vue'; jest.mock('~/lib/utils/common_utils', () => ({ @@ -54,6 +56,7 @@ describe('BlobDeleteFileGroup component', () => { const findDeleteItem = () => wrapper.findComponent(GlDisclosureDropdownItem); const findDeleteBlobModal = () => wrapper.findComponent(CommitChangesModal); + const findForkSuggestionModal = () => wrapper.findComponent(ForkSuggestionModal); beforeEach(async () => { await createComponent(); @@ -99,13 +102,21 @@ describe('BlobDeleteFileGroup component', () => { it('does not trigger the DeleteBlobModal from the delete item', () => { findDeleteItem().vm.$emit('action'); - expect(findDeleteItem().props('item')).toMatchObject({ - extraAttrs: { disabled: true }, - }); - expect(showDeleteBlobModalMock).not.toHaveBeenCalled(); - expect(wrapper.emitted('fork')).toEqual([['view']]); }); + + it('changes ForkSuggestionModal visibility', async () => { + findDeleteItem().vm.$emit('action'); + await nextTick(); + + expect(findForkSuggestionModal().props('visible')).toBe(true); + }); + }); + }); + + it('renders ForkSuggestionModal', () => { + expect(findForkSuggestionModal().props()).toMatchObject({ + forkPath: 'fork/view/path', }); }); diff --git a/spec/frontend/repository/components/header_area/fork_suggestion_modal_spec.js b/spec/frontend/repository/components/header_area/fork_suggestion_modal_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..03bb3e3b34d27c80ddf8d9bcf1a4baedd3788a41 --- /dev/null +++ b/spec/frontend/repository/components/header_area/fork_suggestion_modal_spec.js @@ -0,0 +1,49 @@ +import { GlModal } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue'; + +const DEFAULT_PROPS = { visible: true, forkPath: '/fork/project/path' }; + +describe('ForkSuggestionModal component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(ForkSuggestionModal, { + propsData: { ...DEFAULT_PROPS, ...props }, + stubs: { + GlModal, + }, + }); + }; + + const findModal = () => wrapper.findComponent(GlModal); + const findMessage = () => wrapper.findByTestId('message'); + + beforeEach(() => { + createComponent(); + }); + + it('sets correct modal visibility', () => { + createComponent({ visible: false }); + expect(findModal().props('visible')).toBe(false); + }); + + it('renders the modal with correct variant', () => { + expect(findModal().exists()).toBe(true); + expect(findMessage().text()).toBe( + "You're not allowed to make changes to this project directly. Create a fork to make changes and submit a merge request.", + ); + }); + + describe('reactivity to prop changes', () => { + it('updates the fork path when forkPath prop changes', () => { + createComponent({ forkPath: '/new/fork/path' }); + + expect(findModal().props('actionPrimary')).toMatchObject({ + attributes: { + href: '/new/fork/path', + }, + }); + }); + }); +}); diff --git a/spec/frontend/repository/components/header_area_spec.js b/spec/frontend/repository/components/header_area_spec.js index 28a504ac1527a1b00f6501ec2f06bdda3eb32456..6f68c509cc34f3e78d264236fd5a489686afedce 100644 --- a/spec/frontend/repository/components/header_area_spec.js +++ b/spec/frontend/repository/components/header_area_spec.js @@ -42,6 +42,7 @@ describe('HeaderArea', () => { const findPageHeading = () => wrapper.findByTestId('repository-heading'); const findFileIcon = () => wrapper.findComponent(FileIcon); const findRepositoryOverflowMenu = () => wrapper.findComponent(RepositoryOverflowMenu); + const findBlobControls = () => wrapper.findComponent(BlobControls); const { bindInternalEventDocument } = useMockInternalEventsTracking(); @@ -210,10 +211,9 @@ describe('HeaderArea', () => { describe('when rendered for blob view', () => { it('renders BlobControls component with correct props', () => { wrapper = createComponent({ refType: 'branch' }); - const blobControls = wrapper.findComponent(BlobControls); - expect(blobControls.exists()).toBe(true); - expect(blobControls.props('projectPath')).toBe('test/project'); - expect(blobControls.props('refType')).toBe(''); + expect(findBlobControls().exists()).toBe(true); + expect(findBlobControls().props('projectPath')).toBe('test/project'); + expect(findBlobControls().props('refType')).toBe(''); }); it('does not render CodeDropdown and SourceCodeDownloadDropdown', () => { diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js index e4f023a6a22778fa020a943d03761f262dad842e..bbc18d904b4cb34e55a98440711b2a7f70020ad4 100644 --- a/spec/frontend/repository/mock_data.js +++ b/spec/frontend/repository/mock_data.js @@ -116,6 +116,7 @@ export const blobControlsDataMock = { canCurrentUserPushToBranch: true, canModifyBlob: true, canModifyBlobWithWebIde: true, + forkAndViewPath: 'fork/view/path', simpleViewer: { __typename: 'BlobViewer', collapsed: false, diff --git a/spec/frontend/repository/utils/fork_suggestion_utils_spec.js b/spec/frontend/repository/utils/fork_suggestion_utils_spec.js index d111f65e078e683f7cf24a67ba314ddd913f839c..c3d5b74db9e42ea4f5b34dd3d4aed41e694a4490 100644 --- a/spec/frontend/repository/utils/fork_suggestion_utils_spec.js +++ b/spec/frontend/repository/utils/fork_suggestion_utils_spec.js @@ -1,14 +1,11 @@ -import { createAlert, VARIANT_INFO } from '~/alert'; import * as commonUtils from '~/lib/utils/common_utils'; import { - showForkSuggestionAlert, canFork, showSingleFileEditorForkSuggestion, showWebIdeForkSuggestion, showForkSuggestion, } from '~/repository/utils/fork_suggestion_utils'; -jest.mock('~/alert'); jest.mock('~/lib/utils/common_utils'); describe('forkSuggestionUtils', () => { @@ -106,43 +103,4 @@ describe('forkSuggestionUtils', () => { ).toBe(false); }); }); - - describe('showForkSuggestionAlert', () => { - const forkAndViewPath = '/path/to/fork'; - - beforeEach(() => { - createAlert.mockClear(); - }); - - it('calls createAlert with correct parameters', () => { - showForkSuggestionAlert(forkAndViewPath); - - expect(createAlert).toHaveBeenCalledTimes(1); - expect(createAlert).toHaveBeenCalledWith({ - message: - "You can't edit files directly in this project. Fork this project and submit a merge request with your changes.", - variant: VARIANT_INFO, - primaryButton: { - text: 'Fork', - link: forkAndViewPath, - }, - secondaryButton: { - text: 'Cancel', - clickHandler: expect.any(Function), - }, - }); - }); - - it('secondary button click handler dismisses the alert', () => { - const mockAlert = { dismiss: jest.fn() }; - createAlert.mockReturnValue(mockAlert); - - showForkSuggestionAlert(forkAndViewPath); - - const secondaryButtonClickHandler = createAlert.mock.calls[0][0].secondaryButton.clickHandler; - secondaryButtonClickHandler(mockAlert); - - expect(mockAlert.dismiss).toHaveBeenCalledTimes(1); - }); - }); });
+ {{ + __( + "You're not allowed to make changes to this project directly. Create a fork to make changes and submit a merge request.", + ) + }} +