Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ContextPopup stateless, improve fetching logic #31181

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions routers/web/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -2181,8 +2181,8 @@ func GetIssueInfo(ctx *context.Context) {
}

ctx.JSON(http.StatusOK, map[string]any{
"convertedIssue": convert.ToIssue(ctx, ctx.Doer, issue),
"renderedLabels": templates.RenderLabels(ctx, ctx.Locale, issue.Labels, ctx.Repo.RepoLink, issue),
"issue": convert.ToIssue(ctx, ctx.Doer, issue),
"labelsHtml": templates.RenderLabels(ctx, ctx.Locale, issue.Labels, ctx.Repo.RepoLink, issue),
})
}

Expand Down
1 change: 0 additions & 1 deletion templates/base/head_script.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
i18n: {
copy_success: {{ctx.Locale.Tr "copy_success"}},
copy_error: {{ctx.Locale.Tr "copy_error"}},
error_occurred: {{ctx.Locale.Tr "error.occurred"}},
network_error: {{ctx.Locale.Tr "error.network_error"}},
remove_label_str: {{ctx.Locale.Tr "remove_label_str"}},
modal_confirm: {{ctx.Locale.Tr "modal.confirm"}},
Expand Down
8 changes: 4 additions & 4 deletions tests/integration/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -641,13 +641,13 @@ func TestGetIssueInfo(t *testing.T) {
req := NewRequest(t, "GET", urlStr)
resp := session.MakeRequest(t, req, http.StatusOK)
var respStruct struct {
ConvertedIssue api.Issue
RenderedLabels template.HTML
Issue api.Issue `json:"issue"`
LabelsHTML template.HTML `json:"labelsHtml"`
}
DecodeJSON(t, resp, &respStruct)

assert.EqualValues(t, issue.ID, respStruct.ConvertedIssue.ID)
assert.Contains(t, string(respStruct.RenderedLabels), `"labels-list"`)
assert.EqualValues(t, issue.ID, respStruct.Issue.ID)
assert.Contains(t, string(respStruct.LabelsHTML), `"labels-list"`)
}

func TestUpdateIssueDeadline(t *testing.T) {
Expand Down
82 changes: 26 additions & 56 deletions web_src/js/components/ContextPopup.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
<script>
import {SvgIcon} from '../svg.js';
import {GET} from '../modules/fetch.js';

const {appSubUrl, i18n} = window.config;

export default {
components: {SvgIcon},
data: () => ({
loading: false,
issue: null,
renderedLabels: '',
i18nErrorOccurred: i18n.error_occurred,
i18nErrorMessage: null,
}),
props: {
issue: {
type: Object,
default: null,
},
labelsHtml: {
type: String,
default: '',
},
repoUrl: {
type: String,
default: '',
},
},
computed: {
createdAt() {
return new Date(this.issue.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'});
Expand Down Expand Up @@ -57,56 +61,22 @@ export default {
return 'red'; // Closed Issue
},
},
mounted() {
this.$refs.root.addEventListener('ce-load-context-popup', (e) => {
const data = e.detail;
if (!this.loading && this.issue === null) {
this.load(data);
}
});
},
methods: {
async load(data) {
this.loading = true;
this.i18nErrorMessage = null;

try {
const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); // backend: GetIssueInfo
const respJson = await response.json();
if (!response.ok) {
this.i18nErrorMessage = respJson.message ?? i18n.network_error;
return;
}
this.issue = respJson.convertedIssue;
this.renderedLabels = respJson.renderedLabels;
} catch {
this.i18nErrorMessage = i18n.network_error;
} finally {
this.loading = false;
}
},
},
};
</script>
<template>
<div ref="root">
<div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/>
<div v-if="!loading && issue !== null" class="tw-flex tw-flex-col tw-gap-2">
<div class="tw-text-12">{{ issue.repository.full_name }} on {{ createdAt }}</div>
<div class="flex-text-block">
<svg-icon :name="icon" :class="['text', color]"/>
<span class="issue-title tw-font-semibold tw-break-anywhere">
{{ issue.title }}
<span class="index">#{{ issue.number }}</span>
</span>
</div>
<div v-if="body">{{ body }}</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="issue.labels.length" v-html="renderedLabels"/>
<div class="tw-p-3 tw-flex tw-flex-col tw-gap-2">
<div class="tw-text-12">
<a class="muted" :href="repoUrl">{{ issue.repository.full_name }}</a> on {{ createdAt }}
</div>
<div class="tw-flex tw-flex-col tw-gap-2" v-if="!loading && issue === null">
<div class="tw-text-12">{{ i18nErrorOccurred }}</div>
<div>{{ i18nErrorMessage }}</div>
<div class="flex-text-block tw-gap-2">
<svg-icon :name="icon" :class="['text', color]"/>
<span class="issue-title tw-font-semibold tw-break-anywhere">
{{ issue.title }}
<span class="index">#{{ issue.number }}</span>
</span>
</div>
<div v-if="body">{{ body }}</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="issue.labels.length" v-html="labelsHtml"/>
</div>
</template>
82 changes: 54 additions & 28 deletions web_src/js/features/contextpopup.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,71 @@
import {createApp} from 'vue';
import ContextPopup from '../components/ContextPopup.vue';
import {createVueRoot} from '../utils/vue.js';
import {parseIssueHref} from '../utils.js';
import {createTippy} from '../modules/tippy.js';
import {GET} from '../modules/fetch.js';

export function initContextPopups() {
const refIssues = document.querySelectorAll('.ref-issue');
attachRefIssueContextPopup(refIssues);
}
const {appSubUrl} = window.config;

export function attachRefIssueContextPopup(refIssues) {
for (const refIssue of refIssues) {
if (refIssue.classList.contains('ref-external-issue')) {
return;
}
async function attach(e) {
const link = e.currentTarget;

const {owner, repo, index} = parseIssueHref(refIssue.getAttribute('href'));
if (!owner) return;
// ignore external issues
if (link.classList.contains('ref-external-issue')) return;
// ignore links that are already loading
if (link.hasAttribute('data-issue-ref-loading')) return;

const el = document.createElement('div');
el.classList.add('tw-p-3');
refIssue.parentNode.insertBefore(el, refIssue.nextSibling);
const {owner, repo, index} = parseIssueHref(link.getAttribute('href'));
if (!owner) return;

const view = createApp(ContextPopup);
const url = `${appSubUrl}/${owner}/${repo}/issues/${index}/info`; // backend: GetIssueInfo
if (link.getAttribute('data-issue-ref-info-url') === url) return; // link already has a tooltip with this url

try {
link.setAttribute('data-issue-ref-loading', 'true');
let res;
try {
res = await GET(url);
} catch {}
if (!res.ok) return;

let issue, labelsHtml;
try {
view.mount(el);
} catch (err) {
console.error(err);
el.textContent = 'ContextPopup failed to load';
}
({issue, labelsHtml} = await res.json());
} catch {}
if (!issue) return;

createTippy(refIssue, {
const repoUrl = `${appSubUrl}/${owner}/${repo}`;
const content = createVueRoot(ContextPopup, {issue, labelsHtml, repoUrl});
if (!content) return;

const tippy = createTippy(link, {
theme: 'default',
content: el,
trigger: 'mouseenter focus',
content,
placement: 'top-start',
interactive: true,
role: 'dialog',
interactiveBorder: 5,
onShow: () => {
el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}}));
},
role: 'tooltip',
interactiveBorder: 15,
});

// set attribute on the link that indicates which url the tooltip currently renders
link.setAttribute('data-issue-ref-info-url', url);

// show immediately because this runs during mouseenter and focus
tippy.show();
} finally {
link.removeAttribute('data-issue-ref-loading');
}
}

export function attachRefIssueContextPopup(els) {
for (const el of els) {
el.addEventListener('mouseenter', attach);
el.addEventListener('focus', attach);
}
}

export function initContextPopups() {
// TODO: Use MutationObserver to detect newly inserted .ref-issue
attachRefIssueContextPopup(document.querySelectorAll('.ref-issue'));
}
14 changes: 14 additions & 0 deletions web_src/js/utils/vue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {createApp} from 'vue';

// create a new vue root and container and mount a component into it
export function createVueRoot(component, props) {
const container = document.createElement('div');
const view = createApp(component, props);
try {
view.mount(container);
return container;
} catch (err) {
console.error(err);
return null;
}
}