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: `
+
+
+ Before Actions Content
+
+
+ After Actions Content
+
+
+ `,
+ };
+ },
+ 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',
+ );
+ });
+ });
});