From c3b149653a1ec1b045c8a4bb876a93f1040f652f Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Thu, 27 Feb 2025 17:57:21 +0100 Subject: [PATCH 1/4] Remove alert from fork suggestion utils --- .../repository/utils/fork_suggestion_utils.js | 27 ------------ .../utils/fork_suggestion_utils_spec.js | 42 ------------------- 2 files changed, 69 deletions(-) diff --git a/app/assets/javascripts/repository/utils/fork_suggestion_utils.js b/app/assets/javascripts/repository/utils/fork_suggestion_utils.js index 2e5b22a1d9d39b..24b477e5991acc 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/spec/frontend/repository/utils/fork_suggestion_utils_spec.js b/spec/frontend/repository/utils/fork_suggestion_utils_spec.js index d111f65e078e68..c3d5b74db9e42e 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); - }); - }); }); -- GitLab From 18b51b112fe7eba41f7c613788b87f72d661bf6f Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Thu, 27 Feb 2025 13:46:52 +0100 Subject: [PATCH 2/4] Show fork suggestion on delete and replace overflow actions --- .../repository/components/header_area.vue | 27 +++++- .../header_area/blob_button_group.vue | 28 ++---- .../components/header_area/blob_controls.vue | 4 + .../header_area/blob_delete_file_group.vue | 28 ++---- .../header_area/blob_overflow_menu.vue | 5 ++ .../header_area/fork_suggestion_alert.vue | 54 ++++++++++++ .../queries/blob_controls.query.graphql | 1 + ee/spec/frontend/repository/mock_data.js | 1 + .../header_area/blob_button_group_spec.js | 11 +-- .../header_area/blob_controls_spec.js | 11 ++- .../blob_delete_file_group_spec.js | 11 +-- .../header_area/blob_overflow_menu_spec.js | 10 +++ .../header_area/fork_suggestion_alert_spec.js | 88 +++++++++++++++++++ .../repository/components/header_area_spec.js | 40 ++++++++- spec/frontend/repository/mock_data.js | 1 + 15 files changed, 255 insertions(+), 65 deletions(-) create mode 100644 app/assets/javascripts/repository/components/header_area/fork_suggestion_alert.vue create mode 100644 spec/frontend/repository/components/header_area/fork_suggestion_alert_spec.js diff --git a/app/assets/javascripts/repository/components/header_area.vue b/app/assets/javascripts/repository/components/header_area.vue index 64428119b44419..ceb60addc3c3e2 100644 --- a/app/assets/javascripts/repository/components/header_area.vue +++ b/app/assets/javascripts/repository/components/header_area.vue @@ -22,6 +22,7 @@ import SourceCodeDownloadDropdown from '~/vue_shared/components/download_dropdow import CloneCodeDropdown from '~/vue_shared/components/code_dropdown/clone_code_dropdown.vue'; import AddToTree from '~/repository/components/header_area/add_to_tree.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; +import ForkSuggestionAlert from '~/repository/components/header_area/fork_suggestion_alert.vue'; export default { name: 'HeaderArea', @@ -41,6 +42,7 @@ export default { SourceCodeDownloadDropdown, CloneCodeDropdown, AddToTree, + ForkSuggestionAlert, WebIdeLink: () => import('ee_else_ce/vue_shared/components/web_ide_link.vue'), LockDirectoryButton: () => import('ee_component/repository/components/lock_directory_button.vue'), @@ -91,7 +93,7 @@ export default { ], provide() { return { - currentRef: computed(() => this.currentRef ?? this.blobInfo.ref), + currentRef: computed(() => this.currentRef), }; }, props: { @@ -114,6 +116,12 @@ export default { required: true, }, }, + data() { + return { + showForkSuggestionAlert: false, + blobForkAndViewPath: '', + }; + }, computed: { isTreeView() { return this.$route.name !== 'blobPathDecoded'; @@ -187,12 +195,22 @@ export default { InternalEvents.trackEvent(FIND_FILE_BUTTON_CLICK); Shortcuts.focusSearchFile(); }, + onShowForkSuggestionAlert(forkPath) { + this.showForkSuggestionAlert = true; + this.blobForkAndViewPath = forkPath; + }, }, }; diff --git a/app/assets/javascripts/repository/components/header_area/blob_button_group.vue b/app/assets/javascripts/repository/components/header_area/blob_button_group.vue index 7e793209decfb7..7fcce8192766ae 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_button_group.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_button_group.vue @@ -1,8 +1,8 @@ @@ -238,6 +241,7 @@ export default { :is-empty-repository="project.repository.empty" :is-using-lfs="isUsingLfs" @copy="onCopy" + @showForkSuggestionAlert="onShowForkSuggestionAlert" /> diff --git a/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue b/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue index 3489b1d0882b8d..1800d7c556655a 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue @@ -2,7 +2,7 @@ import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem } from '@gitlab/ui'; import { uniqueId } from 'lodash'; import { sprintf, __ } from '~/locale'; -import { isLoggedIn } from '~/lib/utils/common_utils'; +import { showForkSuggestion } from '~/repository/utils/fork_suggestion_utils'; import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue'; import { DEFAULT_BLOB_INFO } from '~/repository/constants'; @@ -43,19 +43,12 @@ export default { required: true, }, }, - data() { - return { - isLoggedIn: isLoggedIn(), - }; - }, computed: { deleteFileItem() { return { text: __('Delete'), extraAttrs: { 'data-testid': 'delete', - // a temporary solution before resolving https://gitlab.com/gitlab-org/gitlab/-/issues/450774#note_2319974833 - disabled: this.showForkSuggestion, }, }; }, @@ -65,25 +58,14 @@ export default { deleteModalCommitMessage() { return sprintf(__('Delete %{name}'), { name: this.blobInfo.name }); }, - canFork() { - const { createMergeRequestIn, forkProject } = this.userPermissions; - - return this.isLoggedIn && !this.isUsingLfs && createMergeRequestIn && forkProject; - }, - showSingleFileEditorForkSuggestion() { - return this.canFork && !this.blobInfo.canModifyBlob; - }, - showWebIdeForkSuggestion() { - return this.canFork && !this.blobInfo.canModifyBlobWithWebIde; - }, - showForkSuggestion() { - return this.showSingleFileEditorForkSuggestion || this.showWebIdeForkSuggestion; + shouldShowForkSuggestion() { + return showForkSuggestion(this.userPermissions, this.isUsingLfs, this.blobInfo); }, }, methods: { showModal() { - if (this.showForkSuggestion) { - this.$emit('fork', 'view'); + if (this.shouldShowForkSuggestion) { + this.$emit('showForkSuggestionAlert'); return; } diff --git a/app/assets/javascripts/repository/components/header_area/blob_overflow_menu.vue b/app/assets/javascripts/repository/components/header_area/blob_overflow_menu.vue index 2ea47a138a2bce..e34483d805aa05 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_overflow_menu.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_overflow_menu.vue @@ -115,6 +115,9 @@ export default { this.$emit('copy'); } }, + onShowForkSuggestionAlert() { + this.$emit('showForkSuggestionAlert'); + }, }, }; @@ -137,6 +140,7 @@ export default { :user-permissions="userPermissions" :is-loading="isLoading" :path-locks="pathLocks" + @showForkSuggestionAlert="onShowForkSuggestionAlert" /> diff --git a/app/assets/javascripts/repository/components/header_area/fork_suggestion_alert.vue b/app/assets/javascripts/repository/components/header_area/fork_suggestion_alert.vue new file mode 100644 index 00000000000000..766a149cf36fda --- /dev/null +++ b/app/assets/javascripts/repository/components/header_area/fork_suggestion_alert.vue @@ -0,0 +1,54 @@ + + + diff --git a/app/assets/javascripts/repository/queries/blob_controls.query.graphql b/app/assets/javascripts/repository/queries/blob_controls.query.graphql index 9c50ba6236b9df..2be31da1e0eeaa 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/ee/spec/frontend/repository/mock_data.js b/ee/spec/frontend/repository/mock_data.js index 52efd94c53237a..8a5b1a9ac8a183 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/spec/frontend/repository/components/header_area/blob_button_group_spec.js b/spec/frontend/repository/components/header_area/blob_button_group_spec.js index d4485ca0816bf5..a3e5c9e1e281d1 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 @@ -100,12 +100,13 @@ 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('emits showForkSuggestionAlert', () => { + findReplaceItem().vm.$emit('action'); + + expect(wrapper.emitted('showForkSuggestionAlert')).toEqual([[]]); }); }); }); 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 f9df72abe2be30..e1740f0d647a74 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', @@ -235,12 +237,15 @@ describe('Blob controls component', () => { }); it('copies to clipboard raw blob text, when receives copy event', async () => { - await createComponent({ glFeatures: { blobOverflowMenu: true } }); - jest.spyOn(navigator.clipboard, 'writeText'); findOverflowMenu().vm.$emit('copy'); expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Example raw text content'); }); + + it('passes showForkSuggestionAlert when receives showForkSuggestionAlert event', () => { + findOverflowMenu().vm.$emit('showForkSuggestionAlert', 'fork/view/path'); + expect(wrapper.emitted('showForkSuggestionAlert')).toEqual([['fork/view/path']]); + }); }); }); 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 c718e0c1ce73a2..c70fffa12e9361 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 @@ -99,12 +99,13 @@ 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('emits showForkSuggestionAlert', () => { + findDeleteItem().vm.$emit('action'); + + expect(wrapper.emitted('showForkSuggestionAlert')).toEqual([[]]); }); }); }); diff --git a/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js b/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js index 9b1ddeaa5ef517..5ca18d200330bd 100644 --- a/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js +++ b/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js @@ -143,6 +143,11 @@ describe('Blob Overflow Menu', () => { expect(findBlobButtonGroup().exists()).toBe(false); }); + + it('passes showForkSuggestionAlert when receives showForkSuggestionAlert event', () => { + findBlobButtonGroup().vm.$emit('showForkSuggestionAlert'); + expect(wrapper.emitted('showForkSuggestionAlert')).toEqual([[]]); + }); }); describe('Blob Delete File Group', () => { @@ -169,6 +174,11 @@ describe('Blob Overflow Menu', () => { expect(findBlobDeleteFileGroup().exists()).toBe(false); }); + + it('passes showForkSuggestionAlert when receives showForkSuggestionAlert event', () => { + findBlobDeleteFileGroup().vm.$emit('showForkSuggestionAlert'); + expect(wrapper.emitted('showForkSuggestionAlert')).toEqual([[]]); + }); }); describe('Permalink Dropdown Item', () => { diff --git a/spec/frontend/repository/components/header_area/fork_suggestion_alert_spec.js b/spec/frontend/repository/components/header_area/fork_suggestion_alert_spec.js new file mode 100644 index 00000000000000..73b61f1a38d1e7 --- /dev/null +++ b/spec/frontend/repository/components/header_area/fork_suggestion_alert_spec.js @@ -0,0 +1,88 @@ +import { nextTick } from 'vue'; +import { GlAlert } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ForkSuggestionAlert from '~/repository/components/header_area/fork_suggestion_alert.vue'; + +const DEFAULT_PROPS = { showForkSuggestionAlert: false, forkPath: '/fork/project/path' }; + +describe('ForkSuggestionAlert component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(ForkSuggestionAlert, { + propsData: { ...DEFAULT_PROPS, ...props }, + stubs: { + GlAlert, + }, + }); + }; + + const findAlert = () => wrapper.findComponent(GlAlert); + const findMessage = () => wrapper.findByTestId('message'); + const findForkButton = () => wrapper.findByTestId('fork'); + const findCancelButton = () => wrapper.findByTestId('cancel'); + + describe('when showForkSuggestionAlert is false', () => { + beforeEach(() => { + createComponent(); + }); + + it('does not render the alert', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('when showForkSuggestionAlert is true', () => { + beforeEach(() => { + createComponent({ showForkSuggestionAlert: true }); + }); + + it('renders the alert with correct variant', () => { + expect(findAlert().exists()).toBe(true); + expect(findMessage().text()).toBe( + "You can't edit files directly in this project. Fork this project and submit a merge request with your changes.", + ); + }); + + describe('#actions template', () => { + it('renders the fork button with correct props', () => { + expect(findForkButton().exists()).toBe(true); + expect(findForkButton().attributes('href')).toBe('/fork/project/path'); + expect(findForkButton().attributes('data-method')).toBe('post'); + }); + + it('renders the cancel button with correct props', () => { + expect(findCancelButton().exists()).toBe(true); + expect(findCancelButton().text()).toBe('Cancel'); + }); + }); + + describe('dismissal behavior', () => { + it('hides the alert when dismiss event is triggered', async () => { + expect(findAlert().exists()).toBe(true); + + findAlert().vm.$emit('dismiss'); + await nextTick(); + + expect(wrapper.emitted('dismiss')).toEqual([[]]); + }); + + it('hides the alert when cancel button is clicked', async () => { + expect(findAlert().exists()).toBe(true); + + findCancelButton().vm.$emit('click'); + await nextTick(); + + expect(wrapper.emitted('dismiss')).toEqual([[]]); + }); + }); + }); + + describe('reactivity to prop changes', () => { + it('updates the fork path when forkPath prop changes', () => { + createComponent({ showForkSuggestionAlert: true, forkPath: '/new/fork/path' }); + + expect(findForkButton().attributes('href')).toBe('/new/fork/path'); + }); + }); +}); diff --git a/spec/frontend/repository/components/header_area_spec.js b/spec/frontend/repository/components/header_area_spec.js index 28a504ac1527a1..a47ec6cd749da1 100644 --- a/spec/frontend/repository/components/header_area_spec.js +++ b/spec/frontend/repository/components/header_area_spec.js @@ -11,6 +11,7 @@ import FileIcon from '~/vue_shared/components/file_icon.vue'; import CloneCodeDropdown from '~/vue_shared/components/code_dropdown/clone_code_dropdown.vue'; import RepositoryOverflowMenu from '~/repository/components/header_area/repository_overflow_menu.vue'; import BlobControls from '~/repository/components/header_area/blob_controls.vue'; +import ForkSuggestionAlert from '~/repository/components/header_area/fork_suggestion_alert.vue'; import Shortcuts from '~/behaviors/shortcuts/shortcuts'; import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; import { headerAppInjected } from 'ee_else_ce_jest/repository/mock_data'; @@ -30,6 +31,7 @@ const defaultMockRoute = { describe('HeaderArea', () => { let wrapper; + const findForkSuggestionAlert = () => wrapper.findComponent(ForkSuggestionAlert); const findBreadcrumbs = () => wrapper.findComponent(Breadcrumbs); const findRefSelector = () => wrapper.findComponent(RefSelector); const findFindFileButton = () => wrapper.findByTestId('tree-find-file-control'); @@ -42,6 +44,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(); @@ -91,6 +94,36 @@ describe('HeaderArea', () => { expect(findPageHeading().exists()).toBe(true); }); + describe('ForkSuggestionAlert', () => { + const forkPath = '/fork/path'; + + it('does not render by default', () => { + expect(findForkSuggestionAlert().props('showForkSuggestionAlert')).toBe(false); + }); + + it('renders on showForkSuggestionAlert', async () => { + await findBlobControls().vm.$emit('showForkSuggestionAlert', forkPath); + + expect(findForkSuggestionAlert().props('showForkSuggestionAlert')).toBe(true); + expect(findForkSuggestionAlert().props('forkPath')).toBe(forkPath); + }); + + it('hides the alert when dismissed and shows it again when triggered', async () => { + // Show the alert + await findBlobControls().vm.$emit('showForkSuggestionAlert', forkPath); + expect(findForkSuggestionAlert().props('showForkSuggestionAlert')).toBe(true); + + // Simulate dismissal + await findForkSuggestionAlert().vm.$emit('dismiss'); + expect(findForkSuggestionAlert().props('showForkSuggestionAlert')).toBe(false); + + // Show the alert again + await findBlobControls().vm.$emit('showForkSuggestionAlert', '/another/path'); + expect(findForkSuggestionAlert().props('showForkSuggestionAlert')).toBe(true); + expect(findForkSuggestionAlert().props('forkPath')).toBe('/another/path'); + }); + }); + describe('when rendered for tree view', () => { beforeEach(() => { wrapper = createComponent({}, { name: 'treePathDecoded', params: { path: 'project' } }); @@ -210,10 +243,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 e4f023a6a22778..bbc18d904b4cb3 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, -- GitLab From 4de5bc03a97a714c74906f6faca5d6c331a06aea Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Tue, 4 Mar 2025 19:36:58 +0100 Subject: [PATCH 3/4] Use custom modal to show fork suggestion As not being able to edit, replace or delete files is a major obstacle to user task, a suggestion to fork before these actions is shown in a modal. This will ensure the user sees critical context. --- .../repository/components/header_area.vue | 25 +----- .../header_area/blob_button_group.vue | 14 ++- .../components/header_area/blob_controls.vue | 4 - .../header_area/blob_delete_file_group.vue | 14 ++- .../header_area/blob_overflow_menu.vue | 5 -- .../header_area/fork_suggestion_alert.vue | 54 ------------ .../header_area/fork_suggestion_modal.vue | 64 ++++++++++++++ locale/gitlab.pot | 9 +- .../header_area/blob_button_group_spec.js | 14 ++- .../header_area/blob_controls_spec.js | 5 -- .../blob_delete_file_group_spec.js | 14 ++- .../header_area/blob_overflow_menu_spec.js | 10 --- .../header_area/fork_suggestion_alert_spec.js | 88 ------------------- .../header_area/fork_suggestion_modal_spec.js | 77 ++++++++++++++++ .../repository/components/header_area_spec.js | 32 ------- 15 files changed, 198 insertions(+), 231 deletions(-) delete mode 100644 app/assets/javascripts/repository/components/header_area/fork_suggestion_alert.vue create mode 100644 app/assets/javascripts/repository/components/header_area/fork_suggestion_modal.vue delete mode 100644 spec/frontend/repository/components/header_area/fork_suggestion_alert_spec.js create mode 100644 spec/frontend/repository/components/header_area/fork_suggestion_modal_spec.js diff --git a/app/assets/javascripts/repository/components/header_area.vue b/app/assets/javascripts/repository/components/header_area.vue index ceb60addc3c3e2..050843b3ed9d37 100644 --- a/app/assets/javascripts/repository/components/header_area.vue +++ b/app/assets/javascripts/repository/components/header_area.vue @@ -22,7 +22,6 @@ import SourceCodeDownloadDropdown from '~/vue_shared/components/download_dropdow import CloneCodeDropdown from '~/vue_shared/components/code_dropdown/clone_code_dropdown.vue'; import AddToTree from '~/repository/components/header_area/add_to_tree.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; -import ForkSuggestionAlert from '~/repository/components/header_area/fork_suggestion_alert.vue'; export default { name: 'HeaderArea', @@ -42,7 +41,6 @@ export default { SourceCodeDownloadDropdown, CloneCodeDropdown, AddToTree, - ForkSuggestionAlert, WebIdeLink: () => import('ee_else_ce/vue_shared/components/web_ide_link.vue'), LockDirectoryButton: () => import('ee_component/repository/components/lock_directory_button.vue'), @@ -116,12 +114,6 @@ export default { required: true, }, }, - data() { - return { - showForkSuggestionAlert: false, - blobForkAndViewPath: '', - }; - }, computed: { isTreeView() { return this.$route.name !== 'blobPathDecoded'; @@ -195,22 +187,12 @@ export default { InternalEvents.trackEvent(FIND_FILE_BUTTON_CLICK); Shortcuts.focusSearchFile(); }, - onShowForkSuggestionAlert(forkPath) { - this.showForkSuggestionAlert = true; - this.blobForkAndViewPath = forkPath; - }, }, }; diff --git a/app/assets/javascripts/repository/components/header_area/blob_button_group.vue b/app/assets/javascripts/repository/components/header_area/blob_button_group.vue index 7fcce8192766ae..e074672c5d7fcc 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_button_group.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_button_group.vue @@ -5,6 +5,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { showForkSuggestion } from '~/repository/utils/fork_suggestion_utils'; import { DEFAULT_BLOB_INFO } from '~/repository/constants'; import getRefMixin from '~/repository/mixins/get_ref'; +import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; const REPLACE_BLOB_MODAL_ID = 'modal-replace-blob'; @@ -17,6 +18,7 @@ export default { components: { GlDisclosureDropdownItem, GlDisclosureDropdownGroup, + ForkSuggestionModal, UploadBlobModal, LockFileDropdownItem: () => import('ee_component/repository/components/header_area/lock_file_dropdown_item.vue'), @@ -62,6 +64,11 @@ export default { default: () => DEFAULT_BLOB_INFO.pathLocks, }, }, + data() { + return { + isModalVisible: false, + }; + }, computed: { replaceFileItem() { return { @@ -81,7 +88,7 @@ export default { methods: { showModal() { if (this.shouldShowForkSuggestion) { - this.$emit('showForkSuggestionAlert'); + this.isModalVisible = true; return; } @@ -103,6 +110,11 @@ export default { :is-loading="isLoading" /> + @@ -241,7 +238,6 @@ export default { :is-empty-repository="project.repository.empty" :is-using-lfs="isUsingLfs" @copy="onCopy" - @showForkSuggestionAlert="onShowForkSuggestionAlert" /> diff --git a/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue b/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue index 1800d7c556655a..f1f2b024771ac0 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue @@ -3,6 +3,7 @@ import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem } from '@gitlab/ui' import { uniqueId } from 'lodash'; import { sprintf, __ } from '~/locale'; import { showForkSuggestion } from '~/repository/utils/fork_suggestion_utils'; +import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue'; import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue'; import { DEFAULT_BLOB_INFO } from '~/repository/constants'; @@ -10,6 +11,7 @@ export default { components: { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, + ForkSuggestionModal, DeleteBlobModal, }, inject: { @@ -43,6 +45,11 @@ export default { required: true, }, }, + data() { + return { + isModalVisible: false, + }; + }, computed: { deleteFileItem() { return { @@ -65,7 +72,7 @@ export default { methods: { showModal() { if (this.shouldShowForkSuggestion) { - this.$emit('showForkSuggestionAlert'); + this.isModalVisible = true; return; } @@ -78,6 +85,11 @@ export default { diff --git a/app/assets/javascripts/repository/components/header_area/fork_suggestion_alert.vue b/app/assets/javascripts/repository/components/header_area/fork_suggestion_alert.vue deleted file mode 100644 index 766a149cf36fda..00000000000000 --- a/app/assets/javascripts/repository/components/header_area/fork_suggestion_alert.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/app/assets/javascripts/repository/components/header_area/fork_suggestion_modal.vue b/app/assets/javascripts/repository/components/header_area/fork_suggestion_modal.vue new file mode 100644 index 00000000000000..83e8d20e7b0edc --- /dev/null +++ b/app/assets/javascripts/repository/components/header_area/fork_suggestion_modal.vue @@ -0,0 +1,64 @@ + + + diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f4483e682b30a3..41df5dc3d0cd92 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 a3e5c9e1e281d1..1711d49bab61e3 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(); @@ -103,14 +106,21 @@ describe('BlobButtonGroup component', () => { expect(showUploadBlobModalMock).not.toHaveBeenCalled(); }); - it('emits showForkSuggestionAlert', () => { + it('triggers ForkSuggestionModal from the replace item', async () => { findReplaceItem().vm.$emit('action'); + await nextTick(); - expect(wrapper.emitted('showForkSuggestionAlert')).toEqual([[]]); + expect(findForkSuggestionModal().props('visible')).toBe(true); }); }); }); + it('renders ForkSuggestionModal', () => { + expect(findForkSuggestionModal().props()).toMatchObject({ + forkPath: 'fork/view/path', + }); + }); + it('renders UploadBlobModal', () => { expect(findUploadBlobModal().props()).toMatchObject({ commitMessage: 'Replace file.js', 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 e1740f0d647a74..4a0bff4b9bdf3c 100644 --- a/spec/frontend/repository/components/header_area/blob_controls_spec.js +++ b/spec/frontend/repository/components/header_area/blob_controls_spec.js @@ -242,10 +242,5 @@ describe('Blob controls component', () => { expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Example raw text content'); }); - - it('passes showForkSuggestionAlert when receives showForkSuggestionAlert event', () => { - findOverflowMenu().vm.$emit('showForkSuggestionAlert', 'fork/view/path'); - expect(wrapper.emitted('showForkSuggestionAlert')).toEqual([['fork/view/path']]); - }); }); }); 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 c70fffa12e9361..f4e89f8d4971a3 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(); @@ -102,14 +105,21 @@ describe('BlobDeleteFileGroup component', () => { expect(showDeleteBlobModalMock).not.toHaveBeenCalled(); }); - it('emits showForkSuggestionAlert', () => { + it('changes ForkSuggestionModal visibility', async () => { findDeleteItem().vm.$emit('action'); + await nextTick(); - expect(wrapper.emitted('showForkSuggestionAlert')).toEqual([[]]); + expect(findForkSuggestionModal().props('visible')).toBe(true); }); }); }); + it('renders ForkSuggestionModal', () => { + expect(findForkSuggestionModal().props()).toMatchObject({ + forkPath: 'fork/view/path', + }); + }); + it('renders DeleteBlobModal', () => { expect(findDeleteBlobModal().props()).toMatchObject({ commitMessage: 'Delete file.js', diff --git a/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js b/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js index 5ca18d200330bd..9b1ddeaa5ef517 100644 --- a/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js +++ b/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js @@ -143,11 +143,6 @@ describe('Blob Overflow Menu', () => { expect(findBlobButtonGroup().exists()).toBe(false); }); - - it('passes showForkSuggestionAlert when receives showForkSuggestionAlert event', () => { - findBlobButtonGroup().vm.$emit('showForkSuggestionAlert'); - expect(wrapper.emitted('showForkSuggestionAlert')).toEqual([[]]); - }); }); describe('Blob Delete File Group', () => { @@ -174,11 +169,6 @@ describe('Blob Overflow Menu', () => { expect(findBlobDeleteFileGroup().exists()).toBe(false); }); - - it('passes showForkSuggestionAlert when receives showForkSuggestionAlert event', () => { - findBlobDeleteFileGroup().vm.$emit('showForkSuggestionAlert'); - expect(wrapper.emitted('showForkSuggestionAlert')).toEqual([[]]); - }); }); describe('Permalink Dropdown Item', () => { diff --git a/spec/frontend/repository/components/header_area/fork_suggestion_alert_spec.js b/spec/frontend/repository/components/header_area/fork_suggestion_alert_spec.js deleted file mode 100644 index 73b61f1a38d1e7..00000000000000 --- a/spec/frontend/repository/components/header_area/fork_suggestion_alert_spec.js +++ /dev/null @@ -1,88 +0,0 @@ -import { nextTick } from 'vue'; -import { GlAlert } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import ForkSuggestionAlert from '~/repository/components/header_area/fork_suggestion_alert.vue'; - -const DEFAULT_PROPS = { showForkSuggestionAlert: false, forkPath: '/fork/project/path' }; - -describe('ForkSuggestionAlert component', () => { - let wrapper; - - const createComponent = (props = {}) => { - wrapper = shallowMountExtended(ForkSuggestionAlert, { - propsData: { ...DEFAULT_PROPS, ...props }, - stubs: { - GlAlert, - }, - }); - }; - - const findAlert = () => wrapper.findComponent(GlAlert); - const findMessage = () => wrapper.findByTestId('message'); - const findForkButton = () => wrapper.findByTestId('fork'); - const findCancelButton = () => wrapper.findByTestId('cancel'); - - describe('when showForkSuggestionAlert is false', () => { - beforeEach(() => { - createComponent(); - }); - - it('does not render the alert', () => { - expect(findAlert().exists()).toBe(false); - }); - }); - - describe('when showForkSuggestionAlert is true', () => { - beforeEach(() => { - createComponent({ showForkSuggestionAlert: true }); - }); - - it('renders the alert with correct variant', () => { - expect(findAlert().exists()).toBe(true); - expect(findMessage().text()).toBe( - "You can't edit files directly in this project. Fork this project and submit a merge request with your changes.", - ); - }); - - describe('#actions template', () => { - it('renders the fork button with correct props', () => { - expect(findForkButton().exists()).toBe(true); - expect(findForkButton().attributes('href')).toBe('/fork/project/path'); - expect(findForkButton().attributes('data-method')).toBe('post'); - }); - - it('renders the cancel button with correct props', () => { - expect(findCancelButton().exists()).toBe(true); - expect(findCancelButton().text()).toBe('Cancel'); - }); - }); - - describe('dismissal behavior', () => { - it('hides the alert when dismiss event is triggered', async () => { - expect(findAlert().exists()).toBe(true); - - findAlert().vm.$emit('dismiss'); - await nextTick(); - - expect(wrapper.emitted('dismiss')).toEqual([[]]); - }); - - it('hides the alert when cancel button is clicked', async () => { - expect(findAlert().exists()).toBe(true); - - findCancelButton().vm.$emit('click'); - await nextTick(); - - expect(wrapper.emitted('dismiss')).toEqual([[]]); - }); - }); - }); - - describe('reactivity to prop changes', () => { - it('updates the fork path when forkPath prop changes', () => { - createComponent({ showForkSuggestionAlert: true, forkPath: '/new/fork/path' }); - - expect(findForkButton().attributes('href')).toBe('/new/fork/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 00000000000000..8ff2d65ccee2e8 --- /dev/null +++ b/spec/frontend/repository/components/header_area/fork_suggestion_modal_spec.js @@ -0,0 +1,77 @@ +import { nextTick } from 'vue'; +import { GlModal } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component'; +import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue'; + +const DEFAULT_PROPS = { visible: true, forkPath: '/fork/project/path' }; +const hideMock = jest.fn(); + +describe('ForkSuggestionModal component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(ForkSuggestionModal, { + propsData: { ...DEFAULT_PROPS, ...props }, + stubs: { + GlModal: stubComponent(GlModal, { + methods: { + hide: hideMock, + }, + template: RENDER_ALL_SLOTS_TEMPLATE, + }), + }, + }); + }; + + const findModal = () => wrapper.findComponent(GlModal); + const findMessage = () => wrapper.findByTestId('message'); + const findForkButton = () => wrapper.findByTestId('fork'); + const findCancelButton = () => wrapper.findByTestId('cancel'); + + 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('#actions template', () => { + it('renders the fork button with correct props', () => { + expect(findForkButton().exists()).toBe(true); + expect(findForkButton().attributes('href')).toBe('/fork/project/path'); + expect(findForkButton().attributes('data-method')).toBe('post'); + }); + + it('renders the cancel button with correct props', () => { + expect(findCancelButton().exists()).toBe(true); + expect(findCancelButton().text()).toBe('Cancel'); + }); + + it('hides the modal when cancel button is clicked', async () => { + expect(findModal().exists()).toBe(true); + + findCancelButton().vm.$emit('click'); + await nextTick(); + + expect(hideMock).toHaveBeenCalled(); + }); + }); + + describe('reactivity to prop changes', () => { + it('updates the fork path when forkPath prop changes', () => { + createComponent({ forkPath: '/new/fork/path' }); + + expect(findForkButton().attributes('href')).toBe('/new/fork/path'); + }); + }); +}); diff --git a/spec/frontend/repository/components/header_area_spec.js b/spec/frontend/repository/components/header_area_spec.js index a47ec6cd749da1..6f68c509cc34f3 100644 --- a/spec/frontend/repository/components/header_area_spec.js +++ b/spec/frontend/repository/components/header_area_spec.js @@ -11,7 +11,6 @@ import FileIcon from '~/vue_shared/components/file_icon.vue'; import CloneCodeDropdown from '~/vue_shared/components/code_dropdown/clone_code_dropdown.vue'; import RepositoryOverflowMenu from '~/repository/components/header_area/repository_overflow_menu.vue'; import BlobControls from '~/repository/components/header_area/blob_controls.vue'; -import ForkSuggestionAlert from '~/repository/components/header_area/fork_suggestion_alert.vue'; import Shortcuts from '~/behaviors/shortcuts/shortcuts'; import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; import { headerAppInjected } from 'ee_else_ce_jest/repository/mock_data'; @@ -31,7 +30,6 @@ const defaultMockRoute = { describe('HeaderArea', () => { let wrapper; - const findForkSuggestionAlert = () => wrapper.findComponent(ForkSuggestionAlert); const findBreadcrumbs = () => wrapper.findComponent(Breadcrumbs); const findRefSelector = () => wrapper.findComponent(RefSelector); const findFindFileButton = () => wrapper.findByTestId('tree-find-file-control'); @@ -94,36 +92,6 @@ describe('HeaderArea', () => { expect(findPageHeading().exists()).toBe(true); }); - describe('ForkSuggestionAlert', () => { - const forkPath = '/fork/path'; - - it('does not render by default', () => { - expect(findForkSuggestionAlert().props('showForkSuggestionAlert')).toBe(false); - }); - - it('renders on showForkSuggestionAlert', async () => { - await findBlobControls().vm.$emit('showForkSuggestionAlert', forkPath); - - expect(findForkSuggestionAlert().props('showForkSuggestionAlert')).toBe(true); - expect(findForkSuggestionAlert().props('forkPath')).toBe(forkPath); - }); - - it('hides the alert when dismissed and shows it again when triggered', async () => { - // Show the alert - await findBlobControls().vm.$emit('showForkSuggestionAlert', forkPath); - expect(findForkSuggestionAlert().props('showForkSuggestionAlert')).toBe(true); - - // Simulate dismissal - await findForkSuggestionAlert().vm.$emit('dismiss'); - expect(findForkSuggestionAlert().props('showForkSuggestionAlert')).toBe(false); - - // Show the alert again - await findBlobControls().vm.$emit('showForkSuggestionAlert', '/another/path'); - expect(findForkSuggestionAlert().props('showForkSuggestionAlert')).toBe(true); - expect(findForkSuggestionAlert().props('forkPath')).toBe('/another/path'); - }); - }); - describe('when rendered for tree view', () => { beforeEach(() => { wrapper = createComponent({}, { name: 'treePathDecoded', params: { path: 'project' } }); -- GitLab From 236f38933dd27e3e32e30d0db13714bbbfcd2aae Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Wed, 5 Mar 2025 11:46:24 +0100 Subject: [PATCH 4/4] Address reviewers feedback --- .../repository/components/header_area.vue | 3 +- .../header_area/blob_button_group.vue | 8 +-- .../header_area/fork_suggestion_modal.vue | 52 +++++++++---------- .../header_area/blob_controls_spec.js | 2 +- .../header_area/fork_suggestion_modal_spec.js | 40 +++----------- 5 files changed, 38 insertions(+), 67 deletions(-) diff --git a/app/assets/javascripts/repository/components/header_area.vue b/app/assets/javascripts/repository/components/header_area.vue index 050843b3ed9d37..93c8481a3b7548 100644 --- a/app/assets/javascripts/repository/components/header_area.vue +++ b/app/assets/javascripts/repository/components/header_area.vue @@ -1,6 +1,5 @@