Commit f61756d7 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 64bf537f c866f1f5
<script> <script>
import { GlDropdown, GlDropdownItem, GlDropdownText, GlSearchBoxByType } from '@gitlab/ui'; import {
GlAvatarLabeled,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlSearchBoxByType,
} from '@gitlab/ui';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import Api from '~/api'; import Api from '~/api';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -8,6 +14,7 @@ import { SEARCH_DELAY } from '../constants'; ...@@ -8,6 +14,7 @@ import { SEARCH_DELAY } from '../constants';
export default { export default {
name: 'GroupSelect', name: 'GroupSelect',
components: { components: {
GlAvatarLabeled,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownText, GlDropdownText,
...@@ -49,6 +56,7 @@ export default { ...@@ -49,6 +56,7 @@ export default {
id: group.id, id: group.id,
name: group.full_name, name: group.full_name,
path: group.path, path: group.path,
avatarUrl: group.avatar_url,
})); }));
this.isFetching = false; this.isFetching = false;
}) })
...@@ -82,7 +90,7 @@ export default { ...@@ -82,7 +90,7 @@ export default {
menu-class="gl-w-full!" menu-class="gl-w-full!"
> >
<gl-search-box-by-type <gl-search-box-by-type
v-model.trim="searchTerm" v-model="searchTerm"
:is-loading="isFetching" :is-loading="isFetching"
:placeholder="$options.i18n.searchPlaceholder" :placeholder="$options.i18n.searchPlaceholder"
data-qa-selector="group_select_dropdown_search_field" data-qa-selector="group_select_dropdown_search_field"
...@@ -93,7 +101,13 @@ export default { ...@@ -93,7 +101,13 @@ export default {
:name="group.name" :name="group.name"
@click="selectGroup(group)" @click="selectGroup(group)"
> >
{{ group.name }} <gl-avatar-labeled
:label="group.name"
:src="group.avatarUrl"
:entity-id="group.id"
:entity-name="group.name"
:size="32"
/>
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message"> <gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message">
<span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span> <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span>
......
...@@ -30,6 +30,7 @@ export default { ...@@ -30,6 +30,7 @@ export default {
return dateInWords(date); return dateInWords(date);
}, },
}, },
safeHtmlConfig: { ADD_ATTR: ['target'] },
}; };
</script> </script>
...@@ -71,7 +72,10 @@ export default { ...@@ -71,7 +72,10 @@ export default {
<gl-icon name="license" />{{ packageName }} <gl-icon name="license" />{{ packageName }}
</gl-badge> </gl-badge>
</div> </div>
<div v-safe-html="feature.body" class="gl-pt-3 gl-line-height-20"></div> <div
v-safe-html:[$options.safeHtmlConfig]="feature.body"
class="gl-pt-3 gl-line-height-20"
></div>
<gl-button <gl-button
:href="feature.url" :href="feature.url"
target="_blank" target="_blank"
......
...@@ -33,7 +33,7 @@ class ReleaseHighlight ...@@ -33,7 +33,7 @@ class ReleaseHighlight
next unless include_item?(item) next unless include_item?(item)
begin begin
item.tap {|i| i['body'] = Kramdown::Document.new(i['body']).to_html } item.tap {|i| i['body'] = Banzai.render(i['body'], { project: nil }) }
rescue StandardError => e rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, file_path: file_path) Gitlab::ErrorTracking.track_exception(e, file_path: file_path)
......
--- ---
- title: bright and sunshinin' day - title: bright and sunshinin' day
body: | body: |
## bright and sunshinin' day bright and sunshinin' [day](https://en.wikipedia.org/wiki/Day)
self-managed: true self-managed: true
gitlab-com: false gitlab-com: false
packages: ["Premium", "Ultimate"] packages: ["Premium", "Ultimate"]
......
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; import { GlAvatarLabeled, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api'; import Api from '~/api';
...@@ -8,8 +8,8 @@ const createComponent = () => { ...@@ -8,8 +8,8 @@ const createComponent = () => {
return mount(GroupSelect, {}); return mount(GroupSelect, {});
}; };
const group1 = { id: 1, full_name: 'Group One' }; const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' };
const group2 = { id: 2, full_name: 'Group Two' }; const group2 = { id: 2, full_name: 'Group Two', avatar_url: 'test' };
const allGroups = [group1, group2]; const allGroups = [group1, group2];
describe('GroupSelect', () => { describe('GroupSelect', () => {
...@@ -29,10 +29,10 @@ describe('GroupSelect', () => { ...@@ -29,10 +29,10 @@ describe('GroupSelect', () => {
const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="true"]'); const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="true"]');
const findDropdownItemByText = (text) => const findAvatarByLabel = (text) =>
wrapper wrapper
.findAllComponents(GlDropdownItem) .findAllComponents(GlAvatarLabeled)
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.text() === text); .wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('label') === text);
it('renders GlSearchBoxByType with default attributes', () => { it('renders GlSearchBoxByType with default attributes', () => {
expect(findSearchBoxByType().exists()).toBe(true); expect(findSearchBoxByType().exists()).toBe(true);
...@@ -74,9 +74,20 @@ describe('GroupSelect', () => { ...@@ -74,9 +74,20 @@ describe('GroupSelect', () => {
}); });
}); });
describe('avatar label', () => {
it('includes the correct attributes with name and avatar_url', () => {
expect(findAvatarByLabel(group1.full_name).attributes()).toMatchObject({
src: group1.avatar_url,
'entity-id': `${group1.id}`,
'entity-name': group1.full_name,
size: '32',
});
});
});
describe('when group is selected from the dropdown', () => { describe('when group is selected from the dropdown', () => {
beforeEach(() => { beforeEach(() => {
findDropdownItemByText(group1.full_name).vm.$emit('click'); findAvatarByLabel(group1.full_name).trigger('click');
}); });
it('emits `input` event used by `v-model`', () => { it('emits `input` event used by `v-model`', () => {
......
...@@ -8,7 +8,7 @@ describe("What's new single feature", () => { ...@@ -8,7 +8,7 @@ describe("What's new single feature", () => {
const exampleFeature = { const exampleFeature = {
title: 'Compliance pipeline configurations', title: 'Compliance pipeline configurations',
body: body:
'<p>We are thrilled to announce that it is now possible to define enforceable pipelines that will run for any project assigned a corresponding compliance framework.</p>', '<p data-testid="body-content">We are thrilled to announce that it is now possible to define enforceable pipelines that will run for any project assigned a corresponding <a href="https://en.wikipedia.org/wiki/Compliance_(psychology)" target="_blank" rel="noopener noreferrer" onload="alert(xss)">compliance</a> framework.</p>',
stage: 'Manage', stage: 'Manage',
'self-managed': true, 'self-managed': true,
'gitlab-com': true, 'gitlab-com': true,
...@@ -20,6 +20,7 @@ describe("What's new single feature", () => { ...@@ -20,6 +20,7 @@ describe("What's new single feature", () => {
}; };
const findReleaseDate = () => wrapper.find('[data-testid="release-date"]'); const findReleaseDate = () => wrapper.find('[data-testid="release-date"]');
const findBodyAnchor = () => wrapper.find('[data-testid="body-content"] a');
const createWrapper = ({ feature } = {}) => { const createWrapper = ({ feature } = {}) => {
wrapper = shallowMount(Feature, { wrapper = shallowMount(Feature, {
...@@ -43,4 +44,13 @@ describe("What's new single feature", () => { ...@@ -43,4 +44,13 @@ describe("What's new single feature", () => {
expect(findReleaseDate().exists()).toBe(false); expect(findReleaseDate().exists()).toBe(false);
}); });
}); });
it('safe-html config allows target attribute on elements', () => {
createWrapper({ feature: exampleFeature });
expect(findBodyAnchor().attributes()).toEqual({
href: expect.any(String),
rel: 'noopener noreferrer',
target: '_blank',
});
});
}); });
...@@ -67,12 +67,12 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do ...@@ -67,12 +67,12 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
expect(subject[:next_page]).to eq(2) expect(subject[:next_page]).to eq(2)
end end
it 'parses the body as markdown and returns html' do it 'parses the body as markdown and returns html, and links are target="_blank"' do
expect(subject[:items].first['body']).to match("<h2 id=\"bright-and-sunshinin-day\">bright and sunshinin’ day</h2>") expect(subject[:items].first['body']).to match('<p data-sourcepos="1:1-1:62" dir="auto">bright and sunshinin\' <a href="https://en.wikipedia.org/wiki/Day" rel="nofollow noreferrer noopener" target="_blank">day</a></p>')
end end
it 'logs an error if theres an error parsing markdown for an item, and skips it' do it 'logs an error if theres an error parsing markdown for an item, and skips it' do
allow(Kramdown::Document).to receive(:new).and_raise allow(Banzai).to receive(:render).and_raise
expect(Gitlab::ErrorTracking).to receive(:track_exception) expect(Gitlab::ErrorTracking).to receive(:track_exception)
expect(subject[:items]).to be_empty expect(subject[:items]).to be_empty
......
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