From 9688f98177a44b4af3a906b2e986aeb2186e58af Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Mon, 20 Jan 2025 17:53:27 +0100 Subject: [PATCH 01/13] Improve test cases for LockFileDropdownItem --- .../header_area/lock_file_dropdown_item_spec.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js b/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js index 73db421a766cd5..3587e4bc9be96e 100644 --- a/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js +++ b/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js @@ -202,6 +202,17 @@ describe('LockFileDropdownItem component', () => { }); }); + it('executes a lock mutation once lock is confirmed', () => { + findLockFileDropdownItem().vm.$emit('action'); + clickSubmit(); + + expect(lochPathMutationResolver).toHaveBeenCalledWith({ + filePath: 'some/path/locked_file.js', + lock: false, + projectPath: 'some/project/path', + }); + }); + it('does not execute a lock mutation if lock not confirmed', () => { findLockFileDropdownItem().vm.$emit('action'); -- GitLab From c18c150834ffffc04b02536a0108e29ffca40c63 Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Tue, 21 Jan 2025 17:02:14 +0100 Subject: [PATCH 02/13] Create BlobButtonGroup for overflow menu --- .../header_area/blob_button_group.vue | 168 +++++++++++++++++ .../components/header_area/blob_controls.vue | 15 +- .../header_area/blob_overflow_menu.vue | 48 +++++ .../queries/blob_controls.query.graphql | 5 + .../header_area/blob_button_group_spec.js | 177 ++++++++++++++++++ .../header_area/blob_controls_spec.js | 7 + .../header_area/blob_overflow_menu_spec.js | 38 +++- spec/frontend/repository/mock_data.js | 5 + 8 files changed, 456 insertions(+), 7 deletions(-) create mode 100644 app/assets/javascripts/repository/components/header_area/blob_button_group.vue create mode 100644 spec/frontend/repository/components/header_area/blob_button_group_spec.js 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 new file mode 100644 index 00000000000000..c0fdd27ef461ee --- /dev/null +++ b/app/assets/javascripts/repository/components/header_area/blob_button_group.vue @@ -0,0 +1,168 @@ + + + diff --git a/app/assets/javascripts/repository/components/header_area/blob_controls.vue b/app/assets/javascripts/repository/components/header_area/blob_controls.vue index 8948029cfb5700..3c335f1172e02a 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_controls.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_controls.vue @@ -56,6 +56,10 @@ export default { skip() { return !this.filePath; }, + result({ data }) { + const repository = data.project.repository || {}; + this.isEmptyRepository = repository.empty; + }, error() { createAlert({ message: this.$options.i18n.errorMessage }); }, @@ -80,6 +84,7 @@ export default { data() { return { project: {}, + isEmptyRepository: false, }; }, computed: { @@ -95,7 +100,7 @@ export default { blobInfo() { return this.project?.repository?.blobs?.nodes[0] || {}; }, - showBlameButton() { + isUsingLfs() { return !this.blobInfo.storedExternally && this.blobInfo.externalStorage !== 'lfs'; }, isBinaryFileType() { @@ -175,7 +180,7 @@ export default { {{ $options.i18n.findFile }} 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 6c95b78315bb47..fd5a050926a351 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 @@ -1,9 +1,11 @@ diff --git a/app/assets/javascripts/repository/components/header_area/blob_default_actions_group.vue b/app/assets/javascripts/repository/components/header_area/blob_default_actions_group.vue index 9801351f10a617..951d6d2dbfe6a6 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_default_actions_group.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_default_actions_group.vue @@ -1,5 +1,5 @@ -- GitLab From 7a1004ef56f78bb5f1fd837f3699a3b0a052ee61 Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Wed, 22 Jan 2025 13:33:19 +0100 Subject: [PATCH 05/13] Remove duplcated test case --- .../header_area/lock_file_dropdown_item_spec.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js b/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js index 3587e4bc9be96e..73db421a766cd5 100644 --- a/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js +++ b/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js @@ -202,17 +202,6 @@ describe('LockFileDropdownItem component', () => { }); }); - it('executes a lock mutation once lock is confirmed', () => { - findLockFileDropdownItem().vm.$emit('action'); - clickSubmit(); - - expect(lochPathMutationResolver).toHaveBeenCalledWith({ - filePath: 'some/path/locked_file.js', - lock: false, - projectPath: 'some/project/path', - }); - }); - it('does not execute a lock mutation if lock not confirmed', () => { findLockFileDropdownItem().vm.$emit('action'); -- GitLab From a45158cc6facea0d9012d9718d8ac9f91c434b83 Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Wed, 22 Jan 2025 16:25:12 +0100 Subject: [PATCH 06/13] Move project query to group component --- .../header_area/blob_button_group.vue | 8 +++ .../header_area/lock_file_dropdown_item.vue | 45 ++++++------- .../lock_file_dropdown_item_spec.js | 67 +++++-------------- 3 files changed, 44 insertions(+), 76 deletions(-) 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 d4038e4da8a6fb..d02de2ca8377d4 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 @@ -85,6 +85,7 @@ export default { }; }, update({ project }) { + this.pathLocks = project?.pathLocks || DEFAULT_BLOB_INFO.pathLocks; this.userPermissions = project?.userPermissions; }, error() { @@ -94,10 +95,14 @@ export default { }, data() { return { + pathLocks: DEFAULT_BLOB_INFO.pathLocks, userPermissions: DEFAULT_BLOB_INFO.userPermissions, }; }, computed: { + isLoading() { + return this.$apollo?.queries.projectInfo.loading; + }, replaceFileItem() { return { text: this.$options.i18n.replace, @@ -144,6 +149,9 @@ export default { :name="name" :path="path" :project-path="projectPath" + :path-locks="pathLocks" + :user-permissions="userPermissions" + :is-loading="isLoading" /> DEFAULT_BLOB_INFO.pathLocks, + }, + userPermissions: { + type: Object, + required: false, + default: () => DEFAULT_BLOB_INFO.userPermissions, + }, + isLoading: { + type: Boolean, + required: false, + default: false, }, }, data() { @@ -56,14 +51,9 @@ export default { isUpdating: false, isModalVisible: false, locked: false, - pathLocks: DEFAULT_BLOB_INFO.pathLocks, - userPermissions: DEFAULT_BLOB_INFO.userPermissions, }; }, computed: { - isLoading() { - return this.$apollo?.queries.projectInfo.loading; - }, lockButtonTitle() { return this.isLocked ? this.$options.i18n.unlock : this.$options.i18n.lock; }, @@ -112,8 +102,11 @@ export default { }, }, watch: { - isLocked(val) { - this.locked = val; + isLocked: { + immediate: true, + handler(val) { + this.locked = val; + }, }, }, methods: { diff --git a/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js b/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js index 73db421a766cd5..434b60e83ab762 100644 --- a/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js +++ b/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js @@ -5,7 +5,6 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import lockPathMutation from '~/repository/mutations/lock_path.mutation.graphql'; -import projectInfoQuery from 'ee_component/repository/queries/project_info.query.graphql'; import { projectMock, userPermissionsMock } from 'ee_jest/repository/mock_data'; import LockFileDropdownItem from 'ee_component/repository/components/header_area/lock_file_dropdown_item.vue'; import { createAlert } from '~/alert'; @@ -17,22 +16,11 @@ describe('LockFileDropdownItem component', () => { let wrapper; let fakeApollo; - const projectInfoQueryMockResolver = jest - .fn() - .mockResolvedValue({ data: { project: projectMock } }); - const projectInfoQueryErrorResolver = jest.fn().mockRejectedValue(new Error('Request failed')); + const lockPathMutationResolver = jest.fn(); - const lochPathMutationResolver = jest.fn(); - - const createComponent = ({ - mutationResolver = lochPathMutationResolver, - projectInfoResolver = projectInfoQueryMockResolver, - } = {}) => { + const createComponent = ({ props = {}, mutationResolver = lockPathMutationResolver } = {}) => { window.gon = { current_username: projectMock.pathLocks.nodes[0].user.username }; - fakeApollo = createMockApollo([ - [projectInfoQuery, projectInfoResolver], - [lockPathMutation, mutationResolver], - ]); + fakeApollo = createMockApollo([[lockPathMutation, mutationResolver]]); wrapper = shallowMount(LockFileDropdownItem, { apolloProvider: fakeApollo, @@ -40,6 +28,10 @@ describe('LockFileDropdownItem component', () => { name: 'locked_file.js', path: 'some/path/locked_file.js', projectPath: 'some/project/path', + isLoading: false, + userPermissions: userPermissionsMock, + pathLocks: projectMock.pathLocks, + ...props, }, }); }; @@ -60,19 +52,13 @@ describe('LockFileDropdownItem component', () => { }); it('renders disabled the lock dropdown item if user can not lock a file', async () => { - const projectWithNoPushPermission = { - data: { - project: { - ...projectMock, - userPermissions: { - ...userPermissionsMock, - pushCode: false, - }, + createComponent({ + props: { + userPermissions: { + ...userPermissionsMock, + pushCode: false, }, }, - }; - createComponent({ - projectInfoResolver: jest.fn().mockResolvedValue(projectWithNoPushPermission), }); await waitForPromises(); @@ -82,12 +68,11 @@ describe('LockFileDropdownItem component', () => { }); it('renders disabled until query fetches projects info', async () => { - const projectInfoQueryLoading = jest.fn().mockResolvedValue(new Promise(() => {})); createComponent({ - projectInfoResolver: projectInfoQueryLoading, + props: { isLoading: true }, }); await waitForPromises(); - expect(projectInfoQueryLoading).toHaveBeenCalled(); + expect(findLockFileDropdownItem().props('item')).toMatchObject({ extraAttrs: { disabled: true }, }); @@ -110,17 +95,8 @@ describe('LockFileDropdownItem component', () => { }); it('renders the Lock dropdown item label, when file is not locked', async () => { - const projectWithNoLocks = { - data: { - project: { - ...projectMock, - pathLocks: { __typename: 'PathLockConnection', nodes: [] }, - }, - }, - }; - createComponent({ - projectInfoResolver: jest.fn().mockResolvedValue(projectWithNoLocks), + props: { pathLocks: { __typename: 'PathLockConnection', nodes: [] } }, }); await waitForPromises(); @@ -137,15 +113,6 @@ describe('LockFileDropdownItem component', () => { }); }); - it('creates an alert with the correct message, when projectInfo query fails', async () => { - createComponent({ projectInfoResolver: projectInfoQueryErrorResolver }); - await waitForPromises(); - - expect(createAlert).toHaveBeenCalledWith({ - message: 'An error occurred while fetching lock information, please try again.', - }); - }); - describe('Modal', () => { it('displays a confirm modal when the lock dropdown item is clicked', () => { findLockFileDropdownItem().vm.$emit('action'); @@ -195,7 +162,7 @@ describe('LockFileDropdownItem component', () => { findLockFileDropdownItem().vm.$emit('action'); clickSubmit(); - expect(lochPathMutationResolver).toHaveBeenCalledWith({ + expect(lockPathMutationResolver).toHaveBeenCalledWith({ filePath: 'some/path/locked_file.js', lock: false, projectPath: 'some/project/path', @@ -205,7 +172,7 @@ describe('LockFileDropdownItem component', () => { it('does not execute a lock mutation if lock not confirmed', () => { findLockFileDropdownItem().vm.$emit('action'); - expect(lochPathMutationResolver).not.toHaveBeenCalled(); + expect(lockPathMutationResolver).not.toHaveBeenCalled(); }); }); }); -- GitLab From ee0860cc6e7f679d38bb101e850a5b8c3aff484c Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Wed, 22 Jan 2025 19:16:35 +0100 Subject: [PATCH 07/13] Add check for fork suggestion --- .../header_area/blob_button_group.vue | 32 ++++++++++++++--- .../components/header_area/blob_controls.vue | 7 ++++ .../header_area/blob_overflow_menu.vue | 19 ++++------- .../queries/blob_controls.query.graphql | 2 ++ .../header_area/blob_button_group_spec.js | 34 ++++++++++++++----- .../header_area/blob_overflow_menu_spec.js | 2 ++ spec/frontend/repository/mock_data.js | 2 ++ 7 files changed, 73 insertions(+), 25 deletions(-) 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 d02de2ca8377d4..ea550aa5dc7604 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 @@ -3,6 +3,7 @@ import { GlDisclosureDropdownItem, GlDisclosureDropdownGroup } from '@gitlab/ui' import { uniqueId } from 'lodash'; import { sprintf, __ } from '~/locale'; import { createAlert } from '~/alert'; +import { isLoggedIn } from '~/lib/utils/common_utils'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import projectInfoQuery from 'ee_else_ce/repository/queries/project_info.query.graphql'; import { DEFAULT_BLOB_INFO } from '~/repository/constants'; @@ -35,6 +36,12 @@ export default { originalBranch: { default: '', }, + canModifyBlob: { + default: () => false, + }, + canModifyBlobWithWebIde: { + default: () => false, + }, }, props: { name: { @@ -65,10 +72,6 @@ export default { type: String, required: true, }, - showForkSuggestion: { - type: Boolean, - required: true, - }, isUsingLfs: { type: Boolean, required: false, @@ -108,6 +111,8 @@ export default { text: this.$options.i18n.replace, extraAttrs: { 'data-testid': 'replace', + // a temporary solution before resolving https://gitlab.com/gitlab-org/gitlab/-/issues/450774#note_2319974833 + disabled: this.showForkSuggestion, }, }; }, @@ -116,6 +121,8 @@ export default { text: this.$options.i18n.delete, extraAttrs: { 'data-testid': 'delete', + // a temporary solution before resolving https://gitlab.com/gitlab-org/gitlab/-/issues/450774#note_2319974833 + disabled: this.showForkSuggestion, }, }; }, @@ -128,6 +135,23 @@ export default { deleteModalCommitMessage() { return sprintf(__('Delete %{name}'), { name: this.name }); }, + isLoggedIn() { + return isLoggedIn(); + }, + canFork() { + const { createMergeRequestIn, forkProject } = this.userPermissions; + + return this.isLoggedIn && !this.isUsingLfs && createMergeRequestIn && forkProject; + }, + showSingleFileEditorForkSuggestion() { + return this.canFork && !this.canModifyBlob; + }, + showWebIdeForkSuggestion() { + return this.canFork && !this.canModifyBlobWithWebIde; + }, + showForkSuggestion() { + return this.showSingleFileEditorForkSuggestion || this.showWebIdeForkSuggestion; + }, }, methods: { showModal(modalId) { diff --git a/app/assets/javascripts/repository/components/header_area/blob_controls.vue b/app/assets/javascripts/repository/components/header_area/blob_controls.vue index 3c335f1172e02a..f438e5059b2f4e 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_controls.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_controls.vue @@ -1,5 +1,6 @@ -- GitLab