Commit 53eabc3c authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch...

Merge branch '254256-replace-bootstrap-modal-in-app-views-projects-_issuable_by_email-html-haml' into 'master'

Replace bootstrap modal in issuable_by_email HAML template

See merge request gitlab-org/gitlab!53599
parents cde39a7e 3c86a842
......@@ -160,7 +160,6 @@ linters:
- 'app/views/projects/_gitlab_import_modal.html.haml'
- 'app/views/projects/_home_panel.html.haml'
- 'app/views/projects/_import_project_pane.html.haml'
- 'app/views/projects/_issuable_by_email.html.haml'
- 'app/views/projects/_readme.html.haml'
- 'app/views/projects/artifacts/_artifact.html.haml'
- 'app/views/projects/artifacts/_tree_file.html.haml'
......
<script>
import {
GlButton,
GlModal,
GlModalDirective,
GlTooltipDirective,
GlSprintf,
GlLink,
GlFormInputGroup,
GlIcon,
} from '@gitlab/ui';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import { sprintf, __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
export default {
name: 'IssuableByEmail',
components: {
GlButton,
GlModal,
GlSprintf,
GlLink,
GlFormInputGroup,
GlIcon,
ModalCopyButton,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
},
inject: {
initialEmail: {
default: null,
},
issuableType: {
default: '',
},
emailsHelpPagePath: {
default: '',
},
quickActionsHelpPath: {
default: '',
},
markdownHelpPath: {
default: '',
},
resetPath: {
default: '',
},
},
data() {
return {
email: this.initialEmail,
// eslint-disable-next-line @gitlab/require-i18n-strings
issuableName: this.issuableType === 'issue' ? 'issue' : 'merge request',
};
},
computed: {
mailToLink() {
const subject = sprintf(__('Enter the %{name} title'), {
name: this.issuableName,
});
const body = sprintf(__('Enter the %{name} description'), {
name: this.issuableName,
});
// eslint-disable-next-line @gitlab/require-i18n-strings
return `mailto:${this.email}?subject=${subject}&body=${body}`;
},
},
methods: {
async resetIncomingEmailToken() {
try {
const {
data: { new_address: newAddress },
} = await axios.put(this.resetPath);
this.email = newAddress;
} catch {
this.$toast.show(__('There was an error when reseting email token.'), { type: 'error' });
}
},
cancelHandler() {
this.$refs.modal.hide();
},
},
modalId: 'issuable-email-modal',
};
</script>
<template>
<div>
<gl-button v-gl-modal="$options.modalId" variant="link" data-testid="issuable-email-modal-btn"
><gl-sprintf :message="__('Email a new %{name} to this project')"
><template #name>{{ issuableName }}</template></gl-sprintf
></gl-button
>
<gl-modal ref="modal" :modal-id="$options.modalId">
<template #modal-title>
<gl-sprintf :message="__('Create new %{name} by email')">
<template #name>{{ issuableName }}</template>
</gl-sprintf>
</template>
<p>
<gl-sprintf
:message="
__(
'You can create a new %{name} inside this project by sending an email to the following email address:',
)
"
>
<template #name>{{ issuableName }}</template>
</gl-sprintf>
</p>
<gl-form-input-group :value="email" readonly select-on-click class="gl-mb-4">
<template #append>
<modal-copy-button :text="email" :title="__('Copy')" :modal-id="$options.modalId" />
<gl-button
v-gl-tooltip.hover
:href="mailToLink"
:title="__('Send email')"
icon="mail"
data-testid="mail-to-btn"
/>
</template>
</gl-form-input-group>
<p>
<gl-sprintf
:message="
__(
'The subject will be used as the title of the new issue, and the message will be the description. %{quickActionsLinkStart}Quick actions%{quickActionsLinkEnd} and styling with %{markdownLinkStart}Markdown%{markdownLinkEnd} are supported.',
)
"
>
<template #quickActionsLink="{ content }">
<gl-link :href="quickActionsHelpPath" target="_blank">{{ content }}</gl-link>
</template>
<template #markdownLink="{ content }">
<gl-link :href="markdownHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p>
<gl-sprintf
:message="
__(
'This is a private email address %{helpIcon} generated just for you. Anyone who gets ahold of it can create issues or merge requests as if they were you. You should %{resetLinkStart}reset it%{resetLinkEnd} if that ever happens.',
)
"
>
<template #helpIcon>
<gl-link :href="emailsHelpPagePath" target="_blank"
><gl-icon class="gl-text-blue-600" name="question-o"
/></gl-link>
</template>
<template #resetLink="{ content }">
<gl-button
variant="link"
data-testid="incoming-email-token-reset"
@click="resetIncomingEmailToken"
>{{ content }}</gl-button
>
</template>
</gl-sprintf>
</p>
<template #modal-footer>
<gl-button category="secondary" @click="cancelHandler">{{ s__('Cancel') }}</gl-button>
</template>
</gl-modal>
</div>
</template>
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import IssuableByEmail from './components/issuable_by_email.vue';
Vue.use(GlToast);
export default () => {
const el = document.querySelector('.js-issueable-by-email');
if (!el) return null;
const {
initialEmail,
issuableType,
emailsHelpPagePath,
quickActionsHelpPath,
markdownHelpPath,
resetPath,
} = el.dataset;
return new Vue({
el,
provide: {
initialEmail,
issuableType,
emailsHelpPagePath,
quickActionsHelpPath,
markdownHelpPath,
resetPath,
},
render(h) {
return h(IssuableByEmail);
},
});
};
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from './flash';
import { s__, __ } from './locale';
import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar';
export default class IssuableIndex {
constructor(pagePrefix) {
issuableInitBulkUpdateSidebar.init(pagePrefix);
IssuableIndex.resetIncomingEmailToken();
}
static resetIncomingEmailToken() {
const $resetToken = $('.incoming-email-token-reset');
$resetToken.on('click', (e) => {
e.preventDefault();
$resetToken.text(s__('EmailToken|resetting...'));
axios
.put($resetToken.attr('href'))
.then(({ data }) => {
$('#issuable_email').val(data.new_address).focus();
$resetToken.text(s__('EmailToken|reset it'));
})
.catch(() => {
flash(__('There was an error when reseting email token.'));
$resetToken.text(s__('EmailToken|reset it'));
});
});
}
}
......@@ -9,6 +9,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
import initIssuablesList from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import initIssuableByEmail from '~/issuable/init_issuable_by_email';
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
......@@ -24,3 +25,4 @@ new UsersSelect();
initManualOrdering();
initIssuablesList();
initIssuableByEmail();
......@@ -6,6 +6,7 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
import initIssuableByEmail from '~/issuable/init_issuable_by_email';
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
......@@ -19,3 +20,5 @@ initFilteredSearch({
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initIssuableByEmail();
......@@ -108,18 +108,6 @@ ul.related-merge-requests > li {
}
}
.issuable-email-modal-btn {
padding: 0;
color: $blue-600;
background-color: transparent;
border: 0;
outline: 0;
&:hover {
text-decoration: underline;
}
}
.email-modal-input-group {
margin-bottom: 10px;
......
- name = issuable_type == 'issue' ? 'issue' : 'merge request'
.issuable-footer.text-center
%button.issuable-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issuable-email-modal" } }
Email a new #{name} to this project
#issuable-email-modal.modal.fade{ tabindex: "-1", role: "dialog" }
.modal-dialog{ role: "document" }
.modal-content
.modal-header
%h4.modal-title
Create new #{name} by email
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p
You can create a new #{name} inside this project by sending an email to the following email address:
.email-modal-input-group.input-group
= text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append
= clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard input-group-text btn-transparent d-none d-sm-block')
- if issuable_type == 'issue'
- enter_title_text = _('Enter the issue title')
- enter_description_text = _('Enter the issue description')
- else
- enter_title_text = _('Enter the merge request title')
- enter_description_text = _('Enter the merge request description')
= mail_to email, class: 'btn btn-clipboard btn-transparent',
subject: enter_title_text,
body: enter_description_text,
title: _('Send email'),
data: { toggle: 'tooltip', placement: 'bottom' } do
= sprite_icon('mail')
%p
= render 'by_email_description'
%p
This is a private email address
%span<
= link_to help_page_path('development/emails', anchor: 'email-namespace'), target: '_blank', rel: 'noopener', aria: { label: 'Learn more about incoming email addresses' } do
= sprite_icon('question-o')
generated just for you.
Anyone who gets ahold of it can create issues or merge requests as if they were you.
You should
= link_to 'reset it', new_issuable_address_project_path(@project, issuable_type: issuable_type), class: 'incoming-email-token-reset'
if that ever happens.
......@@ -3,6 +3,7 @@
- page_title _("Issues")
- new_issue_email = @project.new_issuable_address(current_user, 'issue')
- add_page_specific_style 'page_bundles/issues_list'
- issuable_type = 'issue'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues")
......@@ -24,7 +25,8 @@
.issues-holder
= render 'issues'
- if new_issue_email
= render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue'
.issuable-footer.text-center
.js-issueable-by-email{ data: { initial_email: new_issue_email, issuable_type: issuable_type, emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'), quick_actions_help_path: help_page_path('user/project/quick_actions'), markdown_help_path: help_page_path('user/markdown'), reset_path: new_issuable_address_project_path(@project, issuable_type: issuable_type) } }
- else
- new_project_issue_button_path = @project.archived? ? false : new_project_issue_path(@project)
= render 'shared/empty_states/issues', new_project_issue_button_path: new_project_issue_button_path, show_import_button: true
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
- merge_project = merge_request_source_project_for_project(@project)
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- issuable_type = 'merge_request'
- page_title _("Merge Requests")
- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request')
......@@ -21,6 +22,7 @@
.merge-requests-holder
= render 'merge_requests'
- if new_merge_request_email
= render 'projects/issuable_by_email', email: new_merge_request_email, issuable_type: 'merge_request'
.issuable-footer.text-center
.js-issueable-by-email{ data: { initial_email: new_merge_request_email, issuable_type: issuable_type, emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'), quick_actions_help_path: help_page_path('user/project/quick_actions'), markdown_help_path: help_page_path('user/markdown'), reset_path: new_issuable_address_project_path(@project, issuable_type: issuable_type) } }
- else
= render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
---
title: Replace bootstrap modal in issuable_by_email HAML template
merge_request: 53599
author:
type: changed
......@@ -8481,6 +8481,9 @@ msgstr ""
msgid "Create new"
msgstr ""
msgid "Create new %{name} by email"
msgstr ""
msgid "Create new Value Stream"
msgstr ""
......@@ -10863,6 +10866,9 @@ msgstr ""
msgid "Email Notification"
msgstr ""
msgid "Email a new %{name} to this project"
msgstr ""
msgid "Email address to use for Support Desk"
msgstr ""
......@@ -10935,12 +10941,6 @@ msgstr ""
msgid "EmailParticipantsWarning|and %{moreCount} more"
msgstr ""
msgid "EmailToken|reset it"
msgstr ""
msgid "EmailToken|resetting..."
msgstr ""
msgid "Emails"
msgstr ""
......@@ -11226,19 +11226,13 @@ msgstr ""
msgid "Enter one or more user ID separated by commas"
msgstr ""
msgid "Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes."
msgstr ""
msgid "Enter the issue description"
msgstr ""
msgid "Enter the issue title"
msgid "Enter the %{name} description"
msgstr ""
msgid "Enter the merge request description"
msgid "Enter the %{name} title"
msgstr ""
msgid "Enter the merge request title"
msgid "Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes."
msgstr ""
msgid "Enter the name of your application, and we'll return a unique %{type}."
......@@ -29323,6 +29317,9 @@ msgstr ""
msgid "The status of the table below only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan."
msgstr ""
msgid "The subject will be used as the title of the new issue, and the message will be the description. %{quickActionsLinkStart}Quick actions%{quickActionsLinkEnd} and styling with %{markdownLinkStart}Markdown%{markdownLinkEnd} are supported."
msgstr ""
msgid "The tag name can't be changed for an existing release."
msgstr ""
......@@ -29929,6 +29926,9 @@ msgstr ""
msgid "This is a merge train pipeline"
msgstr ""
msgid "This is a private email address %{helpIcon} generated just for you. Anyone who gets ahold of it can create issues or merge requests as if they were you. You should %{resetLinkStart}reset it%{resetLinkEnd} if that ever happens."
msgstr ""
msgid "This is a security log of important events involving your account."
msgstr ""
......@@ -33355,6 +33355,9 @@ msgstr ""
msgid "You can create a new %{link}."
msgstr ""
msgid "You can create a new %{name} inside this project by sending an email to the following email address:"
msgstr ""
msgid "You can create a new Personal Access Token by visiting %{link}"
msgstr ""
......
......@@ -12,7 +12,7 @@ RSpec.describe 'Issues > User creates issue by email' do
project.add_developer(user)
end
describe 'new issue by email' do
describe 'new issue by email', :js do
shared_examples 'show the email in the modal' do
let(:issue) { create(:issue, project: project) }
......@@ -28,7 +28,7 @@ RSpec.describe 'Issues > User creates issue by email' do
page.within '#issuable-email-modal' do
email = project.new_issuable_address(user, 'issue')
expect(page).to have_selector("input[value='#{email}']")
expect(page.find('input[type="text"]').value).to eq email
end
end
end
......
......@@ -16,17 +16,17 @@ RSpec.describe 'Issues > User resets their incoming email token' do
end
it 'changes incoming email address token', :js do
find('.issuable-email-modal-btn').click
previous_token = find('input#issuable_email').value
find('.incoming-email-token-reset').click
wait_for_requests
expect(page).to have_no_field('issuable_email', with: previous_token)
new_token = project.new_issuable_address(user.reload, 'issue')
expect(page).to have_field(
'issuable_email',
with: new_token
)
page.find('[data-testid="issuable-email-modal-btn"]').click
page.within '#issuable-email-modal' do
previous_token = page.find('input[type="text"]').value
page.find('[data-testid="incoming-email-token-reset"]').click
wait_for_requests
expect(page.find('input[type="text"]').value).not_to eq previous_token
new_token = project.new_issuable_address(user.reload, 'issue')
expect(page.find('input[type="text"]').value).to eq new_token
end
end
end
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import { GlModal, GlSprintf, GlFormInputGroup, GlButton } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import httpStatus from '~/lib/utils/http_status';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
const initialEmail = 'user@gitlab.com';
const mockToastShow = jest.fn();
describe('IssuableByEmail', () => {
let wrapper;
let mockAxios;
let glModalDirective;
function createComponent(injectedProperties = {}) {
glModalDirective = jest.fn();
return extendedWrapper(
shallowMount(IssuableByEmail, {
stubs: {
GlModal,
GlSprintf,
GlFormInputGroup,
GlButton,
},
directives: {
glModal: {
bind(_, { value }) {
glModalDirective(value);
},
},
},
mocks: {
$toast: {
show: mockToastShow,
},
},
provide: {
issuableType: 'issue',
initialEmail,
...injectedProperties,
},
}),
);
}
beforeEach(() => {
mockAxios = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
mockAxios.restore();
});
const findFormInputGroup = () => wrapper.find(GlFormInputGroup);
const clickResetEmail = async () => {
wrapper.findByTestId('incoming-email-token-reset').vm.$emit('click');
await waitForPromises();
};
describe('modal button', () => {
it.each`
issuableType | buttonText
${'issue'} | ${'Email a new issue to this project'}
${'merge_request'} | ${'Email a new merge request to this project'}
`(
'renders a link with "$buttonText" when type is "$issuableType"',
({ issuableType, buttonText }) => {
wrapper = createComponent({ issuableType });
expect(wrapper.findByTestId('issuable-email-modal-btn').text()).toBe(buttonText);
},
);
it('opens the modal when the user clicks the button', () => {
wrapper = createComponent();
wrapper.findByTestId('issuable-email-modal-btn').vm.$emit('click');
expect(glModalDirective).toHaveBeenCalled();
});
});
describe('modal', () => {
it('renders a read-only email input field', () => {
wrapper = createComponent();
expect(findFormInputGroup().props('value')).toBe('user@gitlab.com');
});
it.each`
issuableType | subject | body
${'issue'} | ${'Enter the issue title'} | ${'Enter the issue description'}
${'merge_request'} | ${'Enter the merge request title'} | ${'Enter the merge request description'}
`('renders a mailto button when type is "$issuableType"', ({ issuableType, subject, body }) => {
wrapper = createComponent({
issuableType,
initialEmail,
});
expect(wrapper.findByTestId('mail-to-btn').attributes('href')).toBe(
`mailto:${initialEmail}?subject=${subject}&body=${body}`,
);
});
describe('reset email', () => {
const resetPath = 'gitlab-test/new_issuable_address?issuable_type=issue';
beforeEach(() => {
jest.spyOn(axios, 'put');
});
it('should send request to reset email token', async () => {
wrapper = createComponent({
issuableType: 'issue',
initialEmail,
resetPath,
});
await clickResetEmail();
expect(axios.put).toHaveBeenCalledWith(resetPath);
});
it('should update the email when the request succeeds', async () => {
mockAxios.onPut(resetPath).reply(httpStatus.OK, { new_address: 'foo@bar.com' });
wrapper = createComponent({
issuableType: 'issue',
initialEmail,
resetPath,
});
await clickResetEmail();
expect(findFormInputGroup().props('value')).toBe('foo@bar.com');
});
it('should show a toast message when the request fails', async () => {
mockAxios.onPut(resetPath).reply(httpStatus.NOT_FOUND, {});
wrapper = createComponent({
issuableType: 'issue',
initialEmail,
resetPath,
});
await clickResetEmail();
expect(mockToastShow).toHaveBeenCalledWith(
'There was an error when reseting email token.',
{ type: 'error' },
);
expect(findFormInputGroup().props('value')).toBe('user@gitlab.com');
});
});
});
});
import $ from 'jquery';
import MockAdaptor from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IssuableIndex from '~/issuable_index';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
......@@ -22,43 +19,4 @@ describe('Issuable', () => {
expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeDefined();
});
});
describe('resetIncomingEmailToken', () => {
let mock;
beforeEach(() => {
const element = document.createElement('a');
element.classList.add('incoming-email-token-reset');
element.setAttribute('href', 'foo');
document.body.appendChild(element);
const input = document.createElement('input');
input.setAttribute('id', 'issuable_email');
document.body.appendChild(input);
new IssuableIndex('issue_'); // eslint-disable-line no-new
mock = new MockAdaptor(axios);
mock.onPut('foo').reply(200, {
new_address: 'testing123',
});
});
afterEach(() => {
mock.restore();
});
it('should send request to reset email token', (done) => {
jest.spyOn(axios, 'put');
document.querySelector('.incoming-email-token-reset').click();
setImmediate(() => {
expect(axios.put).toHaveBeenCalledWith('foo');
expect($('#issuable_email').val()).toBe('testing123');
done();
});
});
});
});
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