From 4a71f0f2403a9d7317af7db9ec4e8b4af9015f95 Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Thu, 6 Mar 2025 14:58:02 +0100 Subject: [PATCH 1/8] Add web ide link to blob controls --- .../blob/components/blob_header.vue | 2 +- .../repository/components/header_area.vue | 7 +- .../components/header_area/blob_controls.vue | 62 ++- .../header_area/blob_overflow_menu.vue | 7 +- .../queries/blob_controls.query.graphql | 11 + .../queries/user_gitpod_info.query.graphql | 9 + ee/spec/frontend/repository/mock_data.js | 13 + .../header_area/blob_controls_spec.js | 374 ++++++++++-------- spec/frontend/repository/mock_data.js | 13 + 9 files changed, 319 insertions(+), 179 deletions(-) create mode 100644 app/assets/javascripts/repository/queries/user_gitpod_info.query.graphql diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue index 9793c5854651df..dcec3df0d4a0cd 100644 --- a/app/assets/javascripts/blob/components/blob_header.vue +++ b/app/assets/javascripts/blob/components/blob_header.vue @@ -178,7 +178,7 @@ export default { - + 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 4c704193e1830b..70eee7da490ec7 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_controls.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_controls.vue @@ -21,7 +21,12 @@ import { sanitize } from '~/lib/dompurify'; import { InternalEvents } from '~/tracking'; import { FIND_FILE_BUTTON_CLICK } from '~/tracking/constants'; import { updateElementsVisibility } from '~/repository/utils/dom'; +import { + showSingleFileEditorForkSuggestion, + showWebIdeForkSuggestion, +} from '~/repository/utils/fork_suggestion_utils'; import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql'; +import userGitpodInfo from '~/repository/queries/user_gitpod_info.query.graphql'; import { getRefType } from '~/repository/utils/ref_type'; import OpenMrBadge from '~/repository/components/header_area/open_mr_badge.vue'; import { TEXT_FILE_TYPE, DEFAULT_BLOB_INFO } from '../../constants'; @@ -40,6 +45,7 @@ export default { OpenMrBadge, GlButton, OverflowMenu, + WebIdeLink: () => import('ee_else_ce/vue_shared/components/web_ide_link.vue'), }, directives: { GlTooltip: GlTooltipDirective, @@ -63,8 +69,14 @@ export default { createAlert({ message: this.$options.i18n.errorMessage }); }, }, + currentUser: { + query: userGitpodInfo, + error() { + createAlert({ message: this.$options.i18n.errorMessage }); + }, + }, }, - inject: ['currentRef'], + inject: ['currentRef', 'gitpodEnabled'], provide() { return { blobInfo: computed(() => this.blobInfo ?? DEFAULT_BLOB_INFO.repository.blobs.nodes[0]), @@ -76,6 +88,10 @@ export default { type: String, required: true, }, + projectIdAsNumber: { + type: Number, + required: true, + }, refType: { type: String, required: false, @@ -90,6 +106,7 @@ export default { data() { return { project: {}, + currentUser: {}, }; }, computed: { @@ -105,6 +122,9 @@ export default { blobInfo() { return this.project?.repository?.blobs?.nodes[0] || {}; }, + userPermissions() { + return this.project?.userPermissions || DEFAULT_BLOB_INFO.userPermissions; + }, storageInfo() { const { storedExternally, externalStorage } = this.blobInfo; return { @@ -153,6 +173,23 @@ export default { const description = this.$options.i18n.permalinkTooltip; return this.formatTooltipWithShortcut(description, this.shortcuts.permalink); }, + showWebIdeLink() { + return !this.blobInfo.archived && this.blobInfo.editBlobPath; + }, + shouldShowSingleFileEditorForkSuggestion() { + return showSingleFileEditorForkSuggestion( + this.userPermissions, + this.isUsingLfs, + this.blobInfo.canModifyBlob, + ); + }, + shouldShowWebIdeForkSuggestion() { + return showWebIdeForkSuggestion( + this.userPermissions, + this.isUsingLfs, + this.blobInfo.canModifyBlobWithWebIde, + ); + }, }, watch: { showBlobControls(shouldShow) { @@ -230,8 +267,31 @@ export default { {{ $options.i18n.permalink }} + + diff --git a/app/assets/javascripts/repository/queries/blob_controls.query.graphql b/app/assets/javascripts/repository/queries/blob_controls.query.graphql index 2be31da1e0eeaa..b18e01e60b2c99 100644 --- a/app/assets/javascripts/repository/queries/blob_controls.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_controls.query.graphql @@ -2,6 +2,13 @@ query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!, $ref project(fullPath: $projectPath) { __typename id + userPermissions { + __typename + pushCode + downloadCode + createMergeRequestIn + forkProject + } repository { __typename empty @@ -27,6 +34,10 @@ query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!, $ref canModifyBlob canModifyBlobWithWebIde forkAndViewPath + editBlobPath + ideEditPath + pipelineEditorPath + gitpodBlobUrl simpleViewer { __typename fileType diff --git a/app/assets/javascripts/repository/queries/user_gitpod_info.query.graphql b/app/assets/javascripts/repository/queries/user_gitpod_info.query.graphql new file mode 100644 index 00000000000000..efcf4c2f9c4cc5 --- /dev/null +++ b/app/assets/javascripts/repository/queries/user_gitpod_info.query.graphql @@ -0,0 +1,9 @@ +query getUserGitpodInfo { + currentUser { + typename + id + gitpodEnabled + preferencesGitpodPath + profileEnableGitpodPath + } +} diff --git a/ee/spec/frontend/repository/mock_data.js b/ee/spec/frontend/repository/mock_data.js index 8a5b1a9ac8a183..ebb574088b7015 100644 --- a/ee/spec/frontend/repository/mock_data.js +++ b/ee/spec/frontend/repository/mock_data.js @@ -201,6 +201,7 @@ export const lockPathMutationMock = { export const blobControlsDataMock = { __typename: 'Project', id: '1234', + userPermissions: userPermissionsMock, repository: { __typename: 'Repository', empty: false, @@ -227,6 +228,10 @@ export const blobControlsDataMock = { canModifyBlob: true, canModifyBlobWithWebIde: true, forkAndViewPath: 'fork/view/path', + editBlobPath: 'edit/blob/path/file.js', + ideEditPath: 'ide/blob/path/file.js', + pipelineEditorPath: 'pipeline/editor/path/file.yml', + gitpodBlobUrl: 'gitpod/blob/url/file.js', simpleViewer: { __typename: 'BlobViewer', collapsed: false, @@ -250,3 +255,11 @@ export const blobControlsDataMock = { }, }, }; + +export const currentUserDataMock = { + __typename: 'User', + id: '1234', + gitpodEnabled: true, + preferencesGitpodPath: 'preferences/gitpod/path', + profileEnableGitpodPath: 'profile/enable/gitpod/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 dbf76ef42e55c1..31bca29cde2538 100644 --- a/spec/frontend/repository/components/header_area/blob_controls_spec.js +++ b/spec/frontend/repository/components/header_area/blob_controls_spec.js @@ -4,6 +4,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import BlobControls from '~/repository/components/header_area/blob_controls.vue'; import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql'; +import userGitpodInfo from '~/repository/queries/user_gitpod_info.query.graphql'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; import createRouter from '~/repository/router'; @@ -15,232 +16,257 @@ import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; import OverflowMenu from '~/repository/components/header_area/blob_overflow_menu.vue'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import OpenMrBadge from '~/repository/components/header_area/open_mr_badge.vue'; -import { blobControlsDataMock, refMock } from '../../mock_data'; +import { blobControlsDataMock, refMock, currentUserDataMock } from '../../mock_data'; +Vue.use(VueApollo); jest.mock('~/repository/utils/dom'); jest.mock('~/behaviors/shortcuts/shortcuts_blob'); jest.mock('~/blob/blob_line_permalink_updater'); -let router; -let wrapper; -let mockResolver; - -const createComponent = async ({ - props = {}, - blobInfoOverrides = {}, - glFeatures = { blobOverflowMenu: false }, - routerOverride = {}, -} = {}) => { - Vue.use(VueApollo); - - const projectPath = 'some/project'; - router = createRouter(projectPath, refMock); - - await router.push({ - name: 'blobPathDecoded', - params: { path: '/some/file.js' }, - ...routerOverride, - }); +describe('Blob controls component', () => { + let router; + let wrapper; + let fakeApollo; + let blobControlsMockResolver; + let currentUserMockResolver; + + const createComponent = async ({ + props = {}, + blobInfoOverrides = {}, + glFeatures = { blobOverflowMenu: false }, + routerOverride = {}, + } = {}) => { + Vue.use(VueApollo); + + const projectPath = 'some/project'; + router = createRouter(projectPath, refMock); + + await router.push({ + name: 'blobPathDecoded', + params: { path: '/some/file.js' }, + ...routerOverride, + }); - mockResolver = jest.fn().mockResolvedValue({ - data: { - project: { - __typename: 'Project', - id: '1234', - repository: { - __typename: 'Repository', - empty: blobControlsDataMock.repository.empty, - blobs: { - __typename: 'RepositoryBlobConnection', - nodes: [{ ...blobControlsDataMock.repository.blobs.nodes[0], ...blobInfoOverrides }], + blobControlsMockResolver = jest.fn().mockResolvedValue({ + data: { + project: { + ...blobControlsDataMock, + repository: { + ...blobControlsDataMock.repository, + blobs: { + ...blobControlsDataMock.repository.blobs, + nodes: [{ ...blobControlsDataMock.repository.blobs.nodes[0], ...blobInfoOverrides }], + }, }, }, }, - }, - }); + }); - await resetShortcutsForTests(); - - wrapper = shallowMountExtended(BlobControls, { - router, - apolloProvider: createMockApollo([[blobControlsQuery, mockResolver]]), - provide: { - glFeatures, - currentRef: refMock, - }, - propsData: { - projectPath, - isBinary: false, - refType: 'heads', - ...props, - }, - mixins: [{ data: () => ({ ref: refMock }) }, glFeatureFlagMixin()], - }); + currentUserMockResolver = jest + .fn() + .mockResolvedValue({ data: { currentUser: currentUserDataMock } }); - await waitForPromises(); -}; + await resetShortcutsForTests(); -describe('Blob controls component', () => { - const findOpenMrBadge = () => wrapper.findComponent(OpenMrBadge); - const findFindButton = () => wrapper.findByTestId('find'); - const findBlameButton = () => wrapper.findByTestId('blame'); - const findPermalinkButton = () => wrapper.findByTestId('permalink'); - const findOverflowMenu = () => wrapper.findComponent(OverflowMenu); - const { bindInternalEventDocument } = useMockInternalEventsTracking(); - - beforeEach(async () => { - await createComponent(); - }); + fakeApollo = createMockApollo([ + [blobControlsQuery, blobControlsMockResolver], + [userGitpodInfo, currentUserMockResolver], + ]); - describe('MR badge', () => { - it('should render the baadge if `filter_blob_path` flag is on', async () => { - await createComponent({ glFeatures: { filterBlobPath: true } }); - expect(findOpenMrBadge().exists()).toBe(true); - expect(findOpenMrBadge().props('blobPath')).toBe('/some/file.js'); - expect(findOpenMrBadge().props('projectPath')).toBe('some/project'); + wrapper = shallowMountExtended(BlobControls, { + router, + apolloProvider: fakeApollo, + provide: { + glFeatures, + currentRef: refMock, + gitpodEnabled: true, + }, + propsData: { + projectPath, + projectIdAsNumber: 1, + isBinary: false, + refType: 'heads', + ...props, + }, + mixins: [{ data: () => ({ ref: refMock }) }, glFeatureFlagMixin()], }); - it('should not render the baadge if `filter_blob_path` flag is off', async () => { - await createComponent({ glFeatures: { filterBlobPath: false } }); - expect(findOpenMrBadge().exists()).toBe(false); + await waitForPromises(); + }; + + describe('Blob controls component', () => { + const findOpenMrBadge = () => wrapper.findComponent(OpenMrBadge); + const findFindButton = () => wrapper.findByTestId('find'); + const findBlameButton = () => wrapper.findByTestId('blame'); + const findPermalinkButton = () => wrapper.findByTestId('permalink'); + const findOverflowMenu = () => wrapper.findComponent(OverflowMenu); + const { bindInternalEventDocument } = useMockInternalEventsTracking(); + + beforeEach(async () => { + await createComponent(); }); - }); - describe('showBlobControls', () => { - it('should not render blob controls when filePath does not exist', async () => { - await createComponent({ - routerOverride: { name: 'blobPathDecoded', params: null }, - }); - expect(wrapper.element).not.toBeVisible(); + afterEach(() => { + fakeApollo = null; }); - it('should not render blob controls when route name is not blobPathDecoded', async () => { - await createComponent({ - routerOverride: { name: 'blobPath', params: { path: '/some/file.js' } }, + describe('MR badge', () => { + it('should render the badge if `filter_blob_path` flag is on', async () => { + await createComponent({ glFeatures: { filterBlobPath: true } }); + expect(findOpenMrBadge().exists()).toBe(true); + expect(findOpenMrBadge().props('blobPath')).toBe('/some/file.js'); + expect(findOpenMrBadge().props('projectPath')).toBe('some/project'); }); - expect(wrapper.element).not.toBeVisible(); - }); - }); - describe('FindFile button', () => { - it('renders FindFile button', () => { - expect(findFindButton().exists()).toBe(true); + it('should not render the badge if `filter_blob_path` flag is off', async () => { + await createComponent({ glFeatures: { filterBlobPath: false } }); + expect(findOpenMrBadge().exists()).toBe(false); + }); }); - it('triggers a `focusSearchFile` shortcut when the findFile button is clicked', () => { - const findFileButton = findFindButton(); - jest.spyOn(Shortcuts, 'focusSearchFile').mockResolvedValue(); - findFileButton.vm.$emit('click'); + describe('showBlobControls', () => { + it('should not render blob controls when filePath does not exist', async () => { + await createComponent({ + routerOverride: { name: 'blobPathDecoded', params: null }, + }); + expect(wrapper.element).not.toBeVisible(); + }); - expect(Shortcuts.focusSearchFile).toHaveBeenCalled(); + it('should not render blob controls when route name is not blobPathDecoded', async () => { + await createComponent({ + routerOverride: { name: 'blobPath', params: { path: '/some/file.js' } }, + }); + expect(wrapper.element).not.toBeVisible(); + }); }); - it('emits a tracking event when the Find file button is clicked', () => { - const { trackEventSpy } = bindInternalEventDocument(wrapper.element); - jest.spyOn(Shortcuts, 'focusSearchFile').mockResolvedValue(); + describe('FindFile button', () => { + it('renders FindFile button', () => { + expect(findFindButton().exists()).toBe(true); + }); - findFindButton().vm.$emit('click'); + it('triggers a `focusSearchFile` shortcut when the findFile button is clicked', () => { + const findFileButton = findFindButton(); + jest.spyOn(Shortcuts, 'focusSearchFile').mockResolvedValue(); + findFileButton.vm.$emit('click'); - expect(trackEventSpy).toHaveBeenCalledWith('click_find_file_button_on_repository_pages'); - }); - }); + expect(Shortcuts.focusSearchFile).toHaveBeenCalled(); + }); - describe('Blame button', () => { - it('renders a blame button with the correct href', () => { - expect(findBlameButton().attributes('href')).toBe('blame/file.js'); - }); + it('emits a tracking event when the Find file button is clicked', () => { + const { trackEventSpy } = bindInternalEventDocument(wrapper.element); + jest.spyOn(Shortcuts, 'focusSearchFile').mockResolvedValue(); - it('does not render blame button when blobInfo.storedExternally is true', async () => { - await createComponent({ blobInfoOverrides: { storedExternally: true } }); + findFindButton().vm.$emit('click'); - expect(findBlameButton().exists()).toBe(false); + expect(trackEventSpy).toHaveBeenCalledWith('click_find_file_button_on_repository_pages'); + }); }); - it('does not render blame button when blobInfo.externalStorage is "lfs"', async () => { - await createComponent({ - blobInfoOverrides: { storedExternally: true, externalStorage: 'lfs' }, + describe('Blame button', () => { + it('renders a blame button with the correct href', () => { + expect(findBlameButton().attributes('href')).toBe('blame/file.js'); }); - expect(findBlameButton().exists()).toBe(false); - }); + it('does not render blame button when blobInfo.storedExternally is true', async () => { + await createComponent({ blobInfoOverrides: { storedExternally: true } }); + + expect(findBlameButton().exists()).toBe(false); + }); - it('renders blame button when blobInfo.storedExternally is false and externalStorage is not "lfs"', async () => { - await createComponent({}, { storedExternally: false, externalStorage: null }); + it('does not render blame button when blobInfo.externalStorage is "lfs"', async () => { + await createComponent({ + blobInfoOverrides: { storedExternally: true, externalStorage: 'lfs' }, + }); - expect(findBlameButton().exists()).toBe(true); + expect(findBlameButton().exists()).toBe(false); + }); + + it('renders blame button when blobInfo.storedExternally is false and externalStorage is not "lfs"', async () => { + await createComponent({}, { storedExternally: false, externalStorage: null }); + + expect(findBlameButton().exists()).toBe(true); + }); }); - }); - it('renders a permalink button with the correct href', () => { - expect(findPermalinkButton().attributes('href')).toBe('permalink/file.js'); - }); + it('renders a permalink button with the correct href', () => { + expect(findPermalinkButton().attributes('href')).toBe('permalink/file.js'); + }); - it.each` - name | path - ${'blobPathDecoded'} | ${null} - ${'treePathDecoded'} | ${'myFile.js'} - `( - 'does not render any buttons if router name is $name and router path is $path', - async ({ name, path }) => { - await router.replace({ name, params: { path } }); - - await nextTick(); - - expect(findFindButton().exists()).toBe(false); - expect(findBlameButton().exists()).toBe(false); - expect(findPermalinkButton().exists()).toBe(false); - expect(updateElementsVisibility).toHaveBeenCalledWith('.tree-controls', true); - }, - ); - - it('loads the ShortcutsBlob', () => { - expect(ShortcutsBlob).toHaveBeenCalled(); - }); + it.each` + name | path + ${'blobPathDecoded'} | ${null} + ${'treePathDecoded'} | ${'myFile.js'} + `( + 'does not render any buttons if router name is $name and router path is $path', + async ({ name, path }) => { + await router.replace({ name, params: { path } }); + + await nextTick(); + + expect(findFindButton().exists()).toBe(false); + expect(findBlameButton().exists()).toBe(false); + expect(findPermalinkButton().exists()).toBe(false); + expect(updateElementsVisibility).toHaveBeenCalledWith('.tree-controls', true); + }, + ); - it('loads the BlobLinePermalinkUpdater', () => { - expect(BlobLinePermalinkUpdater).toHaveBeenCalled(); - }); + it('loads the ShortcutsBlob', () => { + expect(ShortcutsBlob).toHaveBeenCalled(); + }); - describe('BlobOverflow dropdown', () => { - beforeEach(async () => { - await createComponent({ glFeatures: { blobOverflowMenu: true } }); + it('loads the BlobLinePermalinkUpdater', () => { + expect(BlobLinePermalinkUpdater).toHaveBeenCalled(); }); - it('renders BlobOverflow component with correct props', () => { - expect(findOverflowMenu().exists()).toBe(true); - expect(findOverflowMenu().props()).toEqual({ - projectPath: 'some/project', - isBinary: true, - overrideCopy: true, - isEmptyRepository: false, - isUsingLfs: false, + describe('BlobOverflow dropdown', () => { + beforeEach(async () => { + await createComponent({ glFeatures: { blobOverflowMenu: true } }); }); - }); - it('passes the correct isBinary value to BlobOverflow when viewing a binary file', async () => { - await createComponent({ - props: { + it('renders BlobOverflow component with correct props', () => { + expect(findOverflowMenu().exists()).toBe(true); + expect(findOverflowMenu().props()).toEqual({ + projectPath: 'some/project', isBinary: true, - }, - blobInfoOverrides: { - simpleViewer: { - ...blobControlsDataMock.repository.blobs.nodes[0].simpleViewer, - fileType: 'podfile', + overrideCopy: true, + isEmptyRepository: false, + isUsingLfs: false, + userPermissions: { + __typename: 'ProjectPermissions', + createMergeRequestIn: true, + downloadCode: true, + forkProject: true, + pushCode: true, }, - }, - glFeatures: { - blobOverflowMenu: true, - }, + }); }); - expect(findOverflowMenu().props('isBinary')).toBe(true); - }); + it('passes the correct isBinary value to BlobOverflow when viewing a binary file', async () => { + await createComponent({ + props: { + isBinary: true, + }, + blobInfoOverrides: { + simpleViewer: { + ...blobControlsDataMock.repository.blobs.nodes[0].simpleViewer, + fileType: 'podfile', + }, + }, + glFeatures: { + blobOverflowMenu: true, + }, + }); - it('copies to clipboard raw blob text, when receives copy event', () => { - jest.spyOn(navigator.clipboard, 'writeText'); - findOverflowMenu().vm.$emit('copy'); + expect(findOverflowMenu().props('isBinary')).toBe(true); + }); + + it('copies to clipboard raw blob text, when receives copy event', () => { + jest.spyOn(navigator.clipboard, 'writeText'); + findOverflowMenu().vm.$emit('copy'); - expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Example raw text content'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Example raw text content'); + }); }); }); }); diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js index bbc18d904b4cb3..b34a7b2e0e34f0 100644 --- a/spec/frontend/repository/mock_data.js +++ b/spec/frontend/repository/mock_data.js @@ -91,6 +91,7 @@ export const encodedRefWithSpecialCharMock = 'feat/selected-%23-ref-%23'; export const blobControlsDataMock = { __typename: 'Project', id: '1234', + userPermissions: userPermissionsMock, repository: { __typename: 'Repository', empty: false, @@ -117,6 +118,10 @@ export const blobControlsDataMock = { canModifyBlob: true, canModifyBlobWithWebIde: true, forkAndViewPath: 'fork/view/path', + editBlobPath: 'edit/blob/path/file.js', + ideEditPath: 'ide/blob/path/file.js', + pipelineEditorPath: 'pipeline/editor/path/file.yml', + gitpodBlobUrl: 'gitpod/blob/url/file.js', simpleViewer: { __typename: 'BlobViewer', collapsed: false, @@ -280,3 +285,11 @@ export const headerAppInjected = { }; export const FILE_SIZE_3MB = 3000000; + +export const currentUserDataMock = { + __typename: 'User', + id: '1234', + gitpodEnabled: true, + preferencesGitpodPath: 'preferences/gitpod/path', + profileEnableGitpodPath: 'profile/enable/gitpod/path', +}; -- GitLab From 4fa2bdb5817079d8fb727aa7715efee89942ce7e Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Fri, 7 Mar 2025 17:26:14 +0100 Subject: [PATCH 2/8] DefaultActions layout improvements DefaultActions in blob content viewer are shown on layouts larger than sm. On sm layout, default actions are show as a blob overflow menu items. --- .../blob/components/blob_header.vue | 2 +- .../blob_header_default_actions.vue | 7 +- .../components/header_area/blob_controls.vue | 2 +- .../blob_default_actions_group.vue | 2 +- .../queries/user_gitpod_info.query.graphql | 2 +- .../blob_header_default_actions_spec.js | 18 +- .../blob/components/blob_header_spec.js | 148 ++++++++----- .../header_area/blob_controls_spec.js | 206 +++++++++++------- 8 files changed, 245 insertions(+), 142 deletions(-) diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue index dcec3df0d4a0cd..e5796447c5391c 100644 --- a/app/assets/javascripts/blob/components/blob_header.vue +++ b/app/assets/javascripts/blob/components/blob_header.vue @@ -203,7 +203,7 @@ export default {