From 0c12637bb295ce6f6ecd15241de3b39f46051679 Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Thu, 9 Jan 2025 13:16:57 +0100 Subject: [PATCH 1/7] Create dopdown item component for file lock button --- .../components/header_area/blob_controls.vue | 3 + .../header_area/blob_overflow_menu.vue | 36 +++- .../queries/blob_controls.query.graphql | 2 + .../header_area/lock_file_dropdown_item.vue | 164 ++++++++++++++++++ .../components/blob_content_viewer_spec.js | 2 +- .../lock_file_dropdown_item_spec.js | 159 +++++++++++++++++ ee/spec/frontend/repository/mock_data.js | 2 +- .../header_area/blob_overflow_menu_spec.js | 8 +- 8 files changed, 368 insertions(+), 8 deletions(-) create mode 100644 ee/app/assets/javascripts/repository/components/header_area/lock_file_dropdown_item.vue create mode 100644 ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js 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 7fe14d5720e61f..8948029cfb5700 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_controls.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_controls.vue @@ -197,6 +197,9 @@ export default { + import('ee_component/repository/components/header_area/lock_file_dropdown_item.vue'), }, directives: { GlTooltipDirective, }, + mixins: [glFeatureFlagMixin()], inject: { blobHash: { default: '', @@ -29,6 +33,18 @@ export default { }, }, props: { + name: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, rawPath: { type: String, required: true, @@ -174,16 +190,32 @@ export default { :toggle-text="$options.i18n.dropdownLabel" text-sr-only > + - - + + diff --git a/app/assets/javascripts/repository/queries/blob_controls.query.graphql b/app/assets/javascripts/repository/queries/blob_controls.query.graphql index e6e6505932081e..71470717d1a4c4 100644 --- a/app/assets/javascripts/repository/queries/blob_controls.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_controls.query.graphql @@ -5,12 +5,14 @@ query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!, $ref blobs(paths: [$filePath], ref: $ref, refType: $refType) { nodes { id + name blamePath permalinkPath storedExternally externalStorage environmentFormattedExternalUrl environmentExternalUrlForRouteMap + path rawPath rawTextBlob simpleViewer { diff --git a/ee/app/assets/javascripts/repository/components/header_area/lock_file_dropdown_item.vue b/ee/app/assets/javascripts/repository/components/header_area/lock_file_dropdown_item.vue new file mode 100644 index 00000000000000..72a3fc73227bf9 --- /dev/null +++ b/ee/app/assets/javascripts/repository/components/header_area/lock_file_dropdown_item.vue @@ -0,0 +1,164 @@ + + + diff --git a/ee/spec/frontend/repository/components/blob_content_viewer_spec.js b/ee/spec/frontend/repository/components/blob_content_viewer_spec.js index bb082aea5ac177..0dc282431a830c 100644 --- a/ee/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/ee/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -175,7 +175,7 @@ describe('Blob content viewer component', () => { pushCode: canPushCode, downloadCode: canDownloadCode, empty, - path: 'locked_file.js', + path: 'some/path/locked_file.js', }); expect(findBlobButtonGroup().props('canLock')).toBe(canLock); 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 new file mode 100644 index 00000000000000..151ce7daf4c5dc --- /dev/null +++ b/ee/spec/frontend/repository/components/header_area/lock_file_dropdown_item_spec.js @@ -0,0 +1,159 @@ +import { GlDisclosureDropdownItem, GlModal } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +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'; + +Vue.use(VueApollo); +jest.mock('~/alert'); + +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 lochPathMutationResolver = jest.fn(); + + const createComponent = ({ + mutationResolver = lochPathMutationResolver, + projectInfoResolver = projectInfoQueryMockResolver, + } = {}) => { + window.gon = { current_username: projectMock.pathLocks.nodes[0].user.username }; + fakeApollo = createMockApollo([ + [projectInfoQuery, projectInfoResolver], + [lockPathMutation, mutationResolver], + ]); + + wrapper = shallowMount(LockFileDropdownItem, { + apolloProvider: fakeApollo, + propsData: { + name: 'locked_file.js', + path: 'some/path/locked_file.js', + projectPath: 'some/project/path', + }, + }); + }; + + let lockMutationMock; + const findLockFileDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem); + const findModal = () => wrapper.findComponent(GlModal); + const clickSubmit = () => findModal().vm.$emit('primary'); + const clickHide = () => findModal().vm.$emit('hide'); + + beforeEach(async () => { + createComponent(); + await waitForPromises(); + }); + + it('disables the lock dropdown item if user can not lock a file', async () => { + const projectWithNoPushPermission = { + data: { + project: { + ...projectMock, + userPermissions: { + ...userPermissionsMock, + pushCode: false, + }, + }, + }, + }; + createComponent({ + projectInfoResolver: jest.fn().mockResolvedValue(projectWithNoPushPermission), + }); + await waitForPromises(); + + expect(findLockFileDropdownItem().props('item')).toMatchObject({ + extraAttrs: { disabled: true }, + }); + }); + + 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), + }); + await waitForPromises(); + + expect(findLockFileDropdownItem().props('item')).toMatchObject({ + text: 'Lock', + extraAttrs: { disabled: false }, + }); + }); + + it('renders the Unlock dropdown item label, when file is locked', () => { + expect(findLockFileDropdownItem().props('item')).toMatchObject({ + text: 'Unlock', + extraAttrs: { disabled: false }, + }); + }); + + 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'); + expect(findModal().text()).toBe('Are you sure you want to unlock locked_file.js?'); + }); + + it('should hide the confirm modal when a hide action is triggered', async () => { + await findLockFileDropdownItem().vm.$emit('action'); + expect(findModal().props('visible')).toBe(true); + + await clickHide(); + expect(findModal().props('visible')).toBe(false); + }); + + it('executes a lock mutation once lock is confirmed', async () => { + lockMutationMock = jest.fn().mockRejectedValue(new Error('Request failed')); + createComponent({ mutationResolver: lockMutationMock }); + await waitForPromises(); + + findLockFileDropdownItem().vm.$emit('action'); + clickSubmit(); + + expect(lockMutationMock).toHaveBeenCalledWith({ + filePath: 'some/path/locked_file.js', + lock: false, + projectPath: 'some/project/path', + }); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred while editing lock information, please try again.', + captureError: true, + error: expect.any(Error), + }); + }); + + it('does not execute a lock mutation if lock not confirmed', () => { + findLockFileDropdownItem().vm.$emit('action'); + + expect(lockMutationMock).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/ee/spec/frontend/repository/mock_data.js b/ee/spec/frontend/repository/mock_data.js index ac696a07e02f8d..0de032e553aa50 100644 --- a/ee/spec/frontend/repository/mock_data.js +++ b/ee/spec/frontend/repository/mock_data.js @@ -87,7 +87,7 @@ export const projectMock = { { __typename: 'PathLock', id: 'gid://gitlab/PathLock/2', - path: 'locked_file.js', + path: 'some/path/locked_file.js', user: { id: 'gid://gitlab/User/1', username: 'root', 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 b9c6b9b3eac92f..699abb0b82f1b5 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 @@ -72,10 +72,10 @@ describe('Blob Overflow Menu', () => { findDropdownItems().wrappers.find((x) => { return x.props('item').text === text; }); - const findCopyFileContentItem = () => findDropdownItems().at(0); - const findViewRawItem = () => findDropdownItems().at(1); - const findDownloadItem = () => findDropdownItems().at(2); - const findEnvironmentItem = () => findDropdownItems().at(3); + const findCopyFileContentItem = () => findDropdownItemWithText('Copy file contents'); + const findViewRawItem = () => findDropdownItemWithText('Open raw'); + const findDownloadItem = () => findDropdownItemWithText('Download'); + const findEnvironmentItem = () => findDropdownItemWithText('View on my.testing.environment'); beforeEach(() => { createComponent(); -- GitLab From e3f84534f1fb02811a5e48ec2c16f016b1a0b03a Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Mon, 20 Jan 2025 14:28:39 +0100 Subject: [PATCH 2/7] Extract BlobDefaultActions group --- .../blob_default_actions_group.vue | 164 ++++++++++++++++++ .../header_area/blob_overflow_menu.vue | 106 ++--------- .../header_area/blob_controls_spec.js | 3 + .../blob_default_actions_group_spec.js | 148 ++++++++++++++++ .../header_area/blob_overflow_menu_spec.js | 161 ++--------------- spec/frontend/repository/mock_data.js | 2 + 6 files changed, 349 insertions(+), 235 deletions(-) create mode 100644 app/assets/javascripts/repository/components/header_area/blob_default_actions_group.vue create mode 100644 spec/frontend/repository/components/header_area/blob_default_actions_group_spec.js 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 new file mode 100644 index 00000000000000..9801351f10a617 --- /dev/null +++ b/app/assets/javascripts/repository/components/header_area/blob_default_actions_group.vue @@ -0,0 +1,164 @@ + + + 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 04294959240abe..6c95b78315bb47 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,22 +1,19 @@