Skip to content

Commit d26551b

Browse files
authored
Load issue/PR context popup data only when needed (#15955)
* Load issue/PR context popup data only when needed * Add SVG icon Vue component * Remove unneeded check
1 parent 3dba75f commit d26551b

File tree

3 files changed

+152
-55
lines changed

3 files changed

+152
-55
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<template>
2+
<div>
3+
<div v-if="loading" class="ui active centered inline loader"/>
4+
<div v-if="!loading && issue !== null">
5+
<p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
6+
<p><svg-icon :name="icon" :class="[color]" /> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>
7+
<p>{{ body }}</p>
8+
<div>
9+
<div
10+
v-for="label in labels"
11+
:key="label.name"
12+
class="ui label"
13+
:style="{ color: label.textColor, backgroundColor: label.color }"
14+
>
15+
{{ label.name }}
16+
</div>
17+
</div>
18+
</div>
19+
</div>
20+
</template>
21+
22+
<script>
23+
import {SvgIcon} from '../svg.js';
24+
25+
const {AppSubUrl} = window.config;
26+
27+
export default {
28+
name: 'ContextPopup',
29+
30+
components: {
31+
SvgIcon,
32+
},
33+
34+
data: () => ({
35+
loading: false,
36+
issue: null
37+
}),
38+
39+
computed: {
40+
createdAt() {
41+
return new Date(this.issue.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'});
42+
},
43+
44+
body() {
45+
const body = this.issue.body.replace(/\n+/g, ' ');
46+
if (body.length > 85) {
47+
return `${body.substring(0, 85)}`;
48+
}
49+
return body;
50+
},
51+
52+
icon() {
53+
if (this.issue.pull_request !== null) {
54+
if (this.issue.state === 'open') {
55+
return 'octicon-git-pull-request'; // Open PR
56+
} else if (this.issue.pull_request.merged === true) {
57+
return 'octicon-git-merge'; // Merged PR
58+
}
59+
return 'octicon-git-pull-request'; // Closed PR
60+
} else if (this.issue.state === 'open') {
61+
return 'octicon-issue-opened'; // Open Issue
62+
}
63+
return 'octicon-issue-closed'; // Closed Issue
64+
},
65+
66+
color() {
67+
if (this.issue.state === 'open') {
68+
return 'green';
69+
} else if (this.issue.pull_request !== null && this.issue.pull_request.merged === true) {
70+
return 'purple';
71+
}
72+
return 'red';
73+
},
74+
75+
labels() {
76+
return this.issue.labels.map((label) => {
77+
const red = parseInt(label.color.substring(0, 2), 16);
78+
const green = parseInt(label.color.substring(2, 4), 16);
79+
const blue = parseInt(label.color.substring(4, 6), 16);
80+
let color = '#ffffff';
81+
if ((red * 0.299 + green * 0.587 + blue * 0.114) > 125) {
82+
color = '#000000';
83+
}
84+
return {name: label.name, color: `#${label.color}`, textColor: color};
85+
});
86+
}
87+
},
88+
89+
mounted() {
90+
this.$root.$on('load-context-popup', (data, callback) => {
91+
if (!this.loading && this.issue === null) {
92+
this.load(data, callback);
93+
}
94+
});
95+
},
96+
97+
methods: {
98+
load(data, callback) {
99+
this.loading = true;
100+
$.get(`${AppSubUrl}/api/v1/repos/${data.owner}/${data.repo}/issues/${data.index}`, (issue) => {
101+
this.issue = issue;
102+
this.loading = false;
103+
this.$nextTick(() => {
104+
if (callback) {
105+
callback();
106+
}
107+
});
108+
});
109+
}
110+
}
111+
};
112+
</script>

web_src/js/features/contextpopup.js

Lines changed: 22 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,43 @@
1-
import {htmlEscape} from 'escape-goat';
2-
import {svg} from '../svg.js';
1+
import Vue from 'vue';
32

4-
const {AppSubUrl} = window.config;
3+
import ContextPopup from '../components/ContextPopup.vue';
54

65
export default function initContextPopups() {
76
const refIssues = $('.ref-issue');
87
if (!refIssues.length) return;
98

109
refIssues.each(function () {
1110
const [index, _issues, repo, owner] = $(this).attr('href').replace(/[#?].*$/, '').split('/').reverse();
12-
issuePopup(owner, repo, index, $(this));
13-
});
14-
}
1511

16-
function issuePopup(owner, repo, index, $element) {
17-
$.get(`${AppSubUrl}/api/v1/repos/${owner}/${repo}/issues/${index}`, (issue) => {
18-
const createdAt = new Date(issue.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'});
12+
const el = document.createElement('div');
13+
el.className = 'ui custom popup hidden';
14+
el.innerHTML = '<div></div>';
15+
this.parentNode.insertBefore(el, this.nextSibling);
1916

20-
let body = issue.body.replace(/\n+/g, ' ');
21-
if (body.length > 85) {
22-
body = `${body.substring(0, 85)}...`;
23-
}
17+
const View = Vue.extend({
18+
render: (createElement) => createElement(ContextPopup),
19+
});
2420

25-
let labels = '';
26-
for (let i = 0; i < issue.labels.length; i++) {
27-
const label = issue.labels[i];
28-
const red = parseInt(label.color.substring(0, 2), 16);
29-
const green = parseInt(label.color.substring(2, 4), 16);
30-
const blue = parseInt(label.color.substring(4, 6), 16);
31-
let color = '#ffffff';
32-
if ((red * 0.299 + green * 0.587 + blue * 0.114) > 125) {
33-
color = '#000000';
34-
}
35-
labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${htmlEscape(label.name)}</div>`;
36-
}
37-
if (labels.length > 0) {
38-
labels = `<p>${labels}</p>`;
39-
}
21+
const view = new View();
4022

41-
let octicon, color;
42-
if (issue.pull_request !== null) {
43-
if (issue.state === 'open') {
44-
color = 'green';
45-
octicon = 'octicon-git-pull-request'; // Open PR
46-
} else if (issue.pull_request.merged === true) {
47-
color = 'purple';
48-
octicon = 'octicon-git-merge'; // Merged PR
49-
} else {
50-
color = 'red';
51-
octicon = 'octicon-git-pull-request'; // Closed PR
52-
}
53-
} else if (issue.state === 'open') {
54-
color = 'green';
55-
octicon = 'octicon-issue-opened'; // Open Issue
56-
} else {
57-
color = 'red';
58-
octicon = 'octicon-issue-closed'; // Closed Issue
23+
try {
24+
view.$mount(el.firstChild);
25+
} catch (err) {
26+
console.error(err);
27+
el.textContent = 'ContextPopup failed to load';
5928
}
6029

61-
$element.popup({
30+
$(this).popup({
6231
variation: 'wide',
6332
delay: {
6433
show: 250
6534
},
66-
html: `
67-
<div>
68-
<p><small>${htmlEscape(issue.repository.full_name)} on ${createdAt}</small></p>
69-
<p><span class="${color}">${svg(octicon)}</span> <strong>${htmlEscape(issue.title)}</strong> #${index}</p>
70-
<p>${htmlEscape(body)}</p>
71-
${labels}
72-
</div>
73-
`
35+
onShow: () => {
36+
view.$emit('load-context-popup', {owner, repo, index}, () => {
37+
$(this).popup('reposition');
38+
});
39+
},
40+
popup: $(el),
7441
});
7542
});
7643
}

web_src/js/svg.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import octiconRepo from '../../public/img/svg/octicon-repo.svg';
1414
import octiconRepoForked from '../../public/img/svg/octicon-repo-forked.svg';
1515
import octiconRepoTemplate from '../../public/img/svg/octicon-repo-template.svg';
1616

17+
import Vue from 'vue';
18+
1719
export const svgs = {
1820
'octicon-chevron-down': octiconChevronDown,
1921
'octicon-chevron-right': octiconChevronRight,
@@ -47,3 +49,19 @@ export function svg(name, size = 16, className = '') {
4749
if (className) svgNode.classList.add(...className.split(/\s+/));
4850
return serializer.serializeToString(svgNode);
4951
}
52+
53+
export const SvgIcon = Vue.component('SvgIcon', {
54+
props: {
55+
name: {type: String, required: true},
56+
size: {type: Number, default: 16},
57+
className: {type: String, default: ''},
58+
},
59+
60+
computed: {
61+
svg() {
62+
return svg(this.name, this.size, this.className);
63+
},
64+
},
65+
66+
template: `<span v-html="svg" />`
67+
});

0 commit comments

Comments
 (0)