Commit 7d3608a9 authored by Doug Stull's avatar Doug Stull Committed by Mikołaj Wawrzyniak

Move commit dropdown into Vue

- follow pajamas design pattern.
parent fb9c1c99
<script>
import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlDropdownSectionHeader } from '@gitlab/ui';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '../constants';
import eventHub from '../event_hub';
export default {
components: {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlDropdownSectionHeader,
},
inject: {
newProjectTagPath: {
default: '',
},
emailPatchesPath: {
default: '',
},
plainDiffPath: {
default: '',
},
},
props: {
canRevert: {
type: Boolean,
required: true,
},
canCherryPick: {
type: Boolean,
required: true,
},
canTag: {
type: Boolean,
required: true,
},
canEmailPatches: {
type: Boolean,
required: true,
},
},
computed: {
showDivider() {
return this.canRevert || this.canCherryPick || this.canTag;
},
},
methods: {
showModal(modalId) {
eventHub.$emit(modalId);
},
},
openRevertModal: OPEN_REVERT_MODAL,
openCherryPickModal: OPEN_CHERRY_PICK_MODAL,
};
</script>
<template>
<gl-dropdown
:text="__('Options')"
right
data-testid="commit-options-dropdown"
data-qa-selector="options_button"
class="gl-xs-w-full"
>
<gl-dropdown-item
v-if="canRevert"
data-testid="revert-link"
@click="showModal($options.openRevertModal)"
>
{{ s__('ChangeTypeAction|Revert') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="canCherryPick"
data-testid="cherry-pick-link"
@click="showModal($options.openCherryPickModal)"
>
{{ s__('ChangeTypeAction|Cherry-pick') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="canTag" :href="newProjectTagPath" data-testid="tag-link">
{{ s__('CreateTag|Tag') }}
</gl-dropdown-item>
<gl-dropdown-divider v-if="showDivider" />
<gl-dropdown-section-header>
{{ __('Download') }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-if="canEmailPatches"
:href="emailPatchesPath"
download
rel="nofollow"
data-testid="email-patches-link"
data-qa-selector="email_patches"
>
{{ s__('DownloadCommit|Email Patches') }}
</gl-dropdown-item>
<gl-dropdown-item
:href="plainDiffPath"
download
rel="nofollow"
data-testid="plain-diff-link"
data-qa-selector="plain_diff"
>
{{ s__('DownloadCommit|Plain Diff') }}
</gl-dropdown-item>
</gl-dropdown>
</template>
<script>
import { GlLink } from '@gitlab/ui';
import eventHub from '../event_hub';
export default {
components: {
GlLink,
},
inject: {
displayText: {
default: '',
},
testId: {
default: '',
},
},
props: {
openModal: {
type: String,
required: true,
},
},
methods: {
showModal() {
eventHub.$emit(this.openModal);
},
},
};
</script>
<template>
<gl-link data-is-link="true" :data-testid="testId" @click="showModal">
{{ displayText }}
</gl-link>
</template>
......@@ -2,10 +2,8 @@ import { s__, __ } from '~/locale';
export const OPEN_REVERT_MODAL = 'openRevertModal';
export const REVERT_MODAL_ID = 'revert-commit-modal';
export const REVERT_LINK_TEST_ID = 'revert-commit-link';
export const OPEN_CHERRY_PICK_MODAL = 'openCherryPickModal';
export const CHERRY_PICK_MODAL_ID = 'cherry-pick-commit-modal';
export const CHERRY_PICK_LINK_TEST_ID = 'cherry-pick-commit-link';
export const I18N_MODAL = {
startMergeRequest: s__('ChangeTypeAction|Start a %{newMergeRequest} with these changes'),
......
import initCherryPickCommitModal from './init_cherry_pick_commit_modal';
import initCherryPickCommitTrigger from './init_cherry_pick_commit_trigger';
import initCommitOptionsDropdown from './init_commit_options_dropdown';
import initRevertCommitModal from './init_revert_commit_modal';
import initRevertCommitTrigger from './init_revert_commit_trigger';
export default () => {
initRevertCommitModal();
initRevertCommitTrigger();
initCherryPickCommitModal();
initCherryPickCommitTrigger();
initCommitOptionsDropdown();
};
import Vue from 'vue';
import CommitFormTrigger from './components/form_trigger.vue';
import { OPEN_CHERRY_PICK_MODAL, CHERRY_PICK_LINK_TEST_ID } from './constants';
export default function initInviteMembersTrigger() {
const el = document.querySelector('.js-cherry-pick-commit-trigger');
if (!el) {
return false;
}
const { displayText } = el.dataset;
return new Vue({
el,
provide: { displayText, testId: CHERRY_PICK_LINK_TEST_ID },
render: (createElement) =>
createElement(CommitFormTrigger, { props: { openModal: OPEN_CHERRY_PICK_MODAL } }),
});
}
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import CommitOptionsDropdown from './components/commit_options_dropdown.vue';
export default function initCommitOptionsDropdown() {
const el = document.querySelector('#js-commit-options-dropdown');
if (!el) {
return false;
}
const {
newProjectTagPath,
emailPatchesPath,
plainDiffPath,
canRevert,
canCherryPick,
canTag,
canEmailPatches,
} = el.dataset;
return new Vue({
el,
provide: { newProjectTagPath, emailPatchesPath, plainDiffPath },
render: (createElement) =>
createElement(CommitOptionsDropdown, {
props: {
canRevert: parseBoolean(canRevert),
canCherryPick: parseBoolean(canCherryPick),
canTag: parseBoolean(canTag),
canEmailPatches: parseBoolean(canEmailPatches),
},
}),
});
}
import Vue from 'vue';
import CommitFormTrigger from './components/form_trigger.vue';
import { OPEN_REVERT_MODAL, REVERT_LINK_TEST_ID } from './constants';
export default function initInviteMembersTrigger() {
const el = document.querySelector('.js-revert-commit-trigger');
if (!el) {
return false;
}
const { displayText } = el.dataset;
return new Vue({
el,
provide: { displayText, testId: REVERT_LINK_TEST_ID },
render: (createElement) =>
createElement(CommitFormTrigger, { props: { openModal: OPEN_REVERT_MODAL } }),
});
}
......@@ -12,25 +12,6 @@
}
}
.header-action-buttons {
i {
color: $gl-text-color-secondary;
font-size: 13px;
margin-right: 3px;
}
@include media-breakpoint-down(xs) {
.btn {
width: 100%;
margin-top: 10px;
}
.dropdown {
width: 100%;
}
}
}
.avatar {
@extend .avatar-inline;
margin-left: 0;
......
......@@ -86,18 +86,6 @@
padding: 10px 0 9px;
}
.header-action-buttons {
display: flex;
@include media-breakpoint-down(xs) {
.sidebar-toggle-btn {
margin-top: 0;
margin-left: 10px;
max-height: 34px;
}
}
}
.header-content {
a {
color: $gl-text-color;
......
......@@ -110,16 +110,18 @@ module CommitsHelper
end
end
def revert_commit_link
return unless current_user
def commit_options_dropdown_data(project, commit)
can_collaborate = current_user && can_collaborate_with_project?(project)
tag(:div, data: { display_text: 'Revert' }, class: "js-revert-commit-trigger")
end
def cherry_pick_commit_link
return unless current_user
tag(:div, data: { display_text: 'Cherry-pick' }, class: "js-cherry-pick-commit-trigger")
{
new_project_tag_path: new_project_tag_path(project, ref: commit),
email_patches_path: project_commit_path(project, commit, format: :patch),
plain_diff_path: project_commit_path(project, commit, format: :diff),
can_revert: "#{can_collaborate && !commit.has_been_reverted?(current_user)}",
can_cherry_pick: can_collaborate.to_s,
can_tag: can?(current_user, :push_code, project).to_s,
can_email_patches: (commit.parents.length < 2).to_s
}
end
def commit_signature_badge_classes(additional_classes)
......
- can_collaborate = can_collaborate_with_project?(@project)
.page-content-header
.header-main-content
= render partial: 'signature', object: @commit.signature
......@@ -20,36 +18,12 @@
= commit_committer_link(@commit, avatar: true, size: 24)
#{time_ago_with_tooltip(@commit.committed_date)}
.header-action-buttons
- if defined?(@notes_count) && @notes_count > 0
%span.btn.gl-button.btn-default.disabled.gl-button.btn-icon.d-none.d-sm-inline.gl-mr-3.has-tooltip{ title: n_("%d comment on this commit", "%d comments on this commit", @notes_count) % @notes_count }
= sprite_icon('comment')
= @notes_count
= link_to project_tree_path(@project, @commit), class: "btn gl-button btn-default gl-mr-3 d-none d-md-inline" do
#{ _('Browse files') }
.dropdown.inline
%a.btn.gl-button.btn-default.dropdown-toggle.qa-options-button.d-md-inline{ data: { toggle: "dropdown" } }
%span= _('Options')
= sprite_icon('chevron-down', css_class: 'gl-text-gray-500')
%ul.dropdown-menu.dropdown-menu-right
%li.d-block.d-sm-none
= link_to project_tree_path(@project, @commit) do
#{ _('Browse Files') }
- if can_collaborate && !@commit.has_been_reverted?(current_user)
%li.clearfix
= revert_commit_link
- if can_collaborate
%li.clearfix
= cherry_pick_commit_link
- if can?(current_user, :push_code, @project)
%li.clearfix
= link_to s_('CreateTag|Tag'), new_project_tag_path(@project, ref: @commit)
%li.divider
%li.dropdown-header
#{ _('Download') }
- unless @commit.parents.length > 1
%li= link_to s_('DownloadCommit|Email Patches'), project_commit_path(@project, @commit, format: :patch), class: "qa-email-patches", rel: 'nofollow', download: ''
%li= link_to s_('DownloadCommit|Plain Diff'), project_commit_path(@project, @commit, format: :diff), class: "qa-plain-diff", rel: 'nofollow', download: ''
= link_to _('Browse files'), project_tree_path(@project, @commit), class: "btn gl-button btn-default gl-mr-3 gl-xs-w-full gl-xs-mb-3"
#js-commit-options-dropdown{ data: commit_options_dropdown_data(@project, @commit) }
.commit-box{ data: { project_path: project_path(@project) } }
%h3.commit-title
......
---
title: Convert Commit dropdown to Vue
merge_request: 56142
author:
type: other
......@@ -6,10 +6,13 @@ module QA
module Commit
class Show < Page::Base
view 'app/views/projects/commit/_commit_box.html.haml' do
element :commit_sha_content
end
view 'app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue' do
element :options_button
element :email_patches
element :plain_diff
element :commit_sha_content
end
def select_email_patches
......
......@@ -91,7 +91,7 @@ RSpec.describe 'Cherry-pick Commits', :js do
context 'when the project is archived' do
let(:project) { create(:project, :repository, :archived, namespace: user.namespace) }
it 'does not show the cherry-pick link' do
it 'does not show the cherry-pick button' do
open_dropdown
expect(page).not_to have_text("Cherry-pick")
......@@ -106,12 +106,15 @@ RSpec.describe 'Cherry-pick Commits', :js do
end
def open_dropdown
find('.header-action-buttons .dropdown').click
find(dropdown_selector).click
end
def open_modal
open_dropdown
find('[data-testid="cherry-pick-commit-link"]').click
page.within(dropdown_selector) do
click_button 'Cherry-pick'
end
end
def submit_cherry_pick(create_merge_request: false)
......@@ -121,6 +124,10 @@ RSpec.describe 'Cherry-pick Commits', :js do
end
end
def dropdown_selector
'[data-testid="commit-options-dropdown"]'
end
def modal_selector
'[data-testid="modal-commit"]'
end
......
......@@ -62,10 +62,10 @@ RSpec.describe 'User reverts a commit', :js do
context 'when the project is archived' do
let(:project) { create(:project, :repository, :archived, namespace: user.namespace) }
it 'does not show the revert link' do
it 'does not show the revert button' do
open_dropdown
expect(page).not_to have_link('Revert')
expect(page).not_to have_button('Revert')
end
end
end
......@@ -75,17 +75,24 @@ RSpec.describe 'User reverts a commit', :js do
page.within(modal_selector) do
uncheck('create_merge_request') unless create_merge_request
click_button('Revert')
click_button 'Revert'
end
end
def open_dropdown
find('.header-action-buttons .dropdown').click
find(dropdown_selector).click
end
def open_modal
open_dropdown
find('[data-testid="revert-commit-link"]').click
page.within(dropdown_selector) do
click_button 'Revert'
end
end
def dropdown_selector
'[data-testid="commit-options-dropdown"]'
end
def modal_selector
......
......@@ -20,9 +20,14 @@ RSpec.describe 'User browses commits' do
.and have_content('Side-by-side')
end
it 'fill commit sha when click new tag from commit page' do
it 'fill commit sha when click new tag from commit page', :js do
dropdown_selector = '[data-testid="commit-options-dropdown"]'
visit project_commit_path(project, sample_commit.id)
find(dropdown_selector).click
page.within(dropdown_selector) do
click_link 'Tag'
end
expect(page).to have_selector("input[value='#{sample_commit.id}']", visible: false)
end
......
import { GlDropdownDivider, GlDropdownSectionHeader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CommitOptionsDropdown from '~/projects/commit/components/commit_options_dropdown.vue';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
import eventHub from '~/projects/commit/event_hub';
describe('BranchesDropdown', () => {
let wrapper;
const provide = {
newProjectTagPath: '_new_project_tag_path_',
emailPatchesPath: '_email_patches_path_',
plainDiffPath: '_plain_diff_path_',
};
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
shallowMount(CommitOptionsDropdown, {
provide,
propsData: {
canRevert: true,
canCherryPick: true,
canTag: true,
canEmailPatches: true,
...props,
},
}),
);
};
const findRevertLink = () => wrapper.findByTestId('revert-link');
const findCherryPickLink = () => wrapper.findByTestId('cherry-pick-link');
const findTagItem = () => wrapper.findByTestId('tag-link');
const findEmailPatchesItem = () => wrapper.findByTestId('email-patches-link');
const findPlainDiffItem = () => wrapper.findByTestId('plain-diff-link');
const findDivider = () => wrapper.findComponent(GlDropdownDivider);
const findSectionHeader = () => wrapper.findComponent(GlDropdownSectionHeader);
describe('Everything enabled', () => {
beforeEach(() => {
createComponent();
});
it('has expected dropdown button text', () => {
expect(wrapper.attributes('text')).toBe('Options');
});
it('has expected items', () => {
expect(
[
findRevertLink().exists(),
findCherryPickLink().exists(),
findTagItem().exists(),
findDivider().exists(),
findSectionHeader().exists(),
findEmailPatchesItem().exists(),
findPlainDiffItem().exists(),
].every((exists) => exists),
).toBe(true);
});
it('has expected href links', () => {
expect(findTagItem().attributes('href')).toBe(provide.newProjectTagPath);
expect(findEmailPatchesItem().attributes('href')).toBe(provide.emailPatchesPath);
expect(findPlainDiffItem().attributes('href')).toBe(provide.plainDiffPath);
});
});
describe('Different dropdown item permutations', () => {
it('does not have a revert option', () => {
createComponent({ canRevert: false });
expect(findRevertLink().exists()).toBe(false);
});
it('does not have a cherry-pick option', () => {
createComponent({ canCherryPick: false });
expect(findCherryPickLink().exists()).toBe(false);
});
it('does not have a tag option', () => {
createComponent({ canTag: false });
expect(findTagItem().exists()).toBe(false);
});
it('does not have a email patches options', () => {
createComponent({ canEmailPatches: false });
expect(findEmailPatchesItem().exists()).toBe(false);
});
it('only has the download items', () => {
createComponent({ canRevert: false, canCherryPick: false, canTag: false });
expect(findDivider().exists()).toBe(false);
expect(findEmailPatchesItem().exists()).toBe(true);
expect(findPlainDiffItem().exists()).toBe(true);
});
});
describe('Modal triggering', () => {
let spy;
beforeEach(() => {
spy = jest.spyOn(eventHub, '$emit');
createComponent();
});
it('emits openModal for revert', () => {
findRevertLink().vm.$emit('click');
expect(spy).toHaveBeenCalledWith(OPEN_REVERT_MODAL);
});
it('emits openModal for cherry-pick', () => {
findCherryPickLink().vm.$emit('click');
expect(spy).toHaveBeenCalledWith(OPEN_CHERRY_PICK_MODAL);
});
});
});
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import FormTrigger from '~/projects/commit/components/form_trigger.vue';
import eventHub from '~/projects/commit/event_hub';
const displayText = '_display_text_';
const createComponent = () => {
return shallowMount(FormTrigger, {
provide: { displayText },
propsData: { openModal: '_open_modal_' },
});
};
describe('FormTrigger', () => {
let wrapper;
let spy;
beforeEach(() => {
spy = jest.spyOn(eventHub, '$emit');
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findLink = () => wrapper.find(GlLink);
describe('displayText', () => {
it('includes the correct displayText for the link', () => {
expect(findLink().text()).toBe(displayText);
});
});
describe('clicking the link', () => {
it('emits openModal', () => {
findLink().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('_open_modal_');
});
});
});
......@@ -5,58 +5,6 @@ require 'spec_helper'
RSpec.describe CommitsHelper do
include ProjectForksHelper
describe '#revert_commit_link' do
context 'when current_user exists' do
before do
allow(helper).to receive(:current_user).and_return(double('User'))
end
it 'renders a div for Vue' do
result = helper.revert_commit_link
expect(result).to include('js-revert-commit-trigger')
end
end
context 'when current_user does not exist' do
before do
allow(helper).to receive(:current_user).and_return(nil)
end
it 'does not render anything' do
result = helper.revert_commit_link
expect(result).to be_nil
end
end
end
describe '#cherry_pick_commit_link' do
context 'when current_user exists' do
before do
allow(helper).to receive(:current_user).and_return(double('User'))
end
it 'renders a div for Vue' do
result = helper.cherry_pick_commit_link
expect(result).to include('js-cherry-pick-commit-trigger')
end
end
context 'when current_user does not exist' do
before do
allow(helper).to receive(:current_user).and_return(nil)
end
it 'does not render anything' do
result = helper.cherry_pick_commit_link
expect(result).to be_nil
end
end
end
describe 'commit_author_link' do
it 'escapes the author email' do
commit = double(
......@@ -268,4 +216,77 @@ RSpec.describe CommitsHelper do
end
end
end
describe "#commit_options_dropdown_data" do
let(:project) { build(:project, :repository) }
let(:commit) { build(:commit) }
let(:user) { build(:user) }
subject { helper.commit_options_dropdown_data(project, commit) }
context "when user is logged in" do
before do
allow(helper).to receive(:can?).with(user, :push_code, project).and_return(true)
allow(helper).to receive(:current_user).and_return(user)
end
it "returns data as expected" do
is_expected.to eq standard_expected_data
end
context "when can not collaborate on project" do
before do
allow(helper).to receive(:can_collaborate_with_project?).with(project).and_return(false)
end
it "returns data as expected" do
no_collaboration_values = {
can_revert: 'false',
can_cherry_pick: 'false'
}
is_expected.to eq standard_expected_data.merge(no_collaboration_values)
end
end
context "when commit has already been reverted" do
before do
allow(commit).to receive(:has_been_reverted?).with(user).and_return(true)
end
it "returns data as expected" do
is_expected.to eq standard_expected_data.merge({ can_revert: 'false' })
end
end
end
context "when user is not logged in" do
before do
allow(helper).to receive(:can?).with(nil, :push_code, project).and_return(false)
allow(helper).to receive(:current_user).and_return(nil)
end
it "returns data as expected" do
logged_out_values = {
can_revert: '',
can_cherry_pick: '',
can_tag: 'false'
}
is_expected.to eq standard_expected_data.merge(logged_out_values)
end
end
def standard_expected_data
{
new_project_tag_path: new_project_tag_path(project, ref: commit),
email_patches_path: project_commit_path(project, commit, format: :patch),
plain_diff_path: project_commit_path(project, commit, format: :diff),
can_revert: 'true',
can_cherry_pick: 'true',
can_tag: 'true',
can_email_patches: 'true'
}
end
end
end
......@@ -74,30 +74,4 @@ RSpec.describe 'projects/commit/_commit_box.html.haml' do
end
end
end
context 'viewing a commit' do
context 'as a developer' do
before do
allow(view).to receive(:can_collaborate_with_project?).and_return(true)
end
it 'has a link to create a new tag' do
render
expect(rendered).to have_link('Tag')
end
end
context 'as a non-developer' do
before do
project.add_guest(user)
end
it 'does not have a link to create a new tag' do
render
expect(rendered).not_to have_link('Tag')
end
end
end
end
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