diff --git a/app/assets/javascripts/repository/utils/fork_suggestion_utils.js b/app/assets/javascripts/repository/utils/fork_suggestion_utils.js new file mode 100644 index 0000000000000000000000000000000000000000..2e5b22a1d9d39bdad7f115ca0c26b8ae0ce73fd8 --- /dev/null +++ b/app/assets/javascripts/repository/utils/fork_suggestion_utils.js @@ -0,0 +1,75 @@ +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 + * @param {Object} userPermissions - User permissions object + * @param {boolean} isUsingLfs - Whether the project is using LFS + * @returns {boolean} + */ +export const canFork = (userPermissions, isUsingLfs) => { + const { createMergeRequestIn, forkProject } = userPermissions; + return isLoggedIn() && !isUsingLfs && createMergeRequestIn && forkProject; +}; + +/** + * Checks if the fork suggestion should be shown for single file editor + * @param {Object} userPermissions - User permissions object + * @param {boolean} isUsingLfs - Whether the project is using LFS + * @param {boolean} canModifyBlob - Whether the user can modify the blob + * @returns {boolean} + */ +export const showSingleFileEditorForkSuggestion = (userPermissions, isUsingLfs, canModifyBlob) => { + return canFork(userPermissions, isUsingLfs) && !canModifyBlob; +}; + +/** + * Checks if the fork suggestion should be shown for Web IDE + * @param {Object} userPermissions - User permissions object + * @param {boolean} isUsingLfs - Whether the project is using LFS + * @param {boolean} canModifyBlobWithWebIde - Whether the user can modify the blob with Web IDE + * @returns {boolean} + */ +export const showWebIdeForkSuggestion = (userPermissions, isUsingLfs, canModifyBlobWithWebIde) => { + return canFork(userPermissions, isUsingLfs) && !canModifyBlobWithWebIde; +}; + +/** + * Checks if the fork suggestion should be shown + * @param {Object} userPermissions - User permissions object + * @param {boolean} isUsingLfs - Whether the project is using LFS + * @param {Object} blobInfo - blobInfo object, including canModifyBlob and canModifyBlobWithWebIde + * @returns {boolean} + */ +export const showForkSuggestion = (userPermissions, isUsingLfs, blobInfo) => { + return ( + showSingleFileEditorForkSuggestion(userPermissions, isUsingLfs, blobInfo.canModifyBlob) || + showWebIdeForkSuggestion(userPermissions, isUsingLfs, blobInfo.canModifyBlobWithWebIde) + ); +}; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7665952cf708be45b48b6de6380475f040f8a8c9..bb233f6ee2eaf348e5d8bb213763c802be6eaaac 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -66395,6 +66395,9 @@ 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 "" diff --git a/spec/frontend/repository/utils/fork_suggestion_utils_spec.js b/spec/frontend/repository/utils/fork_suggestion_utils_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..d111f65e078e683f7cf24a67ba314ddd913f839c --- /dev/null +++ b/spec/frontend/repository/utils/fork_suggestion_utils_spec.js @@ -0,0 +1,148 @@ +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', () => { + let userPermissions; + const createUserPermissions = (createMergeRequestIn = true, forkProject = true) => ({ + createMergeRequestIn, + forkProject, + }); + + beforeEach(() => { + commonUtils.isLoggedIn.mockReturnValue(true); + userPermissions = createUserPermissions(); + }); + + describe('canFork', () => { + it('returns true when all conditions are met', () => { + expect(canFork(userPermissions, false)).toBe(true); + }); + + it('returns false when user is not logged in', () => { + commonUtils.isLoggedIn.mockReturnValue(false); + expect(canFork(userPermissions, false)).toBe(false); + }); + + it('returns false when project is using LFS', () => { + expect(canFork(userPermissions, true)).toBe(false); + }); + + it('returns false when user cannot create merge request', () => { + userPermissions = createUserPermissions(false, true); + expect(canFork(userPermissions, false)).toBe(false); + }); + + it('returns false when user cannot fork project', () => { + userPermissions = createUserPermissions(true, false); + expect(canFork(userPermissions, false)).toBe(false); + }); + }); + + describe('showSingleFileEditorForkSuggestion', () => { + it('returns true when user can fork but cannot modify blob', () => { + expect(showSingleFileEditorForkSuggestion(userPermissions, false, false)).toBe(true); + }); + + it('returns false when user can fork and can modify blob', () => { + expect(showSingleFileEditorForkSuggestion(userPermissions, false, true)).toBe(false); + }); + }); + + describe('showWebIdeForkSuggestion', () => { + it('returns true when user can fork but cannot modify blob with Web IDE', () => { + expect(showWebIdeForkSuggestion(userPermissions, false, false)).toBe(true); + }); + + it('returns false when user can fork and can modify blob with Web IDE', () => { + expect(showWebIdeForkSuggestion(userPermissions, false, true)).toBe(false); + }); + }); + + describe('showForkSuggestion', () => { + it('returns true when single file editor fork suggestion is true', () => { + expect( + showForkSuggestion(userPermissions, false, { + canModifyBlob: false, + canModifyBlobWithWebIde: true, + }), + ).toBe(true); + }); + + it('returns true when Web IDE fork suggestion is true', () => { + expect( + showForkSuggestion(userPermissions, false, { + canModifyBlob: true, + canModifyBlobWithWebIde: false, + }), + ).toBe(true); + }); + + it('returns false when both fork suggestions are false', () => { + expect( + showForkSuggestion(userPermissions, false, { + canModifyBlob: true, + canModifyBlobWithWebIde: true, + }), + ).toBe(false); + }); + + it('returns false when user cannot fork', () => { + commonUtils.isLoggedIn.mockReturnValue(false); + expect( + showForkSuggestion(userPermissions, false, { + canModifyBlob: false, + canModifyBlobWithWebIde: false, + }), + ).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); + }); + }); +});