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 1b1804a5cfa89271406f1144bac02409d9806d4e..624cd91b89260e5d272711b0d15fc9e5e4bed54d 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_controls.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_controls.vue @@ -35,7 +35,9 @@ export default { i18n: { findFile: __('Find file'), blame: __('Blame'), - errorMessage: __('An error occurred while loading the blob controls.'), + 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: { @@ -175,9 +177,6 @@ export default { return this.formatTooltipWithShortcut(description, shortcutKey); }, - showWebIdeLink() { - return !this.blobInfo.archived && this.blobInfo.editBlobPath; - }, shouldShowSingleFileEditorForkSuggestion() { return showSingleFileEditorForkSuggestion( this.userPermissions, @@ -192,6 +191,34 @@ export default { this.blobInfo.canModifyBlobWithWebIde, ); }, + isWebIdeDisabled() { + return Object.values(this.webIdeDisabledReasons).some( + ({ condition }) => condition() === true, + ); + }, + webIdeDisabledTooltip() { + const disabledReason = Object.values(this.webIdeDisabledReasons).find((reason) => + reason.condition(), + ); + + return disabledReason?.message ?? ''; + }, + webIdeDisabledReasons() { + return { + queryErrors: { + condition: () => this.hasProjectQueryErrors, + message: this.$options.i18n.errorMessage, + }, + archived: { + condition: () => this.blobInfo.archived, + message: this.$options.i18n.archivedProjectTooltip, + }, + lfs: { + condition: () => this.isUsingLfs, + message: this.$options.i18n.lfsFileTooltip, + }, + }; + }, }, watch: { blobInfo() { @@ -267,9 +294,8 @@ export default { ({ - 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: 'You cannot edit files in read-only repositories', + }, +}; + +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, }; 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 5e64fe773e1c964c2cb2f9aaac4408d5f55bdb17..c8eaffe790020977e61bac66caf2171ec0853242 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -144,6 +144,11 @@ export default { required: false, default: false, }, + customTooltipText: { + type: String, + required: false, + default: __('You cannot edit this file'), + }, }, data() { return { @@ -304,7 +309,7 @@ export default { return showWebIdeButton || showEditButton; }, tooltipText() { - return this.disabled ? __('Editing this file is not supported') : ''; + return this.disabled ? this.customTooltipText : ''; }, }, methods: { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5491d5e0338d29bf97b3d007d22997990629943c..a0fc23bed159c74055eb8522ecba612846788955 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 "" @@ -71359,6 +71356,15 @@ msgstr "" msgid "You cannot create projects in your personal namespace. Contact your GitLab administrator." msgstr "" +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" msgstr "" diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index 1c7f33765ce99bac33f96d3b9e772e8d1272a865..12755b244dd0bd7e28afa21c35967f47fa671f01 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' 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 8ea3920ca5608ea1c26ea1dfb1b5c5549385f183..ed34a9182493e7b290f0a8858bed2eb138b1f26f 100644 --- a/spec/frontend/repository/components/header_area/blob_controls_spec.js +++ b/spec/frontend/repository/components/header_area/blob_controls_spec.js @@ -253,9 +253,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,42 +274,37 @@ 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); - }); + describe('when project query has errors', () => { + it('disables the WebIdeLink component with appropriate tooltip', async () => { + await createComponent({ blobControlsResolver: blobControlsErrorResolver }); - 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().props('disabled')).toBe(true); + expect(findWebIdeLink().props('customTooltipText')).toBe( + 'An error occurred while loading file controls. Refresh the page.', + ); }); - - 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, - }); + describe.each` + description | overrides | expectedTooltip + ${'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 = (() => { + return overrideBlobControlsResolver(overrides); + })(); + await createComponent({ + blobControlsResolver: customBlobControlsResolver, + }); - expect(findWebIdeLink().exists()).toBe(false); + expect(findWebIdeLink().props('disabled')).toBe(true); + expect(findWebIdeLink().props('customTooltipText')).toBe(expectedTooltip); + }); }); describe('when can modify blob', () => { @@ -440,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 0d408908f00858b09595444af1fac3faa3b179f3..d394e11263027a11e8a12a95051df030b0efc544 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,25 @@ 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('You cannot edit this file'); + }); + + it('renders custom tooltip', () => { + createComponent({ + disabled: true, + customTooltipText: 'You cannot edit files in read-only repositories', + }); + + expect(findDisclosureDropdown().props('disabled')).toBe(true); + expect(findDisclosureDropdown().attributes('aria-label')).toBe( + 'You cannot edit files in read-only repositories', + ); + }); + }); });