Merge pull request '[BUG] Render emojis in labels in issue info popup' (#2888) from gusted/forgejo-emoji-popup into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2888
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Gusted 2024-03-30 11:20:57 +00:00
commit ec459b23c8
6 changed files with 221 additions and 13 deletions

View file

@ -3010,3 +3010,7 @@ tbody.commit-list {
margin-top: -1px;
border-top: 1px solid var(--color-secondary);
}
#issue-info-popup .emoji {
font-size: inherit;
line-height: inherit;
}

View file

@ -0,0 +1,39 @@
import {mount, flushPromises} from '@vue/test-utils';
import ContextPopup from './ContextPopup.vue';
test('renders a issue info popup', async () => {
const owner = 'user2';
const repo = 'repo1';
const index = 1;
vi.spyOn(global, 'fetch').mockResolvedValue({
json: vi.fn().mockResolvedValue({
ok: true,
created_at: '2023-09-30T19:00:00Z',
repository: {full_name: owner},
pull_request: null,
state: 'open',
title: 'Normal issue',
body: 'Lorem ipsum...',
number: index,
labels: [{color: 'ee0701', name: "Bug :+1: <script class='evil'>alert('Oh no!');</script>"}],
}),
ok: true,
});
const wrapper = mount(ContextPopup);
wrapper.vm.$el.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}}));
await flushPromises();
// Header
expect(wrapper.get('p:nth-of-type(1)').text()).toEqual('user2 on Sep 30, 2023');
// Title
expect(wrapper.get('p:nth-of-type(2)').text()).toEqual('Normal issue #1');
// Body
expect(wrapper.get('p:nth-of-type(3)').text()).toEqual('Lorem ipsum...');
// Check that the state is correct.
expect(wrapper.get('svg').classes()).toContain('octicon-issue-opened');
// Ensure that script is not an element.
expect(() => wrapper.get('.evil')).toThrowError();
// Check content of label
expect(wrapper.get('.ui.label').text()).toContain("Bug 👍 <script class='evil'>alert('Oh no!');</script>");
});

View file

@ -3,6 +3,8 @@ import {SvgIcon} from '../svg.js';
import {useLightTextOnBackground} from '../utils/color.js';
import tinycolor from 'tinycolor2';
import {GET} from '../modules/fetch.js';
import {emojiHTML} from '../features/emoji.js';
import {htmlEscape} from 'escape-goat';
const {appSubUrl, i18n} = window.config;
@ -67,6 +69,10 @@ export default {
} else {
textColor = '#111111';
}
label.name = htmlEscape(label.name);
label.name = label.name.replaceAll(/:[-+\w]+:/g, (emoji) => {
return emojiHTML(emoji.substring(1, emoji.length - 1));
});
return {name: label.name, color: `#${label.color}`, textColor};
});
},
@ -104,19 +110,13 @@ export default {
<template>
<div ref="root">
<div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/>
<div v-if="!loading && issue !== null">
<div v-if="!loading && issue !== null" id="issue-info-popup">
<p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
<p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>
<p>{{ body }}</p>
<div>
<div
v-for="label in labels"
:key="label.name"
class="ui label"
:style="{ color: label.textColor, backgroundColor: label.color }"
>
{{ label.name }}
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-for="label in labels" :key="label.name" class="ui label" :style="{ color: label.textColor, backgroundColor: label.color }" v-html="label.name"/>
</div>
</div>
<div v-if="!loading && issue === null">

View file

@ -4,6 +4,7 @@ window.config = {
csrfToken: 'test-csrf-token-123456',
pageData: {},
i18n: {},
customEmojis: {},
appSubUrl: '',
mentionValues: [
{key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'},