From 59a3931fad12d194fc3d05bda4e37efb4a8f0253 Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Thu, 29 May 2025 18:28:12 +0200 Subject: [PATCH 1/8] Disable the edit button, instead of not rendering it Adds tooltips with help texts for each scenario where previously Edit button was not rendered. --- .../components/header_area/blob_controls.vue | 58 ++++++++++++++++--- .../vue_shared/components/web_ide_link.vue | 7 ++- locale/gitlab.pot | 18 ++++++ 3 files changed, 75 insertions(+), 8 deletions(-) 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 1b1804a5cfa892..11bcc3072a9700 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_controls.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_controls.vue @@ -36,6 +36,12 @@ export default { findFile: __('Find file'), blame: __('Blame'), errorMessage: __('An error occurred while loading the blob controls.'), + archivedProjectTooltip: __('You cannot edit files in an archived project'), + lfsFileTooltip: __('This file is stored in LFS and cannot be edited in the Web IDE'), + noPermissionTooltip: __('You do not have permission to edit this file'), + notLatestCommitTooltip: __('You must be viewing the latest commit to edit this file'), + readOnlyTooltip: __('This repository is in a read-only state'), + binaryFileTooltip: __('Binary files cannot be edited in the Web IDE'), }, buttonClassList: 'sm:gl-w-auto gl-w-full sm:gl-mt-0 gl-mt-3', components: { @@ -97,7 +103,7 @@ export default { }, }, }, - inject: ['currentRef'], + inject: ['currentRef', 'rootRef'], provide() { return { blobInfo: computed(() => this.blobInfo ?? DEFAULT_BLOB_INFO.repository.blobs.nodes[0]), @@ -175,9 +181,6 @@ export default { return this.formatTooltipWithShortcut(description, shortcutKey); }, - showWebIdeLink() { - return !this.blobInfo.archived && this.blobInfo.editBlobPath; - }, shouldShowSingleFileEditorForkSuggestion() { return showSingleFileEditorForkSuggestion( this.userPermissions, @@ -192,6 +195,48 @@ export default { this.blobInfo.canModifyBlobWithWebIde, ); }, + isWebIdeDisabled() { + return Object.values(this.webIdeDisabledReasons).some(({ condition }) => condition()); + }, + webIdeDisabledTooltip() { + const disabledReason = Object.values(this.webIdeDisabledReasons).find( + (reason) => reason.condition() ?? reason, + ); + + return disabledReason ? disabledReason.message : ''; + }, + webIdeDisabledReasons() { + return { + archived: { + condition: () => this.blobInfo.archived, + message: this.$options.i18n.archivedProjectTooltip, + }, + lfs: { + condition: () => this.isUsingLfs, + message: this.$options.i18n.lfsFileTooltip, + }, + noPermission: { + condition: () => !this.blobInfo.canModifyBlob && !this.userPermissions.pushCode, + message: this.$options.i18n.noPermissionTooltip, + }, + notLatestCommit: { + condition: () => this.currentRef !== this.rootRef, + message: this.$options.i18n.notLatestCommitTooltip, + }, + readOnly: { + condition: () => this.blobInfo.repository && this.blobInfo.repository.readOnly, + message: this.$options.i18n.readOnlyTooltip, + }, + binaryFile: { + condition: () => this.isBinaryFileType, + message: this.$options.i18n.binaryFileTooltip, + }, + queryErrors: { + condition: () => this.hasProjectQueryErrors, + message: this.$options.i18n.errorMessage, + }, + }; + }, }, watch: { blobInfo() { @@ -267,8 +312,6 @@ export default { Date: Thu, 5 Jun 2025 15:14:42 +0200 Subject: [PATCH 2/8] Add missing stories for WebIdeLink component --- .../components/web_ide_link.stories.js | 133 +++++++++++++++--- 1 file changed, 114 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.stories.js b/app/assets/javascripts/vue_shared/components/web_ide_link.stories.js index 1dfd44c312a102..1b5ff142b3c6b9 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.stories.js +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.stories.js @@ -1,3 +1,6 @@ +import getSomeWritableForksResponse from 'test_fixtures/graphql/vue_shared/components/web_ide/get_writable_forks.query.graphql_some.json'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import getWritableForksQuery from '~/vue_shared/components/web_ide/get_writable_forks.query.graphql'; import WebIdeLink from './web_ide_link.vue'; export default { @@ -5,18 +8,26 @@ export default { title: 'vue_shared/web_ide_link', }; -const Template = (args, { argTypes }) => ({ - components: { WebIdeLink }, - props: Object.keys(argTypes), - template: ` - - - `, -}); +const createTemplate = (config = {}) => { + let { apolloProvider } = config; -export const Default = Template.bind({}); -export const Blob = Template.bind({}); -export const WithButtonVariant = Template.bind({}); + if (apolloProvider == null) { + const requestHandlers = [ + [getWritableForksQuery, () => Promise.resolve(getSomeWritableForksResponse)], + ]; + apolloProvider = createMockApollo(requestHandlers); + } + + return (args, { argTypes }) => ({ + components: { WebIdeLink }, + apolloProvider, + props: Object.keys(argTypes), + template: ` + + + `, + }); +}; const defaultArgs = { isFork: false, @@ -28,18 +39,102 @@ const defaultArgs = { showPipelineEditorButton: true, disableForkModal: true, gitpodUrl: 'http://example.com', + editUrl: '/edit', + webIdeUrl: '/ide', + pipelineEditorUrl: '/pipeline-editor', +}; + +export const Default = { + render: createTemplate(), + args: defaultArgs, +}; + +export const Blob = { + render: createTemplate(), + args: { + ...defaultArgs, + isBlob: true, + }, +}; + +export const WithButtonVariant = { + render: createTemplate(), + args: { + ...defaultArgs, + buttonVariant: 'confirm', + }, }; -Default.args = { - ...defaultArgs, +export const Fork = { + render: createTemplate(), + args: { + ...defaultArgs, + isFork: true, + }, }; -Blob.args = { - ...defaultArgs, - isBlob: true, +export const NeedsToFork = { + render: createTemplate(), + args: { + ...defaultArgs, + needsToFork: true, + needsToForkWithWebIde: true, + disableForkModal: false, + forkPath: '/fork', + forkModalId: 'fork-modal', + isGitpodEnabledForUser: false, + showPipelineEditorButton: false, + }, }; -WithButtonVariant.args = { - ...defaultArgs, - buttonVariant: 'confirm', +export const WithCustomText = { + render: createTemplate(), + args: { + ...defaultArgs, + webIdeText: 'Custom Web IDE Text', + gitpodText: 'Custom Gitpod Text', + }, +}; + +export const Disabled = { + render: createTemplate(), + args: { + ...defaultArgs, + disabled: true, + }, +}; + +export const CustomTooltipText = { + render: createTemplate(), + args: { + ...defaultArgs, + disabled: true, + customTooltipText: 'This repository is in a read-only state', + }, +}; + +export const WithSlots = { + render: (args, { argTypes }) => { + const requestHandlers = [ + [getWritableForksQuery, () => Promise.resolve(getSomeWritableForksResponse)], + ]; + const apolloProvider = createMockApollo(requestHandlers); + + return { + components: { WebIdeLink }, + apolloProvider, + props: Object.keys(argTypes), + template: ` + + + + + `, + }; + }, + args: defaultArgs, }; -- GitLab From 2948a7fbe020f9535490d49ad84b77474cb71298 Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Thu, 5 Jun 2025 18:00:54 +0200 Subject: [PATCH 3/8] Add test cases for changed components --- .../components/header_area/blob_controls.vue | 12 +- locale/gitlab.pot | 4 +- .../header_area/blob_controls_spec.js | 104 ++++++++++++------ .../components/web_ide_link_spec.js | 23 ++++ 4 files changed, 99 insertions(+), 44 deletions(-) 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 11bcc3072a9700..1acd9ad2be6ef3 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_controls.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_controls.vue @@ -37,11 +37,11 @@ export default { blame: __('Blame'), errorMessage: __('An error occurred while loading the blob controls.'), archivedProjectTooltip: __('You cannot edit files in an archived project'), - lfsFileTooltip: __('This file is stored in LFS and cannot be edited in the Web IDE'), + lfsFileTooltip: __('This file is stored in LFS and cannot be edited'), noPermissionTooltip: __('You do not have permission to edit this file'), notLatestCommitTooltip: __('You must be viewing the latest commit to edit this file'), readOnlyTooltip: __('This repository is in a read-only state'), - binaryFileTooltip: __('Binary files cannot be edited in the Web IDE'), + binaryFileTooltip: __('Binary files cannot be edited'), }, buttonClassList: 'sm:gl-w-auto gl-w-full sm:gl-mt-0 gl-mt-3', components: { @@ -223,10 +223,10 @@ export default { condition: () => this.currentRef !== this.rootRef, message: this.$options.i18n.notLatestCommitTooltip, }, - readOnly: { - condition: () => this.blobInfo.repository && this.blobInfo.repository.readOnly, - message: this.$options.i18n.readOnlyTooltip, - }, + // readOnly: { + // condition: () => this.blobInfo.repository && this.blobInfo.repository.readOnly, + // message: this.$options.i18n.readOnlyTooltip, + // }, binaryFile: { condition: () => this.isBinaryFileType, message: this.$options.i18n.binaryFileTooltip, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ee859a7f808b00..77678c3a84c789 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10265,7 +10265,7 @@ msgstr "" msgid "Billing|Your group recently changed to use the Free plan. %{over_limit_message} You can free up space for new members by removing those who no longer need access or toggling them to over-limit. To get an unlimited number of members, you can %{link_start}upgrade%{link_end} to a paid tier." msgstr "" -msgid "Binary files cannot be edited in the Web IDE" +msgid "Binary files cannot be edited" msgstr "" msgid "Bitbucket Server Import" @@ -62938,7 +62938,7 @@ msgstr "" msgid "This field is required." msgstr "" -msgid "This file is stored in LFS and cannot be edited in the Web IDE" +msgid "This file is stored in LFS and cannot be edited" msgstr "" msgid "This file was linked in the page URL and will appear as the first one in the list" 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 8ea3920ca5608e..471ed3ac8a41aa 100644 --- a/spec/frontend/repository/components/header_area/blob_controls_spec.js +++ b/spec/frontend/repository/components/header_area/blob_controls_spec.js @@ -113,6 +113,7 @@ describe('Blob controls component', () => { provide: { glFeatures, currentRef: refMock, + rootRef: refMock, }, directives: { GlTooltip: createMockDirective('gl-tooltip'), @@ -253,9 +254,17 @@ describe('Blob controls component', () => { }); describe('WebIdeLink component', () => { - it('renders the WebIdeLink component with the correct props', () => { + it('renders the WebIdeLink component with the correct props', async () => { + const blobOverwriteResolver = overrideBlobControlsResolver({ + simpleViewer: { + ...blobControlsDataMock.repository.blobs.nodes[0].simpleViewer, + fileType: 'text', + }, + }); + await createComponent({ + blobControlsResolver: blobOverwriteResolver, + }); expect(findWebIdeLink().props()).toMatchObject({ - showEditButton: false, editUrl: 'https://edit/blob/path/file.js', webIdeUrl: 'https://ide/blob/path/file.js', needsToFork: false, @@ -266,43 +275,66 @@ describe('Blob controls component', () => { isGitpodEnabledForInstance: true, isGitpodEnabledForUser: true, disabled: false, + customTooltipText: '', }); }); - it('disables the WebIdeLink component when file is LFS', async () => { - const blobOverwriteResolver = overrideBlobControlsResolver({ - storedExternally: true, - externalStorage: 'lfs', - }); - await createComponent({ - blobControlsResolver: blobOverwriteResolver, - }); - expect(findWebIdeLink().props('disabled')).toBe(true); - }); - - it('does not render WebIdeLink component if file is archived', async () => { - const blobOverwriteResolver = overrideBlobControlsResolver({ - ...blobControlsDataMock.repository.blobs.nodes[0], - archived: true, - }); - await createComponent({ - blobControlsResolver: blobOverwriteResolver, - }); - - expect(findWebIdeLink().exists()).toBe(false); - }); - - it('does not render WebIdeLink component if file is not editable', async () => { - const blobOverwriteResolver = overrideBlobControlsResolver({ - ...blobControlsDataMock.repository.blobs.nodes[0], - editBlobPath: '', - }); - await createComponent({ - blobControlsResolver: blobOverwriteResolver, - }); - - expect(findWebIdeLink().exists()).toBe(false); - }); + describe.each` + description | overrides | expectedTooltip | shouldMockError | hasOverwritePermissions + ${'file is archived'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], archived: true }} | ${'You cannot edit files in an archived project'} | ${false} | ${false} + ${'file is LFS'} | ${{ storedExternally: true, externalStorage: 'lfs' }} | ${'This file is stored in LFS and cannot be edited'} | ${false} | ${false} + ${'user does not have permissions to edit'} | ${{ canModifyBlob: false, pushCode: false }} | ${'You do not have permission to edit this file'} | ${false} | ${true} + ${'ref is not on latest commit'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], editBlobPath: '' }} | ${'You must be viewing the latest commit to edit this file'} | ${false} | ${false} + ${'file is not editable'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], editBlobPath: '' }} | ${'Binary files cannot be edited'} | ${false} | ${false} + ${'project query has errors'} | ${{}} | ${'An error occurred while loading the blob controls'} | ${true} | ${false} + `( + 'when $description', + ({ shouldMockError, hasOverwritePermissions, overrides, expectedTooltip }) => { + it('disables the WebIdeLink component', async () => { + const customBlobControlsResolver = (() => { + if (shouldMockError) { + return jest.fn().mockResolvedValue(blobControlsErrorResolver); + } + if (hasOverwritePermissions) { + return jest.fn().mockResolvedValue({ + data: { + project: { + ...blobControlsDataMock, + userPermissions: { + ...blobControlsDataMock.userPermissions, + ...overrides, + }, + repository: { + ...blobControlsDataMock.repository, + blobs: { + ...blobControlsDataMock.repository.blobs, + nodes: [ + { + ...blobControlsDataMock.repository.blobs.nodes[0], + canModifyBlob: false, + simpleViewer: { + ...blobControlsDataMock.repository.blobs.nodes[0].simpleViewer, + fileType: 'text', + }, + }, + ], + }, + }, + }, + }, + }); + } + return overrideBlobControlsResolver(overrides); + })(); + await createComponent({ + blobControlsResolver: customBlobControlsResolver, + }); + + expect(findWebIdeLink().props('disabled')).toBe(true); + expect(findWebIdeLink().props('customTooltipText')).toBe(expectedTooltip); + }); + }, + ); describe('when can modify blob', () => { it('redirects to WebIDE to edit the file', async () => { diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index 0d408908f00858..1f409cb3444303 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -398,4 +398,27 @@ describe('vue_shared/components/web_ide_link', () => { }); }); }); + + describe('disabled state', () => { + it('renders default tooltip', () => { + createComponent({ disabled: true }); + + expect(findDisclosureDropdown().props('disabled')).toBe(true); + expect(findDisclosureDropdown().attributes('aria-label')).toBe( + 'Editing this file is not supported', + ); + }); + + it('renders custom tooltip', () => { + createComponent({ + disabled: true, + customTooltipText: 'This repository is in a read-only state', + }); + + expect(findDisclosureDropdown().props('disabled')).toBe(true); + expect(findDisclosureDropdown().attributes('aria-label')).toBe( + 'This repository is in a read-only state', + ); + }); + }); }); -- GitLab From 97ff0255e49ccc52e9d57d7d0994f615a7eac875 Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Fri, 6 Jun 2025 13:15:47 +0200 Subject: [PATCH 4/8] Remove already covered scenarios Edit limitations from permissions, non-current ref and no edit path are already covered with fork suggestion logic for both types of editors. Changelog: changed --- .../components/header_area/blob_controls.vue | 25 ++---- locale/gitlab.pot | 9 -- .../header_area/blob_controls_spec.js | 82 ++++++------------- 3 files changed, 32 insertions(+), 84 deletions(-) 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 1acd9ad2be6ef3..7d98d9adf920c6 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_controls.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_controls.vue @@ -38,9 +38,6 @@ export default { errorMessage: __('An error occurred while loading the blob controls.'), archivedProjectTooltip: __('You cannot edit files in an archived project'), lfsFileTooltip: __('This file is stored in LFS and cannot be edited'), - noPermissionTooltip: __('You do not have permission to edit this file'), - notLatestCommitTooltip: __('You must be viewing the latest commit to edit this file'), - readOnlyTooltip: __('This repository is in a read-only state'), binaryFileTooltip: __('Binary files cannot be edited'), }, buttonClassList: 'sm:gl-w-auto gl-w-full sm:gl-mt-0 gl-mt-3', @@ -103,7 +100,7 @@ export default { }, }, }, - inject: ['currentRef', 'rootRef'], + inject: ['currentRef'], provide() { return { blobInfo: computed(() => this.blobInfo ?? DEFAULT_BLOB_INFO.repository.blobs.nodes[0]), @@ -207,6 +204,10 @@ export default { }, webIdeDisabledReasons() { return { + queryErrors: { + condition: () => this.hasProjectQueryErrors, + message: this.$options.i18n.errorMessage, + }, archived: { condition: () => this.blobInfo.archived, message: this.$options.i18n.archivedProjectTooltip, @@ -215,26 +216,10 @@ export default { condition: () => this.isUsingLfs, message: this.$options.i18n.lfsFileTooltip, }, - noPermission: { - condition: () => !this.blobInfo.canModifyBlob && !this.userPermissions.pushCode, - message: this.$options.i18n.noPermissionTooltip, - }, - notLatestCommit: { - condition: () => this.currentRef !== this.rootRef, - message: this.$options.i18n.notLatestCommitTooltip, - }, - // readOnly: { - // condition: () => this.blobInfo.repository && this.blobInfo.repository.readOnly, - // message: this.$options.i18n.readOnlyTooltip, - // }, binaryFile: { condition: () => this.isBinaryFileType, message: this.$options.i18n.binaryFileTooltip, }, - queryErrors: { - condition: () => this.hasProjectQueryErrors, - message: this.$options.i18n.errorMessage, - }, }; }, }, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 77678c3a84c789..a40a3040aa60df 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -63360,9 +63360,6 @@ msgstr "" msgid "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch." msgstr "" -msgid "This repository is in a read-only state" -msgstr "" - msgid "This repository was last checked %{last_check_timestamp}. The check %{strong_start}failed.%{strong_end} See the 'repocheck.log' file for error messages." msgstr "" @@ -71437,9 +71434,6 @@ msgstr "" msgid "You do not have permission to approve a member" msgstr "" -msgid "You do not have permission to edit this file" -msgstr "" - msgid "You do not have permission to leave this %{namespaceType}." msgstr "" @@ -71653,9 +71647,6 @@ msgstr "" msgid "You must be authenticated to access this path." msgstr "" -msgid "You must be viewing the latest commit to edit this file" -msgstr "" - msgid "You must confirm your email within %{cut_off_days} days of signing up. If you do not confirm your email in this timeframe, your account will be deleted and you will need to sign up for GitLab again." msgstr "" 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 471ed3ac8a41aa..9e2b046c3cd562 100644 --- a/spec/frontend/repository/components/header_area/blob_controls_spec.js +++ b/spec/frontend/repository/components/header_area/blob_controls_spec.js @@ -113,7 +113,6 @@ describe('Blob controls component', () => { provide: { glFeatures, currentRef: refMock, - rootRef: refMock, }, directives: { GlTooltip: createMockDirective('gl-tooltip'), @@ -279,62 +278,35 @@ describe('Blob controls component', () => { }); }); + describe('when project query has errors', () => { + it('disables the WebIdeLink component with appropriate tooltip', async () => { + await createComponent({ blobControlsResolver: blobControlsErrorResolver }); + + expect(findWebIdeLink().props('disabled')).toBe(true); + expect(findWebIdeLink().props('customTooltipText')).toBe( + 'An error occurred while loading the blob controls.', + ); + }); + }); + describe.each` - description | overrides | expectedTooltip | shouldMockError | hasOverwritePermissions - ${'file is archived'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], archived: true }} | ${'You cannot edit files in an archived project'} | ${false} | ${false} - ${'file is LFS'} | ${{ storedExternally: true, externalStorage: 'lfs' }} | ${'This file is stored in LFS and cannot be edited'} | ${false} | ${false} - ${'user does not have permissions to edit'} | ${{ canModifyBlob: false, pushCode: false }} | ${'You do not have permission to edit this file'} | ${false} | ${true} - ${'ref is not on latest commit'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], editBlobPath: '' }} | ${'You must be viewing the latest commit to edit this file'} | ${false} | ${false} - ${'file is not editable'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], editBlobPath: '' }} | ${'Binary files cannot be edited'} | ${false} | ${false} - ${'project query has errors'} | ${{}} | ${'An error occurred while loading the blob controls'} | ${true} | ${false} - `( - 'when $description', - ({ shouldMockError, hasOverwritePermissions, overrides, expectedTooltip }) => { - it('disables the WebIdeLink component', async () => { - const customBlobControlsResolver = (() => { - if (shouldMockError) { - return jest.fn().mockResolvedValue(blobControlsErrorResolver); - } - if (hasOverwritePermissions) { - return jest.fn().mockResolvedValue({ - data: { - project: { - ...blobControlsDataMock, - userPermissions: { - ...blobControlsDataMock.userPermissions, - ...overrides, - }, - repository: { - ...blobControlsDataMock.repository, - blobs: { - ...blobControlsDataMock.repository.blobs, - nodes: [ - { - ...blobControlsDataMock.repository.blobs.nodes[0], - canModifyBlob: false, - simpleViewer: { - ...blobControlsDataMock.repository.blobs.nodes[0].simpleViewer, - fileType: 'text', - }, - }, - ], - }, - }, - }, - }, - }); - } - return overrideBlobControlsResolver(overrides); - })(); - await createComponent({ - blobControlsResolver: customBlobControlsResolver, - }); - - expect(findWebIdeLink().props('disabled')).toBe(true); - expect(findWebIdeLink().props('customTooltipText')).toBe(expectedTooltip); + description | overrides | expectedTooltip + ${'file is archived'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], archived: true }} | ${'You cannot edit files in an archived project'} + ${'file is LFS'} | ${{ storedExternally: true, externalStorage: 'lfs' }} | ${'This file is stored in LFS and cannot be edited'} + ${'file is binary file'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], editBlobPath: '' }} | ${'Binary files cannot be edited'} + `('when $description', ({ overrides, expectedTooltip }) => { + it('disables the WebIdeLink component with appropriate tooltip', async () => { + const customBlobControlsResolver = (() => { + return overrideBlobControlsResolver(overrides); + })(); + await createComponent({ + blobControlsResolver: customBlobControlsResolver, }); - }, - ); + + expect(findWebIdeLink().props('disabled')).toBe(true); + expect(findWebIdeLink().props('customTooltipText')).toBe(expectedTooltip); + }); + }); describe('when can modify blob', () => { it('redirects to WebIDE to edit the file', async () => { -- GitLab From 5a9907a70d1d9b99037d9b35850f020a93b7ac12 Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Mon, 9 Jun 2025 11:54:15 +0200 Subject: [PATCH 5/8] Do not disable Edit for binary file type Binary files can be edited with WebIDE so the Edit button should not be disabled. On triggering, it will show the option to edit with WebIDE, but single file editor will not be rendered in the dropdown. Changelog: fixed --- .../repository/components/header_area/blob_controls.vue | 6 +----- locale/gitlab.pot | 3 --- .../components/header_area/blob_controls_spec.js | 7 +++---- 3 files changed, 4 insertions(+), 12 deletions(-) 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 7d98d9adf920c6..b280d3ec3fee62 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_controls.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_controls.vue @@ -38,7 +38,6 @@ export default { errorMessage: __('An error occurred while loading the blob controls.'), archivedProjectTooltip: __('You cannot edit files in an archived project'), lfsFileTooltip: __('This file is stored in LFS and cannot be edited'), - binaryFileTooltip: __('Binary files cannot be edited'), }, buttonClassList: 'sm:gl-w-auto gl-w-full sm:gl-mt-0 gl-mt-3', components: { @@ -216,10 +215,6 @@ export default { condition: () => this.isUsingLfs, message: this.$options.i18n.lfsFileTooltip, }, - binaryFile: { - condition: () => this.isBinaryFileType, - message: this.$options.i18n.binaryFileTooltip, - }, }; }, }, @@ -298,6 +293,7 @@ export default { { }); describe.each` - description | overrides | expectedTooltip - ${'file is archived'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], archived: true }} | ${'You cannot edit files in an archived project'} - ${'file is LFS'} | ${{ storedExternally: true, externalStorage: 'lfs' }} | ${'This file is stored in LFS and cannot be edited'} - ${'file is binary file'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], editBlobPath: '' }} | ${'Binary files cannot be edited'} + description | overrides | expectedTooltip + ${'file is archived'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], archived: true }} | ${'You cannot edit files in an archived project'} + ${'file is LFS'} | ${{ storedExternally: true, externalStorage: 'lfs' }} | ${'This file is stored in LFS and cannot be edited'} `('when $description', ({ overrides, expectedTooltip }) => { it('disables the WebIdeLink component with appropriate tooltip', async () => { const customBlobControlsResolver = (() => { -- GitLab From a9fd30e2d2197d3d24b1f0ca70fe6f28e33860c6 Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Tue, 10 Jun 2025 13:34:38 +0200 Subject: [PATCH 6/8] Apply tech writer suggestions --- .../components/header_area/blob_controls.vue | 6 +++--- .../components/web_ide_link.stories.js | 2 +- .../vue_shared/components/web_ide_link.vue | 2 +- locale/gitlab.pot | 20 +++++++++---------- .../header_area/blob_controls_spec.js | 8 ++++---- .../components/web_ide_link_spec.js | 8 +++----- 6 files changed, 22 insertions(+), 24 deletions(-) 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 b280d3ec3fee62..05c28a8fd173ed 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_controls.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_controls.vue @@ -35,9 +35,9 @@ export default { i18n: { findFile: __('Find file'), blame: __('Blame'), - errorMessage: __('An error occurred while loading the blob controls.'), - archivedProjectTooltip: __('You cannot edit files in an archived project'), - lfsFileTooltip: __('This file is stored in LFS and cannot be edited'), + errorMessage: __('An error occurred while loading file controls. Refresh the page.'), + archivedProjectTooltip: __('You cannot edit files in archived projects'), + lfsFileTooltip: __('You cannot edit files stored in LFS'), }, buttonClassList: 'sm:gl-w-auto gl-w-full sm:gl-mt-0 gl-mt-3', components: { diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.stories.js b/app/assets/javascripts/vue_shared/components/web_ide_link.stories.js index 1b5ff142b3c6b9..15c4bba90541d3 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.stories.js +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.stories.js @@ -109,7 +109,7 @@ export const CustomTooltipText = { args: { ...defaultArgs, disabled: true, - customTooltipText: 'This repository is in a read-only state', + customTooltipText: 'You cannot edit files in read-only repositories', }, }; diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue index 96a2fc7cf8b4da..c8eaffe7900209 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -147,7 +147,7 @@ export default { customTooltipText: { type: String, required: false, - default: __('Editing this file is not supported'), + default: __('You cannot edit this file'), }, }, data() { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4344d8207af11f..a0fc23bed159c7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7197,6 +7197,9 @@ msgstr "" msgid "An error occurred while loading diff" msgstr "" +msgid "An error occurred while loading file controls. Refresh the page." +msgstr "" + msgid "An error occurred while loading filenames" msgstr "" @@ -7215,9 +7218,6 @@ msgstr "" msgid "An error occurred while loading projects." msgstr "" -msgid "An error occurred while loading the blob controls." -msgstr "" - msgid "An error occurred while loading the file" msgstr "" @@ -23892,9 +23892,6 @@ msgstr "" msgid "Editing" msgstr "" -msgid "Editing this file is not supported" -msgstr "" - msgid "Editor Extensions" msgstr "" @@ -62935,9 +62932,6 @@ msgstr "" msgid "This field is required." msgstr "" -msgid "This file is stored in LFS and cannot be edited" -msgstr "" - msgid "This file was linked in the page URL and will appear as the first one in the list" msgstr "" @@ -71362,7 +71356,13 @@ msgstr "" msgid "You cannot create projects in your personal namespace. Contact your GitLab administrator." msgstr "" -msgid "You cannot edit files in an archived project" +msgid "You cannot edit files in archived projects" +msgstr "" + +msgid "You cannot edit files stored in LFS" +msgstr "" + +msgid "You cannot edit this file" msgstr "" msgid "You cannot impersonate a blocked user" 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 08803175f1887d..ed34a9182493e7 100644 --- a/spec/frontend/repository/components/header_area/blob_controls_spec.js +++ b/spec/frontend/repository/components/header_area/blob_controls_spec.js @@ -284,15 +284,15 @@ describe('Blob controls component', () => { expect(findWebIdeLink().props('disabled')).toBe(true); expect(findWebIdeLink().props('customTooltipText')).toBe( - 'An error occurred while loading the blob controls.', + 'An error occurred while loading file controls. Refresh the page.', ); }); }); describe.each` description | overrides | expectedTooltip - ${'file is archived'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], archived: true }} | ${'You cannot edit files in an archived project'} - ${'file is LFS'} | ${{ storedExternally: true, externalStorage: 'lfs' }} | ${'This file is stored in LFS and cannot be edited'} + ${'file is archived'} | ${{ ...blobControlsDataMock.repository.blobs.nodes[0], archived: true }} | ${'You cannot edit files in archived projects'} + ${'file is LFS'} | ${{ storedExternally: true, externalStorage: 'lfs' }} | ${'You cannot edit files stored in LFS'} `('when $description', ({ overrides, expectedTooltip }) => { it('disables the WebIdeLink component with appropriate tooltip', async () => { const customBlobControlsResolver = (() => { @@ -443,7 +443,7 @@ describe('Blob controls component', () => { await createComponent(resolverParam); expect(createAlert).toHaveBeenCalledWith({ - message: 'An error occurred while loading the blob controls.', + message: 'An error occurred while loading file controls. Refresh the page.', }); expect(logError).toHaveBeenCalledWith(loggedError, mockError); expect(Sentry.captureException).toHaveBeenCalledWith(mockError); diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index 1f409cb3444303..d394e11263027a 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -404,20 +404,18 @@ describe('vue_shared/components/web_ide_link', () => { createComponent({ disabled: true }); expect(findDisclosureDropdown().props('disabled')).toBe(true); - expect(findDisclosureDropdown().attributes('aria-label')).toBe( - 'Editing this file is not supported', - ); + expect(findDisclosureDropdown().attributes('aria-label')).toBe('You cannot edit this file'); }); it('renders custom tooltip', () => { createComponent({ disabled: true, - customTooltipText: 'This repository is in a read-only state', + customTooltipText: 'You cannot edit files in read-only repositories', }); expect(findDisclosureDropdown().props('disabled')).toBe(true); expect(findDisclosureDropdown().attributes('aria-label')).toBe( - 'This repository is in a read-only state', + 'You cannot edit files in read-only repositories', ); }); }); -- GitLab From 8e34dd094b476294a7ee75627c0858635ca2e54c Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Tue, 10 Jun 2025 13:46:43 +0200 Subject: [PATCH 7/8] Simplify logic for disabling WebIdeLink --- .../components/header_area/blob_controls.vue | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 05c28a8fd173ed..624cd91b89260e 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_controls.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_controls.vue @@ -192,14 +192,16 @@ export default { ); }, isWebIdeDisabled() { - return Object.values(this.webIdeDisabledReasons).some(({ condition }) => condition()); + return Object.values(this.webIdeDisabledReasons).some( + ({ condition }) => condition() === true, + ); }, webIdeDisabledTooltip() { - const disabledReason = Object.values(this.webIdeDisabledReasons).find( - (reason) => reason.condition() ?? reason, + const disabledReason = Object.values(this.webIdeDisabledReasons).find((reason) => + reason.condition(), ); - return disabledReason ? disabledReason.message : ''; + return disabledReason?.message ?? ''; }, webIdeDisabledReasons() { return { -- GitLab From ae58fc59fadff2da8ae2434aad63540873caba06 Mon Sep 17 00:00:00 2001 From: psjakubowska Date: Tue, 10 Jun 2025 15:53:53 +0200 Subject: [PATCH 8/8] Adjust feature test to new UI --- spec/features/projects/files/user_edits_files_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index 1c7f33765ce99b..12755b244dd0bd 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -28,15 +28,14 @@ end shared_examples 'unavailable for an archived project' do - it 'does not show the edit link for an archived project', :js do + it 'shows disabled edit link for an archived project', :js do project.update!(archived: true) visit project_tree_path(project, project.repository.root_ref) click_link('.gitignore') aggregate_failures 'available edit buttons' do - expect(page).not_to have_text('Edit') - expect(page).not_to have_text('Web IDE') + expect(page).to have_button('Edit', disabled: true) within_testid('blob-controls') do click_button 'File actions' -- GitLab