Commit 22aa138b authored by Phil Hughes's avatar Phil Hughes Committed by Kushal Pandya

Move diff header actions into dropdown menu

Cleans up the diff header by moving the action buttons
into a more actions dropdown.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/199725
parent 04706487
...@@ -2,32 +2,36 @@ ...@@ -2,32 +2,36 @@
import { escape } from 'lodash'; import { escape } from 'lodash';
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import { import {
GlDeprecatedButton,
GlTooltipDirective, GlTooltipDirective,
GlSafeHtmlDirective, GlSafeHtmlDirective,
GlLoadingIcon,
GlIcon, GlIcon,
GlButton, GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlDropdownDivider,
} from '@gitlab/ui'; } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants'; import { diffViewerModes } from '~/ide/constants';
import EditButton from './edit_button.vue';
import DiffStats from './diff_stats.vue'; import DiffStats from './diff_stats.vue';
import { scrollToElement } from '~/lib/utils/common_utils'; import { scrollToElement } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
GlLoadingIcon,
GlDeprecatedButton,
ClipboardButton, ClipboardButton,
EditButton,
GlIcon, GlIcon,
FileIcon, FileIcon,
DiffStats, DiffStats,
GlButton, GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlDropdownDivider,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -70,7 +74,7 @@ export default { ...@@ -70,7 +74,7 @@ export default {
}, },
data() { data() {
return { return {
hasDropdownOpen: false, moreActionsShown: false,
}; };
}, },
computed: { computed: {
...@@ -155,6 +159,13 @@ export default { ...@@ -155,6 +159,13 @@ export default {
} }
return s__('MRDiff|Show full file'); return s__('MRDiff|Show full file');
}, },
showEditButton() {
return (
this.diffFile.blob?.readable_text &&
!this.diffFile.deleted_file &&
(this.diffFile.edit_path || this.diffFile.ide_edit_path)
);
},
}, },
methods: { methods: {
...mapActions('diffs', [ ...mapActions('diffs', [
...@@ -166,8 +177,11 @@ export default { ...@@ -166,8 +177,11 @@ export default {
handleToggleFile() { handleToggleFile() {
this.$emit('toggleFile'); this.$emit('toggleFile');
}, },
showForkMessage() { showForkMessage(e) {
if (this.canCurrentUserFork && !this.diffFile.can_modify_blob) {
e.preventDefault();
this.$emit('showForkMessage'); this.$emit('showForkMessage');
}
}, },
handleFileNameClick(e) { handleFileNameClick(e) {
const isLinkToOtherPage = const isLinkToOtherPage =
...@@ -183,8 +197,8 @@ export default { ...@@ -183,8 +197,8 @@ export default {
} }
} }
}, },
setDropdownOpen(val) { setMoreActionsShown(val) {
this.hasDropdownOpen = val; this.moreActionsShown = val;
}, },
}, },
}; };
...@@ -193,8 +207,8 @@ export default { ...@@ -193,8 +207,8 @@ export default {
<template> <template>
<div <div
ref="header" ref="header"
:class="{ 'gl-z-dropdown-menu!': moreActionsShown }"
class="js-file-title file-title file-title-flex-parent" class="js-file-title file-title file-title-flex-parent"
:class="{ 'gl-z-dropdown-menu!': hasDropdownOpen }"
@click.self="handleToggleFile" @click.self="handleToggleFile"
> >
<div class="file-header-content"> <div class="file-header-content">
...@@ -255,96 +269,95 @@ export default { ...@@ -255,96 +269,95 @@ export default {
<div <div
v-if="!diffFile.submodule && addMergeRequestButtons" v-if="!diffFile.submodule && addMergeRequestButtons"
class="file-actions d-none d-sm-flex align-items-center flex-wrap" class="file-actions d-flex align-items-center flex-wrap"
> >
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" /> <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
<div class="btn-group" role="group"> <gl-button-group class="gl-pt-0!">
<template v-if="diffFile.blob && diffFile.blob.readable_text"> <gl-button
<span v-gl-tooltip.hover :title="s__('MergeRequests|Toggle comments for this file')"> v-if="diffFile.external_url"
<gl-deprecated-button ref="externalLink"
ref="toggleDiscussionsButton" v-gl-tooltip.hover
:disabled="!diffHasDiscussions(diffFile)" :href="diffFile.external_url"
:class="{ active: diffHasExpandedDiscussions(diffFile) }" :title="`View on ${diffFile.formatted_external_url}`"
class="js-btn-vue-toggle-comments btn" target="_blank"
data-qa-selector="toggle_comments_button" data-track-event="click_toggle_external_button"
data-track-event="click_toggle_comments_button" data-track-label="diff_toggle_external_button"
data-track-label="diff_toggle_comments_button" data-track-property="diff_toggle_external"
data-track-property="diff_toggle_comments" icon="external-link"
type="button"
@click="toggleFileDiscussionWrappers(diffFile)"
>
<gl-icon name="comment" />
</gl-deprecated-button>
</span>
<edit-button
v-if="!diffFile.deleted_file"
:can-current-user-fork="canCurrentUserFork"
:edit-path="diffFile.edit_path"
:ide-edit-path="diffFile.ide_edit_path"
:can-modify-blob="diffFile.can_modify_blob"
data-track-event="click_toggle_edit_button"
data-track-label="diff_toggle_edit_button"
data-track-property="diff_toggle_edit"
@showForkMessage="showForkMessage"
@open="setDropdownOpen(true)"
@close="setDropdownOpen(false)"
/> />
<gl-dropdown
v-gl-tooltip.hover.focus="__('More actions')"
right
toggle-class="btn-icon js-diff-more-actions"
class="gl-pt-0!"
@show="setMoreActionsShown(true)"
@hidden="setMoreActionsShown(false)"
>
<template #button-content>
<gl-icon name="ellipsis_v" class="mr-0" />
<span class="sr-only">{{ __('More actions') }}</span>
</template> </template>
<gl-dropdown-section-header>
<a {{ __('More actions') }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-if="diffFile.replaced_view_path" v-if="diffFile.replaced_view_path"
ref="replacedFileButton" ref="replacedFileButton"
v-safe-html="viewReplacedFileButtonText" v-safe-html="viewReplacedFileButtonText"
:href="diffFile.replaced_view_path" :href="diffFile.replaced_view_path"
class="btn view-file" target="_blank"
/>
<gl-dropdown-item ref="viewButton" :href="diffFile.view_path" target="_blank">
{{ viewFileButtonText }}
</gl-dropdown-item>
<template v-if="showEditButton">
<gl-dropdown-item
v-if="diffFile.edit_path"
ref="editButton"
:href="diffFile.edit_path"
class="js-edit-blob"
@click="showForkMessage"
> >
</a> {{ __('Edit in single-file editor') }}
<gl-deprecated-button </gl-dropdown-item>
<gl-dropdown-item
v-if="diffFile.edit_path"
ref="ideEditButton"
:href="diffFile.ide_edit_path"
class="js-ide-edit-blob"
>
{{ __('Edit in Web IDE') }}
</gl-dropdown-item>
</template>
<template v-if="!diffFile.viewer.collapsed">
<gl-dropdown-divider
v-if="!diffFile.is_fully_expanded || diffHasDiscussions(diffFile)"
/>
<gl-dropdown-item
v-if="diffHasDiscussions(diffFile)"
ref="toggleDiscussionsButton"
data-qa-selector="toggle_comments_button"
@click="toggleFileDiscussionWrappers(diffFile)"
>
<template v-if="diffHasExpandedDiscussions(diffFile)">
{{ __('Hide comments on this file') }}
</template>
<template v-else>
{{ __('Show comments on this file') }}
</template>
</gl-dropdown-item>
<gl-dropdown-item
v-if="!diffFile.is_fully_expanded" v-if="!diffFile.is_fully_expanded"
ref="expandDiffToFullFileButton" ref="expandDiffToFullFileButton"
v-gl-tooltip.hover
:title="expandDiffToFullFileTitle"
class="expand-file"
data-track-event="click_toggle_view_full_button"
data-track-label="diff_toggle_view_full_button"
data-track-property="diff_toggle_view_full"
@click="toggleFullDiff(diffFile.file_path)" @click="toggleFullDiff(diffFile.file_path)"
> >
<gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline /> {{ expandDiffToFullFileTitle }}
<gl-icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" /> </gl-dropdown-item>
<gl-icon v-else name="doc-expand" /> </template>
</gl-deprecated-button> </gl-dropdown>
<gl-deprecated-button </gl-button-group>
ref="viewButton"
v-gl-tooltip.hover
:href="diffFile.view_path"
target="_blank"
class="view-file"
data-track-event="click_toggle_view_sha_button"
data-track-label="diff_toggle_view_sha_button"
data-track-property="diff_toggle_view_sha"
:title="viewFileButtonText"
>
<gl-icon name="doc-text" />
</gl-deprecated-button>
<a
v-if="diffFile.external_url"
ref="externalLink"
v-gl-tooltip.hover
:href="diffFile.external_url"
:title="`View on ${diffFile.formatted_external_url}`"
target="_blank"
rel="noopener noreferrer"
data-track-event="click_toggle_external_button"
data-track-label="diff_toggle_external_button"
data-track-property="diff_toggle_external"
class="btn btn-file-option"
>
<gl-icon name="external-link" />
</a>
</div>
</div> </div>
<div <div
......
...@@ -42,7 +42,7 @@ export default { ...@@ -42,7 +42,7 @@ export default {
class="diff-stats" class="diff-stats"
:class="{ :class="{
'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader, 'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader,
'd-inline-flex': !isCompareVersionsHeader, 'd-none d-sm-inline-flex': !isCompareVersionsHeader,
}" }"
> >
<div v-if="hasDiffFiles" class="diff-stats-group"> <div v-if="hasDiffFiles" class="diff-stats-group">
......
<script>
import { uniqueId } from 'lodash';
import {
GlTooltipDirective,
GlIcon,
GlDeprecatedDropdown as GlDropdown,
GlDeprecatedDropdownItem as GlDropdownItem,
} from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlDropdown,
GlDropdownItem,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
editPath: {
type: String,
required: false,
default: '',
},
ideEditPath: {
type: String,
required: false,
default: '',
},
canCurrentUserFork: {
type: Boolean,
required: true,
},
canModifyBlob: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return { tooltipId: uniqueId('edit_button_tooltip_') };
},
computed: {
tooltipTitle() {
if (this.isDisabled) {
return __("Can't edit as source branch was deleted");
}
return __('Edit file in...');
},
isDisabled() {
return !this.editPath;
},
},
methods: {
handleShow(evt) {
// We must hide the tooltip because it is redundant and doesn't close itself
// when dropdown opens because we are still "focused".
this.$root.$emit('bv::hide::tooltip', this.tooltipId);
if (this.canCurrentUserFork && !this.canModifyBlob) {
evt.preventDefault();
this.$emit('showForkMessage');
} else {
this.$emit('open');
}
},
handleHide() {
this.$emit('close');
},
},
};
</script>
<template>
<div v-gl-tooltip.top="{ title: tooltipTitle, id: tooltipId }" class="gl-display-flex">
<gl-dropdown
toggle-class="rounded-0"
:disabled="isDisabled"
:class="{ 'cursor-not-allowed': isDisabled }"
right
data-testid="edit_file"
@show="handleShow"
@hide="handleHide"
>
<template #button-content>
<span class="gl-dropdown-toggle-text"><gl-icon name="pencil"/></span>
<gl-icon class="gl-dropdown-caret" name="chevron-down" aria-hidden="true" />
</template>
<gl-dropdown-item v-if="editPath" :href="editPath">{{
__('Edit in single-file editor')
}}</gl-dropdown-item>
<gl-dropdown-item v-if="ideEditPath" :href="ideEditPath">{{
__('Edit in Web IDE')
}}</gl-dropdown-item>
</gl-dropdown>
</div>
</template>
...@@ -46,15 +46,24 @@ export const diffHasAllCollapsedDiscussions = (state, getters) => diff => { ...@@ -46,15 +46,24 @@ export const diffHasAllCollapsedDiscussions = (state, getters) => diff => {
* @param {Object} diff * @param {Object} diff
* @returns {Boolean} * @returns {Boolean}
*/ */
export const diffHasExpandedDiscussions = (state, getters) => diff => { export const diffHasExpandedDiscussions = state => diff => {
const discussions = getters.getDiffFileDiscussions(diff); const lines = {
[INLINE_DIFF_VIEW_TYPE]: diff.highlighted_diff_lines || [],
[PARALLEL_DIFF_VIEW_TYPE]: (diff.parallel_diff_lines || []).reduce((acc, line) => {
if (line.left) {
acc.push(line.left);
}
return ( if (line.right) {
(discussions && acc.push(line.right);
discussions.length && }
discussions.find(discussion => discussion.expanded) !== undefined) ||
false return acc;
); }, []),
};
return lines[window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType]
.filter(l => l.discussions.length >= 1)
.some(l => l.discussionsExpanded);
}; };
/** /**
...@@ -62,8 +71,25 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => { ...@@ -62,8 +71,25 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => {
* @param {Boolean} diff * @param {Boolean} diff
* @returns {Boolean} * @returns {Boolean}
*/ */
export const diffHasDiscussions = (state, getters) => diff => export const diffHasDiscussions = state => diff => {
getters.getDiffFileDiscussions(diff).length > 0; const lines = {
[INLINE_DIFF_VIEW_TYPE]: diff.highlighted_diff_lines || [],
[PARALLEL_DIFF_VIEW_TYPE]: (diff.parallel_diff_lines || []).reduce((acc, line) => {
if (line.left) {
acc.push(line.left);
}
if (line.right) {
acc.push(line.right);
}
return acc;
}, []),
};
return lines[window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType].some(
l => l.discussions.length >= 1,
);
};
/** /**
* Returns an array with the discussions of the given diff * Returns an array with the discussions of the given diff
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
right: 15px; right: 15px;
margin-left: auto; margin-left: auto;
.btn { .btn:not(.btn-icon) {
padding: 0 10px; padding: 0 10px;
font-size: 13px; font-size: 13px;
line-height: 28px; line-height: 28px;
...@@ -372,7 +372,7 @@ span.idiff { ...@@ -372,7 +372,7 @@ span.idiff {
color: $gl-text-color; color: $gl-text-color;
} }
.file-actions .btn { .file-actions .btn:not(.btn-icon) {
padding: 0 10px; padding: 0 10px;
font-size: 13px; font-size: 13px;
line-height: 28px; line-height: 28px;
......
...@@ -13,6 +13,21 @@ ...@@ -13,6 +13,21 @@
box-shadow: 0 -2px 0 0 var(--white); box-shadow: 0 -2px 0 0 var(--white);
cursor: pointer; cursor: pointer;
.dropdown-menu {
cursor: auto;
}
@media (max-width: map-get($grid-breakpoints, sm)-1) {
.file-header-content {
width: 0;
flex: 1;
}
.file-actions {
margin-left: $gl-spacing-scale-2;
}
}
@media (min-width: map-get($grid-breakpoints, md)) { @media (min-width: map-get($grid-breakpoints, md)) {
// The `+11` is to ensure the file header border shows when scrolled - // The `+11` is to ensure the file header border shows when scrolled -
// the bottom of the compare-versions header and the top of the file header // the bottom of the compare-versions header and the top of the file header
...@@ -420,6 +435,10 @@ ...@@ -420,6 +435,10 @@
margin-left: 0; margin-left: 0;
border-left: 0; border-left: 0;
} }
.file-actions .dropdown {
height: 28px;
}
} }
table.code { table.code {
......
---
title: Move diff header actions into dropdown menu
merge_request:
author:
type: changed
...@@ -4483,9 +4483,6 @@ msgstr "" ...@@ -4483,9 +4483,6 @@ msgstr ""
msgid "Can't create snippet: %{err}" msgid "Can't create snippet: %{err}"
msgstr "" msgstr ""
msgid "Can't edit as source branch was deleted"
msgstr ""
msgid "Can't fetch content for the blob: %{err}" msgid "Can't fetch content for the blob: %{err}"
msgstr "" msgstr ""
...@@ -9318,9 +9315,6 @@ msgstr "" ...@@ -9318,9 +9315,6 @@ msgstr ""
msgid "Edit environment" msgid "Edit environment"
msgstr "" msgstr ""
msgid "Edit file in..."
msgstr ""
msgid "Edit files in the editor and commit changes here" msgid "Edit files in the editor and commit changes here"
msgstr "" msgstr ""
...@@ -12991,6 +12985,9 @@ msgid_plural "Hide charts" ...@@ -12991,6 +12985,9 @@ msgid_plural "Hide charts"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Hide comments on this file"
msgstr ""
msgid "Hide details" msgid "Hide details"
msgstr "" msgstr ""
...@@ -15835,9 +15832,6 @@ msgstr "" ...@@ -15835,9 +15832,6 @@ msgstr ""
msgid "MergeRequests|Thread will be unresolved" msgid "MergeRequests|Thread will be unresolved"
msgstr "" msgstr ""
msgid "MergeRequests|Toggle comments for this file"
msgstr ""
msgid "MergeRequests|View file @ %{commitId}" msgid "MergeRequests|View file @ %{commitId}"
msgstr "" msgstr ""
...@@ -23414,6 +23408,9 @@ msgstr "" ...@@ -23414,6 +23408,9 @@ msgstr ""
msgid "Show comments" msgid "Show comments"
msgstr "" msgstr ""
msgid "Show comments on this file"
msgstr ""
msgid "Show comments only" msgid "Show comments only"
msgstr "" msgstr ""
......
...@@ -26,10 +26,12 @@ RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork ...@@ -26,10 +26,12 @@ RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork
visit project_merge_request_path(target_project, merge_request) visit project_merge_request_path(target_project, merge_request)
click_link 'Changes' click_link 'Changes'
wait_for_requests wait_for_requests
within first('.js-file-title') do
find('[data-testid="edit_file"]').click page.within(first('.js-file-title')) do
click_link 'Edit in single-file editor' find('.js-diff-more-actions').click
find('.js-edit-blob').click
end end
wait_for_requests wait_for_requests
end end
......
...@@ -34,7 +34,8 @@ RSpec.describe 'User comments on a diff', :js do ...@@ -34,7 +34,8 @@ RSpec.describe 'User comments on a diff', :js do
page.within('.diff-files-holder > div:nth-child(3)') do page.within('.diff-files-holder > div:nth-child(3)') do
expect(page).to have_content('Line is wrong') expect(page).to have_content('Line is wrong')
find('.js-btn-vue-toggle-comments').click find('.js-diff-more-actions').click
click_button 'Hide comments on this file'
expect(page).not_to have_content('Line is wrong') expect(page).not_to have_content('Line is wrong')
end end
...@@ -67,7 +68,8 @@ RSpec.describe 'User comments on a diff', :js do ...@@ -67,7 +68,8 @@ RSpec.describe 'User comments on a diff', :js do
# Hide the comment. # Hide the comment.
page.within('.diff-files-holder > div:nth-child(3)') do page.within('.diff-files-holder > div:nth-child(3)') do
find('.js-btn-vue-toggle-comments').click find('.js-diff-more-actions').click
click_button 'Hide comments on this file'
expect(page).not_to have_content('Line is wrong') expect(page).not_to have_content('Line is wrong')
end end
...@@ -80,7 +82,8 @@ RSpec.describe 'User comments on a diff', :js do ...@@ -80,7 +82,8 @@ RSpec.describe 'User comments on a diff', :js do
# Show the comment. # Show the comment.
page.within('.diff-files-holder > div:nth-child(3)') do page.within('.diff-files-holder > div:nth-child(3)') do
find('.js-btn-vue-toggle-comments').click find('.js-diff-more-actions').click
click_button 'Show comments on this file'
end end
# Now both the comments should be shown. # Now both the comments should be shown.
......
...@@ -63,7 +63,7 @@ RSpec.describe 'Merge request > User sees diff', :js do ...@@ -63,7 +63,7 @@ RSpec.describe 'Merge request > User sees diff', :js do
visit diffs_project_merge_request_path(project, merge_request) visit diffs_project_merge_request_path(project, merge_request)
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
expect(page).to have_selector("[id=\"#{changelog_id}\"] [data-testid='edit_file']") expect(page).to have_selector("[id=\"#{changelog_id}\"] .js-edit-blob", visible: false)
end end
end end
...@@ -73,7 +73,8 @@ RSpec.describe 'Merge request > User sees diff', :js do ...@@ -73,7 +73,8 @@ RSpec.describe 'Merge request > User sees diff', :js do
visit diffs_project_merge_request_path(project, merge_request) visit diffs_project_merge_request_path(project, merge_request)
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
find("[id=\"#{changelog_id}\"] [data-testid=\"edit_file\"").click find("[id=\"#{changelog_id}\"] .js-diff-more-actions").click
find("[id=\"#{changelog_id}\"] .js-edit-blob").click
expect(page).to have_selector('.js-fork-suggestion-button', count: 1) expect(page).to have_selector('.js-fork-suggestion-button', count: 1)
expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1) expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1)
......
...@@ -119,7 +119,8 @@ RSpec.describe 'User comments on a diff', :js do ...@@ -119,7 +119,8 @@ RSpec.describe 'User comments on a diff', :js do
it 'can add and remove suggestions from a batch' do it 'can add and remove suggestions from a batch' do
files.each_with_index do |file, index| files.each_with_index do |file, index|
page.within("[id='#{file[:hash]}']") do page.within("[id='#{file[:hash]}']") do
find("button[title='Show full file']").click find('.js-diff-more-actions').click
click_button 'Show full file'
wait_for_requests wait_for_requests
click_diff_line(find("[id='#{file[:line_code]}']")) click_diff_line(find("[id='#{file[:line_code]}']"))
......
...@@ -20,22 +20,14 @@ RSpec.describe 'Editing file blob', :js do ...@@ -20,22 +20,14 @@ RSpec.describe 'Editing file blob', :js do
sign_in(user) sign_in(user)
end end
def edit_and_commit(commit_changes: true) def edit_and_commit(commit_changes: true, is_diff: false)
wait_for_requests wait_for_requests
find('.js-edit-blob').click
fill_and_commit(commit_changes) if is_diff
first('.js-diff-more-actions').click
end end
def mr_edit_and_commit(commit_changes: true) first('.js-edit-blob').click
wait_for_requests
find('[data-testid="edit_file"]').click
click_link 'Edit in single-file editor'
fill_and_commit(commit_changes)
end
def fill_and_commit(commit_changes)
fill_editor(content: 'class NextFeature\\nend\\n') fill_editor(content: 'class NextFeature\\nend\\n')
if commit_changes if commit_changes
...@@ -51,7 +43,7 @@ RSpec.describe 'Editing file blob', :js do ...@@ -51,7 +43,7 @@ RSpec.describe 'Editing file blob', :js do
context 'from MR diff' do context 'from MR diff' do
before do before do
visit diffs_project_merge_request_path(project, merge_request) visit diffs_project_merge_request_path(project, merge_request)
mr_edit_and_commit edit_and_commit(is_diff: true)
end end
it 'returns me to the mr' do it 'returns me to the mr' do
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { GlIcon } from '@gitlab/ui';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import EditButton from '~/diffs/components/edit_button.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import diffDiscussionsMockData from '../mock_data/diff_discussions'; import diffDiscussionsMockData from '../mock_data/diff_discussions';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
...@@ -76,16 +74,7 @@ describe('DiffFileHeader component', () => { ...@@ -76,16 +74,7 @@ describe('DiffFileHeader component', () => {
const findReplacedFileButton = () => wrapper.find({ ref: 'replacedFileButton' }); const findReplacedFileButton = () => wrapper.find({ ref: 'replacedFileButton' });
const findViewFileButton = () => wrapper.find({ ref: 'viewButton' }); const findViewFileButton = () => wrapper.find({ ref: 'viewButton' });
const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' }); const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' });
const hasZDropdownMenuClass = () => wrapper.classes('gl-z-dropdown-menu!'); const findEditButton = () => wrapper.find({ ref: 'editButton' });
const findIconByName = iconName => {
const icons = wrapper.findAll(GlIcon).filter(w => w.props('name') === iconName);
if (icons.length === 0) return icons;
if (icons.length > 1) {
throw new Error(`Multiple icons found for ${iconName}`);
}
return icons.at(0);
};
const createComponent = props => { const createComponent = props => {
mockStoreConfig = cloneDeep(defaultMockStoreConfig); mockStoreConfig = cloneDeep(defaultMockStoreConfig);
...@@ -152,10 +141,6 @@ describe('DiffFileHeader component', () => { ...@@ -152,10 +141,6 @@ describe('DiffFileHeader component', () => {
expect(wrapper.find(ClipboardButton).exists()).toBe(true); expect(wrapper.find(ClipboardButton).exists()).toBe(true);
}); });
it('should not have z dropdown menu class', () => {
expect(hasZDropdownMenuClass()).toBe(false);
});
describe('for submodule', () => { describe('for submodule', () => {
const submoduleDiffFile = { const submoduleDiffFile = {
...diffFile, ...diffFile,
...@@ -208,16 +193,6 @@ describe('DiffFileHeader component', () => { ...@@ -208,16 +193,6 @@ describe('DiffFileHeader component', () => {
describe('for any file', () => { describe('for any file', () => {
const otherModes = Object.keys(diffViewerModes).filter(m => m !== 'mode_changed'); const otherModes = Object.keys(diffViewerModes).filter(m => m !== 'mode_changed');
it('when edit button emits showForkMessage event it is re-emitted', () => {
createComponent({
addMergeRequestButtons: true,
});
wrapper.find(EditButton).vm.$emit('showForkMessage');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().showForkMessage).toBeDefined();
});
});
it('for mode_changed file mode displays mode changes', () => { it('for mode_changed file mode displays mode changes', () => {
createComponent({ createComponent({
diffFile: { diffFile: {
...@@ -276,16 +251,16 @@ describe('DiffFileHeader component', () => { ...@@ -276,16 +251,16 @@ describe('DiffFileHeader component', () => {
}); });
it('should not render edit button', () => { it('should not render edit button', () => {
createComponent({ addMergeRequestButtons: false }); createComponent({ addMergeRequestButtons: false });
expect(wrapper.find(EditButton).exists()).toBe(false); expect(findEditButton().exists()).toBe(false);
}); });
}); });
describe('when addMergeRequestButtons is true', () => { describe('when addMergeRequestButtons is true', () => {
describe('without discussions', () => { describe('without discussions', () => {
it('renders a disabled toggle discussions button', () => { it('does not render a toggle discussions button', () => {
diffHasDiscussionsResultMock.mockReturnValue(false); diffHasDiscussionsResultMock.mockReturnValue(false);
createComponent({ addMergeRequestButtons: true }); createComponent({ addMergeRequestButtons: true });
expect(findToggleDiscussionsButton().attributes('disabled')).toBe('true'); expect(findToggleDiscussionsButton().exists()).toBe(false);
}); });
}); });
...@@ -293,7 +268,7 @@ describe('DiffFileHeader component', () => { ...@@ -293,7 +268,7 @@ describe('DiffFileHeader component', () => {
it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => { it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => {
diffHasDiscussionsResultMock.mockReturnValue(true); diffHasDiscussionsResultMock.mockReturnValue(true);
createComponent({ addMergeRequestButtons: true }); createComponent({ addMergeRequestButtons: true });
expect(findToggleDiscussionsButton().attributes('disabled')).toBeFalsy(); expect(findToggleDiscussionsButton().exists()).toBe(true);
findToggleDiscussionsButton().vm.$emit('click'); findToggleDiscussionsButton().vm.$emit('click');
expect( expect(
mockStoreConfig.modules.diffs.actions.toggleFileDiscussionWrappers, mockStoreConfig.modules.diffs.actions.toggleFileDiscussionWrappers,
...@@ -305,28 +280,7 @@ describe('DiffFileHeader component', () => { ...@@ -305,28 +280,7 @@ describe('DiffFileHeader component', () => {
createComponent({ createComponent({
addMergeRequestButtons: true, addMergeRequestButtons: true,
}); });
expect(wrapper.find(EditButton).exists()).toBe(true); expect(findEditButton().exists()).toBe(true);
});
describe('when edit button opens', () => {
beforeEach(async () => {
createComponent({ addMergeRequestButtons: true });
wrapper.find(EditButton).vm.$emit('open');
await wrapper.vm.$nextTick();
});
it('should add z dropdown menu class when edit button opens', async () => {
expect(hasZDropdownMenuClass()).toBe(true);
});
it('when closes again, should remove class', async () => {
wrapper.find(EditButton).vm.$emit('close');
await wrapper.vm.$nextTick();
expect(hasZDropdownMenuClass()).toBe(false);
});
}); });
describe('view on environment button', () => { describe('view on environment button', () => {
...@@ -360,7 +314,7 @@ describe('DiffFileHeader component', () => { ...@@ -360,7 +314,7 @@ describe('DiffFileHeader component', () => {
}); });
it('should not render edit button', () => { it('should not render edit button', () => {
expect(wrapper.find(EditButton).exists()).toBe(false); expect(findEditButton().exists()).toBe(false);
}); });
}); });
describe('with file blob', () => { describe('with file blob', () => {
...@@ -371,7 +325,7 @@ describe('DiffFileHeader component', () => { ...@@ -371,7 +325,7 @@ describe('DiffFileHeader component', () => {
addMergeRequestButtons: true, addMergeRequestButtons: true,
}); });
expect(findViewFileButton().attributes('href')).toBe(viewPath); expect(findViewFileButton().attributes('href')).toBe(viewPath);
expect(findViewFileButton().attributes('title')).toEqual( expect(findViewFileButton().text()).toEqual(
`View file @ ${diffFile.content_sha.substr(0, 8)}`, `View file @ ${diffFile.content_sha.substr(0, 8)}`,
); );
}); });
...@@ -401,21 +355,6 @@ describe('DiffFileHeader component', () => { ...@@ -401,21 +355,6 @@ describe('DiffFileHeader component', () => {
addMergeRequestButtons: true, addMergeRequestButtons: true,
}; };
it.each`
iconName | isShowingFullFile
${'doc-expand'} | ${false}
${'doc-changes'} | ${true}
`(
'shows $iconName when isShowingFullFile set to $isShowingFullFile',
({ iconName, isShowingFullFile }) => {
createComponent({
...fullyNotExpandedFileProps,
diffFile: { ...fullyNotExpandedFileProps.diffFile, isShowingFullFile },
});
expect(findIconByName(iconName).exists()).toBe(true);
},
);
it('renders expand to full file button if not showing full file already', () => { it('renders expand to full file button if not showing full file already', () => {
createComponent(fullyNotExpandedFileProps); createComponent(fullyNotExpandedFileProps);
expect(findExpandButton().exists()).toBe(true); expect(findExpandButton().exists()).toBe(true);
...@@ -481,7 +420,7 @@ describe('DiffFileHeader component', () => { ...@@ -481,7 +420,7 @@ describe('DiffFileHeader component', () => {
it('does not show edit button', () => { it('does not show edit button', () => {
createComponent({ diffFile: { ...diffFile, deleted_file: true } }); createComponent({ diffFile: { ...diffFile, deleted_file: true } });
expect(wrapper.find(EditButton).exists()).toBe(false); expect(findEditButton().exists()).toBe(false);
}); });
}); });
......
import { shallowMount, mount } from '@vue/test-utils';
import { GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlIcon } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import EditButton from '~/diffs/components/edit_button.vue';
jest.mock('lodash/uniqueId', () => (str = '') => `${str}fake`);
const TOOLTIP_ID = 'edit_button_tooltip_fake';
const EDIT_ITEM = {
href: 'test-path',
text: 'Edit in single-file editor',
};
const IDE_EDIT_ITEM = {
href: 'ide-test-path',
text: 'Edit in Web IDE',
};
describe('EditButton', () => {
let wrapper;
const createComponent = (props = {}, mountFn = shallowMount) => {
wrapper = mountFn(EditButton, {
propsData: {
editPath: EDIT_ITEM.href,
ideEditPath: IDE_EDIT_ITEM.href,
canCurrentUserFork: false,
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
afterEach(() => {
wrapper.destroy();
});
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
const findDropdown = () => wrapper.find(GlDeprecatedDropdown);
const parseDropdownItems = () =>
wrapper.findAll(GlDeprecatedDropdownItem).wrappers.map(x => ({
text: x.text(),
href: x.attributes('href'),
}));
const triggerShow = () => {
const event = new Event('');
jest.spyOn(event, 'preventDefault');
findDropdown().vm.$emit('show', event);
return event;
};
it.each`
props | expectedItems
${{}} | ${[EDIT_ITEM, IDE_EDIT_ITEM]}
${{ editPath: '' }} | ${[IDE_EDIT_ITEM]}
${{ ideEditPath: '' }} | ${[EDIT_ITEM]}
`('should render items with=$props', ({ props, expectedItems }) => {
createComponent(props);
expect(parseDropdownItems()).toEqual(expectedItems);
});
describe('with default', () => {
beforeEach(() => {
createComponent({}, mount);
});
it('does not have tooltip', () => {
expect(getTooltip()).toEqual({ id: TOOLTIP_ID, title: 'Edit file in...' });
});
it('shows pencil dropdown', () => {
expect(wrapper.find(GlIcon).props('name')).toBe('pencil');
expect(wrapper.find('.gl-dropdown-caret').exists()).toBe(true);
});
describe.each`
event | expectedEmit | expectedRootEmit
${'show'} | ${'open'} | ${[['bv::hide::tooltip', TOOLTIP_ID]]}
${'hide'} | ${'close'} | ${[]}
`('when dropdown emits $event', ({ event, expectedEmit, expectedRootEmit }) => {
let rootEmitSpy;
beforeEach(() => {
rootEmitSpy = jest.spyOn(wrapper.vm.$root, '$emit');
findDropdown().vm.$emit(event);
});
it(`emits ${expectedEmit}`, () => {
expect(wrapper.emitted(expectedEmit)).toEqual([[]]);
});
it(`emits root = ${JSON.stringify(expectedRootEmit)}`, () => {
expect(rootEmitSpy.mock.calls).toEqual(expectedRootEmit);
});
});
});
describe('with cant modify blob and can fork', () => {
beforeEach(() => {
createComponent({
canModifyBlob: false,
canCurrentUserFork: true,
});
});
it('when try to open, emits showForkMessage', () => {
expect(wrapper.emitted('showForkMessage')).toBeUndefined();
const event = triggerShow();
expect(wrapper.emitted('showForkMessage')).toEqual([[]]);
expect(event.preventDefault).toHaveBeenCalled();
expect(wrapper.emitted('open')).toBeUndefined();
});
});
describe('with editPath is falsey', () => {
beforeEach(() => {
createComponent({
editPath: '',
});
});
it('should disable dropdown', () => {
expect(findDropdown().attributes('disabled')).toBe('true');
});
it('should have tooltip', () => {
expect(getTooltip()).toEqual({
id: TOOLTIP_ID,
title: "Can't edit as source branch was deleted",
});
});
});
});
...@@ -139,50 +139,74 @@ describe('Diffs Module Getters', () => { ...@@ -139,50 +139,74 @@ describe('Diffs Module Getters', () => {
describe('diffHasExpandedDiscussions', () => { describe('diffHasExpandedDiscussions', () => {
it('returns true when one of the discussions is expanded', () => { it('returns true when one of the discussions is expanded', () => {
discussionMock1.expanded = false; const diffFile = {
parallel_diff_lines: [],
highlighted_diff_lines: [
{
discussions: [discussionMock, discussionMock],
discussionsExpanded: true,
},
],
};
expect( expect(getters.diffHasExpandedDiscussions(localState)(diffFile)).toEqual(true);
getters.diffHasExpandedDiscussions(localState, {
getDiffFileDiscussions: () => [discussionMock, discussionMock],
})(diffFileMock),
).toEqual(true);
}); });
it('returns false when there are no discussions', () => { it('returns false when there are no discussions', () => {
expect( const diffFile = {
getters.diffHasExpandedDiscussions(localState, { getDiffFileDiscussions: () => [] })( parallel_diff_lines: [],
diffFileMock, highlighted_diff_lines: [
), {
).toEqual(false); discussions: [],
discussionsExpanded: true,
},
],
};
expect(getters.diffHasExpandedDiscussions(localState)(diffFile)).toEqual(false);
}); });
it('returns false when no discussion is expanded', () => { it('returns false when no discussion is expanded', () => {
discussionMock.expanded = false; const diffFile = {
discussionMock1.expanded = false; parallel_diff_lines: [],
highlighted_diff_lines: [
{
discussions: [discussionMock, discussionMock],
discussionsExpanded: false,
},
],
};
expect( expect(getters.diffHasExpandedDiscussions(localState)(diffFile)).toEqual(false);
getters.diffHasExpandedDiscussions(localState, {
getDiffFileDiscussions: () => [discussionMock, discussionMock1],
})(diffFileMock),
).toEqual(false);
}); });
}); });
describe('diffHasDiscussions', () => { describe('diffHasDiscussions', () => {
it('returns true when getDiffFileDiscussions returns discussions', () => { it('returns true when getDiffFileDiscussions returns discussions', () => {
expect( const diffFile = {
getters.diffHasDiscussions(localState, { parallel_diff_lines: [],
getDiffFileDiscussions: () => [discussionMock], highlighted_diff_lines: [
})(diffFileMock), {
).toEqual(true); discussions: [discussionMock, discussionMock],
discussionsExpanded: false,
},
],
};
expect(getters.diffHasDiscussions(localState)(diffFile)).toEqual(true);
}); });
it('returns false when getDiffFileDiscussions returns no discussions', () => { it('returns false when getDiffFileDiscussions returns no discussions', () => {
expect( const diffFile = {
getters.diffHasDiscussions(localState, { parallel_diff_lines: [],
getDiffFileDiscussions: () => [], highlighted_diff_lines: [
})(diffFileMock), {
).toEqual(false); discussions: [],
discussionsExpanded: false,
},
],
};
expect(getters.diffHasDiscussions(localState)(diffFile)).toEqual(false);
}); });
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment