Commit 51ea80b0 authored by Simon Knox's avatar Simon Knox

Merge branch '289921-add-copy-issue-url-button-to-vulnerability-error-message' into 'master'

Add copy issue URL button to vulnerability error message

See merge request gitlab-org/gitlab!76215
parents e0eb631f 46c207eb
......@@ -66,6 +66,11 @@ export default {
required: false,
default: 'medium',
},
variant: {
type: String,
required: false,
default: 'default',
},
},
computed: {
clipboardText() {
......@@ -92,6 +97,7 @@ export default {
:size="size"
icon="copy-to-clipboard"
:aria-label="__('Copy this value')"
:variant="variant"
v-on="$listeners"
>
<slot></slot>
......
-# We currently only support `alert`, `notice`, `success`, 'toast'
-# We currently only support `alert`, `notice`, `success`, 'toast', and 'raw'
- icons = {'alert' => 'error', 'notice' => 'information-o', 'success' => 'check-circle'}
.flash-container.flash-container-page.sticky{ data: { qa_selector: 'flash_container' } }
- flash.each do |key, value|
- if key == 'toast' && value
.js-toast-message{ data: { message: value } }
- elsif key == 'raw' && value
= value
- elsif value == I18n.t('devise.failure.unconfirmed')
= render 'shared/confirm_your_email_alert'
- elsif value
......
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
components: { GlAlert, GlSprintf, GlLink, ClipboardButton },
props: {
vulnerabilityLink: {
type: String,
required: true,
},
},
data() {
return {
isVisible: true,
};
},
methods: {
hideAlert() {
this.isVisible = false;
},
},
i18n: {
alertTitle: __('Unable to create link to vulnerability'),
alertMessage: __(
'Manually link this issue by adding it to the linked issue section of the %{linkStart}originating vulnerability%{linkEnd}.',
),
clipboardButtonText: __('Copy issue URL to clipboard'),
},
currentUrl: window.location.href,
};
</script>
<template>
<gl-alert
v-if="isVisible"
variant="danger"
:title="$options.i18n.alertTitle"
class="gl-mt-4"
@dismiss="hideAlert"
>
<p>
<gl-sprintf :message="$options.i18n.alertMessage">
<template #link="{ content }">
<gl-link :href="vulnerabilityLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<clipboard-button
:text="$options.currentUrl"
:title="$options.i18n.clipboardButtonText"
category="primary"
variant="confirm"
>
{{ $options.i18n.clipboardButtonText }}
</clipboard-button>
</gl-alert>
</template>
import Vue from 'vue';
import App from './components/unable_to_link_vulnerability_error.vue';
export default () => {
const el = document.querySelector('#js-unable-to-link-vulnerability');
if (!el) return null;
const { vulnerabilityLink } = el.dataset;
return new Vue({
el,
render: (h) =>
h(App, {
props: { vulnerabilityLink },
}),
});
};
......@@ -3,12 +3,14 @@ import { store } from '~/notes/stores';
import initShow from '~/issues/show';
import initRelatedIssues from '~/related_issues';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initUnableToLinkVulnerabilityError from 'ee/issues/init_unable_to_link_vulnerability_error';
import UserCallout from '~/user_callout';
initShow();
initSidebarBundle(store);
initRelatedIssues();
initRelatedFeatureFlags();
initUnableToLinkVulnerabilityError();
// eslint-disable-next-line no-new
new UserCallout({ className: 'js-epics-sidebar-callout' });
......
......@@ -59,12 +59,7 @@ module EE
vulnerability_issue_feedback_params(issue, vulnerability)
).execute
errors = []
result[:message].full_messages.each do |error|
errors << render_vulnerability_link_alert(error)
end
flash[:alert] = errors.join('<br\>').html_safe unless errors.blank?
flash[:raw] = render_vulnerability_link_alert.html_safe unless result[:message].errors.blank?
end
def vulnerability
......@@ -103,12 +98,11 @@ module EE
)
end
def render_vulnerability_link_alert(error_message)
def render_vulnerability_link_alert
render_to_string(
partial: 'vulnerabilities/unable_to_link_vulnerability',
locals: {
vulnerability_link: vulnerability_path(vulnerability),
error_message: error_message
vulnerability_link: vulnerability_path(vulnerability)
}
)
end
......
%span.gl-alert-title
= _('Unable to create link to vulnerability')
.gl-alert-body
= error_message
%br
- originating_vulnerability_link = link_to _('originating vulnerability'), vulnerability_link
= _('Manually link this issue by adding it to the linked issue section of the %{originating_vulnerability}.').html_safe % { originating_vulnerability: originating_vulnerability_link }
#js-unable-to-link-vulnerability{ data: { vulnerability_link: vulnerability_link } }
......@@ -137,7 +137,9 @@ RSpec.describe Projects::IssuesController do
it 'shows an error message' do
send_request
expect(flash[:alert]).to include('Unable to create link to vulnerability')
expect(flash[:raw]).to include('id="js-unable-to-link-vulnerability"')
expect(flash[:raw]).to include("data-vulnerability-link=\"/#{namespace.path}/#{project.path}/-/security/vulnerabilities/#{vulnerabilities_issue_link.vulnerability.id}\"")
expect(vulnerability.issue_links.map(&:issue)).to eq([vulnerabilities_issue_link.issue])
end
end
......
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import UnableToLinkVulnerabilityError from 'ee/issues/components/unable_to_link_vulnerability_error.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('Unable To Link Vulnerability Error component', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
const createWrapper = ({ vulnerabilityLink = '' } = {}) => {
wrapper = shallowMount(UnableToLinkVulnerabilityError, {
propsData: {
vulnerabilityLink,
},
stubs: { GlSprintf },
});
};
afterEach(() => {
wrapper.destroy();
});
it('shows the vulnerability link', () => {
const vulnerabilityLink = 'https://gitlab.com';
createWrapper({ vulnerabilityLink });
expect(wrapper.html()).toContain(vulnerabilityLink);
});
it('hides the error when the alert is dismissed', async () => {
createWrapper();
findAlert().vm.$emit('dismiss');
await nextTick();
expect(findAlert().exists()).toBe(false);
});
it('passes the expected props to the ClipboardButton component', () => {
createWrapper();
expect(wrapper.findComponent(ClipboardButton).props()).toMatchObject({
category: 'primary',
variant: 'confirm',
text: window.location.href,
});
});
});
......@@ -9664,6 +9664,9 @@ msgstr ""
msgid "Copy file path"
msgstr ""
msgid "Copy issue URL to clipboard"
msgstr ""
msgid "Copy key"
msgstr ""
......@@ -21528,7 +21531,7 @@ msgstr ""
msgid "ManualOrdering|Couldn't save the order of the issues"
msgstr ""
msgid "Manually link this issue by adding it to the linked issue section of the %{originating_vulnerability}."
msgid "Manually link this issue by adding it to the linked issue section of the %{linkStart}originating vulnerability%{linkEnd}."
msgstr ""
msgid "Map a FogBugz account ID to a GitLab user"
......@@ -42438,9 +42441,6 @@ msgstr ""
msgid "or"
msgstr ""
msgid "originating vulnerability"
msgstr ""
msgid "other card matches"
msgstr ""
......
......@@ -34,6 +34,7 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
text="foo/bar/dummy.md"
title="Copy file path"
tooltipplacement="top"
variant="default"
/>
</div>
`;
......@@ -37,6 +37,7 @@ exports[`publish_method renders 1`] = `
text="b83d6e391c22777fca1ed3012fce84f633d7fed0"
title="Copy commit SHA"
tooltipplacement="top"
variant="default"
/>
</div>
`;
......@@ -37,6 +37,7 @@ exports[`publish_method renders 1`] = `
text="sha-baz"
title="Copy commit SHA"
tooltipplacement="top"
variant="default"
/>
</div>
`;
......@@ -99,6 +99,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
text="123456789"
title="Copy commit SHA"
tooltipplacement="top"
variant="default"
/>
</gl-button-group-stub>
</div>
......@@ -209,6 +210,7 @@ exports[`Repository last commit component renders the signature HTML as returned
text="123456789"
title="Copy commit SHA"
tooltipplacement="top"
variant="default"
/>
</gl-button-group-stub>
</div>
......
......@@ -89,6 +89,16 @@ describe('clipboard button', () => {
expect(onClick).toHaveBeenCalled();
});
it('passes the category and variant props to the GlButton', () => {
const category = 'tertiary';
const variant = 'confirm';
createWrapper({ title: '', text: '', category, variant });
expect(findButton().props('category')).toBe(category);
expect(findButton().props('variant')).toBe(variant);
});
describe('integration', () => {
it('actually copies to clipboard', () => {
initCopyToClipboard();
......
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