Commit 75044a7f authored by Brett Walker's avatar Brett Walker Committed by sstern

Add copy email to issue sidebar

Add ability for a user to copy email
to clipboard from the sidebar
parent cf4abe87
<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.
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
import Translate from '../vue_shared/translate';
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
import createDefaultClient from '~/lib/graphql';
import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
......@@ -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) {
mountAssigneesComponent(mediator);
mountReviewersComponent(mediator);
......@@ -279,6 +295,7 @@ export function mountSidebar(mediator) {
mountLockComponent();
mountParticipantsComponent(mediator);
mountSubscriptionsComponent(mediator);
mountCopyEmailComponent();
new SidebarMoveIssue(
mediator,
......
......@@ -58,6 +58,19 @@
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 {
......
......@@ -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
- issuable_type = issuable_sidebar[:type]
- show_forwarding_email = !issuable_sidebar[:create_note_email].nil?
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
......@@ -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 }
= 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)
.block.js-sidebar-move-issue-block
.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:
- **12.** [Participants](#participants)
- **13.** [Notifications](#notifications)
- **14.** [Reference](#reference)
- [Issue email](#email)
- **15.** [Edit](#edit)
- **16.** [Description](#description)
- **17.** [Mentions](#mentions)
......@@ -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
`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
Clicking this icon opens the issue for editing. All the fields which
......
......@@ -7899,6 +7899,9 @@ msgstr ""
msgid "Copy commit SHA"
msgstr ""
msgid "Copy email address"
msgstr ""
msgid "Copy environment"
msgstr ""
......@@ -24309,6 +24312,9 @@ msgstr ""
msgid "Revoked project access token %{project_access_token_name}!"
msgstr ""
msgid "RightSidebar|Issue email: %{copyText}"
msgstr ""
msgid "RightSidebar|adding a"
msgstr ""
......
......@@ -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;') }
before do
sign_in(user)
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
end
context 'when concerning the assignee', :js do
let(:user2) { create(:user) }
let(:issue2) { create(:issue, project: project, author: user2) }
include_examples 'issuable invite members experiments' do
let(:issuable_path) { project_issue_path(project, issue2) }
context 'when signed in' do
before do
sign_in(user)
end
context 'when user is a developer' do
before do
project.add_developer(user)
visit_issue(project, issue2)
find('.block.assignee .edit-link').click
context 'when concerning the assignee', :js do
let(:user2) { create(:user) }
let(:issue2) { create(:issue, project: project, author: user2) }
wait_for_requests
include_examples 'issuable invite members experiments' do
let(:issuable_path) { project_issue_path(project, issue2) }
end
it 'shows author in assignee dropdown' do
page.within '.dropdown-menu-user' do
expect(page).to have_content(user2.name)
end
end
context 'when user is a developer' do
before do
project.add_developer(user)
visit_issue(project, issue2)
it 'shows author when filtering assignee dropdown' do
page.within '.dropdown-menu-user' do
find('.dropdown-input-field').set(user2.name)
find('.block.assignee .edit-link').click
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
it 'assigns yourself' do
find('.block.assignee .dropdown-menu-toggle').click
it 'shows author when filtering assignee dropdown' do
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
expect(page.find('.dropdown-header')).to be_visible
expect(page.find('.dropdown-menu-user-link.is-active')).to have_content(user.name)
end
end
click_button 'assign yourself'
it 'keeps your filtered term after filtering and dismissing the dropdown' do
find('.dropdown-input-field').set(user2.name)
wait_for_requests
wait_for_requests
find('.block.assignee .edit-link').click
page.within '.dropdown-menu-user' do
expect(page).not_to have_content 'Unassigned'
click_link user2.name
page.within '.dropdown-menu-user' do
expect(page.find('.dropdown-header')).to be_visible
expect(page.find('.dropdown-menu-user-link.is-active')).to have_content(user.name)
end
end
find('.js-right-sidebar').click
find('.block.assignee .edit-link').click
expect(page.all('.dropdown-menu-user li').length).to eq(1)
expect(find('.dropdown-input-field').value).to eq(user2.name)
end
end
it 'keeps your filtered term after filtering and dismissing the dropdown' do
find('.dropdown-input-field').set(user2.name)
it 'shows label text as "Apply" when assignees are changed' do
project.add_developer(user)
visit_issue(project, issue2)
wait_for_requests
find('.block.assignee .edit-link').click
wait_for_requests
page.within '.dropdown-menu-user' do
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')
end
end
expect(page.all('.dropdown-menu-user li').length).to eq(1)
expect(find('.dropdown-input-field').value).to eq(user2.name)
end
end
context 'as a allowed user' do
before do
project.add_developer(user)
visit_issue(project, issue)
end
it 'shows label text as "Apply" when assignees are changed' do
project.add_developer(user)
visit_issue(project, issue2)
context 'sidebar', :js do
it 'changes size when the screen size is smaller' do
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
find('.block.assignee .edit-link').click
wait_for_requests
it 'escapes XSS when viewing issue labels' do
page.within('.block.labels') do
click_on 'Edit'
click_on 'Unassigned'
expect(page).to have_content '<script>alert("xss");</script>'
end
expect(page).to have_link('Apply')
end
end
context 'editing issue labels', :js do
context 'as a allowed user' do
before do
issue.update(labels: [label])
page.within('.block.labels') do
click_on 'Edit'
end
project.add_developer(user)
visit_issue(project, issue)
end
it 'shows the current set of labels' do
page.within('.issuable-show-labels') do
expect(page).to have_content label.title
context 'sidebar', :js do
it 'changes size when the screen size is smaller' do
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
it 'shows option to create a project label' do
page.within('.block.labels') do
expect(page).to have_content 'Create project'
it 'escapes XSS when viewing issue labels' do
page.within('.block.labels') do
click_on 'Edit'
expect(page).to have_content '<script>alert("xss");</script>'
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
issue.update(labels: [label])
page.within('.block.labels') do
click_link 'Create project'
click_on 'Edit'
end
end
it 'shows dropdown switches to "create label" section' do
page.within('.block.labels') do
expect(page).to have_content 'Create project label'
it 'shows the current set of labels' do
page.within('.issuable-show-labels') do
expect(page).to have_content label.title
end
end
it 'adds new label' do
it 'shows option to create a project 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
expect(page).to have_content 'Create project'
end
end
page.within('.dropdown-page-one') do
expect(page).to have_content 'wontfix'
context 'creating a project label', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/27992' do
before do
page.within('.block.labels') do
click_link 'Create project'
end
end
end
it 'shows error message if label title is taken' do
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
it 'shows dropdown switches to "create label" section' do
page.within('.block.labels') do
expect(page).to have_content 'Create project label'
end
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
expect(page).to have_content 'Title has already been taken'
it 'shows error message if label title is taken' do
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
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'
confidentiality_sidebar_block = '.block.confidentiality'
lock_sidebar_block = '.block.lock'
collapsed_sidebar_block_icon = '.sidebar-collapsed-icon'
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'
confidentiality_sidebar_block = '.block.confidentiality'
lock_sidebar_block = '.block.lock'
collapsed_sidebar_block_icon = '.sidebar-collapsed-icon'
before do
resize_screen_sm
end
before do
resize_screen_sm
end
it 'confidentiality block expands then collapses sidebar' do
expect(page).to have_css(collapsed_sidebar_selector)
it 'confidentiality block expands then collapses sidebar' do
expect(page).to have_css(collapsed_sidebar_selector)
page.within(confidentiality_sidebar_block) do
find(collapsed_sidebar_block_icon).click
page.within(confidentiality_sidebar_block) do
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
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
page.find('button', text: 'Cancel').click
expect(page).to have_css(collapsed_sidebar_selector)
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
it 'lock block expands then collapses sidebar' do
expect(page).to have_css(collapsed_sidebar_selector)
it 'does not have a option to edit labels' do
expect(page).not_to have_selector('.block.labels .js-sidebar-dropdown-toggle')
end
page.within(lock_sidebar_block) do
find(collapsed_sidebar_block_icon).click
context 'sidebar', :js do
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
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
page.find('button', text: 'Cancel').click
before do
resize_screen_sm
end
expect(page).to have_css(collapsed_sidebar_selector)
end
end
end
it 'expands then does not show the lock dialog form' do
expect(page).to have_css(collapsed_sidebar_selector)
context 'as a guest' do
before do
project.add_guest(user)
visit_issue(project, issue)
end
page.within(lock_sidebar_block) do
find(collapsed_sidebar_block_icon).click
end
it 'does not have a option to edit labels' do
expect(page).not_to have_selector('.block.labels .js-sidebar-dropdown-toggle')
expect(page).to have_css(expanded_sidebar_selector)
expect(page).not_to have_selector(lock_button)
end
end
end
end
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'
context 'when not signed in' do
context 'sidebar', :js do
before do
resize_screen_sm
visit_issue(project, issue)
end
it 'expands then does not show the lock dialog form' 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)
expect(page).not_to have_selector(lock_button)
it 'does not find issue email' do
expect(page).not_to have_selector('[data-qa-selector="copy-forward-email"]')
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 { GlButton } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import initCopyToClipboard from '~/behaviors/copy_to_clipboard';
describe('clipboard button', () => {
let wrapper;
......@@ -87,4 +88,25 @@ describe('clipboard button', () => {
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