Commit d654bd28 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'ss/copy-clipboard-email' into 'master'

Copy forward email address in issue sidebar

See merge request gitlab-org/gitlab!50127
parents 8589324e 75044a7f
<script>
import { s__, __, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
i18n: {
copyEmail: __('Copy email address'),
},
components: {
ClipboardButton,
},
props: {
copyText: {
type: String,
required: true,
},
},
computed: {
emailText() {
return sprintf(s__('RightSidebar|Issue email: %{copyText}'), { copyText: this.copyText });
},
},
};
</script>
<template>
<div
data-qa-selector="copy-forward-email"
class="copy-email-address gl-display-flex gl-align-items-center gl-justify-content-space-between"
>
<span
class="gl-overflow-hidden gl-text-overflow-ellipsis gl-white-space-nowrap hide-collapsed gl-w-85p"
>{{ emailText }}</span
>
<clipboard-button
class="copy-email-button gl-bg-none!"
category="tertiary"
:title="$options.i18n.copyEmail"
:text="copyText"
tooltip-placement="left"
/>
</div>
</template>
...@@ -12,6 +12,7 @@ import sidebarParticipants from './components/participants/sidebar_participants. ...@@ -12,6 +12,7 @@ import sidebarParticipants from './components/participants/sidebar_participants.
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue'; import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue'; import SidebarSeverity from './components/severity/sidebar_severity.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils'; import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -272,6 +273,21 @@ function mountSeverityComponent() { ...@@ -272,6 +273,21 @@ function mountSeverityComponent() {
}); });
} }
function mountCopyEmailComponent() {
const el = document.getElementById('issuable-copy-email');
if (!el) return;
const { createNoteEmail } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
render: (createElement) =>
createElement(CopyEmailToClipboard, { props: { copyText: createNoteEmail } }),
});
}
export function mountSidebar(mediator) { export function mountSidebar(mediator) {
mountAssigneesComponent(mediator); mountAssigneesComponent(mediator);
mountReviewersComponent(mediator); mountReviewersComponent(mediator);
...@@ -279,6 +295,7 @@ export function mountSidebar(mediator) { ...@@ -279,6 +295,7 @@ export function mountSidebar(mediator) {
mountLockComponent(); mountLockComponent();
mountParticipantsComponent(mediator); mountParticipantsComponent(mediator);
mountSubscriptionsComponent(mediator); mountSubscriptionsComponent(mediator);
mountCopyEmailComponent();
new SidebarMoveIssue( new SidebarMoveIssue(
mediator, mediator,
......
...@@ -58,6 +58,19 @@ ...@@ -58,6 +58,19 @@
height: $gl-padding; height: $gl-padding;
} }
} }
.copy-email-button { // TODO: replace with utility
@include gl-w-full;
@include gl-h-full;
}
.copy-email-address {
height: 60px;
&:hover {
background: $gray-100;
}
}
} }
.right-sidebar-expanded { .right-sidebar-expanded {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
This should be removed when this sidebar is converted to Vue since assignee data is also available in the `issuable_sidebar` hash This should be removed when this sidebar is converted to Vue since assignee data is also available in the `issuable_sidebar` hash
- issuable_type = issuable_sidebar[:type] - issuable_type = issuable_sidebar[:type]
- show_forwarding_email = !issuable_sidebar[:create_note_email].nil?
- signed_in = !!issuable_sidebar.dig(:current_user, :id) - signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit) - can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras" - add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
...@@ -145,6 +146,9 @@ ...@@ -145,6 +146,9 @@
= _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<cite class='ref-name' title='#{source_branch}'>".html_safe, source_branch_close: "</cite>".html_safe, source_branch: source_branch } = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<cite class='ref-name' title='#{source_branch}'>".html_safe, source_branch_close: "</cite>".html_safe, source_branch: source_branch }
= clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport') = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport')
- if show_forwarding_email
.block
#issuable-copy-email
- if issuable_sidebar.dig(:current_user, :can_move) - if issuable_sidebar.dig(:current_user, :can_move)
.block.js-sidebar-move-issue-block .block.js-sidebar-move-issue-block
.sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') } .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
......
---
title: Add copy email to issue sidebar
merge_request: 50127
author:
type: added
...@@ -35,6 +35,7 @@ The numbers in the image correspond to the following features: ...@@ -35,6 +35,7 @@ The numbers in the image correspond to the following features:
- **12.** [Participants](#participants) - **12.** [Participants](#participants)
- **13.** [Notifications](#notifications) - **13.** [Notifications](#notifications)
- **14.** [Reference](#reference) - **14.** [Reference](#reference)
- [Issue email](#email)
- **15.** [Edit](#edit) - **15.** [Edit](#edit)
- **16.** [Description](#description) - **16.** [Description](#description)
- **17.** [Mentions](#mentions) - **17.** [Mentions](#mentions)
...@@ -174,6 +175,12 @@ for the issue. Notifications are automatically enabled after you participate in ...@@ -174,6 +175,12 @@ for the issue. Notifications are automatically enabled after you participate in
`foo/bar#xxx`, where `foo` is the `username` or `groupname`, `bar` is the `foo/bar#xxx`, where `foo` is the `username` or `groupname`, `bar` is the
`project-name`, and `xxx` is the issue number. `project-name`, and `xxx` is the issue number.
### Email
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18816) in GitLab 13.8.
Guest users can see a button to copy the email address for the issue. Sending an email to this address creates a comment containing the email body.
### Edit ### Edit
Clicking this icon opens the issue for editing. All the fields which Clicking this icon opens the issue for editing. All the fields which
......
...@@ -7899,6 +7899,9 @@ msgstr "" ...@@ -7899,6 +7899,9 @@ msgstr ""
msgid "Copy commit SHA" msgid "Copy commit SHA"
msgstr "" msgstr ""
msgid "Copy email address"
msgstr ""
msgid "Copy environment" msgid "Copy environment"
msgstr "" msgstr ""
...@@ -24309,6 +24312,9 @@ msgstr "" ...@@ -24309,6 +24312,9 @@ msgstr ""
msgid "Revoked project access token %{project_access_token_name}!" msgid "Revoked project access token %{project_access_token_name}!"
msgstr "" msgstr ""
msgid "RightSidebar|Issue email: %{copyText}"
msgstr ""
msgid "RightSidebar|adding a" msgid "RightSidebar|adding a"
msgstr "" msgstr ""
......
...@@ -13,256 +13,280 @@ RSpec.describe 'Issue Sidebar' do ...@@ -13,256 +13,280 @@ RSpec.describe 'Issue Sidebar' do
let!(:xss_label) { create(:label, project: project, title: '&lt;script&gt;alert("xss");&lt;&#x2F;script&gt;') } let!(:xss_label) { create(:label, project: project, title: '&lt;script&gt;alert("xss");&lt;&#x2F;script&gt;') }
before do before do
sign_in(user) stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
end end
context 'when concerning the assignee', :js do context 'when signed in' do
let(:user2) { create(:user) } before do
let(:issue2) { create(:issue, project: project, author: user2) } sign_in(user)
include_examples 'issuable invite members experiments' do
let(:issuable_path) { project_issue_path(project, issue2) }
end end
context 'when user is a developer' do context 'when concerning the assignee', :js do
before do let(:user2) { create(:user) }
project.add_developer(user) let(:issue2) { create(:issue, project: project, author: user2) }
visit_issue(project, issue2)
find('.block.assignee .edit-link').click
wait_for_requests include_examples 'issuable invite members experiments' do
let(:issuable_path) { project_issue_path(project, issue2) }
end end
it 'shows author in assignee dropdown' do context 'when user is a developer' do
page.within '.dropdown-menu-user' do before do
expect(page).to have_content(user2.name) project.add_developer(user)
end visit_issue(project, issue2)
end
it 'shows author when filtering assignee dropdown' do find('.block.assignee .edit-link').click
page.within '.dropdown-menu-user' do
find('.dropdown-input-field').set(user2.name)
wait_for_requests wait_for_requests
end
expect(page).to have_content(user2.name) it 'shows author in assignee dropdown' do
page.within '.dropdown-menu-user' do
expect(page).to have_content(user2.name)
end
end end
end
it 'assigns yourself' do it 'shows author when filtering assignee dropdown' do
find('.block.assignee .dropdown-menu-toggle').click page.within '.dropdown-menu-user' do
find('.dropdown-input-field').set(user2.name)
click_button 'assign yourself' wait_for_requests
wait_for_requests expect(page).to have_content(user2.name)
end
end
find('.block.assignee .edit-link').click it 'assigns yourself' do
find('.block.assignee .dropdown-menu-toggle').click
page.within '.dropdown-menu-user' do click_button 'assign yourself'
expect(page.find('.dropdown-header')).to be_visible
expect(page.find('.dropdown-menu-user-link.is-active')).to have_content(user.name)
end
end
it 'keeps your filtered term after filtering and dismissing the dropdown' do wait_for_requests
find('.dropdown-input-field').set(user2.name)
wait_for_requests find('.block.assignee .edit-link').click
page.within '.dropdown-menu-user' do page.within '.dropdown-menu-user' do
expect(page).not_to have_content 'Unassigned' expect(page.find('.dropdown-header')).to be_visible
click_link user2.name expect(page.find('.dropdown-menu-user-link.is-active')).to have_content(user.name)
end
end end
find('.js-right-sidebar').click it 'keeps your filtered term after filtering and dismissing the dropdown' do
find('.block.assignee .edit-link').click find('.dropdown-input-field').set(user2.name)
expect(page.all('.dropdown-menu-user li').length).to eq(1)
expect(find('.dropdown-input-field').value).to eq(user2.name)
end
end
it 'shows label text as "Apply" when assignees are changed' do wait_for_requests
project.add_developer(user)
visit_issue(project, issue2)
find('.block.assignee .edit-link').click page.within '.dropdown-menu-user' do
wait_for_requests expect(page).not_to have_content 'Unassigned'
click_link user2.name
end
click_on 'Unassigned' find('.js-right-sidebar').click
find('.block.assignee .edit-link').click
expect(page).to have_link('Apply') expect(page.all('.dropdown-menu-user li').length).to eq(1)
end expect(find('.dropdown-input-field').value).to eq(user2.name)
end end
end
context 'as a allowed user' do it 'shows label text as "Apply" when assignees are changed' do
before do project.add_developer(user)
project.add_developer(user) visit_issue(project, issue2)
visit_issue(project, issue)
end
context 'sidebar', :js do find('.block.assignee .edit-link').click
it 'changes size when the screen size is smaller' do wait_for_requests
sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
# Resize the window
resize_screen_sm
# Make sure the sidebar is collapsed
find(sidebar_selector)
expect(page).to have_css(sidebar_selector)
# Once is collapsed let's open the sidebard and reload
open_issue_sidebar
refresh
find(sidebar_selector)
expect(page).to have_css(sidebar_selector)
# Restore the window size as it was including the sidebar
restore_window_size
open_issue_sidebar
end
it 'escapes XSS when viewing issue labels' do click_on 'Unassigned'
page.within('.block.labels') do
click_on 'Edit'
expect(page).to have_content '<script>alert("xss");</script>' expect(page).to have_link('Apply')
end
end end
end end
context 'editing issue labels', :js do context 'as a allowed user' do
before do before do
issue.update(labels: [label]) project.add_developer(user)
page.within('.block.labels') do visit_issue(project, issue)
click_on 'Edit'
end
end end
it 'shows the current set of labels' do context 'sidebar', :js do
page.within('.issuable-show-labels') do it 'changes size when the screen size is smaller' do
expect(page).to have_content label.title sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
# Resize the window
resize_screen_sm
# Make sure the sidebar is collapsed
find(sidebar_selector)
expect(page).to have_css(sidebar_selector)
# Once is collapsed let's open the sidebard and reload
open_issue_sidebar
refresh
find(sidebar_selector)
expect(page).to have_css(sidebar_selector)
# Restore the window size as it was including the sidebar
restore_window_size
open_issue_sidebar
end end
end
it 'shows option to create a project label' do it 'escapes XSS when viewing issue labels' do
page.within('.block.labels') do page.within('.block.labels') do
expect(page).to have_content 'Create project' click_on 'Edit'
expect(page).to have_content '<script>alert("xss");</script>'
end
end end
end end
context 'creating a project label', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/27992' do context 'editing issue labels', :js do
before do before do
issue.update(labels: [label])
page.within('.block.labels') do page.within('.block.labels') do
click_link 'Create project' click_on 'Edit'
end end
end end
it 'shows dropdown switches to "create label" section' do it 'shows the current set of labels' do
page.within('.block.labels') do page.within('.issuable-show-labels') do
expect(page).to have_content 'Create project label' expect(page).to have_content label.title
end end
end end
it 'adds new label' do it 'shows option to create a project label' do
page.within('.block.labels') do page.within('.block.labels') do
fill_in 'new_label_name', with: 'wontfix' expect(page).to have_content 'Create project'
page.find('.suggest-colors a', match: :first).click end
page.find('button', text: 'Create').click end
page.within('.dropdown-page-one') do context 'creating a project label', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/27992' do
expect(page).to have_content 'wontfix' before do
page.within('.block.labels') do
click_link 'Create project'
end end
end end
end
it 'shows error message if label title is taken' do it 'shows dropdown switches to "create label" section' do
page.within('.block.labels') do page.within('.block.labels') do
fill_in 'new_label_name', with: label.title expect(page).to have_content 'Create project label'
page.find('.suggest-colors a', match: :first).click end
page.find('button', text: 'Create').click end
it 'adds new label' do
page.within('.block.labels') do
fill_in 'new_label_name', with: 'wontfix'
page.find('.suggest-colors a', match: :first).click
page.find('button', text: 'Create').click
page.within('.dropdown-page-one') do
expect(page).to have_content 'wontfix'
end
end
end
page.within('.dropdown-page-two') do it 'shows error message if label title is taken' do
expect(page).to have_content 'Title has already been taken' page.within('.block.labels') do
fill_in 'new_label_name', with: label.title
page.find('.suggest-colors a', match: :first).click
page.find('button', text: 'Create').click
page.within('.dropdown-page-two') do
expect(page).to have_content 'Title has already been taken'
end
end end
end end
end end
end end
end
context 'interacting with collapsed sidebar', :js do context 'interacting with collapsed sidebar', :js do
collapsed_sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed' collapsed_sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
expanded_sidebar_selector = 'aside.right-sidebar.right-sidebar-expanded' expanded_sidebar_selector = 'aside.right-sidebar.right-sidebar-expanded'
confidentiality_sidebar_block = '.block.confidentiality' confidentiality_sidebar_block = '.block.confidentiality'
lock_sidebar_block = '.block.lock' lock_sidebar_block = '.block.lock'
collapsed_sidebar_block_icon = '.sidebar-collapsed-icon' collapsed_sidebar_block_icon = '.sidebar-collapsed-icon'
before do before do
resize_screen_sm resize_screen_sm
end end
it 'confidentiality block expands then collapses sidebar' do it 'confidentiality block expands then collapses sidebar' do
expect(page).to have_css(collapsed_sidebar_selector) expect(page).to have_css(collapsed_sidebar_selector)
page.within(confidentiality_sidebar_block) do page.within(confidentiality_sidebar_block) do
find(collapsed_sidebar_block_icon).click find(collapsed_sidebar_block_icon).click
end
expect(page).to have_css(expanded_sidebar_selector)
page.within(confidentiality_sidebar_block) do
page.find('button', text: 'Cancel').click
end
expect(page).to have_css(collapsed_sidebar_selector)
end end
expect(page).to have_css(expanded_sidebar_selector) it 'lock block expands then collapses sidebar' do
expect(page).to have_css(collapsed_sidebar_selector)
page.within(lock_sidebar_block) do
find(collapsed_sidebar_block_icon).click
end
expect(page).to have_css(expanded_sidebar_selector)
page.within(lock_sidebar_block) do
page.find('button', text: 'Cancel').click
end
page.within(confidentiality_sidebar_block) do expect(page).to have_css(collapsed_sidebar_selector)
page.find('button', text: 'Cancel').click
end end
end
end
expect(page).to have_css(collapsed_sidebar_selector) context 'as a guest' do
before do
project.add_guest(user)
visit_issue(project, issue)
end end
it 'lock block expands then collapses sidebar' do it 'does not have a option to edit labels' do
expect(page).to have_css(collapsed_sidebar_selector) expect(page).not_to have_selector('.block.labels .js-sidebar-dropdown-toggle')
end
page.within(lock_sidebar_block) do context 'sidebar', :js do
find(collapsed_sidebar_block_icon).click it 'finds issue copy forwarding email' do
expect(find('[data-qa-selector="copy-forward-email"]').text).to eq "Issue email: #{issue.creatable_note_email_address(user)}"
end end
end
expect(page).to have_css(expanded_sidebar_selector) context 'interacting with collapsed sidebar', :js do
collapsed_sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
expanded_sidebar_selector = 'aside.right-sidebar.right-sidebar-expanded'
lock_sidebar_block = '.block.lock'
lock_button = '.block.lock .btn-close'
collapsed_sidebar_block_icon = '.sidebar-collapsed-icon'
page.within(lock_sidebar_block) do before do
page.find('button', text: 'Cancel').click resize_screen_sm
end end
expect(page).to have_css(collapsed_sidebar_selector) it 'expands then does not show the lock dialog form' do
end expect(page).to have_css(collapsed_sidebar_selector)
end
end
context 'as a guest' do page.within(lock_sidebar_block) do
before do find(collapsed_sidebar_block_icon).click
project.add_guest(user) end
visit_issue(project, issue)
end
it 'does not have a option to edit labels' do expect(page).to have_css(expanded_sidebar_selector)
expect(page).not_to have_selector('.block.labels .js-sidebar-dropdown-toggle') expect(page).not_to have_selector(lock_button)
end
end
end end
end
context 'interacting with collapsed sidebar', :js do context 'when not signed in' do
collapsed_sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed' context 'sidebar', :js do
expanded_sidebar_selector = 'aside.right-sidebar.right-sidebar-expanded'
lock_sidebar_block = '.block.lock'
lock_button = '.block.lock .btn-close'
collapsed_sidebar_block_icon = '.sidebar-collapsed-icon'
before do before do
resize_screen_sm visit_issue(project, issue)
end end
it 'expands then does not show the lock dialog form' do it 'does not find issue email' do
expect(page).to have_css(collapsed_sidebar_selector) expect(page).not_to have_selector('[data-qa-selector="copy-forward-email"]')
page.within(lock_sidebar_block) do
find(collapsed_sidebar_block_icon).click
end
expect(page).to have_css(expanded_sidebar_selector)
expect(page).not_to have_selector(lock_button)
end end
end end
end end
......
import { mount } from '@vue/test-utils';
import { getByText } from '@testing-library/dom';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CopyEmailToClipboard from '~/sidebar/components/copy_email_to_clipboard.vue';
describe('CopyEmailToClipboard component', () => {
const sampleEmail = 'sample+email@test.com';
const wrapper = mount(CopyEmailToClipboard, {
propsData: {
copyText: sampleEmail,
},
});
it('renders the Issue email text with the forwardable email', () => {
expect(getByText(wrapper.element, `Issue email: ${sampleEmail}`)).not.toBeNull();
});
it('finds ClipboardButton with the correct props', () => {
expect(wrapper.find(ClipboardButton).props('text')).toBe(sampleEmail);
});
});
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import initCopyToClipboard from '~/behaviors/copy_to_clipboard';
describe('clipboard button', () => { describe('clipboard button', () => {
let wrapper; let wrapper;
...@@ -87,4 +88,25 @@ describe('clipboard button', () => { ...@@ -87,4 +88,25 @@ describe('clipboard button', () => {
expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalled();
}); });
describe('integration', () => {
it('actually copies to clipboard', () => {
initCopyToClipboard();
document.execCommand = () => {};
jest.spyOn(document, 'execCommand').mockImplementation(() => true);
createWrapper(
{
text: 'copy me',
title: 'Copy this value',
},
{ attachTo: document.body },
);
findButton().trigger('click');
expect(document.execCommand).toHaveBeenCalledWith('copy');
});
});
}); });
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