Skip to content

Commit a0567d8

Browse files
committed
Rewrite context popup to stateless
1 parent fb7b743 commit a0567d8

File tree

5 files changed

+79
-97
lines changed

5 files changed

+79
-97
lines changed

routers/web/repo/issue.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2181,8 +2181,8 @@ func GetIssueInfo(ctx *context.Context) {
21812181
}
21822182

21832183
ctx.JSON(http.StatusOK, map[string]any{
2184-
"convertedIssue": convert.ToIssue(ctx, ctx.Doer, issue),
2185-
"renderedLabels": templates.RenderLabels(ctx, ctx.Locale, issue.Labels, ctx.Repo.RepoLink, issue),
2184+
"issue": convert.ToIssue(ctx, ctx.Doer, issue),
2185+
"labelsHtml": templates.RenderLabels(ctx, ctx.Locale, issue.Labels, ctx.Repo.RepoLink, issue),
21862186
})
21872187
}
21882188

templates/base/head_script.tmpl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
3636
i18n: {
3737
copy_success: {{ctx.Locale.Tr "copy_success"}},
3838
copy_error: {{ctx.Locale.Tr "copy_error"}},
39-
error_occurred: {{ctx.Locale.Tr "error.occurred"}},
4039
network_error: {{ctx.Locale.Tr "error.network_error"}},
4140
remove_label_str: {{ctx.Locale.Tr "remove_label_str"}},
4241
modal_confirm: {{ctx.Locale.Tr "modal.confirm"}},
Lines changed: 21 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
<script>
22
import {SvgIcon} from '../svg.js';
3-
import {GET} from '../modules/fetch.js';
4-
5-
const {appSubUrl, i18n} = window.config;
63
74
export default {
85
components: {SvgIcon},
9-
data: () => ({
10-
loading: false,
11-
issue: null,
12-
renderedLabels: '',
13-
i18nErrorOccurred: i18n.error_occurred,
14-
i18nErrorMessage: null,
15-
}),
6+
props: {
7+
issue: {
8+
type: Object,
9+
default: null,
10+
},
11+
labelsHtml: {
12+
type: String,
13+
default: '',
14+
},
15+
},
1616
computed: {
1717
createdAt() {
1818
return new Date(this.issue.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'});
@@ -57,56 +57,20 @@ export default {
5757
return 'red'; // Closed Issue
5858
},
5959
},
60-
mounted() {
61-
this.$refs.root.addEventListener('ce-load-context-popup', (e) => {
62-
const data = e.detail;
63-
if (!this.loading && this.issue === null) {
64-
this.load(data);
65-
}
66-
});
67-
},
68-
methods: {
69-
async load(data) {
70-
this.loading = true;
71-
this.i18nErrorMessage = null;
72-
73-
try {
74-
const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); // backend: GetIssueInfo
75-
const respJson = await response.json();
76-
if (!response.ok) {
77-
this.i18nErrorMessage = respJson.message ?? i18n.network_error;
78-
return;
79-
}
80-
this.issue = respJson.convertedIssue;
81-
this.renderedLabels = respJson.renderedLabels;
82-
} catch {
83-
this.i18nErrorMessage = i18n.network_error;
84-
} finally {
85-
this.loading = false;
86-
}
87-
},
88-
},
8960
};
9061
</script>
9162
<template>
92-
<div ref="root">
93-
<div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/>
94-
<div v-if="!loading && issue !== null" class="tw-flex tw-flex-col tw-gap-2">
95-
<div class="tw-text-12">{{ issue.repository.full_name }} on {{ createdAt }}</div>
96-
<div class="flex-text-block">
97-
<svg-icon :name="icon" :class="['text', color]"/>
98-
<span class="issue-title tw-font-semibold tw-break-anywhere">
99-
{{ issue.title }}
100-
<span class="index">#{{ issue.number }}</span>
101-
</span>
102-
</div>
103-
<div v-if="body">{{ body }}</div>
104-
<!-- eslint-disable-next-line vue/no-v-html -->
105-
<div v-if="issue.labels.length" v-html="renderedLabels"/>
106-
</div>
107-
<div class="tw-flex tw-flex-col tw-gap-2" v-if="!loading && issue === null">
108-
<div class="tw-text-12">{{ i18nErrorOccurred }}</div>
109-
<div>{{ i18nErrorMessage }}</div>
63+
<div class="tw-p-3 tw-flex tw-flex-col tw-gap-2">
64+
<div class="tw-text-12">{{ issue.repository.full_name }} on {{ createdAt }}</div>
65+
<div class="flex-text-block tw-gap-2">
66+
<svg-icon :name="icon" :class="['text', color]"/>
67+
<span class="issue-title tw-font-semibold tw-break-anywhere">
68+
{{ issue.title }}
69+
<span class="index">#{{ issue.number }}</span>
70+
</span>
11071
</div>
72+
<div v-if="body">{{ body }}</div>
73+
<!-- eslint-disable-next-line vue/no-v-html -->
74+
<div v-if="issue.labels.length" v-html="labelsHtml"/>
11175
</div>
11276
</template>

web_src/js/features/contextpopup.js

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,50 @@
1-
import {createApp} from 'vue';
21
import ContextPopup from '../components/ContextPopup.vue';
2+
import {createVueRoot} from '../utils/vue.js';
33
import {parseIssueHref} from '../utils.js';
44
import {createTippy} from '../modules/tippy.js';
5+
import {GET} from '../modules/fetch.js';
56

6-
export function initContextPopups() {
7-
const refIssues = document.querySelectorAll('.ref-issue');
8-
attachRefIssueContextPopup(refIssues);
7+
const {appSubUrl} = window.config;
8+
9+
async function show(e) {
10+
const link = e.currentTarget;
11+
const {owner, repo, index} = parseIssueHref(link.getAttribute('href'));
12+
if (!owner) return;
13+
14+
const res = await GET(`${appSubUrl}/${owner}/${repo}/issues/${index}/info`); // backend: GetIssueInfo
15+
if (!res.ok) return;
16+
17+
let issue, labelsHtml;
18+
try {
19+
({issue, labelsHtml} = await res.json());
20+
} catch {}
21+
if (!issue) return;
22+
23+
const content = createVueRoot(ContextPopup, {issue, labelsHtml});
24+
if (!content) return;
25+
26+
const tippy = createTippy(link, {
27+
theme: 'default',
28+
trigger: 'mouseenter focus',
29+
content,
30+
placement: 'top-start',
31+
interactive: true,
32+
role: 'dialog',
33+
interactiveBorder: 15,
34+
});
35+
36+
// show immediately because this runs during mouseenter and focus
37+
tippy.show();
938
}
1039

11-
export function attachRefIssueContextPopup(refIssues) {
12-
for (const refIssue of refIssues) {
13-
if (refIssue.classList.contains('ref-external-issue')) {
14-
return;
15-
}
16-
17-
const {owner, repo, index} = parseIssueHref(refIssue.getAttribute('href'));
18-
if (!owner) return;
19-
20-
const el = document.createElement('div');
21-
el.classList.add('tw-p-3');
22-
refIssue.parentNode.insertBefore(el, refIssue.nextSibling);
23-
24-
const view = createApp(ContextPopup);
25-
26-
try {
27-
view.mount(el);
28-
} catch (err) {
29-
console.error(err);
30-
el.textContent = 'ContextPopup failed to load';
31-
}
32-
33-
createTippy(refIssue, {
34-
theme: 'default',
35-
content: el,
36-
placement: 'top-start',
37-
interactive: true,
38-
role: 'dialog',
39-
interactiveBorder: 5,
40-
onShow: () => {
41-
el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}}));
42-
},
43-
});
40+
export function attachRefIssueContextPopup(els) {
41+
for (const link of els) {
42+
link.addEventListener('mouseenter', show);
43+
link.addEventListener('focus', show);
4444
}
4545
}
46+
47+
export function initContextPopups() {
48+
// TODO: Use MutationObserver to detect newly inserted .ref-issue
49+
attachRefIssueContextPopup(document.querySelectorAll('.ref-issue:not(.ref-external-issue)'));
50+
}

web_src/js/utils/vue.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {createApp} from 'vue';
2+
3+
// create a new vue root and container and mount a component into it
4+
export function createVueRoot(component, rootProps) {
5+
const container = document.createElement('div');
6+
const view = createApp(component, rootProps);
7+
try {
8+
view.mount(container);
9+
return container;
10+
} catch (err) {
11+
console.error(err);
12+
return null;
13+
}
14+
}

0 commit comments

Comments
 (0)