diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue index 175aef59ae553ac45cc116dff1ff12d773ee1c17..c3f3226c46e38e8961cb92f79d5d532cccb2c121 100644 --- a/app/assets/javascripts/vue_shared/components/actions_button.vue +++ b/app/assets/javascripts/vue_shared/components/actions_button.vue @@ -1,29 +1,25 @@ <script> -import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlButton, GlTooltip } from '@gitlab/ui'; +import { + GlDisclosureDropdown, + GlDisclosureDropdownGroup, + GlDisclosureDropdownItem, +} from '@gitlab/ui'; export default { components: { - GlDropdown, - GlDropdownItem, - GlDropdownDivider, - GlButton, - GlTooltip, + GlDisclosureDropdown, + GlDisclosureDropdownGroup, + GlDisclosureDropdownItem, }, props: { - id: { + toggleText: { type: String, - required: false, - default: '', + required: true, }, actions: { type: Array, required: true, }, - selectedKey: { - type: String, - required: false, - default: '', - }, category: { type: String, required: false, @@ -34,78 +30,40 @@ export default { required: false, default: 'default', }, - showActionTooltip: { - type: Boolean, - required: false, - default: true, - }, - }, - computed: { - hasMultipleActions() { - return this.actions.length > 1; - }, - selectedAction() { - return this.actions.find((x) => x.key === this.selectedKey) || this.actions[0]; - }, }, methods: { handleItemClick(action) { - this.$emit('select', action.key); - }, - handleClick(action, evt) { - this.$emit('actionClicked', { action }); - return action.handle?.(evt); + return action.handle?.(); }, }, }; </script> <template> - <span> - <gl-dropdown - v-if="hasMultipleActions" - :id="id" - :text="selectedAction.text" - :split-href="selectedAction.href" - :variant="variant" - :category="category" - split - data-qa-selector="action_dropdown" - @click="handleClick(selectedAction, $event)" - > - <template #button-content> - <span class="gl-dropdown-button-text" v-bind="selectedAction.attrs"> - {{ selectedAction.text }} - </span> - </template> - <template v-for="(action, index) in actions"> - <gl-dropdown-item - :key="action.key" - is-check-item - :is-checked="action.key === selectedAction.key" - :secondary-text="action.secondaryText" - :data-qa-selector="`${action.key}_menu_item`" - :data-testid="`action_${action.key}`" - @click="handleItemClick(action)" - > - <span class="gl-font-weight-bold">{{ action.text }}</span> - </gl-dropdown-item> - <gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" /> - </template> - </gl-dropdown> - <gl-button - v-else-if="selectedAction" - :id="id" - v-bind="selectedAction.attrs" - :variant="variant" - :category="category" - :href="selectedAction.href" - @click="handleClick(selectedAction, $event)" - > - {{ selectedAction.text }} - </gl-button> - <gl-tooltip v-if="selectedAction.tooltip && showActionTooltip" :target="id"> - {{ selectedAction.tooltip }} - </gl-tooltip> - </span> + <gl-disclosure-dropdown + :variant="variant" + :category="category" + :toggle-text="toggleText" + data-qa-selector="action_dropdown" + > + <gl-disclosure-dropdown-group> + <gl-disclosure-dropdown-item + v-for="action in actions" + :key="action.key" + v-bind="action.attrs" + :item="action" + :data-qa-selector="`${action.key}_menu_item`" + @action="handleItemClick(action)" + > + <template #list-item> + <div class="gl-display-flex gl-flex-direction-column"> + <span class="gl-font-weight-bold gl-mb-2">{{ action.text }}</span> + <span class="gl-text-gray-700"> + {{ action.secondaryText }} + </span> + </div> + </template> + </gl-disclosure-dropdown-item> + </gl-disclosure-dropdown-group> + </gl-disclosure-dropdown> </template> 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 3c08142e2b926dff0e360759886c53aecdb9af87..96944877f618248522e0e490c6b5c0f7c3ee3ae6 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -3,9 +3,7 @@ import { GlModal, GlSprintf, GlLink } from '@gitlab/ui'; import { s__, __ } from '~/locale'; import { visitUrl } from '~/lib/utils/url_utility'; import ActionsButton from '~/vue_shared/components/actions_button.vue'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { KEY_EDIT, KEY_WEB_IDE, KEY_GITPOD, KEY_PIPELINE_EDITOR } from './constants'; export const i18n = { @@ -21,22 +19,18 @@ export const i18n = { webIdeTooltip: s__( 'WebIDE|Quickly and easily edit multiple files in your project. Press . to open', ), + toggleText: __('Edit'), }; -export const PREFERRED_EDITOR_KEY = 'gl-web-ide-button-selected'; -export const PREFERRED_EDITOR_RESET_KEY = 'gl-web-ide-button-selected-reset'; - export default { components: { ActionsButton, - LocalStorageSync, GlModal, GlSprintf, GlLink, ConfirmForkModal, }, i18n, - mixins: [glFeatureFlagsMixin()], props: { isFork: { type: Boolean, @@ -141,7 +135,6 @@ export default { }, data() { return { - selection: this.showPipelineEditorButton ? KEY_PIPELINE_EDITOR : KEY_WEB_IDE, showEnableGitpodModal: false, showForkModal: false, }; @@ -155,6 +148,9 @@ export default { this.gitpodAction, ].filter((action) => action); }, + hasActions() { + return this.actions.length > 0; + }, editAction() { if (!this.showEditButton) { return null; @@ -176,9 +172,8 @@ export default { return { key: KEY_EDIT, - text: __('Edit'), + text: __('Edit single file'), secondaryText: __('Edit this file only.'), - tooltip: '', attrs: { 'data-qa-selector': 'edit_button', 'data-track-action': 'click_consolidated_edit', @@ -205,7 +200,6 @@ export default { const handleOptions = this.needsToFork ? { - href: '#modal-confirm-fork-webide', handle: () => { if (this.disableForkModal) { this.$emit('edit', 'ide'); @@ -216,9 +210,7 @@ export default { }, } : { - href: this.webIdeUrl, - handle: (evt) => { - evt.preventDefault(); + handle: () => { visitUrl(this.webIdeUrl, true); }, }; @@ -227,7 +219,6 @@ export default { key: KEY_WEB_IDE, text: this.webIdeActionText, secondaryText: this.$options.i18n.webIdeText, - tooltip: this.$options.i18n.webIdeTooltip, attrs: { 'data-qa-selector': 'web_ide_button', 'data-track-action': 'click_consolidated_edit_ide', @@ -258,7 +249,6 @@ export default { key: KEY_PIPELINE_EDITOR, text: __('Edit in pipeline editor'), secondaryText, - tooltip: secondaryText, attrs: { 'data-qa-selector': 'pipeline_editor_button', }, @@ -283,7 +273,6 @@ export default { key: KEY_GITPOD, text: this.gitpodActionText, secondaryText, - tooltip: secondaryText, attrs: { 'data-qa-selector': 'gitpod_button', }, @@ -310,52 +299,24 @@ export default { }; }, }, - mounted() { - this.resetPreferredEditor(); - }, methods: { - select(key) { - this.selection = key; - }, showModal(dataKey) { this[dataKey] = true; }, - resetPreferredEditor() { - if (!this.glFeatures.vscodeWebIde || this.showEditButton) { - return; - } - - if (localStorage.getItem(PREFERRED_EDITOR_RESET_KEY) === 'true') { - return; - } - - localStorage.setItem(PREFERRED_EDITOR_KEY, KEY_WEB_IDE); - localStorage.setItem(PREFERRED_EDITOR_RESET_KEY, true); - - this.select(KEY_WEB_IDE); - }, }, webIdeButtonId: 'web-ide-link', - PREFERRED_EDITOR_KEY, }; </script> <template> <div class="gl-sm-ml-3"> <actions-button + v-if="hasActions" :id="$options.webIdeButtonId" :actions="actions" - :selected-key="selection" + :toggle-text="$options.i18n.toggleText" :variant="isBlob ? 'confirm' : 'default'" :category="isBlob ? 'primary' : 'secondary'" - show-action-tooltip - @select="select" - /> - <local-storage-sync - :storage-key="$options.PREFERRED_EDITOR_KEY" - :value="selection" - as-string - @input="select" /> <gl-modal v-if="computedShowGitpodButton && !gitpodEnabled" diff --git a/doc/development/contributing/index.md b/doc/development/contributing/index.md index bc8ed6e2cd4b0fe32c581aa879b9cc3d04702f03..17e45f8c96da6dd10850ba9c1ef09bf95614135f 100644 --- a/doc/development/contributing/index.md +++ b/doc/development/contributing/index.md @@ -60,8 +60,9 @@ To write and test your code, you will use the GitLab Development Kit. consider doing so with an empty rails app and port it to GDK after. - To run a pre-configured GDK instance in the cloud, use [GDK with Gitpod](../../integration/gitpod.md). - From a project's repository, select the caret (angle-down) next to **Web IDE**, - and select **Gitpod** from the list. + From a project repository: + 1. On the top bar, select **Main menu > Projects** and find your project. + 1. In the upper right, select **Edit > Gitpod**. 1. If you want to contribute to the [website](https://about.gitlab.com/) or the [handbook](https://about.gitlab.com/handbook/), go to the footer of any page and select **Edit in Web IDE** to open the [Web IDE](../../user/project/web_ide/index.md). diff --git a/doc/development/documentation/contribute.md b/doc/development/documentation/contribute.md index 8b08743c6e9474ee48bc10389dd83ce8d94c0916..e86cf4318b84c49997ae8abfaaccd88506fb0963 100644 --- a/doc/development/documentation/contribute.md +++ b/doc/development/documentation/contribute.md @@ -50,9 +50,7 @@ When you are ready to update the documentation: 1. In your fork, find the documentation page in the `\doc` directory. 1. If you know Git, make your changes and open a merge request. If not, follow these steps: - 1. In the upper-right corner, select **Edit** if it is visible. - If it is not, select the down arrow (**{chevron-lg-down}**) next to - **Open in Web IDE** or **Gitpod**, and select **Edit**. + 1. In the upper right, select **Edit > Edit single file**. 1. In the **Commit message** text box, enter a commit message. Use 3-5 words, start with a capital letter, and do not end with a period. 1. Select **Commit changes**. diff --git a/doc/integration/gitpod.md b/doc/integration/gitpod.md index 0ba227c2a8571aeb7adb546976aee420782fb875..c47aca2c7d869ad52b92d8ad515a7b60123924ca 100644 --- a/doc/integration/gitpod.md +++ b/doc/integration/gitpod.md @@ -59,16 +59,12 @@ GitLab users can then [enable the Gitpod integration for themselves](#enable-git You can launch Gitpod directly from GitLab in one of these ways: -- *From your project's page:* - 1. Go to your project, then go to the page you want to edit. - 1. Select the caret (**{chevron-lg-down}**) next to **Web IDE**, and select **Gitpod** - from the list: +- **From a project repository:** + 1. On the top bar, select **Main menu > Projects** and find your project. + 1. In the upper right, select **Edit > Gitpod**. -  - - 1. Select **Open in Gitpod**. -- *From a merge request:* +- **From a merge request:** 1. Go to your merge request. - 1. In the upper-right corner, select **Code**, then select **Open in Gitpod**. + 1. In the upper-right corner, select **Code > Open in Gitpod**. Gitpod builds your development environment for your branch. diff --git a/doc/integration/img/gitpod_button_project_page_v13_4.png b/doc/integration/img/gitpod_button_project_page_v13_4.png deleted file mode 100644 index 55a70d891699d0377e8f0a0f4d2d86a16723cbaa..0000000000000000000000000000000000000000 Binary files a/doc/integration/img/gitpod_button_project_page_v13_4.png and /dev/null differ diff --git a/doc/tutorials/dependency_scanning.md b/doc/tutorials/dependency_scanning.md index c5f65a8affc847285cb3f3e845515a4202edb823..53f329d5bfd320d67af197a2057e483f8c840717 100644 --- a/doc/tutorials/dependency_scanning.md +++ b/doc/tutorials/dependency_scanning.md @@ -138,8 +138,8 @@ need to upgrade the `fastify` package. To fix the vulnerability: -1. Go to **Repository > Files**. -1. From the **WebIDE** dropdown list select **GitPod**, then right-click on **GitPod** to open +1. On the top bar, select **Main menu > Projects** and find your project. +1. In the upper right, select **Edit > GitPod** and open GitPod in a new tab. 1. If you are prompted to, select **Continue with GitLab**, then select **Authorize**. 1. On the **New Workspace** page, select **Continue**. diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md index dc988846676a5116cd3d9845072ec405a799c998..c81f4e931666b6a897a0a2b449561a30d3b3e43d 100644 --- a/doc/user/project/repository/web_editor.md +++ b/doc/user/project/repository/web_editor.md @@ -58,13 +58,7 @@ To edit a text file in the Web Editor: 1. On the top bar, select **Main menu > Projects** and find your project. 1. Go to your file. -1. In the upper-right corner of the file, select **Edit**. - - If **Edit** is not visible: - - 1. Next to **Open in Web IDE** or **Open in Gitpod**, select the down arrow (**{chevron-lg-down}**). - 1. From the dropdown list, select **Edit** as your default setting. - 1. Select **Edit**. +1. In the upper right, select **Edit > Edit single file**. ### Keyboard shortcuts diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index cb04957a2b6da7caaef43e70231588c4b425a520..1e54566ab3f604240ae7ab484f77856958d15979 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -36,13 +36,7 @@ You can also open the Web IDE from: To open the Web IDE from a file or the repository file list: -- In the upper-right corner of the page, select **Open in Web IDE**. - -If **Open in Web IDE** is not visible: - -1. Next to **Edit** or **Gitpod**, select the down arrow (**{chevron-lg-down}**). -1. From the dropdown list, select **Open in Web IDE**. -1. Select **Open in Web IDE**. +- In the upper right, select **Edit > Open in Web IDE**. ### From a merge request diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d38db8931dd11d8ca7bddae39fe7a2b7da0a2bab..d72b2a6e2a8d54de7fbdfe7e688453716f31ebed 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16664,6 +16664,9 @@ msgstr "" msgid "Edit sidebar" msgstr "" +msgid "Edit single file" +msgstr "" + msgid "Edit table" msgstr "" diff --git a/qa/qa/page/file/show.rb b/qa/qa/page/file/show.rb index 011b1ea5d81a8f5e46eba48cdc876f2a9f806aa2..173baa211606527d288b7283e43f3978fb7785b2 100644 --- a/qa/qa/page/file/show.rb +++ b/qa/qa/page/file/show.rb @@ -12,21 +12,15 @@ class Show < Page::Base element :lock_button end - view 'app/assets/javascripts/vue_shared/components/web_ide_link.vue' do - element :edit_button - end - view 'app/assets/javascripts/vue_shared/components/actions_button.vue' do element :action_dropdown element :edit_menu_item, ':data-qa-selector="`${action.key}_menu_item`"' # rubocop:disable QA/ElementWithPattern + element :webide_menu_item, ':data-qa-selector="`${action.key}_menu_item`"' # rubocop:disable QA/ElementWithPattern end def click_edit - within_element(:action_dropdown) do - click_button(class: 'dropdown-toggle-split') - click_element(:edit_menu_item) - click_element(:edit_button) - end + click_element(:action_dropdown) + click_element(:edit_menu_item) end def click_delete diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index daaee280b8499b9c720fdf9f27e92721c498effc..95a6c840684adad00f6485eebf46954b4146653d 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -62,10 +62,6 @@ class Show < Page::Base element :new_file_menu_item end - view 'app/assets/javascripts/vue_shared/components/web_ide_link.vue' do - element :web_ide_button - end - view 'app/views/projects/blob/viewers/_loading.html.haml' do element :spinner_placeholder end @@ -154,7 +150,8 @@ def new_merge_request end def open_web_ide! - click_element(:web_ide_button) + click_element(:action_dropdown) + click_element(:webide_menu_item) page.driver.browser.switch_to.window(page.driver.browser.window_handles.last) end @@ -164,7 +161,8 @@ def open_web_ide_via_shortcut end def has_edit_fork_button? - has_element?(:web_ide_button, text: 'Edit fork in Web IDE') + click_element(:action_dropdown) + has_element?(:webide_menu_item, text: 'Edit fork in Web IDE') end def project_name diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 3c68f31074435691292950585abdb73159302dd1..e8a9edcc0cc48187e5d9a1279038f10608d98498 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -23,15 +23,11 @@ end def edit_and_commit(commit_changes: true, is_diff: false) - set_default_button('edit') - refresh - wait_for_requests - if is_diff first('.js-diff-more-actions').click click_link('Edit in single-file editor') else - click_link('Edit') + edit_in_single_file_editor end fill_editor(content: 'class NextFeature\\nend\\n') diff --git a/spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb b/spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb index 3e4ee13269f49122091cdc36bf8505cc0ff16db1..bef4e5f89b173a075d9db4fe5e4f5f8460f09d73 100644 --- a/spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb +++ b/spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb @@ -19,6 +19,8 @@ project.repository.create_file(user, project.ci_config_path_or_default, 'test', message: 'testing', branch_name: 'master') visit project_blob_path(project, File.join('master', '.my-config.yml')) + click_button 'Edit' + expect(page).to have_content('Edit in pipeline editor') end @@ -26,6 +28,8 @@ project.repository.create_file(user, '.my-sub-config.yml', 'test', message: 'testing', branch_name: 'master') visit project_blob_path(project, File.join('master', '.my-sub-config.yml')) + click_button 'Edit' + expect(page).not_to have_content('Edit in pipeline editor') end end @@ -36,6 +40,9 @@ end it 'does not shows the Pipeline Editor button' do visit project_blob_path(project, File.join('master', '.my-config.yml')) + + click_button 'Edit' + expect(page).not_to have_content('Edit in pipeline editor') end end diff --git a/spec/features/projects/files/user_browses_lfs_files_spec.rb b/spec/features/projects/files/user_browses_lfs_files_spec.rb index 04094ae2d6fa97889949476d6070b69878a0410a..d8c1c8e4f2a33d0b881f29eb1a213428a520a5d2 100644 --- a/spec/features/projects/files/user_browses_lfs_files_spec.rb +++ b/spec/features/projects/files/user_browses_lfs_files_spec.rb @@ -71,7 +71,9 @@ expect(page).not_to have_content('Annotate') expect(page).not_to have_content('Blame') - expect(page).not_to have_selector(:link_or_button, text: /^Edit$/) + click_button 'Edit' + + expect(page).not_to have_selector(:link_or_button, text: /^Edit single file$/) expect(page).to have_selector(:link_or_button, 'Open in Web IDE') end end diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb index 7d888aabf53f92afda9439213a51eb5713d51a0c..de82f3062a2ee9d6ad56f143ea832f654f4ccceb 100644 --- a/spec/features/projects/files/user_creates_files_spec.rb +++ b/spec/features/projects/files/user_creates_files_spec.rb @@ -105,8 +105,6 @@ def submit_new_file(options) end it 'creates and commit a new file with new lines at the end of file' do - set_default_button('edit') - editor_set_value('Sample\n\n\n') fill_in(:file_name, with: 'not_a_file.md') fill_in(:commit_message, with: 'New commit message', visible: true) @@ -116,7 +114,7 @@ def submit_new_file(options) expect(page).to have_current_path(new_file_path, ignore_query: true) - click_link('Edit') + edit_in_single_file_editor expect(find('.monaco-editor')).to have_content('Sample\n\n\n') end diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index acaeffe5ef49d8e28c38c17b20bb4e1116703594..10fa4a2135938d5d925add0bec6531897be25469 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -19,10 +19,6 @@ sign_in(user) end - after do - unset_default_button - end - shared_examples 'unavailable for an archived project' do it 'does not show the edit link for an archived project', :js do project.update!(archived: true) @@ -48,9 +44,8 @@ end it 'inserts a content of a file' do - set_default_button('edit') click_link('.gitignore') - click_link_or_button('Edit') + edit_in_single_file_editor find('.file-editor', match: :first) editor_set_value('*.rbca') @@ -69,9 +64,8 @@ end it 'commits an edited file' do - set_default_button('edit') click_link('.gitignore') - click_link_or_button('Edit') + edit_in_single_file_editor find('.file-editor', match: :first) editor_set_value('*.rbca') @@ -86,9 +80,8 @@ end it 'commits an edited file to a new branch' do - set_default_button('edit') click_link('.gitignore') - click_link_or_button('Edit') + edit_in_single_file_editor find('.file-editor', match: :first) @@ -105,10 +98,8 @@ end it 'shows loader on commit changes' do - set_default_button('edit') click_link('.gitignore') - click_link_or_button('Edit') - + edit_in_single_file_editor # why: We don't want the form to actually submit, so that we can assert the button's changed state page.execute_script("document.querySelector('.js-edit-blob-form').addEventListener('submit', e => e.preventDefault())") @@ -120,9 +111,8 @@ end it 'shows the diff of an edited file' do - set_default_button('edit') click_link('.gitignore') - click_link_or_button('Edit') + edit_in_single_file_editor find('.file-editor', match: :first) editor_set_value('*.rbca') @@ -158,9 +148,8 @@ def expect_fork_status end it 'inserts a content of a file in a forked project', :sidekiq_might_not_need_inline do - set_default_button('edit') click_link('.gitignore') - click_link_or_button('Edit') + edit_in_single_file_editor expect_fork_prompt @@ -176,9 +165,8 @@ def expect_fork_status end it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do - set_default_button('webide') click_link('.gitignore') - click_link_or_button('Web IDE') + edit_in_web_ide expect_fork_prompt @@ -191,9 +179,8 @@ def expect_fork_status end it 'commits an edited file in a forked project', :sidekiq_might_not_need_inline do - set_default_button('edit') click_link('.gitignore') - click_link_or_button('Edit') + edit_in_single_file_editor expect_fork_prompt click_link_or_button('Fork') @@ -222,9 +209,8 @@ def expect_fork_status end it 'links to the forked project for editing', :sidekiq_might_not_need_inline do - set_default_button('edit') click_link('.gitignore') - click_link_or_button('Edit') + edit_in_single_file_editor expect(page).not_to have_link('Fork') diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb index a86bc0ae97a21b9f20e0c4163b3ec19ae4a4ec4f..29fb20841fdbe23983287a2afa856073f6a31f86 100644 --- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb +++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb @@ -51,7 +51,8 @@ def find_new_menu_toggle end # The Web IDE - expect(page).to have_link('Web IDE') + click_button 'Edit' + expect(page).to have_button('Web IDE') end it 'hides the links when the project is archived' do @@ -73,7 +74,7 @@ def find_new_menu_toggle expect(page).not_to have_selector('[data-testid="add-to-tree"]') - expect(page).not_to have_link('Web IDE') + expect(page).not_to have_button('Edit') end end @@ -95,7 +96,7 @@ def find_new_menu_toggle end it "updates Web IDE link" do - expect(page.has_link?('Web IDE')).to be(expect_ide_link) + expect(page.has_button?('Edit')).to be(expect_ide_link) end end end diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js index 8c2f2b52f8e506039b9bdd0754b30eefd9020109..e7663e2adb2c2c58b10cf1f3edc91cc80bdbbec4 100644 --- a/spec/frontend/vue_shared/components/actions_button_spec.js +++ b/spec/frontend/vue_shared/components/actions_button_spec.js @@ -1,12 +1,15 @@ -import { GlDropdown, GlDropdownDivider, GlButton, GlTooltip } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { + GlDisclosureDropdown, + GlDisclosureDropdownGroup, + GlDisclosureDropdownItem, +} from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ActionsButton from '~/vue_shared/components/actions_button.vue'; const TEST_ACTION = { key: 'action1', text: 'Sample', secondaryText: 'Lorem ipsum.', - tooltip: '', href: '/sample', attrs: { 'data-test': '123', @@ -14,191 +17,75 @@ const TEST_ACTION = { href: '/sample', variant: 'default', }, + handle: jest.fn(), }; const TEST_ACTION_2 = { key: 'action2', text: 'Sample 2', secondaryText: 'Dolar sit amit.', - tooltip: 'Dolar sit amit.', href: '#', attrs: { 'data-test': '456' }, + handle: jest.fn(), }; -const TEST_TOOLTIP = 'Lorem ipsum dolar sit'; -describe('Actions button component', () => { +describe('vue_shared/components/actions_button', () => { let wrapper; function createComponent(props) { - wrapper = shallowMount(ActionsButton, { - propsData: { ...props }, + wrapper = shallowMountExtended(ActionsButton, { + propsData: { actions: [TEST_ACTION, TEST_ACTION_2], toggleText: 'Edit', ...props }, + stubs: { + GlDisclosureDropdownItem, + }, }); } + const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); - const findButton = () => wrapper.findComponent(GlButton); - const findTooltip = () => wrapper.findComponent(GlTooltip); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const parseDropdownItems = () => - findDropdown() - .findAll('gl-dropdown-item-stub,gl-dropdown-divider-stub') - .wrappers.map((x) => { - if (x.is(GlDropdownDivider)) { - return { type: 'divider' }; - } - - const { isCheckItem, isChecked, secondaryText } = x.props(); - - return { - type: 'item', - isCheckItem, - isChecked, - secondaryText, - text: x.text(), - }; - }); - const clickOn = (child, evt = new Event('click')) => child.vm.$emit('click', evt); - const clickLink = (...args) => clickOn(findButton(), ...args); - const clickDropdown = (...args) => clickOn(findDropdown(), ...args); - - describe('with 1 action', () => { - beforeEach(() => { - createComponent({ actions: [TEST_ACTION] }); - }); - - it('should not render dropdown', () => { - expect(findDropdown().exists()).toBe(false); - }); - - it('should render single button', () => { - expect(findButton().attributes()).toMatchObject({ - href: TEST_ACTION.href, - ...TEST_ACTION.attrs, - }); - expect(findButton().text()).toBe(TEST_ACTION.text); - }); - - it('should not have tooltip', () => { - expect(findTooltip().exists()).toBe(false); - }); + it('dropdown toggle displays provided toggleLabel', () => { + createComponent(); - it('should have attrs', () => { - expect(findButton().attributes()).toMatchObject(TEST_ACTION.attrs); - }); - - it('can click', () => { - expect(clickLink).not.toThrow(); - }); + expect(findDropdown().props().toggleText).toBe('Edit'); }); - describe('with 1 action with tooltip', () => { - it('should have tooltip', () => { - createComponent({ actions: [{ ...TEST_ACTION, tooltip: TEST_TOOLTIP }] }); + it('allows customizing variant and category', () => { + const variant = 'confirm'; + const category = 'secondary'; - expect(findTooltip().text()).toBe(TEST_TOOLTIP); - }); + createComponent({ variant, category }); + + expect(findDropdown().props()).toMatchObject({ category, variant }); }); - describe('when showActionTooltip is false', () => { - it('should not have tooltip', () => { - createComponent({ - actions: [{ ...TEST_ACTION, tooltip: TEST_TOOLTIP }], - showActionTooltip: false, - }); + it('displays a single dropdown group', () => { + createComponent(); - expect(findTooltip().exists()).toBe(false); - }); + expect(wrapper.findAllComponents(GlDisclosureDropdownGroup)).toHaveLength(1); }); - describe('with 1 action with handle', () => { - it('can click and trigger handle', () => { - const handleClick = jest.fn(); - createComponent({ actions: [{ ...TEST_ACTION, handle: handleClick }] }); + it('create dropdown items for every action', () => { + createComponent(); - const event = new Event('click'); - clickLink(event); + [TEST_ACTION, TEST_ACTION_2].forEach((action, index) => { + const dropdownItem = wrapper.findAllComponents(GlDisclosureDropdownItem).at(index); - expect(handleClick).toHaveBeenCalledWith(event); + expect(dropdownItem.props().item).toBe(action); + expect(dropdownItem.attributes()).toMatchObject(action.attrs); + expect(dropdownItem.text()).toContain(action.text); + expect(dropdownItem.text()).toContain(action.secondaryText); }); }); - describe('with multiple actions', () => { - let handleAction; + describe('when clicking a dropdown item', () => { + it("invokes the action's handle method", () => { + createComponent(); - beforeEach(() => { - handleAction = jest.fn(); + [TEST_ACTION, TEST_ACTION_2].forEach((action, index) => { + const dropdownItem = wrapper.findAllComponents(GlDisclosureDropdownItem).at(index); - createComponent({ actions: [{ ...TEST_ACTION, handle: handleAction }, TEST_ACTION_2] }); - }); + dropdownItem.vm.$emit('action'); - it('should default to selecting first action', () => { - expect(findDropdown().attributes()).toMatchObject({ - text: TEST_ACTION.text, - 'split-href': TEST_ACTION.href, + expect(action.handle).toHaveBeenCalled(); }); }); - - it('should handle first action click', () => { - const event = new Event('click'); - - clickDropdown(event); - - expect(handleAction).toHaveBeenCalledWith(event); - }); - - it('should render dropdown items', () => { - expect(parseDropdownItems()).toEqual([ - { - type: 'item', - isCheckItem: true, - isChecked: true, - secondaryText: TEST_ACTION.secondaryText, - text: TEST_ACTION.text, - }, - { type: 'divider' }, - { - type: 'item', - isCheckItem: true, - isChecked: false, - secondaryText: TEST_ACTION_2.secondaryText, - text: TEST_ACTION_2.text, - }, - ]); - }); - - it('should select action 2 when clicked', () => { - expect(wrapper.emitted('select')).toBeUndefined(); - - const action2 = wrapper.find(`[data-testid="action_${TEST_ACTION_2.key}"]`); - action2.vm.$emit('click'); - - expect(wrapper.emitted('select')).toEqual([[TEST_ACTION_2.key]]); - }); - - it('should not have tooltip value', () => { - expect(findTooltip().exists()).toBe(false); - }); - }); - - describe('with multiple actions and selectedKey', () => { - beforeEach(() => { - createComponent({ actions: [TEST_ACTION, TEST_ACTION_2], selectedKey: TEST_ACTION_2.key }); - }); - - it('should show action 2 as selected', () => { - expect(parseDropdownItems()).toEqual([ - expect.objectContaining({ - type: 'item', - isChecked: false, - }), - { type: 'divider' }, - expect.objectContaining({ - type: 'item', - isChecked: true, - }), - ]); - }); - - it('should have tooltip value', () => { - expect(findTooltip().text()).toBe(TEST_ACTION_2.tooltip); - }); }); }); 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 d888abc19efcc1903e1fd7c85da0693606e67005..26557c63a77ecd2b28cd62ac3b408772ef93d478 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -1,19 +1,12 @@ -import { GlButton, GlModal } from '@gitlab/ui'; +import { GlModal } from '@gitlab/ui'; import { nextTick } from 'vue'; import ActionsButton from '~/vue_shared/components/actions_button.vue'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import WebIdeLink, { - i18n, - PREFERRED_EDITOR_RESET_KEY, - PREFERRED_EDITOR_KEY, -} from '~/vue_shared/components/web_ide_link.vue'; +import WebIdeLink, { i18n } from '~/vue_shared/components/web_ide_link.vue'; import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue'; -import { KEY_WEB_IDE } from '~/vue_shared/components/constants'; import { stubComponent } from 'helpers/stub_component'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; -import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { visitUrl } from '~/lib/utils/url_utility'; @@ -30,9 +23,8 @@ const forkPath = '/some/fork/path'; const ACTION_EDIT = { href: TEST_EDIT_URL, key: 'edit', - text: 'Edit', + text: 'Edit single file', secondaryText: 'Edit this file only.', - tooltip: '', attrs: { 'data-qa-selector': 'edit_button', 'data-track-action': 'click_consolidated_edit', @@ -45,10 +37,8 @@ const ACTION_EDIT_CONFIRM_FORK = { handle: expect.any(Function), }; const ACTION_WEB_IDE = { - href: TEST_WEB_IDE_URL, key: 'webide', secondaryText: i18n.webIdeText, - tooltip: i18n.webIdeTooltip, text: 'Web IDE', attrs: { 'data-qa-selector': 'web_ide_button', @@ -59,7 +49,6 @@ const ACTION_WEB_IDE = { }; const ACTION_WEB_IDE_CONFIRM_FORK = { ...ACTION_WEB_IDE, - href: '#modal-confirm-fork-webide', handle: expect.any(Function), }; const ACTION_WEB_IDE_EDIT_FORK = { ...ACTION_WEB_IDE, text: 'Edit fork in Web IDE' }; @@ -67,7 +56,6 @@ const ACTION_GITPOD = { href: TEST_GITPOD_URL, key: 'gitpod', secondaryText: 'Launch a ready-to-code development environment for your project.', - tooltip: 'Launch a ready-to-code development environment for your project.', text: 'Gitpod', attrs: { 'data-qa-selector': 'gitpod_button', @@ -82,16 +70,13 @@ const ACTION_PIPELINE_EDITOR = { href: TEST_PIPELINE_EDITOR_URL, key: 'pipeline_editor', secondaryText: 'Edit, lint, and visualize your pipeline.', - tooltip: 'Edit, lint, and visualize your pipeline.', text: 'Edit in pipeline editor', attrs: { 'data-qa-selector': 'pipeline_editor_button', }, }; -describe('Web IDE link component', () => { - useLocalStorageSpy(); - +describe('vue_shared/components/web_ide_link', () => { let wrapper; function createComponent(props, { mountFn = shallowMountExtended, glFeatures = {} } = {}) { @@ -120,12 +105,7 @@ describe('Web IDE link component', () => { }); } - beforeEach(() => { - localStorage.setItem(PREFERRED_EDITOR_RESET_KEY, 'true'); - }); - const findActionsButton = () => wrapper.findComponent(ActionsButton); - const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); const findModal = () => wrapper.findComponent(GlModal); const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal); @@ -238,64 +218,16 @@ describe('Web IDE link component', () => { }); }); - it('selected Pipeline Editor by default', () => { + it('displays Pipeline Editor as the first action', () => { expect(findActionsButton().props()).toMatchObject({ actions: [ACTION_PIPELINE_EDITOR, ACTION_WEB_IDE, ACTION_GITPOD], - selectedKey: ACTION_PIPELINE_EDITOR.key, }); }); it('when web ide button is clicked it opens in a new tab', async () => { - findActionsButton().props('actions')[1].handle({ - preventDefault: jest.fn(), - }); - await nextTick(); - expect(visitUrl).toHaveBeenCalledWith(ACTION_WEB_IDE.href, true); - }); - }); - - describe('with multiple actions', () => { - beforeEach(() => { - createComponent({ - showEditButton: false, - showWebIdeButton: true, - showGitpodButton: true, - showPipelineEditorButton: false, - userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH, - userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH, - gitpodEnabled: true, - }); - }); - - it('selected Web IDE by default', () => { - expect(findActionsButton().props()).toMatchObject({ - actions: [ACTION_WEB_IDE, ACTION_GITPOD], - selectedKey: ACTION_WEB_IDE.key, - }); - }); - - it('should set selection with local storage value', async () => { - expect(findActionsButton().props('selectedKey')).toBe(ACTION_WEB_IDE.key); - - findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key); - + findActionsButton().props('actions')[1].handle(); await nextTick(); - - expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key); - }); - - it('should update local storage when selection changes', async () => { - expect(findLocalStorageSync().props()).toMatchObject({ - asString: true, - value: ACTION_WEB_IDE.key, - }); - - findActionsButton().vm.$emit('select', ACTION_GITPOD.key); - - await nextTick(); - - expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key); - expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key); + expect(visitUrl).toHaveBeenCalledWith(TEST_WEB_IDE_URL, true); }); }); @@ -348,7 +280,10 @@ describe('Web IDE link component', () => { it.each(testActions)('opens the modal when the button is clicked', async ({ props }) => { createComponent({ ...props, needsToFork: true }, { mountFn: mountExtended }); - await findActionsButton().findComponent(GlButton).trigger('click'); + wrapper.findComponent(ActionsButton).props().actions[0].handle(); + + await nextTick(); + await wrapper.findByRole('button', { name: /Web IDE|Edit/im }).trigger('click'); expect(findForkConfirmModal().props()).toEqual({ visible: true, @@ -404,10 +339,8 @@ describe('Web IDE link component', () => { { mountFn: mountExtended }, ); - findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key); - await nextTick(); - await wrapper.findByRole('button', { name: gitpodText }).trigger('click'); + await wrapper.findByRole('button', { name: new RegExp(gitpodText, 'm') }).trigger('click'); expect(findModal().props('visible')).toBe(true); }); @@ -425,58 +358,4 @@ describe('Web IDE link component', () => { expect(findModal().exists()).toBe(false); }); }); - - describe('when vscode_web_ide feature flag is enabled', () => { - describe('when is not showing edit button', () => { - describe(`when ${PREFERRED_EDITOR_RESET_KEY} is unset`, () => { - beforeEach(() => { - localStorage.setItem.mockReset(); - localStorage.getItem.mockReturnValueOnce(null); - createComponent({ showEditButton: false }, { glFeatures: { vscodeWebIde: true } }); - }); - - it(`sets ${PREFERRED_EDITOR_KEY} local storage key to ${KEY_WEB_IDE}`, () => { - expect(localStorage.getItem).toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY); - expect(localStorage.setItem).toHaveBeenCalledWith(PREFERRED_EDITOR_KEY, KEY_WEB_IDE); - }); - - it(`sets ${PREFERRED_EDITOR_RESET_KEY} local storage key to true`, () => { - expect(localStorage.setItem).toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY, true); - }); - - it(`selects ${KEY_WEB_IDE} as the preferred editor`, () => { - expect(findActionsButton().props().selectedKey).toBe(KEY_WEB_IDE); - }); - }); - - describe(`when ${PREFERRED_EDITOR_RESET_KEY} is set to true`, () => { - beforeEach(() => { - localStorage.setItem.mockReset(); - localStorage.getItem.mockReturnValueOnce('true'); - createComponent({ showEditButton: false }, { glFeatures: { vscodeWebIde: true } }); - }); - - it(`does not update the persisted preferred editor`, () => { - expect(localStorage.getItem).toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY); - expect(localStorage.setItem).not.toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY); - }); - }); - }); - - describe('when is showing the edit button', () => { - it(`does not try to reset the ${PREFERRED_EDITOR_KEY}`, () => { - createComponent({ showEditButton: true }, { glFeatures: { vscodeWebIde: true } }); - - expect(localStorage.getItem).not.toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY); - }); - }); - }); - - describe('when vscode_web_ide feature flag is disabled', () => { - it(`does not try to reset the ${PREFERRED_EDITOR_KEY}`, () => { - createComponent({}, { glFeatures: { vscodeWebIde: false } }); - - expect(localStorage.getItem).not.toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY); - }); - }); }); diff --git a/spec/support/helpers/features/blob_spec_helpers.rb b/spec/support/helpers/features/blob_spec_helpers.rb index 8254e1d76bdf2b00b532e485700786d389f74c70..91969107a17e327e42b732b49ab03f2a6e589db6 100644 --- a/spec/support/helpers/features/blob_spec_helpers.rb +++ b/spec/support/helpers/features/blob_spec_helpers.rb @@ -5,12 +5,14 @@ module Features module BlobSpecHelpers include ActionView::Helpers::JavaScriptHelper - def set_default_button(type) - evaluate_script("localStorage.setItem('gl-web-ide-button-selected', '#{type}')") + def edit_in_single_file_editor + click_button 'Edit' + click_link_or_button 'Edit single file' end - def unset_default_button - set_default_button('') + def edit_in_web_ide + click_button 'Edit' + click_link_or_button 'Web IDE' end end end diff --git a/spec/support/helpers/features/web_ide_spec_helpers.rb b/spec/support/helpers/features/web_ide_spec_helpers.rb index c51116b55b20277384dc08113167c19055d02b05..32b27864e0be97b51f552651b2317d96a755ab93 100644 --- a/spec/support/helpers/features/web_ide_spec_helpers.rb +++ b/spec/support/helpers/features/web_ide_spec_helpers.rb @@ -12,6 +12,7 @@ module Features module WebIdeSpecHelpers include Features::SourceEditorSpecHelpers + include Features::BlobSpecHelpers # Open the IDE from anywhere by first visiting the given project's page def ide_visit(project) @@ -21,8 +22,10 @@ def ide_visit(project) end # Open the IDE from the current page by clicking the Web IDE link - def ide_visit_from_link(link_sel = 'Web IDE') - new_tab = window_opened_by { click_link(link_sel) } + def ide_visit_from_link + new_tab = window_opened_by do + edit_in_web_ide + end switch_to_window new_tab end