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 ""
......
...@@ -12,6 +12,11 @@ RSpec.describe 'Issue Sidebar' do ...@@ -12,6 +12,11 @@ RSpec.describe 'Issue Sidebar' do
let(:issue) { create(:labeled_issue, project: project, labels: [label]) } let(:issue) { create(:labeled_issue, project: project, labels: [label]) }
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
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
end
context 'when signed in' do
before do before do
sign_in(user) sign_in(user)
end end
...@@ -243,6 +248,12 @@ RSpec.describe 'Issue Sidebar' do ...@@ -243,6 +248,12 @@ RSpec.describe 'Issue Sidebar' do
expect(page).not_to have_selector('.block.labels .js-sidebar-dropdown-toggle') expect(page).not_to have_selector('.block.labels .js-sidebar-dropdown-toggle')
end end
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
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'
...@@ -266,6 +277,19 @@ RSpec.describe 'Issue Sidebar' do ...@@ -266,6 +277,19 @@ RSpec.describe 'Issue Sidebar' do
end end
end end
end end
end
context 'when not signed in' do
context 'sidebar', :js do
before do
visit_issue(project, issue)
end
it 'does not find issue email' do
expect(page).not_to have_selector('[data-qa-selector="copy-forward-email"]')
end
end
end
def visit_issue(project, issue) def visit_issue(project, issue)
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
......
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